Separate verified process cleanup from output closure. Bound additional retention from owned stop entry through cleanup retry, drain before detaching capture, and mark unreadable or abandoned output incomplete. Release owned event-loop references on drain timeout without changing the proven cleanup disposition; preserve borrowed handle reference state. Cover held and failed cleanup, late tail, bounded drain, stream errors, borrowed authority, and a real POSIX inherited-stderr release barrier. The real pipe control drops its final ten bytes on the baseline and retains them with this change. Focused consumer checks passed, including simulated Windows ownership; native Windows and real provider failure scope remain unverified. Android still constructs its startup error snapshot before retirement; that consumer correction remains separate work. Fixes #4840
96 lines
2.8 KiB
JavaScript
96 lines
2.8 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
|
|
const [mode, value = "0"] = process.argv.slice(2);
|
|
let keepAliveTimer;
|
|
let detachedChild;
|
|
|
|
const keepAlive = () => {
|
|
keepAliveTimer = setInterval(() => undefined, 1_000);
|
|
};
|
|
|
|
const stopDetachedFixture = (signal) => {
|
|
if (keepAliveTimer !== undefined) clearInterval(keepAliveTimer);
|
|
if (detachedChild?.exitCode === null) detachedChild.kill(signal);
|
|
process.exitCode = signal === "SIGINT" ? 130 : 143;
|
|
};
|
|
|
|
const write = (stream, data) =>
|
|
new Promise((resolve, reject) => {
|
|
stream.write(data, (cause) => {
|
|
if (cause) reject(cause);
|
|
else resolve();
|
|
});
|
|
});
|
|
|
|
switch (mode) {
|
|
case "late-stderr": {
|
|
// Keep stderr open after leader exit. FD3 is the test's explicit release barrier.
|
|
const writer = spawn(
|
|
process.execPath,
|
|
[
|
|
"-e",
|
|
"const deadline=setTimeout(()=>process.exit(64),5000); require('node:fs').readFile(3,error=>{clearTimeout(deadline);if(error)process.exitCode=65;else process.stderr.write('final-tail');});",
|
|
],
|
|
{ stdio: ["ignore", "ignore", process.stderr, 3] },
|
|
);
|
|
await new Promise((resolve, reject) => {
|
|
writer.once("spawn", resolve);
|
|
writer.once("error", reject);
|
|
});
|
|
writer.unref();
|
|
break;
|
|
}
|
|
case "burst": {
|
|
const bytes = Number(value);
|
|
await Promise.all([
|
|
write(process.stdout, "o".repeat(bytes)),
|
|
write(process.stderr, "e".repeat(bytes)),
|
|
]);
|
|
process.exitCode = 23;
|
|
break;
|
|
}
|
|
case "invalid-utf8":
|
|
await write(process.stdout, Buffer.from([0x61, 0xff, 0x62]));
|
|
await write(process.stderr, Buffer.from([0x63, 0xfe, 0x64]));
|
|
break;
|
|
case "valid-multibyte":
|
|
await write(process.stdout, Buffer.from([0xe2, 0x82, 0xac]));
|
|
break;
|
|
case "exit":
|
|
process.exitCode = Number(value);
|
|
break;
|
|
case "graceful":
|
|
process.on("SIGTERM", () => process.exit(0));
|
|
await write(process.stdout, "ready\n");
|
|
keepAlive();
|
|
break;
|
|
case "stubborn":
|
|
process.on("SIGTERM", () => undefined);
|
|
await write(process.stdout, "ready\n");
|
|
keepAlive();
|
|
break;
|
|
case "detached-child": {
|
|
detachedChild = spawn(
|
|
process.execPath,
|
|
[
|
|
"-e",
|
|
"setInterval(() => undefined, 1000); setTimeout(() => process.exit(0), 10000)",
|
|
],
|
|
{ detached: true, stdio: "ignore" },
|
|
);
|
|
await new Promise((resolve, reject) => {
|
|
detachedChild.once("spawn", resolve);
|
|
detachedChild.once("error", reject);
|
|
});
|
|
process.once("SIGINT", () => stopDetachedFixture("SIGINT"));
|
|
process.once("SIGTERM", () => stopDetachedFixture("SIGTERM"));
|
|
await write(process.stdout, `ready:${detachedChild.pid}\n`);
|
|
detachedChild.unref();
|
|
keepAlive();
|
|
break;
|
|
}
|
|
default:
|
|
process.stderr.write(`unknown provider-process fixture mode: ${mode}\n`);
|
|
process.exitCode = 64;
|
|
}
|