diff --git a/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs b/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs index 329c8bfc56ae..6e53a2d10a04 100644 --- a/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs +++ b/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs @@ -641,8 +641,16 @@ function processTreeIsAlive(child) { } } -function signalChildProcessTree(child, signal) { - if (process.platform !== "win32" && typeof child.pid === "number") { +function defaultRunTaskkill(command, args, options) { + return childProcess.spawnSync(command, args, options); +} + +export function signalChildProcessTree( + child, + signal, + { platform = process.platform, runTaskkill = defaultRunTaskkill } = {}, +) { + if (platform !== "win32" && typeof child.pid === "number") { try { process.kill(-child.pid, signal); return; @@ -651,6 +659,16 @@ function signalChildProcessTree(child, signal) { // the legacy direct-child kill path as the fallback. } } + 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; + } + } try { child.kill(signal); } catch (error) { diff --git a/test/scripts/bundled-plugin-install-uninstall-probe.test.ts b/test/scripts/bundled-plugin-install-uninstall-probe.test.ts index 3dcee832f085..72c9a559d552 100644 --- a/test/scripts/bundled-plugin-install-uninstall-probe.test.ts +++ b/test/scripts/bundled-plugin-install-uninstall-probe.test.ts @@ -456,6 +456,32 @@ describe("bundled plugin install/uninstall probe", () => { expect(child.kill).not.toHaveBeenCalled(); }); + it("signals Windows runtime child process trees with taskkill", async () => { + const runtimeSmoke = await import(pathToFileURL(runtimeSmokePath).href); + const child = { + kill: vi.fn(), + pid: 12345, + }; + const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 })); + + runtimeSmoke.signalChildProcessTree(child, "SIGTERM", { + platform: "win32", + runTaskkill, + }); + expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], { + stdio: "ignore", + }); + + runtimeSmoke.signalChildProcessTree(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")("stops runtime gateway process groups", async () => { const runtimeSmoke = await importRuntimeSmokeWithEnv({ OPENCLAW_BUNDLED_PLUGIN_RUNTIME_TEARDOWN_GRACE_MS: "50",