diff --git a/scripts/prepare-extension-package-boundary-artifacts.mjs b/scripts/prepare-extension-package-boundary-artifacts.mjs index 1f7558525e32..df961f4ff185 100644 --- a/scripts/prepare-extension-package-boundary-artifacts.mjs +++ b/scripts/prepare-extension-package-boundary-artifacts.mjs @@ -1,6 +1,6 @@ // Prepares declaration and entry-shim artifacts that prove plugin package // boundary imports resolve through public package surfaces. -import { spawn } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; import fs from "node:fs"; import path, { resolve } from "node:path"; import { isLocalCheckEnabled } from "./lib/local-heavy-check-runtime.mjs"; @@ -399,8 +399,16 @@ function abortSiblingSteps(abortController) { } } -function signalNodeStep(child, signal) { - if (process.platform !== "win32" && typeof child.pid === "number") { +export function signalNodeStep( + child, + signal, + { + platform = process.platform, + runTaskkill = spawnSync, + useProcessGroup = platform !== "win32", + } = {}, +) { + if (useProcessGroup && typeof child.pid === "number") { try { process.kill(-child.pid, signal); return; @@ -408,6 +416,16 @@ function signalNodeStep(child, signal) { // The child process group can already be gone by the time cleanup runs. } } + if (platform === "win32" && typeof child.pid === "number") { + const args = ["/PID", String(child.pid), "/T"]; + if (signal === "SIGKILL") { + args.push("/F"); + } + const result = runTaskkill("taskkill", args, { stdio: "ignore" }); + if (!result?.error && result?.status === 0) { + return; + } + } child.kill(signal); } @@ -464,9 +482,10 @@ export function runNodeStep(label, args, timeoutMs, params = {}) { let killDeadlineAt = 0; const stdoutWriter = createPrefixedOutputWriter(label, process.stdout); const stderrWriter = createPrefixedOutputWriter(label, process.stderr); - const killNodeStep = (signal) => signalNodeStep(child, signal); + const useProcessGroup = process.platform !== "win32"; + const killNodeStep = (signal) => signalNodeStep(child, signal, { useProcessGroup }); const processGroupAlive = () => { - if (process.platform === "win32" || !child.pid) { + if (!useProcessGroup || !child.pid) { return false; } try { diff --git a/test/scripts/prepare-extension-package-boundary-artifacts.test.ts b/test/scripts/prepare-extension-package-boundary-artifacts.test.ts index 08b7e249a8e2..8d7e8ede51f4 100644 --- a/test/scripts/prepare-extension-package-boundary-artifacts.test.ts +++ b/test/scripts/prepare-extension-package-boundary-artifacts.test.ts @@ -7,7 +7,7 @@ import os from "node:os"; import path from "node:path"; import { setTimeout as delay } from "node:timers/promises"; import { pathToFileURL } from "node:url"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { createPrefixedOutputWriter, isArtifactSetFresh, @@ -17,6 +17,7 @@ import { runNodeStep, runNodeSteps, runNodeStepsInParallel, + signalNodeStep, } from "../../scripts/prepare-extension-package-boundary-artifacts.mjs"; import { makeTempDir } from "../helpers/temp-dir.js"; @@ -123,6 +124,31 @@ describe("prepare-extension-package-boundary-artifacts", () => { expect(Date.now() - startedAt).toBeLessThan(abortBudgetMs); }, 45_000); + it("signals Windows node step process trees with taskkill", () => { + const child = { + kill: vi.fn(), + pid: 12345, + }; + const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 })); + + signalNodeStep(child, "SIGTERM", { + platform: "win32", + runTaskkill, + }); + expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], { + stdio: "ignore", + }); + + signalNodeStep(child, "SIGKILL", { + platform: "win32", + runTaskkill, + }); + expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], { + stdio: "ignore", + }); + expect(child.kill).not.toHaveBeenCalled(); + }); + it.runIf(process.platform !== "win32")( "force-kills aborted sibling step process groups", async () => {