From 26acb7745042be50d307fa3133b44fd897fd2100 Mon Sep 17 00:00:00 2001 From: jeffr Date: Sun, 22 Feb 2026 01:16:09 -0800 Subject: [PATCH] fix: guard entry.ts top-level code with isMainModule to prevent duplicate gateway start The bundler exports shared symbols from dist/entry.js, so other chunks import it as a dependency. When dist/index.js is the actual entry point (e.g. systemd service), lazy module loading eventually imports entry.js, triggering its unguarded top-level code which calls runCli(process.argv) a second time. This starts a duplicate gateway that fails on lock/port contention and crashes the process with exit(1), causing a restart loop. Wrap all top-level executable code in an isMainModule() check so it only runs when entry.ts is the actual main module, not when imported as a shared dependency by the bundler. --- src/entry.ts | 177 +++++++++++++++++++++++++++------------------------ 1 file changed, 94 insertions(+), 83 deletions(-) diff --git a/src/entry.ts b/src/entry.ts index e066432893b..5d0ceeb2e59 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -1,108 +1,119 @@ #!/usr/bin/env node import { spawn } from "node:child_process"; import process from "node:process"; +import { fileURLToPath } from "node:url"; import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js"; import { shouldSkipRespawnForArgv } from "./cli/respawn-policy.js"; import { normalizeWindowsArgv } from "./cli/windows-argv.js"; import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js"; +import { isMainModule } from "./infra/is-main.js"; import { installProcessWarningFilter } from "./infra/warning-filter.js"; import { attachChildProcessBridge } from "./process/child-process-bridge.js"; -process.title = "openclaw"; -installProcessWarningFilter(); -normalizeEnv(); +// Guard: only run entry-point logic when this file is the main module. +// The bundler may import entry.js as a shared dependency when dist/index.js +// is the actual entry point; without this guard the top-level code below +// would call runCli a second time, starting a duplicate gateway that fails +// on the lock / port and crashes the process. +if (!isMainModule({ currentFile: fileURLToPath(import.meta.url) })) { + // Imported as a dependency — skip all entry-point side effects. +} else { + process.title = "openclaw"; + installProcessWarningFilter(); + normalizeEnv(); -if (process.argv.includes("--no-color")) { - process.env.NO_COLOR = "1"; - process.env.FORCE_COLOR = "0"; -} - -const EXPERIMENTAL_WARNING_FLAG = "--disable-warning=ExperimentalWarning"; - -function hasExperimentalWarningSuppressed(): boolean { - const nodeOptions = process.env.NODE_OPTIONS ?? ""; - if (nodeOptions.includes(EXPERIMENTAL_WARNING_FLAG) || nodeOptions.includes("--no-warnings")) { - return true; + if (process.argv.includes("--no-color")) { + process.env.NO_COLOR = "1"; + process.env.FORCE_COLOR = "0"; } - for (const arg of process.execArgv) { - if (arg === EXPERIMENTAL_WARNING_FLAG || arg === "--no-warnings") { + + const EXPERIMENTAL_WARNING_FLAG = "--disable-warning=ExperimentalWarning"; + + function hasExperimentalWarningSuppressed(): boolean { + const nodeOptions = process.env.NODE_OPTIONS ?? ""; + if (nodeOptions.includes(EXPERIMENTAL_WARNING_FLAG) || nodeOptions.includes("--no-warnings")) { return true; } - } - return false; -} - -function ensureExperimentalWarningSuppressed(): boolean { - if (shouldSkipRespawnForArgv(process.argv)) { - return false; - } - if (isTruthyEnvValue(process.env.OPENCLAW_NO_RESPAWN)) { - return false; - } - if (isTruthyEnvValue(process.env.OPENCLAW_NODE_OPTIONS_READY)) { - return false; - } - if (hasExperimentalWarningSuppressed()) { - return false; - } - - // Respawn guard (and keep recursion bounded if something goes wrong). - process.env.OPENCLAW_NODE_OPTIONS_READY = "1"; - // Pass flag as a Node CLI option, not via NODE_OPTIONS (--disable-warning is disallowed in NODE_OPTIONS). - const child = spawn( - process.execPath, - [EXPERIMENTAL_WARNING_FLAG, ...process.execArgv, ...process.argv.slice(1)], - { - stdio: "inherit", - env: process.env, - }, - ); - - attachChildProcessBridge(child); - - child.once("exit", (code, signal) => { - if (signal) { - process.exitCode = 1; - return; + for (const arg of process.execArgv) { + if (arg === EXPERIMENTAL_WARNING_FLAG || arg === "--no-warnings") { + return true; + } } - process.exit(code ?? 1); - }); + return false; + } - child.once("error", (error) => { - console.error( - "[openclaw] Failed to respawn CLI:", - error instanceof Error ? (error.stack ?? error.message) : error, + function ensureExperimentalWarningSuppressed(): boolean { + if (shouldSkipRespawnForArgv(process.argv)) { + return false; + } + if (isTruthyEnvValue(process.env.OPENCLAW_NO_RESPAWN)) { + return false; + } + if (isTruthyEnvValue(process.env.OPENCLAW_NODE_OPTIONS_READY)) { + return false; + } + if (hasExperimentalWarningSuppressed()) { + return false; + } + + // Respawn guard (and keep recursion bounded if something goes wrong). + process.env.OPENCLAW_NODE_OPTIONS_READY = "1"; + // Pass flag as a Node CLI option, not via NODE_OPTIONS (--disable-warning is disallowed in NODE_OPTIONS). + const child = spawn( + process.execPath, + [EXPERIMENTAL_WARNING_FLAG, ...process.execArgv, ...process.argv.slice(1)], + { + stdio: "inherit", + env: process.env, + }, ); - process.exit(1); - }); - // Parent must not continue running the CLI. - return true; -} + attachChildProcessBridge(child); -process.argv = normalizeWindowsArgv(process.argv); + child.once("exit", (code, signal) => { + if (signal) { + process.exitCode = 1; + return; + } + process.exit(code ?? 1); + }); -if (!ensureExperimentalWarningSuppressed()) { - const parsed = parseCliProfileArgs(process.argv); - if (!parsed.ok) { - // Keep it simple; Commander will handle rich help/errors after we strip flags. - console.error(`[openclaw] ${parsed.error}`); - process.exit(2); - } - - if (parsed.profile) { - applyCliProfileEnv({ profile: parsed.profile }); - // Keep Commander and ad-hoc argv checks consistent. - process.argv = parsed.argv; - } - - import("./cli/run-main.js") - .then(({ runCli }) => runCli(process.argv)) - .catch((error) => { + child.once("error", (error) => { console.error( - "[openclaw] Failed to start CLI:", + "[openclaw] Failed to respawn CLI:", error instanceof Error ? (error.stack ?? error.message) : error, ); - process.exitCode = 1; + process.exit(1); }); + + // Parent must not continue running the CLI. + return true; + } + + process.argv = normalizeWindowsArgv(process.argv); + + if (!ensureExperimentalWarningSuppressed()) { + const parsed = parseCliProfileArgs(process.argv); + if (!parsed.ok) { + // Keep it simple; Commander will handle rich help/errors after we strip flags. + console.error(`[openclaw] ${parsed.error}`); + process.exit(2); + } + + if (parsed.profile) { + applyCliProfileEnv({ profile: parsed.profile }); + // Keep Commander and ad-hoc argv checks consistent. + process.argv = parsed.argv; + } + + import("./cli/run-main.js") + .then(({ runCli }) => runCli(process.argv)) + .catch((error) => { + console.error( + "[openclaw] Failed to start CLI:", + error instanceof Error ? (error.stack ?? error.message) : error, + ); + process.exitCode = 1; + }); + } }