UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

282 lines (281 loc) 8.86 kB
import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js"; import { r as runCommandWithTimeout } from "./exec-DubsSJS2.js"; import "./config-CEJFxFUy.js"; import { n as hasBinary } from "./config-eval-CAHzuwOy.js"; import { D as resolveGogServeInvocation, E as resolveGogExecutable, T as resolveGmailHookRuntimeConfig, _ as buildGogWatchServeLogArgs, g as buildGogWatchServeArgs, i as ensureTailscaleEndpoint, v as buildGogWatchStartArgs } from "./gmail-setup-utils-Byhm4lh1.js"; import { spawn } from "node:child_process"; //#region src/hooks/gmail-watcher-errors.ts const ADDRESS_IN_USE_RE = /address already in use|EADDRINUSE/i; /** Detect watcher startup failures caused by an occupied bind port. */ function isAddressInUseError(line) { return ADDRESS_IN_USE_RE.test(line); } //#endregion //#region src/hooks/gmail-watcher.ts /** * Gmail Watcher Service * * Automatically starts `gog gmail watch serve` when the gateway starts, * if hooks.gmail is configured with an account. */ const log = createSubsystemLogger("gmail-watcher"); let watcherProcess = null; let renewInterval = null; let shuttingDown = false; let currentConfig = null; let respawnTimeout = null; /** * Check if gog binary is available */ function isGogAvailable() { return hasBinary("gog"); } /** * Start the Gmail watch (registers with Gmail API) */ async function startGmailWatch(cfg, options = {}) { const args = [resolveGogExecutable(), ...buildGogWatchStartArgs(cfg)]; try { const result = await runCommandWithTimeout(args, { timeoutMs: 12e4, signal: options.signal }); 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) { const args = buildGogWatchServeArgs(cfg); log.info(`starting gog ${buildGogWatchServeLogArgs(cfg).join(" ")}`); let addressInUse = false; const invocation = resolveGogServeInvocation(args); const child = spawn(invocation.command, invocation.args, { stdio: [ "ignore", "pipe", "pipe" ], detached: false, windowsHide: invocation.windowsHide, windowsVerbatimArguments: invocation.windowsVerbatimArguments }); child.stdout?.on("data", (data) => { const line = data.toString().trim(); if (line) log.info(`[gog] ${line}`); }); child.stderr?.on("data", (data) => { 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 (watcherProcess !== null && watcherProcess !== child) return; 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 5s`); watcherProcess = null; respawnTimeout = setTimeout(() => { respawnTimeout = null; if (shuttingDown || !currentConfig) return; watcherProcess = spawnGogServe(currentConfig); }, 5e3); }); return child; } /** * Send SIGTERM, escalate to SIGKILL after 3 s, and resolve on exit/close/error * or a final 5 s timeout after SIGKILL so the caller never hangs. */ function settleProcess(proc) { return new Promise((resolve) => { let settled = false; const settle = () => { if (settled) return; settled = true; clearTimeout(escalation); clearTimeout(finalTimeout); resolve(); }; proc.on("exit", settle); proc.on("close", settle); proc.on("error", settle); proc.kill("SIGTERM"); const escalation = setTimeout(() => { try { proc.kill("SIGKILL"); } catch {} }, 3e3); const finalTimeout = setTimeout(() => { if (!settled) { log.warn("gog process did not exit after SIGKILL; giving up"); settle(); } }, 8e3); }); } function cancelledGmailWatcherStart(expectedConfig) { if (currentConfig === expectedConfig) currentConfig = null; return { started: false, reason: "startup cancelled" }; } function isGmailWatcherStartCancelled(options) { return options.signal?.aborted === true || options.isCancelled?.() === true; } function createGmailWatcherCancellation(options) { if (!options.signal && !options.isCancelled) return { dispose: () => {}, isCancelled: () => false }; const abortController = new AbortController(); const abort = () => { if (!abortController.signal.aborted) abortController.abort(); }; const onAbort = () => abort(); options.signal?.addEventListener("abort", onAbort, { once: true }); let cancelPoll = null; if (options.isCancelled) { cancelPoll = setInterval(() => { if (options.isCancelled?.()) abort(); }, 100); cancelPoll.unref?.(); } if (isGmailWatcherStartCancelled(options)) abort(); return { dispose: () => { if (cancelPoll) { clearInterval(cancelPoll); cancelPoll = null; } options.signal?.removeEventListener("abort", onAbort); }, isCancelled: () => abortController.signal.aborted || isGmailWatcherStartCancelled(options), signal: abortController.signal }; } /** * Start the Gmail watcher service. * Called automatically by the gateway if hooks.gmail is configured. */ async function startGmailWatcher(cfg, options = {}) { if (!cfg.hooks?.enabled) return { started: false, reason: "hooks not enabled" }; if (!cfg.hooks?.gmail?.account) return { started: false, reason: "no gmail account configured" }; if (!isGogAvailable()) return { started: false, reason: "gog binary not found" }; const resolved = resolveGmailHookRuntimeConfig(cfg, {}); if (!resolved.ok) return { started: false, reason: resolved.error }; const runtimeConfig = resolved.value; if (isGmailWatcherStartCancelled(options)) return cancelledGmailWatcherStart(runtimeConfig); currentConfig = runtimeConfig; if (watcherProcess || renewInterval || respawnTimeout) { shuttingDown = true; if (respawnTimeout) { clearTimeout(respawnTimeout); respawnTimeout = null; } if (renewInterval) { clearInterval(renewInterval); renewInterval = null; } if (watcherProcess) { const oldProcess = watcherProcess; watcherProcess = null; await settleProcess(oldProcess); oldProcess.removeAllListeners(); } shuttingDown = false; } if (runtimeConfig.tailscale.mode !== "off") { const cancellation = createGmailWatcherCancellation(options); try { await ensureTailscaleEndpoint({ mode: runtimeConfig.tailscale.mode, path: runtimeConfig.tailscale.path, port: runtimeConfig.serve.port, signal: cancellation.signal, target: runtimeConfig.tailscale.target }); log.info(`tailscale ${runtimeConfig.tailscale.mode} configured for port ${runtimeConfig.serve.port}`); if (cancellation.isCancelled()) return cancelledGmailWatcherStart(runtimeConfig); } catch (err) { if (cancellation.isCancelled()) return cancelledGmailWatcherStart(runtimeConfig); log.error(`tailscale setup failed: ${String(err)}`); return { started: false, reason: `tailscale setup failed: ${String(err)}` }; } finally { cancellation.dispose(); } } const cancellation = createGmailWatcherCancellation(options); const watchStarted = await startGmailWatch(runtimeConfig, { signal: cancellation.signal }); cancellation.dispose(); if (cancellation.isCancelled()) return cancelledGmailWatcherStart(runtimeConfig); if (!watchStarted) log.warn("gmail watch start failed, but continuing with serve"); if (isGmailWatcherStartCancelled(options)) return cancelledGmailWatcherStart(runtimeConfig); shuttingDown = false; watcherProcess = spawnGogServe(runtimeConfig); const renewMs = runtimeConfig.renewEveryMinutes * 6e4; renewInterval = setInterval(() => { if (shuttingDown) return; startGmailWatch(runtimeConfig); }, renewMs); log.info(`gmail watcher started for ${runtimeConfig.account} (renew every ${runtimeConfig.renewEveryMinutes}m)`); return { started: true }; } /** * Stop the Gmail watcher service. */ async function stopGmailWatcher() { shuttingDown = true; if (respawnTimeout) { clearTimeout(respawnTimeout); respawnTimeout = null; } if (renewInterval) { clearInterval(renewInterval); renewInterval = null; } if (watcherProcess) { log.info("stopping gmail watcher"); const proc = watcherProcess; watcherProcess = null; await settleProcess(proc); } currentConfig = null; log.info("gmail watcher stopped"); } //#endregion export { stopGmailWatcher as n, startGmailWatcher as t };