From 55b33b4e6969b771643becd1f2cfd6420b10986a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 6 Jan 2026 01:40:15 +0000 Subject: [PATCH] fix: stop gmail watcher restart on bind error --- CHANGELOG.md | 1 + src/hooks/gmail-watcher.test.ts | 14 ++++++++++++++ src/hooks/gmail-watcher.ts | 21 ++++++++++++++++++++- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/hooks/gmail-watcher.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c3c42610..7b29b4a0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - CLI: auto-migrate legacy config entries on command start (same behavior as gateway startup). - Auth: prioritize OAuth profiles but fall back to API keys when refresh fails; stored profiles now load without explicit auth order. - Docs: add group chat participation guidance to the AGENTS template. +- Gmail: stop restart loop when `gog gmail watch serve` fails to bind (address already in use). - Linux: auto-attempt lingering during onboarding (try without sudo, fallback to sudo) and prompt on install/restart to keep the gateway alive after logout/idle. Thanks @tobiasbischoff for PR #237. - TUI: migrate key handling to the updated pi-tui Key matcher API. - Logging: redact sensitive tokens in verbose tool summaries by default (configurable patterns). diff --git a/src/hooks/gmail-watcher.test.ts b/src/hooks/gmail-watcher.test.ts new file mode 100644 index 000000000..aa16e4ad2 --- /dev/null +++ b/src/hooks/gmail-watcher.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { isAddressInUseError } from "./gmail-watcher.js"; + +describe("gmail watcher", () => { + it("detects address already in use errors", () => { + expect( + isAddressInUseError( + "listen tcp 127.0.0.1:8788: bind: address already in use", + ), + ).toBe(true); + expect(isAddressInUseError("EADDRINUSE: address already in use")).toBe(true); + expect(isAddressInUseError("some other error")).toBe(false); + }); +}); diff --git a/src/hooks/gmail-watcher.ts b/src/hooks/gmail-watcher.ts index bd4e07969..0b3133066 100644 --- a/src/hooks/gmail-watcher.ts +++ b/src/hooks/gmail-watcher.ts @@ -20,6 +20,12 @@ import { ensureTailscaleEndpoint } from "./gmail-setup-utils.js"; const log = createSubsystemLogger("gmail-watcher"); +const ADDRESS_IN_USE_RE = /address already in use|EADDRINUSE/i; + +export function isAddressInUseError(line: string): boolean { + return ADDRESS_IN_USE_RE.test(line); +} + let watcherProcess: ChildProcess | null = null; let renewInterval: ReturnType | null = null; let shuttingDown = false; @@ -61,6 +67,7 @@ async function startGmailWatch( function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess { const args = buildGogWatchServeArgs(cfg); log.info(`starting gog ${args.join(" ")}`); + let addressInUse = false; const child = spawn("gog", args, { stdio: ["ignore", "pipe", "pipe"], @@ -74,7 +81,11 @@ function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess { child.stderr?.on("data", (data: Buffer) => { const line = data.toString().trim(); - if (line) log.warn(`[gog] ${line}`); + if (!line) return; + if (isAddressInUseError(line)) { + addressInUse = true; + } + log.warn(`[gog] ${line}`); }); child.on("error", (err) => { @@ -83,6 +94,14 @@ function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess { 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 CLAWDBOT_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; setTimeout(() => {