diff --git a/src/cli/daemon-cli/lifecycle.ts b/src/cli/daemon-cli/lifecycle.ts index cd54df8035a..7fa7396d0b0 100644 --- a/src/cli/daemon-cli/lifecycle.ts +++ b/src/cli/daemon-cli/lifecycle.ts @@ -5,6 +5,7 @@ import { readBestEffortConfig, resolveGatewayPort } from "../../config/config.js import { parseCmdScriptCommandLine } from "../../daemon/cmd-argv.js"; import { resolveGatewayService } from "../../daemon/service.js"; import { probeGateway } from "../../gateway/probe.js"; +import { isGatewayArgv, parseProcCmdline } from "../../infra/gateway-process-argv.js"; import { findGatewayPidsOnPortSync } from "../../infra/restart.js"; import { defaultRuntime } from "../../runtime.js"; import { theme } from "../../terminal/theme.js"; @@ -42,17 +43,6 @@ async function resolveGatewayLifecyclePort(service = resolveGatewayService()) { return portFromArgs ?? resolveGatewayPort(await readBestEffortConfig(), mergedEnv); } -function normalizeProcArg(arg: string): string { - return arg.replaceAll("\\", "/").toLowerCase(); -} - -function parseProcCmdline(raw: string): string[] { - return raw - .split("\0") - .map((entry) => entry.trim()) - .filter(Boolean); -} - function extractWindowsCommandLine(raw: string): string | null { const lines = raw .split(/\r?\n/) @@ -68,31 +58,6 @@ function extractWindowsCommandLine(raw: string): string | null { return lines.find((line) => line.toLowerCase() !== "commandline") ?? null; } -function stripExecutableExtension(value: string): string { - return value.replace(/\.(bat|cmd|exe)$/i, ""); -} - -function isGatewayArgv(args: string[]): boolean { - const normalized = args.map(normalizeProcArg); - if (!normalized.includes("gateway")) { - return false; - } - - const entryCandidates = [ - "dist/index.js", - "dist/entry.js", - "openclaw.mjs", - "scripts/run-node.mjs", - "src/index.ts", - ]; - if (normalized.some((arg) => entryCandidates.some((entry) => arg.endsWith(entry)))) { - return true; - } - - const exe = stripExecutableExtension(normalized[0] ?? ""); - return exe.endsWith("/openclaw") || exe === "openclaw" || exe.endsWith("/openclaw-gateway"); -} - function readGatewayProcessArgsSync(pid: number): string[] | null { if (process.platform === "linux") { try { @@ -135,7 +100,7 @@ function resolveGatewayListenerPids(port: number): number[] { .filter((pid): pid is number => Number.isFinite(pid) && pid > 0) .filter((pid) => { const args = readGatewayProcessArgsSync(pid); - return args != null && isGatewayArgv(args); + return args != null && isGatewayArgv(args, { allowGatewayBinary: true }); }); } @@ -147,7 +112,7 @@ function resolveGatewayPortFallback(): Promise { function signalGatewayPid(pid: number, signal: "SIGTERM" | "SIGUSR1") { const args = readGatewayProcessArgsSync(pid); - if (!args || !isGatewayArgv(args)) { + if (!args || !isGatewayArgv(args, { allowGatewayBinary: true })) { throw new Error(`refusing to signal non-gateway process pid ${pid}`); } process.kill(pid, signal); diff --git a/src/infra/gateway-lock.ts b/src/infra/gateway-lock.ts index 6e6b71cf2d1..502e06dec3a 100644 --- a/src/infra/gateway-lock.ts +++ b/src/infra/gateway-lock.ts @@ -5,6 +5,7 @@ import net from "node:net"; import path from "node:path"; import { resolveConfigPath, resolveGatewayLockDir, resolveStateDir } from "../config/paths.js"; import { isPidAlive } from "../shared/pid-alive.js"; +import { isGatewayArgv, parseProcCmdline } from "./gateway-process-argv.js"; const DEFAULT_TIMEOUT_MS = 5000; const DEFAULT_POLL_INTERVAL_MS = 100; @@ -46,38 +47,6 @@ export class GatewayLockError extends Error { type LockOwnerStatus = "alive" | "dead" | "unknown"; -function normalizeProcArg(arg: string): string { - return arg.replaceAll("\\", "/").toLowerCase(); -} - -function parseProcCmdline(raw: string): string[] { - return raw - .split("\0") - .map((entry) => entry.trim()) - .filter(Boolean); -} - -function isGatewayArgv(args: string[]): boolean { - const normalized = args.map(normalizeProcArg); - if (!normalized.includes("gateway")) { - return false; - } - - const entryCandidates = [ - "dist/index.js", - "dist/entry.js", - "openclaw.mjs", - "scripts/run-node.mjs", - "src/index.ts", - ]; - if (normalized.some((arg) => entryCandidates.some((entry) => arg.endsWith(entry)))) { - return true; - } - - const exe = normalized[0] ?? ""; - return exe.endsWith("/openclaw") || exe === "openclaw"; -} - function readLinuxCmdline(pid: number): string[] | null { try { const raw = fsSync.readFileSync(`/proc/${pid}/cmdline`, "utf8"); diff --git a/src/infra/gateway-process-argv.ts b/src/infra/gateway-process-argv.ts new file mode 100644 index 00000000000..59f042ead88 --- /dev/null +++ b/src/infra/gateway-process-argv.ts @@ -0,0 +1,35 @@ +function normalizeProcArg(arg: string): string { + return arg.replaceAll("\\", "/").toLowerCase(); +} + +export function parseProcCmdline(raw: string): string[] { + return raw + .split("\0") + .map((entry) => entry.trim()) + .filter(Boolean); +} + +export function isGatewayArgv(args: string[], opts?: { allowGatewayBinary?: boolean }): boolean { + const normalized = args.map(normalizeProcArg); + if (!normalized.includes("gateway")) { + return false; + } + + const entryCandidates = [ + "dist/index.js", + "dist/entry.js", + "openclaw.mjs", + "scripts/run-node.mjs", + "src/index.ts", + ]; + if (normalized.some((arg) => entryCandidates.some((entry) => arg.endsWith(entry)))) { + return true; + } + + const exe = (normalized[0] ?? "").replace(/\.(bat|cmd|exe)$/i, ""); + return ( + exe.endsWith("/openclaw") || + exe === "openclaw" || + (opts?.allowGatewayBinary === true && exe.endsWith("/openclaw-gateway")) + ); +}