From cfba0ab68f7d5ffbe6d560f0ef8bda02d95acc27 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Sat, 28 Mar 2026 16:55:15 +0530 Subject: [PATCH] fix(process): wait for windows close state settlement --- src/process/exec.ts | 34 +++++++++++++++++++++++++------- src/process/exec.windows.test.ts | 14 +++++++++---- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/process/exec.ts b/src/process/exec.ts index 4223d873f2f..1b19bdecccc 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -173,6 +173,9 @@ export type CommandOptions = { noOutputTimeoutMs?: number; }; +const WINDOWS_CLOSE_STATE_SETTLE_TIMEOUT_MS = 250; +const WINDOWS_CLOSE_STATE_POLL_MS = 10; + export function resolveCommandEnv(params: { argv: string[]; env?: NodeJS.ProcessEnv; @@ -366,16 +369,33 @@ export async function runCommandWithTimeout( }; child.on("close", (code, signal) => { if ( - childExitState == null && - code == null && - signal == null && - child.exitCode == null && - child.signalCode == null + process.platform !== "win32" || + childExitState != null || + code != null || + signal != null || + child.exitCode != null || + child.signalCode != null ) { - setImmediate(() => resolveFromClose(code, signal)); + resolveFromClose(code, signal); return; } - resolveFromClose(code, signal); + + const startedAt = Date.now(); + const waitForExitState = () => { + if (settled) { + return; + } + if (childExitState != null || child.exitCode != null || child.signalCode != null) { + resolveFromClose(code, signal); + return; + } + if (Date.now() - startedAt >= WINDOWS_CLOSE_STATE_SETTLE_TIMEOUT_MS) { + resolveFromClose(code, signal); + return; + } + setTimeout(waitForExitState, WINDOWS_CLOSE_STATE_POLL_MS); + }; + waitForExitState(); }); }); } diff --git a/src/process/exec.windows.test.ts b/src/process/exec.windows.test.ts index 26a4f0c2a22..69d469f215a 100644 --- a/src/process/exec.windows.test.ts +++ b/src/process/exec.windows.test.ts @@ -32,6 +32,7 @@ function createMockChild(params?: { closeSignal?: NodeJS.Signals | null; exitCode?: number | null; exitCodeAfterClose?: number | null; + exitCodeAfterCloseDelayMs?: number; signal?: NodeJS.Signals | null; }): MockChild { const child = new EventEmitter() as MockChild; @@ -49,9 +50,9 @@ function createMockChild(params?: { queueMicrotask(() => { child.emit("close", params?.closeCode ?? 0, params?.closeSignal ?? params?.signal ?? null); if (params?.exitCodeAfterClose !== undefined) { - setImmediate(() => { + setTimeout(() => { child.exitCode = params.exitCodeAfterClose ?? null; - }); + }, params.exitCodeAfterCloseDelayMs ?? 0); } }); return child; @@ -123,9 +124,14 @@ describe("windows command wrapper behavior", () => { } }); - it("waits one tick when Windows close reports null before exitCode settles", async () => { + it("waits for Windows exitCode settlement after close reports null", async () => { const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); - const child = createMockChild({ closeCode: null, exitCode: null, exitCodeAfterClose: 0 }); + const child = createMockChild({ + closeCode: null, + exitCode: null, + exitCodeAfterClose: 0, + exitCodeAfterCloseDelayMs: 50, + }); spawnMock.mockImplementation(() => child);