From 9bd439892f2f6da12921f684908872b654f60e16 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 7 Jan 2026 20:59:49 +0000 Subject: [PATCH] refactor: centralize unhandled rejection setup --- src/cli/run-main.ts | 11 ++--------- src/index.ts | 11 ++--------- src/infra/bonjour-ciao.ts | 14 ++++++++++++++ src/infra/bonjour-errors.ts | 7 +++++++ src/infra/bonjour.ts | 21 +++------------------ src/infra/unhandled-rejections.ts | 13 +++++++++++++ src/macos/relay.ts | 11 ++--------- 7 files changed, 43 insertions(+), 45 deletions(-) create mode 100644 src/infra/bonjour-ciao.ts create mode 100644 src/infra/bonjour-errors.ts diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index b9edf7b47..b9a4fa533 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -6,7 +6,7 @@ import { normalizeEnv } from "../infra/env.js"; import { isMainModule } from "../infra/is-main.js"; import { ensureClawdbotCliOnPath } from "../infra/path-env.js"; import { assertSupportedRuntime } from "../infra/runtime-guard.js"; -import { isUnhandledRejectionHandled } from "../infra/unhandled-rejections.js"; +import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js"; import { enableConsoleCapture } from "../logging.js"; export async function runCli(argv: string[] = process.argv) { @@ -25,14 +25,7 @@ export async function runCli(argv: string[] = process.argv) { // Global error handlers to prevent silent crashes from unhandled rejections/exceptions. // These log the error and exit gracefully instead of crashing without trace. - process.on("unhandledRejection", (reason, _promise) => { - if (isUnhandledRejectionHandled(reason)) return; - console.error( - "[clawdbot] Unhandled promise rejection:", - reason instanceof Error ? (reason.stack ?? reason.message) : reason, - ); - process.exit(1); - }); + installUnhandledRejectionHandler(); process.on("uncaughtException", (error) => { console.error( diff --git a/src/index.ts b/src/index.ts index b4e755bba..2fd81d1fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,7 +27,7 @@ import { PortInUseError, } from "./infra/ports.js"; import { assertSupportedRuntime } from "./infra/runtime-guard.js"; -import { isUnhandledRejectionHandled } from "./infra/unhandled-rejections.js"; +import { installUnhandledRejectionHandler } from "./infra/unhandled-rejections.js"; import { enableConsoleCapture } from "./logging.js"; import { runCommandWithTimeout, runExec } from "./process/exec.js"; import { monitorWebProvider } from "./provider-web.js"; @@ -79,14 +79,7 @@ const isMain = isMainModule({ if (isMain) { // Global error handlers to prevent silent crashes from unhandled rejections/exceptions. // These log the error and exit gracefully instead of crashing without trace. - process.on("unhandledRejection", (reason, _promise) => { - if (isUnhandledRejectionHandled(reason)) return; - console.error( - "[clawdbot] Unhandled promise rejection:", - reason instanceof Error ? (reason.stack ?? reason.message) : reason, - ); - process.exit(1); - }); + installUnhandledRejectionHandler(); process.on("uncaughtException", (error) => { console.error( diff --git a/src/infra/bonjour-ciao.ts b/src/infra/bonjour-ciao.ts new file mode 100644 index 000000000..9ca24aa21 --- /dev/null +++ b/src/infra/bonjour-ciao.ts @@ -0,0 +1,14 @@ +import { logDebug } from "../logger.js"; + +import { formatBonjourError } from "./bonjour-errors.js"; + +export function ignoreCiaoCancellationRejection(reason: unknown): boolean { + const message = formatBonjourError(reason).toUpperCase(); + if (!message.includes("CIAO ANNOUNCEMENT CANCELLED")) { + return false; + } + logDebug( + `bonjour: ignoring unhandled ciao rejection: ${formatBonjourError(reason)}`, + ); + return true; +} diff --git a/src/infra/bonjour-errors.ts b/src/infra/bonjour-errors.ts new file mode 100644 index 000000000..7af8e3f3f --- /dev/null +++ b/src/infra/bonjour-errors.ts @@ -0,0 +1,7 @@ +export function formatBonjourError(err: unknown): string { + if (err instanceof Error) { + const msg = err.message || String(err); + return err.name && err.name !== "Error" ? `${err.name}: ${msg}` : msg; + } + return String(err); +} diff --git a/src/infra/bonjour.ts b/src/infra/bonjour.ts index dee2e0cda..43ea728fc 100644 --- a/src/infra/bonjour.ts +++ b/src/infra/bonjour.ts @@ -2,6 +2,8 @@ import os from "node:os"; import { logDebug, logWarn } from "../logger.js"; import { getLogger } from "../logging.js"; +import { ignoreCiaoCancellationRejection } from "./bonjour-ciao.js"; +import { formatBonjourError } from "./bonjour-errors.js"; import { registerUnhandledRejectionHandler } from "./unhandled-rejections.js"; export type GatewayBonjourAdvertiser = { @@ -45,14 +47,6 @@ type BonjourService = { serviceState: string; }; -function formatBonjourError(err: unknown): string { - if (err instanceof Error) { - const msg = err.message || String(err); - return err.name && err.name !== "Error" ? `${err.name}: ${msg}` : msg; - } - return String(err); -} - function serviceSummary(label: string, svc: BonjourService): string { let fqdn = "unknown"; let hostname = "unknown"; @@ -147,16 +141,7 @@ export async function startGatewayBonjourAdvertiser( let ciaoCancellationRejectionHandler: (() => void) | undefined; if (services.length > 0) { ciaoCancellationRejectionHandler = registerUnhandledRejectionHandler( - (reason) => { - const message = formatBonjourError(reason).toUpperCase(); - if (!message.includes("CIAO ANNOUNCEMENT CANCELLED")) { - return false; - } - logDebug( - `bonjour: ignoring unhandled ciao rejection: ${formatBonjourError(reason)}`, - ); - return true; - }, + ignoreCiaoCancellationRejection, ); } diff --git a/src/infra/unhandled-rejections.ts b/src/infra/unhandled-rejections.ts index 5a789fab1..3ce17aa18 100644 --- a/src/infra/unhandled-rejections.ts +++ b/src/infra/unhandled-rejections.ts @@ -1,3 +1,5 @@ +import process from "node:process"; + type UnhandledRejectionHandler = (reason: unknown) => boolean; const handlers = new Set(); @@ -24,3 +26,14 @@ export function isUnhandledRejectionHandled(reason: unknown): boolean { } return false; } + +export function installUnhandledRejectionHandler(): void { + process.on("unhandledRejection", (reason, _promise) => { + if (isUnhandledRejectionHandled(reason)) return; + console.error( + "[clawdbot] Unhandled promise rejection:", + reason instanceof Error ? (reason.stack ?? reason.message) : reason, + ); + process.exit(1); + }); +} diff --git a/src/macos/relay.ts b/src/macos/relay.ts index d9dcc8f0f..65627999b 100644 --- a/src/macos/relay.ts +++ b/src/macos/relay.ts @@ -59,21 +59,14 @@ async function main() { const { assertSupportedRuntime } = await import("../infra/runtime-guard.js"); assertSupportedRuntime(); - const { isUnhandledRejectionHandled } = await import( + const { installUnhandledRejectionHandler } = await import( "../infra/unhandled-rejections.js" ); const { buildProgram } = await import("../cli/program.js"); const program = buildProgram(); - process.on("unhandledRejection", (reason, _promise) => { - if (isUnhandledRejectionHandled(reason)) return; - console.error( - "[clawdbot] Unhandled promise rejection:", - reason instanceof Error ? (reason.stack ?? reason.message) : reason, - ); - process.exit(1); - }); + installUnhandledRejectionHandler(); process.on("uncaughtException", (error) => { console.error(