File size: 4,301 Bytes
3144483 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | import type { ChildProcess } from "node:child_process";
import { EventEmitter } from "node:events";
import { PassThrough } from "node:stream";
import { execa, type ResultPromise } from "execa";
import { afterEach, describe, expect, it, vi } from "vitest";
import { releaseChildProcessOutputAfterExit } from "./child-process.js";
describe.skipIf(process.platform === "win32")("releaseChildProcessOutputAfterExit", () => {
let child: ResultPromise | undefined;
afterEach(() => {
if (child?.pid) {
try {
process.kill(-child.pid, "SIGKILL");
} catch {}
}
child = undefined;
vi.useRealTimers();
});
it.each([250, 1_250])(
"drains descendant output across a %ims event-loop stall",
async (stallMs) => {
const command =
'printf "HEAD\\n"; printf "HEAD\\n" >&2; ( sleep 0.05; printf "TAIL\\n"; printf "TAIL\\n" >&2 ) &';
child = execa("/bin/sh", ["-c", command], {
detached: true,
reject: false,
stdio: ["ignore", "pipe", "pipe"],
stripFinalNewline: false,
});
const releaseOutput = releaseChildProcessOutputAfterExit(child.nodeChildProcess);
// Simulate a contended worker after the direct child exits. The descendant
// writes while JS is parked past the idle or hard deadline, so buffered
// pipe data and release timers are ready when the event loop resumes.
await new Promise<void>((resolve) => {
child?.nodeChildProcess.once("exit", () => {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, stallMs);
resolve();
});
});
expect(await child.finally(releaseOutput)).toMatchObject({
exitCode: 0,
stdout: "HEAD\nTAIL\n",
stderr: "HEAD\nTAIL\n",
});
},
);
it("releases a quiet inherited pipe after the idle grace", async () => {
child = execa("/bin/sh", ["-c", 'printf "DONE\\n"; ( sleep 30 ) &'], {
buffer: false,
detached: true,
reject: false,
stdio: ["ignore", "pipe", "pipe"],
});
const releaseOutput = releaseChildProcessOutputAfterExit(child.nodeChildProcess);
let output = "";
child.stdout?.on("data", (chunk: Buffer) => {
output += chunk.toString();
});
const startedAt = Date.now();
await child.finally(releaseOutput);
expect(output).toContain("DONE");
expect(Date.now() - startedAt).toBeLessThan(2_000);
});
it.each([false, true])(
"bounds continuously writing descendants when drain starts after exit=%s",
async (alreadyExited) => {
vi.useFakeTimers();
const stdout = new PassThrough();
const stderr = new PassThrough();
const fakeChild = Object.assign(new EventEmitter(), {
stdout,
stderr,
exitCode: alreadyExited ? 0 : null,
signalCode: null,
}) as unknown as ChildProcess;
const cleanup = releaseChildProcessOutputAfterExit(fakeChild);
if (!alreadyExited) {
fakeChild.emit("exit", 0);
}
const writer = setInterval(() => stdout.write("TICK\n"), 30);
try {
await vi.advanceTimersByTimeAsync(999);
expect(stdout.destroyed).toBe(false);
await vi.advanceTimersByTimeAsync(1);
expect(stdout.destroyed).toBe(false);
stdout.write("AFTER DEADLINE\n");
await vi.advanceTimersByTimeAsync(1);
expect(stdout.destroyed).toBe(true);
expect(stderr.destroyed).toBe(true);
} finally {
clearInterval(writer);
cleanup();
}
},
);
it.each(["idle", "hard"])("cancels pending %s release when cleaned up", async (deadline) => {
vi.useFakeTimers();
const stdout = new PassThrough();
const stderr = new PassThrough();
const fakeChild = Object.assign(new EventEmitter(), {
stdout,
stderr,
}) as unknown as ChildProcess;
const cleanup = releaseChildProcessOutputAfterExit(fakeChild);
fakeChild.emit("exit", 0);
const writer = deadline === "hard" ? setInterval(() => stdout.write("TICK\n"), 30) : undefined;
await vi.advanceTimersByTimeAsync(deadline === "hard" ? 1_000 : 100);
cleanup();
clearInterval(writer);
await vi.runAllTimersAsync();
expect(stdout.destroyed).toBe(false);
expect(stderr.destroyed).toBe(false);
});
});
|