Skip to content

Commit 063049c

Browse files
committed
fix(process): wait for close after Windows exit fallback
1 parent 4b6b1a3 commit 063049c

2 files changed

Lines changed: 48 additions & 46 deletions

File tree

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,32 @@ 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+
});
3048
const killMock = vi.fn(() => true);
3149
child.kill = killMock as ChildProcess["kill"];
3250
const emitClose = (code: number | null, signal: NodeJS.Signals | null = null) => {
51+
emittedClose = true;
3352
child.emit("close", code, signal);
3453
};
3554
const emitExit = (code: number | null, signal: NodeJS.Signals | null = null) => {
55+
emittedExit = true;
3656
child.exitCode = code;
3757
child.signalCode = signal;
3858
child.emit("exit", code, signal);

src/process/supervisor/adapters/child.ts

Lines changed: 28 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { toStringEnv } from "./env.js";
77

88
const FORCE_KILL_WAIT_FALLBACK_MS = 4000;
99
const WINDOWS_CLOSE_STATE_SETTLE_TIMEOUT_MS = 250;
10-
const WINDOWS_CLOSE_STATE_POLL_MS = 10;
1110

1211
function resolveCommand(command: string): string {
1312
return resolveWindowsCommandShim({
@@ -125,7 +124,7 @@ export async function createChildAdapter(params: {
125124
let waitPromise: Promise<{ code: number | null; signal: NodeJS.Signals | null }> | null = null;
126125
let forceKillWaitFallbackTimer: NodeJS.Timeout | null = null;
127126
let childExitState: { code: number | null; signal: NodeJS.Signals | null } | null = null;
128-
let windowsExitSettleTimer: NodeJS.Timeout | null = null;
127+
let windowsCloseFallbackTimer: NodeJS.Timeout | null = null;
129128

130129
const clearForceKillWaitFallback = () => {
131130
if (!forceKillWaitFallbackTimer) {
@@ -135,20 +134,20 @@ export async function createChildAdapter(params: {
135134
forceKillWaitFallbackTimer = null;
136135
};
137136

138-
const clearWindowsExitSettleTimer = () => {
139-
if (!windowsExitSettleTimer) {
137+
const clearWindowsCloseFallbackTimer = () => {
138+
if (!windowsCloseFallbackTimer) {
140139
return;
141140
}
142-
clearTimeout(windowsExitSettleTimer);
143-
windowsExitSettleTimer = null;
141+
clearTimeout(windowsCloseFallbackTimer);
142+
windowsCloseFallbackTimer = null;
144143
};
145144

146145
const settleWait = (value: { code: number | null; signal: NodeJS.Signals | null }) => {
147146
if (waitResult || waitError !== undefined) {
148147
return;
149148
}
150149
clearForceKillWaitFallback();
151-
clearWindowsExitSettleTimer();
150+
clearWindowsCloseFallbackTimer();
152151
waitResult = value;
153152
if (resolveWait) {
154153
const resolve = resolveWait;
@@ -163,7 +162,7 @@ export async function createChildAdapter(params: {
163162
return;
164163
}
165164
clearForceKillWaitFallback();
166-
clearWindowsExitSettleTimer();
165+
clearWindowsCloseFallbackTimer();
167166
waitError = error;
168167
if (rejectWait) {
169168
const reject = rejectWait;
@@ -185,57 +184,40 @@ export async function createChildAdapter(params: {
185184
const resolveObservedExitState = (fallback: {
186185
code: number | null;
187186
signal: NodeJS.Signals | null;
188-
}) => ({
189-
code: childExitState?.code ?? child.exitCode ?? fallback.code,
190-
signal: childExitState?.signal ?? child.signalCode ?? fallback.signal,
191-
});
192-
193-
const hasObservedExitState = () =>
194-
childExitState != null || child.exitCode != null || child.signalCode != null;
195-
196-
const scheduleWindowsExitStateSettle = (fallback: {
197-
code: number | null;
198-
signal: NodeJS.Signals | null;
199187
}) => {
188+
if (childExitState != null) {
189+
return childExitState;
190+
}
191+
return {
192+
code: child.exitCode ?? fallback.code,
193+
signal: child.signalCode ?? fallback.signal,
194+
};
195+
};
196+
197+
const scheduleWindowsCloseFallback = () => {
200198
if (process.platform !== "win32") {
201199
return;
202200
}
203-
clearWindowsExitSettleTimer();
204-
windowsExitSettleTimer = setTimeout(() => {
205-
settleWait(resolveObservedExitState(fallback));
201+
clearWindowsCloseFallbackTimer();
202+
windowsCloseFallbackTimer = setTimeout(() => {
203+
if (waitResult || waitError !== undefined) {
204+
return;
205+
}
206+
child.stdout?.destroy();
207+
child.stderr?.destroy();
206208
}, WINDOWS_CLOSE_STATE_SETTLE_TIMEOUT_MS);
209+
windowsCloseFallbackTimer.unref?.();
207210
};
208211

209212
child.once("error", (error) => {
210213
rejectPendingWait(error);
211214
});
212215
child.once("exit", (code, signal) => {
213216
childExitState = { code, signal };
214-
scheduleWindowsExitStateSettle({ code, signal });
217+
scheduleWindowsCloseFallback();
215218
});
216219
child.once("close", (code, signal) => {
217-
if (process.platform !== "win32" || hasObservedExitState() || code != null || signal != null) {
218-
settleWait(resolveObservedExitState({ code, signal }));
219-
return;
220-
}
221-
222-
const startedAt = Date.now();
223-
const waitForExitState = () => {
224-
if (waitResult || waitError !== undefined) {
225-
return;
226-
}
227-
if (hasObservedExitState()) {
228-
settleWait(resolveObservedExitState({ code, signal }));
229-
return;
230-
}
231-
if (Date.now() - startedAt >= WINDOWS_CLOSE_STATE_SETTLE_TIMEOUT_MS) {
232-
settleWait({ code, signal });
233-
return;
234-
}
235-
setTimeout(waitForExitState, WINDOWS_CLOSE_STATE_POLL_MS);
236-
};
237-
238-
waitForExitState();
220+
settleWait(resolveObservedExitState({ code, signal }));
239221
});
240222

241223
const wait = async () => {
@@ -292,7 +274,7 @@ export async function createChildAdapter(params: {
292274

293275
const dispose = () => {
294276
clearForceKillWaitFallback();
295-
clearWindowsExitSettleTimer();
277+
clearWindowsCloseFallbackTimer();
296278
child.removeAllListeners();
297279
};
298280

0 commit comments

Comments
 (0)