fix(process): bound output draining after a stoppable command's grace

execFileOutput settles only when every output pipe closes. When a stoppable
command printed, exited, and left a descendant holding its stdout, the
deadline and SIGKILL grace had no process left to stop, so callers such as
the Kuna doctor version probe waited for the descendant instead of settling
within the deadline plus grace. Destroy the command's output streams when
the grace expires so the timeout is reported on time.

Fixes #5557

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 committed 2026-10-20 15:18:50 +00:00
1 parent c2858d2aa0
commit b7c2646dcf
2 files changed
+43 -1

No files matched your search

+35
View File
@@ -272,6 +272,41 @@ describe.skipIf(process.platform === "win32")(
}
});
it("settles after the grace when an exited command's descendant holds its output open", async () => {
const directory = await createTestTempDirectory("rea-exec-descendant-");
const pidFile = join(directory, "descendant.pid");
// The command prints, then exits at once, leaving a descendant that
// inherited stdout and stderr and keeps both pipes open for 60 s.
const script = [
'const { spawn } = require("node:child_process");',
'const fs = require("node:fs");',
'const child = spawn(process.execPath, ["-e", "setTimeout(() => {}, 60000)"], { stdio: ["ignore", "inherit", "inherit"] });',
`fs.writeFileSync(${JSON.stringify(pidFile)}, String(child.pid));`,
"child.unref();",
'process.stdout.write("kuna 1.833\\n");',
].join("\n");
const started = Date.now();
try {
await expect(
execFileOutput(process.execPath, ["-e", script], {
timeout: 500,
stopSignal: "SIGTERM",
}),
).rejects.toMatchObject({ code: "ETIMEDOUT", killed: true });
// Deadline plus the 5 s stop grace, with scheduling slack.
expect(Date.now() - started).toBeLessThan(15_000);
} finally {
const pid = Number(await readFile(pidFile, "utf8").catch(() => ""));
if (Number.isSafeInteger(pid) && pid > 0)
try {
process.kill(pid, "SIGKILL");
} catch {
// The descendant may already have exited.
}
await rm(directory, { recursive: true, force: true });
}
}, 30_000);
it("kills a timed-out process that ignores the stop signal", async () => {
const script = [
'for (const name of ["SIGINT", "SIGTERM"]) process.on(name, () => {});',