Skip to content

Commit 851b65c

Browse files
committed
fix(scripts): clamp package docker timers
1 parent 75c6a8f commit 851b65c

2 files changed

Lines changed: 78 additions & 5 deletions

File tree

scripts/package-openclaw-for-docker.mjs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const DEFAULT_TIMEOUT_KILL_AFTER_MS = 5_000;
1717
const PROCESS_GROUP_EXIT_POLL_MS = 25;
1818
const POST_FORCE_KILL_WAIT_MS = 1_000;
1919
const DEFAULT_CAPTURED_STDOUT_MAX_BYTES = 1024 * 1024;
20+
const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
2021
const ACTIVE_CHILD_KILLERS = new Set();
2122
const SIGNAL_EXIT_CODES = {
2223
SIGHUP: 129,
@@ -65,6 +66,23 @@ function resolveTimeoutMs(envName, defaultValue) {
6566
return parsed;
6667
}
6768

69+
function numericTimerValueMs(valueMs) {
70+
const value = Number(valueMs);
71+
return Number.isFinite(value) ? Math.floor(value) : undefined;
72+
}
73+
74+
function resolveTimerTimeoutMs(valueMs, fallbackMs = MAX_TIMER_TIMEOUT_MS) {
75+
const value = numericTimerValueMs(valueMs) ?? numericTimerValueMs(fallbackMs);
76+
return Math.min(Math.max(value ?? MAX_TIMER_TIMEOUT_MS, 1), MAX_TIMER_TIMEOUT_MS);
77+
}
78+
79+
function resolveOptionalTimerTimeoutMs(valueMs) {
80+
if (valueMs === undefined) {
81+
return undefined;
82+
}
83+
return resolveTimerTimeoutMs(valueMs, 1);
84+
}
85+
6886
function readOptionValue(argv, index, optionName) {
6987
const value = argv[index + 1];
7088
if (value === undefined || value === "" || value.startsWith("-")) {
@@ -149,6 +167,11 @@ export function parseArgs(argv) {
149167

150168
function run(command, args, cwd, options = {}) {
151169
return new Promise((resolve, reject) => {
170+
const resolvedTimeoutMs = resolveOptionalTimerTimeoutMs(options.timeoutMs);
171+
const resolvedKillAfterMs = resolveTimerTimeoutMs(
172+
options.killAfterMs,
173+
DEFAULT_TIMEOUT_KILL_AFTER_MS,
174+
);
152175
const useProcessGroup = process.platform !== "win32";
153176
const child = spawn(command, args, {
154177
cwd,
@@ -230,21 +253,21 @@ function run(command, args, cwd, options = {}) {
230253
return;
231254
}
232255
killChild("SIGKILL");
233-
}, options.killAfterMs ?? DEFAULT_TIMEOUT_KILL_AFTER_MS);
256+
}, resolvedKillAfterMs);
234257
forceKillTimeout.unref?.();
235258
};
236259
ACTIVE_CHILD_KILLERS.add(killChild);
237260
const timeout =
238-
options.timeoutMs === undefined
261+
resolvedTimeoutMs === undefined
239262
? undefined
240263
: setTimeout(() => {
241264
timedOut = true;
242265
terminateChild();
243-
}, options.timeoutMs);
266+
}, resolvedTimeoutMs);
244267
timeout?.unref?.();
245268
const finishAfterTeardown = async (error, value = "") => {
246269
if (processGroupAlive()) {
247-
await waitForProcessGroupExit(options.killAfterMs ?? DEFAULT_TIMEOUT_KILL_AFTER_MS);
270+
await waitForProcessGroupExit(resolvedKillAfterMs);
248271
}
249272
if (processGroupAlive()) {
250273
killChild("SIGKILL");
@@ -275,7 +298,7 @@ function run(command, args, cwd, options = {}) {
275298
child.on("close", (status, signal) => {
276299
if (timedOut) {
277300
void finishAfterTeardown(
278-
new Error(`${command} ${args.join(" ")} timed out after ${options.timeoutMs}ms`),
301+
new Error(`${command} ${args.join(" ")} timed out after ${resolvedTimeoutMs}ms`),
279302
);
280303
return;
281304
}

test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import fs from "node:fs";
44
import os from "node:os";
55
import path from "node:path";
66
import { pathToFileURL } from "node:url";
7+
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
78
import { describe, expect, it, vi } from "vitest";
89
import {
910
buildPackageArtifacts,
@@ -316,6 +317,20 @@ describe("package-openclaw-for-docker", () => {
316317
expect(calls).toEqual(["prepare:/repo", "pack", "restore:/repo"]);
317318
});
318319

320+
it("clamps oversized command timers before scheduling", async () => {
321+
await expect(
322+
runCommandForTest(
323+
process.execPath,
324+
["-e", "setTimeout(() => process.exit(0), 25);"],
325+
process.cwd(),
326+
{
327+
killAfterMs: MAX_TIMER_TIMEOUT_MS + 1,
328+
timeoutMs: MAX_TIMER_TIMEOUT_MS + 1,
329+
},
330+
),
331+
).resolves.toBe("");
332+
});
333+
319334
it("kills timed-out child process groups", async () => {
320335
if (process.platform === "win32") {
321336
return;
@@ -354,6 +369,41 @@ describe("package-openclaw-for-docker", () => {
354369
}
355370
});
356371

372+
it("clamps oversized kill grace before scheduling", async () => {
373+
if (process.platform === "win32") {
374+
return;
375+
}
376+
377+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-package-grace-"));
378+
const donePath = path.join(tempDir, "done");
379+
const childPidPath = path.join(tempDir, "child.pid");
380+
let childPid;
381+
try {
382+
const script = [
383+
"const fs = require('node:fs');",
384+
`fs.writeFileSync(${JSON.stringify(childPidPath)}, String(process.pid));`,
385+
"process.on('SIGTERM', () => {",
386+
` setTimeout(() => { fs.writeFileSync(${JSON.stringify(donePath)}, 'done'); process.exit(0); }, 75);`,
387+
"});",
388+
"setInterval(() => {}, 1000);",
389+
].join("\n");
390+
391+
const runPromise = runCommandForTest(process.execPath, ["-e", script], process.cwd(), {
392+
killAfterMs: MAX_TIMER_TIMEOUT_MS + 1,
393+
timeoutMs: 500,
394+
});
395+
childPid = await readPid(childPidPath, 2000);
396+
397+
await expect(runPromise).rejects.toThrow(/timed out after 500ms/u);
398+
expect(fs.readFileSync(donePath, "utf8")).toBe("done");
399+
} finally {
400+
if (childPid && isProcessAlive(childPid)) {
401+
process.kill(childPid, "SIGKILL");
402+
}
403+
fs.rmSync(tempDir, { force: true, recursive: true });
404+
}
405+
});
406+
357407
it("keeps fallback SIGKILL armed for descendants after the direct child exits", async () => {
358408
if (process.platform === "win32") {
359409
return;

0 commit comments

Comments
 (0)