From 3921e1b0b7c7282a8a8e9a450c3386b7f166c7d5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 4 May 2026 14:07:54 +0100 Subject: [PATCH] fix(process): kill Windows command trees on timeout (cherry picked from commit 9cc3ae100b846437dd3dcbcfaf20b242d9f6f6a2) --- src/process/exec.ts | 11 +++++++++++ src/process/exec.windows.test.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/process/exec.ts b/src/process/exec.ts index c0ed7128900..6907b63cc2e 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -333,6 +333,17 @@ export async function runCommandWithTimeout( return; } killIssuedByTimeout = true; + if (process.platform === "win32" && typeof child.pid === "number" && child.pid > 0) { + try { + spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], { + stdio: "ignore", + windowsHide: true, + }); + return; + } catch { + // Fall through to Node's direct child kill as a last resort. + } + } child.kill("SIGKILL"); }; diff --git a/src/process/exec.windows.test.ts b/src/process/exec.windows.test.ts index 66065bec8be..9be869c0bb8 100644 --- a/src/process/exec.windows.test.ts +++ b/src/process/exec.windows.test.ts @@ -370,6 +370,37 @@ describe("windows command wrapper behavior", () => { } }); + it("kills the Windows process tree when the overall timeout elapses", async () => { + vi.useFakeTimers(); + const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + const child = createMockChild({ autoClose: false }); + const taskkillChild = createMockChild(); + + spawnMock.mockImplementationOnce(() => child).mockImplementationOnce(() => taskkillChild); + + try { + const resultPromise = runCommandWithTimeout(["node", "idle.js"], { timeoutMs: 80 }); + + await vi.advanceTimersByTimeAsync(81); + expect(child.kill).not.toHaveBeenCalled(); + expect(spawnMock).toHaveBeenCalledTimes(2); + expect(spawnMock.mock.calls[1]?.[0]).toBe("taskkill"); + expect(spawnMock.mock.calls[1]?.[1]).toEqual(["/PID", "1234", "/T", "/F"]); + expect(spawnMock.mock.calls[1]?.[2]).toMatchObject({ + stdio: "ignore", + windowsHide: true, + }); + + child.emit("close", null, "SIGKILL"); + const result = await resultPromise; + expect(result.termination).toBe("timeout"); + expect(result.code).not.toBe(0); + } finally { + platformSpy.mockRestore(); + vi.useRealTimers(); + } + }); + it("decodes GBK stdout and stderr from runExec on Windows", async () => { const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const stdout = Buffer.from([0xb2, 0xe2, 0xca, 0xd4]);