diff --git a/scripts/run-with-env.mjs b/scripts/run-with-env.mjs index 602c62dd6e5f..2fc4b88984ba 100644 --- a/scripts/run-with-env.mjs +++ b/scripts/run-with-env.mjs @@ -61,6 +61,25 @@ export function resolveSpawnCommand(command, args, execPath = process.execPath) }; } +/** + * Reads the signal-forwarding force-kill grace period. + */ +export function resolveForceKillDelayMs(env = process.env) { + const raw = env.OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS; + if (raw === undefined || raw === "") { + return 5_000; + } + const text = raw.trim(); + if (!/^\d+$/u.test(text)) { + throw new Error("OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS must be a positive integer"); + } + const parsed = Number(text); + if (!Number.isSafeInteger(parsed) || parsed < 1) { + throw new Error("OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS must be a positive integer"); + } + return parsed; +} + function main(argv = process.argv.slice(2)) { if (isRunWithEnvHelpRequest(argv)) { console.log(USAGE); @@ -75,6 +94,14 @@ function main(argv = process.argv.slice(2)) { process.exit(2); } + let forceKillDelayMs; + try { + forceKillDelayMs = resolveForceKillDelayMs(); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(2); + } + const spawnCommand = resolveSpawnCommand(parsed.command, parsed.args); const useChildProcessGroup = process.platform !== "win32" && !process.stdin.isTTY; const child = spawn(spawnCommand.command, spawnCommand.args, { @@ -85,10 +112,6 @@ function main(argv = process.argv.slice(2)) { }, stdio: "inherit", }); - const forceKillDelayMs = Math.max( - 1, - Number.parseInt(process.env.OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS ?? "5000", 10) || 5_000, - ); let forwardedSignal = null; let forceKillTimer = null; // Keep the child in the foreground process group so TTY signals such as diff --git a/test/scripts/run-with-env.test.ts b/test/scripts/run-with-env.test.ts index 740602a7d13a..068e7acde618 100644 --- a/test/scripts/run-with-env.test.ts +++ b/test/scripts/run-with-env.test.ts @@ -7,6 +7,7 @@ import { describe, expect, it } from "vitest"; import { isRunWithEnvHelpRequest, parseRunWithEnvArgs, + resolveForceKillDelayMs, resolveSpawnCommand, } from "../../scripts/run-with-env.mjs"; @@ -124,6 +125,39 @@ describe("run-with-env", () => { }); }); + it("rejects malformed force-kill grace configuration before spawning", () => { + expect(resolveForceKillDelayMs({})).toBe(5_000); + expect(resolveForceKillDelayMs({ OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS: "250" })).toBe(250); + for (const value of ["0", "-1", "1e3", "100ms"]) { + expect(() => resolveForceKillDelayMs({ OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS: value })).toThrow( + "OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS must be a positive integer", + ); + } + + const result = spawnSync( + process.execPath, + [ + "scripts/run-with-env.mjs", + "OPENCLAW_RUN_WITH_ENV_SIGNAL_TEST=1", + "--", + "node", + "-e", + "process.stdout.write('spawned')", + ], + { + cwd: process.cwd(), + encoding: "utf8", + env: { ...process.env, OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS: "100ms" }, + }, + ); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain( + "OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS must be a positive integer", + ); + }); + it.runIf(process.platform !== "win32").each(["SIGTERM", "SIGHUP", "SIGINT"] as const)( "forwards parent %s to the wrapped command", async (signal) => {