fix(process): preserve command completion and input ownership

This commit is contained in:
morluto committed 2026-10-27 07:55:16 +08:00
1 parent 8a47cbbcce
commit 7fa54e2d62
4 files changed
+229 -37

No files matched your search

+15
View File
@@ -53,6 +53,21 @@ it.each([undefined, "SIGTERM"] as const)(
},
);
it.each([undefined, "SIGTERM"] as const)(
"closes unused command input with stop signal %s",
async (stopSignal) => {
const result = await execFileOutput(
process.execPath,
[
"-e",
'process.stdin.resume(); process.stdin.on("end", () => process.stdout.write("input closed"));',
],
{ timeout: 2_000, ...(stopSignal === undefined ? {} : { stopSignal }) },
);
expect(result.stdout).toBe("input closed");
},
);
describe("execFileOutput", () => {
it("ignores non-Error and incomplete failure metadata", () => {
expect(execFileOutputFailure("failure")).toBeUndefined();
+5 -3
View File
@@ -142,7 +142,7 @@ export const execFileFailureCapturedOutput = (
truncated: failure.outputTruncated,
});
/** Run a shell-free command while capturing its complete output bytes. */
/** Run a shell-free command with no input while capturing its complete output bytes. */
export const execFileBytes = (
command: string,
arguments_: readonly string[],
@@ -175,7 +175,7 @@ export const execFileBytes = (
resolve({ stdout, stderr });
};
if (stopSignal === undefined) {
execFile(
const child = execFile(
command,
[...arguments_],
{
@@ -194,6 +194,8 @@ export const execFileBytes = (
stderr,
),
);
// This promise API exposes no input writer; EOF belongs to the owner.
child.stdin?.end();
return;
}
startStoppableCommand(
@@ -229,7 +231,7 @@ const startStoppableCommand = (
// pipe lifetime of descendants still using caller-owned temporary files.
const child = spawn(command, [...arguments_], {
...execOptions,
stdio: "pipe",
stdio: ["ignore", "pipe", "pipe"],
killSignal: stopSignal,
});
// The first stop request names the failure; a later one is not resent.