Skip to content

Commit c003e98

Browse files
committed
fix(process): drain Windows stdio before exit fallback settle
1 parent 063049c commit c003e98

2 files changed

Lines changed: 35 additions & 26 deletions

File tree

src/process/supervisor/adapters/child.test.ts

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,32 +27,12 @@ function createStubChild(pid = 1234) {
2727
Object.defineProperty(child, "killed", { value: false, configurable: true, writable: true });
2828
Object.defineProperty(child, "exitCode", { value: null, configurable: true, writable: true });
2929
Object.defineProperty(child, "signalCode", { value: null, configurable: true, writable: true });
30-
let emittedClose = false;
31-
let emittedExit = false;
32-
let closedStreams = 0;
33-
const maybeEmitCloseAfterStreamShutdown = () => {
34-
if (emittedClose || !emittedExit || closedStreams < 2) {
35-
return;
36-
}
37-
emittedClose = true;
38-
child.emit("close", child.exitCode, child.signalCode);
39-
};
40-
child.stdout.on("close", () => {
41-
closedStreams += 1;
42-
maybeEmitCloseAfterStreamShutdown();
43-
});
44-
child.stderr.on("close", () => {
45-
closedStreams += 1;
46-
maybeEmitCloseAfterStreamShutdown();
47-
});
4830
const killMock = vi.fn(() => true);
4931
child.kill = killMock as ChildProcess["kill"];
5032
const emitClose = (code: number | null, signal: NodeJS.Signals | null = null) => {
51-
emittedClose = true;
5233
child.emit("close", code, signal);
5334
};
5435
const emitExit = (code: number | null, signal: NodeJS.Signals | null = null) => {
55-
emittedExit = true;
5636
child.exitCode = code;
5737
child.signalCode = signal;
5838
child.emit("exit", code, signal);
@@ -197,7 +177,7 @@ describe("createChildAdapter", () => {
197177
vi.useFakeTimers();
198178
setPlatform("win32");
199179

200-
const { adapter, emitExit } = await (async () => {
180+
const { adapter, emitExit, child } = await (async () => {
201181
const stub = createStubChild(8642);
202182
spawnWithFallbackMock.mockResolvedValue({
203183
child: stub.child,
@@ -216,6 +196,8 @@ describe("createChildAdapter", () => {
216196
});
217197

218198
emitExit(0, null);
199+
child.stdout?.emit("end");
200+
child.stderr?.emit("end");
219201
await vi.advanceTimersByTimeAsync(300);
220202

221203
expect(settled).toHaveBeenCalledWith({ code: 0, signal: null });

src/process/supervisor/adapters/child.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ export async function createChildAdapter(params: {
125125
let forceKillWaitFallbackTimer: NodeJS.Timeout | null = null;
126126
let childExitState: { code: number | null; signal: NodeJS.Signals | null } | null = null;
127127
let windowsCloseFallbackTimer: NodeJS.Timeout | null = null;
128+
let stdoutDrained = child.stdout == null;
129+
let stderrDrained = child.stderr == null;
128130

129131
const clearForceKillWaitFallback = () => {
130132
if (!forceKillWaitFallbackTimer) {
@@ -194,21 +196,46 @@ export async function createChildAdapter(params: {
194196
};
195197
};
196198

199+
const maybeSettleAfterWindowsExit = () => {
200+
if (
201+
process.platform !== "win32" ||
202+
childExitState == null ||
203+
!stdoutDrained ||
204+
!stderrDrained
205+
) {
206+
return;
207+
}
208+
settleWait(resolveObservedExitState(childExitState));
209+
};
210+
197211
const scheduleWindowsCloseFallback = () => {
198212
if (process.platform !== "win32") {
199213
return;
200214
}
201215
clearWindowsCloseFallbackTimer();
202216
windowsCloseFallbackTimer = setTimeout(() => {
203-
if (waitResult || waitError !== undefined) {
204-
return;
205-
}
206-
child.stdout?.destroy();
207-
child.stderr?.destroy();
217+
maybeSettleAfterWindowsExit();
208218
}, WINDOWS_CLOSE_STATE_SETTLE_TIMEOUT_MS);
209219
windowsCloseFallbackTimer.unref?.();
210220
};
211221

222+
child.stdout?.once("end", () => {
223+
stdoutDrained = true;
224+
maybeSettleAfterWindowsExit();
225+
});
226+
child.stdout?.once("close", () => {
227+
stdoutDrained = true;
228+
maybeSettleAfterWindowsExit();
229+
});
230+
child.stderr?.once("end", () => {
231+
stderrDrained = true;
232+
maybeSettleAfterWindowsExit();
233+
});
234+
child.stderr?.once("close", () => {
235+
stderrDrained = true;
236+
maybeSettleAfterWindowsExit();
237+
});
238+
212239
child.once("error", (error) => {
213240
rejectPendingWait(error);
214241
});

0 commit comments

Comments
 (0)