diff --git a/scripts/package-openclaw-for-docker.mjs b/scripts/package-openclaw-for-docker.mjs index 017d1032a7ff..c403a4539bca 100644 --- a/scripts/package-openclaw-for-docker.mjs +++ b/scripts/package-openclaw-for-docker.mjs @@ -142,10 +142,27 @@ function run(command, args, cwd, options = {}) { } child.kill(signal); }; + const processGroupAlive = () => { + if (!useProcessGroup || !child.pid) { + return false; + } + try { + process.kill(-child.pid, 0); + return true; + } catch (error) { + return error?.code === "EPERM"; + } + }; const terminateChild = () => { killChild("SIGTERM"); forceKillTimeout = setTimeout( - () => killChild("SIGKILL"), + () => { + forceKillTimeout = undefined; + if (settled && !processGroupAlive()) { + return; + } + killChild("SIGKILL"); + }, options.killAfterMs ?? DEFAULT_TIMEOUT_KILL_AFTER_MS, ); forceKillTimeout.unref?.(); diff --git a/test/scripts/package-openclaw-for-docker.test.ts b/test/scripts/package-openclaw-for-docker.test.ts index 302849ab6295..632104af3cbe 100644 --- a/test/scripts/package-openclaw-for-docker.test.ts +++ b/test/scripts/package-openclaw-for-docker.test.ts @@ -3,7 +3,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { buildPackageArtifacts, packOpenClawPackageForDocker, @@ -246,6 +246,37 @@ describe("package-openclaw-for-docker", () => { } }); + it("does not fire delayed SIGKILL after a timed-out child exits during grace", async () => { + if (process.platform === "win32") { + return; + } + + const killSpy = vi.spyOn(process, "kill"); + try { + const script = [ + "process.on('SIGTERM', () => process.exit(0));", + "setInterval(() => {}, 1000);", + ].join(""); + + await expect( + runCommandForTest(process.execPath, ["-e", script], process.cwd(), { + killAfterMs: 100, + timeoutMs: 25, + }), + ).rejects.toThrow(/timed out after 25ms/u); + + const sigkillCallsAfterExit = killSpy.mock.calls.filter( + ([, signal]) => signal === "SIGKILL", + ).length; + await sleep(150); + expect(killSpy.mock.calls.filter(([, signal]) => signal === "SIGKILL")).toHaveLength( + sigkillCallsAfterExit, + ); + } finally { + killSpy.mockRestore(); + } + }); + it("fails captured commands that exceed the stdout limit", async () => { const script = [ "process.stdout.write('x'.repeat(2048));",