diff --git a/scripts/prepare-extension-package-boundary-artifacts.mjs b/scripts/prepare-extension-package-boundary-artifacts.mjs index 2bf43fb72a9f..e0acdbac30f8 100644 --- a/scripts/prepare-extension-package-boundary-artifacts.mjs +++ b/scripts/prepare-extension-package-boundary-artifacts.mjs @@ -18,6 +18,7 @@ const ROOT_SHIMS_MAX_OLD_SPACE_SIZE = const ROOT_SHIMS_NODE_OPTIONS = `${process.env.NODE_OPTIONS ?? ""} --max-old-space-size=${ROOT_SHIMS_MAX_OLD_SPACE_SIZE}`.trim(); const NODE_STEP_ABORT_KILL_GRACE_MS = 1_000; +const MAX_TIMER_TIMEOUT_MS = 2_147_000_000; const NODE_STEP_PARENT_SIGNALS = ["SIGHUP", "SIGINT", "SIGTERM"]; const NODE_STEP_PARENT_SIGNAL_EXIT_CODES = new Map([ ["SIGHUP", 129], @@ -469,10 +470,19 @@ function installNodeStepParentSignalForwarders() { }); } +function resolveNodeStepTimerTimeoutMs(valueMs) { + const value = Number(valueMs); + if (!Number.isFinite(value)) { + return MAX_TIMER_TIMEOUT_MS; + } + return Math.min(Math.max(Math.floor(value), 1), MAX_TIMER_TIMEOUT_MS); +} + /** * Runs one artifact step with timeout, abort propagation, and prefixed output. */ export function runNodeStep(label, args, timeoutMs, params = {}) { + const resolvedTimeoutMs = resolveNodeStepTimerTimeoutMs(timeoutMs); const abortController = params.abortController; const spawnImpl = params.spawnImpl ?? spawn; installNodeStepParentSignalForwarders(); @@ -555,8 +565,8 @@ export function runNodeStep(label, args, timeoutMs, params = {}) { stdoutWriter.flush(); stderrWriter.flush(); abortSiblingSteps(abortController); - rejectPromise(new Error(`${label} timed out after ${timeoutMs}ms`)); - }, timeoutMs); + rejectPromise(new Error(`${label} timed out after ${resolvedTimeoutMs}ms`)); + }, resolvedTimeoutMs); abortController?.signal.addEventListener("abort", abortStep, { once: true }); child.stdout.setEncoding("utf8"); diff --git a/test/scripts/prepare-extension-package-boundary-artifacts.test.ts b/test/scripts/prepare-extension-package-boundary-artifacts.test.ts index 9e18a5b8352b..145a2d7dde6f 100644 --- a/test/scripts/prepare-extension-package-boundary-artifacts.test.ts +++ b/test/scripts/prepare-extension-package-boundary-artifacts.test.ts @@ -7,6 +7,7 @@ import os from "node:os"; import path from "node:path"; import { setTimeout as delay } from "node:timers/promises"; import { pathToFileURL } from "node:url"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { afterEach, describe, expect, it, vi } from "vitest"; import { resolveWindowsTaskkillPath } from "../../scripts/lib/windows-taskkill.mjs"; import { @@ -317,6 +318,16 @@ describe("prepare-extension-package-boundary-artifacts", () => { expect(signals).toEqual(["SIGKILL"]); }); + it("clamps oversized prep step timers before scheduling", async () => { + await expect( + runNodeStep( + "slow-success", + ["--eval", "setTimeout(() => process.exit(0), 25);"], + MAX_TIMER_TIMEOUT_MS + 1, + ), + ).resolves.toBeUndefined(); + }); + it.runIf(process.platform !== "win32")("kills timed-out prep step process groups", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-boundary-timeout-group-")); tempRoots.add(rootDir);