/**
* Gmail Watcher Service
*
* Automatically starts ` gog gmail watch serve ` when the gateway starts ,
* if hooks . gmail is configured with an account .
*/
import { type ChildProcess, spawn } from "node:child_process" ;
import { hasBinary } from "../agents/skills.js" ;
import type { OpenClawConfig } from "../config/types.openclaw.js" ;
import { createSubsystemLogger } from "../logging/subsystem.js" ;
import { runCommandWithTimeout } from "../process/exec.js" ;
import { ensureTailscaleEndpoint } from "./gmail-setup-utils.js" ;
import { isAddressInUseError } from "./gmail-watcher-errors.js" ;
import {
buildGogWatchServeLogArgs,
buildGogWatchServeArgs,
buildGogWatchStartArgs,
type GmailHookRuntimeConfig,
resolveGmailHookRuntimeConfig,
} from "./gmail.js" ;
const log = createSubsystemLogger("gmail-watcher" );
let watcherProcess: ChildProcess | null = null ;
let renewInterval: ReturnType<typeof setInterval> | null = null ;
let shuttingDown = false ;
let currentConfig: GmailHookRuntimeConfig | null = null ;
/**
* Check if gog binary is available
*/
function isGogAvailable(): boolean {
return hasBinary("gog" );
}
/**
* Start the Gmail watch ( registers with Gmail API )
*/
async function startGmailWatch(
cfg: Pick<GmailHookRuntimeConfig, "account" | "label" | "topic" >,
): Promise<boolean > {
const args = ["gog" , ...buildGogWatchStartArgs(cfg)];
try {
const result = await runCommandWithTimeout(args, { timeoutMs: 120 _000 });
if (result.code !== 0 ) {
const message = result.stderr || result.stdout || "gog watch start failed" ;
log.error(`watch start failed: ${message}`);
return false ;
}
log.info(`watch started for ${cfg.account}`);
return true ;
} catch (err) {
log.error(`watch start error: ${String(err)}`);
return false ;
}
}
/**
* Spawn the gog gmail watch serve process
*/
function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess {
const args = buildGogWatchServeArgs(cfg);
log.info(`starting gog ${buildGogWatchServeLogArgs(cfg).join(" " )}`);
let addressInUse = false ;
const child = spawn("gog" , args, {
stdio: ["ignore" , "pipe" , "pipe" ],
detached: false ,
});
child.stdout?.on("data" , (data: Buffer) => {
const line = data.toString().trim();
if (line) {
log.info(`[gog] ${line}`);
}
});
child.stderr?.on("data" , (data: Buffer) => {
const line = data.toString().trim();
if (!line) {
return ;
}
if (isAddressInUseError(line)) {
addressInUse = true ;
}
log.warn(`[gog] ${line}`);
});
child.on("error" , (err) => {
log.error(`gog process error: ${String(err)}`);
});
child.on("exit" , (code, signal) => {
if (shuttingDown) {
return ;
}
if (addressInUse) {
log.warn(
"gog serve failed to bind (address already in use); stopping restarts. " +
"Another watcher is likely running. Set OPENCLAW_SKIP_GMAIL_WATCHER=1 or stop the other process." ,
);
watcherProcess = null ;
return ;
}
log.warn(`gog exited (code=${code}, signal=${signal}); restarting in 5 s`);
watcherProcess = null ;
setTimeout(() => {
if (shuttingDown || !currentConfig) {
return ;
}
watcherProcess = spawnGogServe(currentConfig);
}, 5000 );
});
return child;
}
export type GmailWatcherStartResult = {
started: boolean ;
reason?: string;
};
/**
* Start the Gmail watcher service .
* Called automatically by the gateway if hooks . gmail is configured .
*/
export async function startGmailWatcher(cfg: OpenClawConfig): Promise<GmailWatcherStartResult> {
// Check if gmail hooks are configured
if (!cfg.hooks?.enabled) {
return { started: false , reason: "hooks not enabled" };
}
if (!cfg.hooks?.gmail?.account) {
return { started: false , reason: "no gmail account configured" };
}
// Check if gog is available
const gogAvailable = isGogAvailable();
if (!gogAvailable) {
return { started: false , reason: "gog binary not found" };
}
// Resolve the full runtime config
const resolved = resolveGmailHookRuntimeConfig(cfg, {});
if (!resolved.ok) {
return { started: false , reason: resolved.error };
}
const runtimeConfig = resolved.value;
currentConfig = runtimeConfig;
// Set up Tailscale endpoint if needed
if (runtimeConfig.tailscale.mode !== "off" ) {
try {
await ensureTailscaleEndpoint({
mode: runtimeConfig.tailscale.mode,
path: runtimeConfig.tailscale.path,
port: runtimeConfig.serve.port,
target: runtimeConfig.tailscale.target,
});
log.info(
`tailscale ${runtimeConfig.tailscale.mode} configured for port ${runtimeConfig.serve.port}`,
);
} catch (err) {
log.error(`tailscale setup failed: ${String(err)}`);
return {
started: false ,
reason: `tailscale setup failed: ${String(err)}`,
};
}
}
// Start the Gmail watch (register with Gmail API)
const watchStarted = await startGmailWatch(runtimeConfig);
if (!watchStarted) {
log.warn("gmail watch start failed, but continuing with serve" );
}
// Spawn the gog serve process
shuttingDown = false ;
watcherProcess = spawnGogServe(runtimeConfig);
// Set up renewal interval
const renewMs = runtimeConfig.renewEveryMinutes * 60 _000 ;
renewInterval = setInterval(() => {
if (shuttingDown) {
return ;
}
void startGmailWatch(runtimeConfig);
}, renewMs);
log.info(
`gmail watcher started for ${runtimeConfig.account} (renew every ${runtimeConfig.renewEveryMinutes}m)`,
);
return { started: true };
}
/**
* Stop the Gmail watcher service .
*/
export async function stopGmailWatcher(): Promise<void > {
shuttingDown = true ;
if (renewInterval) {
clearInterval(renewInterval);
renewInterval = null ;
}
if (watcherProcess) {
log.info("stopping gmail watcher" );
watcherProcess.kill("SIGTERM" );
// Wait a bit for graceful shutdown
await new Promise<void >((resolve) => {
const timeout = setTimeout(() => {
if (watcherProcess) {
watcherProcess.kill("SIGKILL" );
}
resolve();
}, 3000 );
watcherProcess?.on("exit" , () => {
clearTimeout(timeout);
resolve();
});
});
watcherProcess = null ;
}
currentConfig = null ;
log.info("gmail watcher stopped" );
}
/**
* Check if the Gmail watcher is running .
*/
export function isGmailWatcherRunning(): boolean {
return watcherProcess !== null && !shuttingDown;
}
Messung V0.5 in Prozent C=99 H=97 G=97
¤ Dauer der Verarbeitung: 0.13 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland