fix(process): stop timed-out commands with the caller's stop signal

execFileOutput forwarded stopSignal only together with an AbortSignal, so
a caller with just a timeout (the native UI helper's swiftc build) still
got Node's SIGTERM. When the timeout did send the stop signal, nothing
killed a process that ignored it, and a process that exited cleanly
after the signal resolved as a success.

A stoppable command now owns its timeout: it sends the stop signal,
kills the process after the grace period, and rejects with ETIMEDOUT
and killed: true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 committed 2026-10-09 07:57:14 +00:00
1 parent e6905c18e0
commit 063de5014f
2 files changed
+105 -20

No files matched your search

+47
View File
@@ -95,3 +95,50 @@ describe("execFileOutput", () => {
).rejects.toMatchObject({ name: "AbortError" });
});
});
describe.skipIf(process.platform === "win32")(
"execFileOutput stop signal at a timeout",
() => {
it("stops a timed-out process with the caller's signal and reports the timeout", async () => {
const directory = await createTestTempDirectory("rea-exec-timeout-");
const stopped = join(directory, "stopped");
// The child exits cleanly once it has recorded the signal it received.
const script = [
'const fs = require("node:fs");',
'for (const name of ["SIGINT", "SIGTERM"])',
` process.on(name, () => setTimeout(() => { fs.writeFileSync(${JSON.stringify(stopped)}, name); process.exit(0); }, 200));`,
"setInterval(() => {}, 1000);",
].join("\n");
const failure: unknown = await execFileOutput(
process.execPath,
["-e", script],
{ timeout: 2_000, stopSignal: "SIGINT" },
).then(
() => undefined,
(cause: unknown) => cause,
);
expect(failure).toMatchObject({ code: "ETIMEDOUT", killed: true });
expect(await readFile(stopped, "utf8")).toBe("SIGINT");
});
it("kills a timed-out process that ignores the stop signal", async () => {
const script = [
'for (const name of ["SIGINT", "SIGTERM"]) process.on(name, () => {});',
"setInterval(() => {}, 1000);",
].join("\n");
await expect(
execFileOutput(process.execPath, ["-e", script], {
timeout: 1_000,
stopSignal: "SIGINT",
}),
).rejects.toMatchObject({
code: "ETIMEDOUT",
killed: true,
signal: "SIGKILL",
});
});
},
);