fix(process): drain Windows stdio before exit fallback settle

This commit is contained in:
Ayaan Zaidi
2026-04-10 09:55:48 +05:30
parent 063049c0d4
commit c003e982a2
2 changed files with 35 additions and 26 deletions

View File

@@ -27,32 +27,12 @@ function createStubChild(pid = 1234) {
Object.defineProperty(child, "killed", { value: false, configurable: true, writable: true });
Object.defineProperty(child, "exitCode", { value: null, configurable: true, writable: true });
Object.defineProperty(child, "signalCode", { value: null, configurable: true, writable: true });
let emittedClose = false;
let emittedExit = false;
let closedStreams = 0;
const maybeEmitCloseAfterStreamShutdown = () => {
if (emittedClose || !emittedExit || closedStreams < 2) {
return;
}
emittedClose = true;
child.emit("close", child.exitCode, child.signalCode);
};
child.stdout.on("close", () => {
closedStreams += 1;
maybeEmitCloseAfterStreamShutdown();
});
child.stderr.on("close", () => {
closedStreams += 1;
maybeEmitCloseAfterStreamShutdown();
});
const killMock = vi.fn(() => true);
child.kill = killMock as ChildProcess["kill"];
const emitClose = (code: number | null, signal: NodeJS.Signals | null = null) => {
emittedClose = true;
child.emit("close", code, signal);
};
const emitExit = (code: number | null, signal: NodeJS.Signals | null = null) => {
emittedExit = true;
child.exitCode = code;
child.signalCode = signal;
child.emit("exit", code, signal);
@@ -197,7 +177,7 @@ describe("createChildAdapter", () => {
vi.useFakeTimers();
setPlatform("win32");
const { adapter, emitExit } = await (async () => {
const { adapter, emitExit, child } = await (async () => {
const stub = createStubChild(8642);
spawnWithFallbackMock.mockResolvedValue({
child: stub.child,
@@ -216,6 +196,8 @@ describe("createChildAdapter", () => {
});
emitExit(0, null);
child.stdout?.emit("end");
child.stderr?.emit("end");
await vi.advanceTimersByTimeAsync(300);
expect(settled).toHaveBeenCalledWith({ code: 0, signal: null });

View File

@@ -125,6 +125,8 @@ export async function createChildAdapter(params: {
let forceKillWaitFallbackTimer: NodeJS.Timeout | null = null;
let childExitState: { code: number | null; signal: NodeJS.Signals | null } | null = null;
let windowsCloseFallbackTimer: NodeJS.Timeout | null = null;
let stdoutDrained = child.stdout == null;
let stderrDrained = child.stderr == null;
const clearForceKillWaitFallback = () => {
if (!forceKillWaitFallbackTimer) {
@@ -194,21 +196,46 @@ export async function createChildAdapter(params: {
};
};
const maybeSettleAfterWindowsExit = () => {
if (
process.platform !== "win32" ||
childExitState == null ||
!stdoutDrained ||
!stderrDrained
) {
return;
}
settleWait(resolveObservedExitState(childExitState));
};
const scheduleWindowsCloseFallback = () => {
if (process.platform !== "win32") {
return;
}
clearWindowsCloseFallbackTimer();
windowsCloseFallbackTimer = setTimeout(() => {
if (waitResult || waitError !== undefined) {
return;
}
child.stdout?.destroy();
child.stderr?.destroy();
maybeSettleAfterWindowsExit();
}, WINDOWS_CLOSE_STATE_SETTLE_TIMEOUT_MS);
windowsCloseFallbackTimer.unref?.();
};
child.stdout?.once("end", () => {
stdoutDrained = true;
maybeSettleAfterWindowsExit();
});
child.stdout?.once("close", () => {
stdoutDrained = true;
maybeSettleAfterWindowsExit();
});
child.stderr?.once("end", () => {
stderrDrained = true;
maybeSettleAfterWindowsExit();
});
child.stderr?.once("close", () => {
stderrDrained = true;
maybeSettleAfterWindowsExit();
});
child.once("error", (error) => {
rejectPendingWait(error);
});