diff --git a/scripts/e2e/kitchen-sink-rpc-walk.mjs b/scripts/e2e/kitchen-sink-rpc-walk.mjs index 2d760a87574f..7575a41cf28d 100644 --- a/scripts/e2e/kitchen-sink-rpc-walk.mjs +++ b/scripts/e2e/kitchen-sink-rpc-walk.mjs @@ -8,6 +8,7 @@ import path from "node:path"; import process from "node:process"; import { setTimeout as delay } from "node:timers/promises"; import { fileURLToPath, pathToFileURL } from "node:url"; +import { resolveWindowsTaskkillPath } from "../lib/windows-taskkill.mjs"; const PLUGIN_SPEC = process.env.OPENCLAW_KITCHEN_SINK_NPM_SPEC || "npm:@openclaw/kitchen-sink@latest"; @@ -575,11 +576,12 @@ function commandProcessTreeIsAlive(child) { } function signalWindowsProcessTree(pid, signal, runTaskkill = childProcess.spawnSync) { + const taskkillPath = resolveWindowsTaskkillPath(); const args = ["/PID", String(pid), "/T"]; if (signal === "SIGKILL") { args.push("/F"); } - const result = runTaskkill("taskkill", args, { stdio: "ignore" }); + const result = runTaskkill(taskkillPath, args, { stdio: "ignore" }); return !result?.error && result?.status === 0; } diff --git a/scripts/measure-rpc-rtt.mjs b/scripts/measure-rpc-rtt.mjs index 88f1f52d2752..612b4d604ba6 100644 --- a/scripts/measure-rpc-rtt.mjs +++ b/scripts/measure-rpc-rtt.mjs @@ -10,6 +10,7 @@ import path from "node:path"; import { performance } from "node:perf_hooks"; import { fileURLToPath, pathToFileURL } from "node:url"; import { readBoundedResponseText } from "./lib/bounded-response.mjs"; +import { resolveWindowsTaskkillPath } from "./lib/windows-taskkill.mjs"; const DEFAULT_METHODS = ["health", "config.get"]; const DEFAULT_ITERATIONS = 10; @@ -301,16 +302,17 @@ export function signalGatewayProcess( } } if (platform === "win32" && typeof child.pid === "number") { + const taskkillPath = resolveWindowsTaskkillPath(); const args = ["/PID", String(child.pid), "/T"]; if (signal === "SIGKILL") { args.push("/F"); } - const result = runTaskkill("taskkill", args, { stdio: "ignore" }); + const result = runTaskkill(taskkillPath, args, { stdio: "ignore" }); if (!result?.error && result?.status === 0) { return true; } if (signal !== "SIGKILL") { - const forceResult = runTaskkill("taskkill", [...args, "/F"], { stdio: "ignore" }); + const forceResult = runTaskkill(taskkillPath, [...args, "/F"], { stdio: "ignore" }); if (!forceResult?.error && forceResult?.status === 0) { return true; } diff --git a/test/scripts/kitchen-sink-rpc-walk.test.ts b/test/scripts/kitchen-sink-rpc-walk.test.ts index e0a97de6c209..86e6a2c6d0c7 100644 --- a/test/scripts/kitchen-sink-rpc-walk.test.ts +++ b/test/scripts/kitchen-sink-rpc-walk.test.ts @@ -62,10 +62,15 @@ import { validateCliArgs, waitForGatewayReady, } from "../../scripts/e2e/kitchen-sink-rpc-walk.mjs"; +import { resolveWindowsTaskkillPath } from "../../scripts/lib/windows-taskkill.mjs"; import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js"; const posixIt = process.platform === "win32" ? it.skip : it; +function expectedTaskkillPath(): string { + return resolveWindowsTaskkillPath(); +} + afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); @@ -296,9 +301,14 @@ describe("kitchen-sink RPC gateway teardown", () => { runTaskkill, }), ).toBe(true); - expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], { - stdio: "ignore", - }); + expect(runTaskkill).toHaveBeenNthCalledWith( + 1, + expectedTaskkillPath(), + ["/PID", "12345", "/T"], + { + stdio: "ignore", + }, + ); expect( signalGateway(child, "SIGKILL", killProcess, { @@ -306,9 +316,14 @@ describe("kitchen-sink RPC gateway teardown", () => { runTaskkill, }), ).toBe(true); - expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], { - stdio: "ignore", - }); + expect(runTaskkill).toHaveBeenNthCalledWith( + 2, + expectedTaskkillPath(), + ["/PID", "12345", "/T", "/F"], + { + stdio: "ignore", + }, + ); expect(killProcess).not.toHaveBeenCalled(); expect(child.kill).not.toHaveBeenCalled(); }); @@ -331,12 +346,22 @@ describe("kitchen-sink RPC gateway teardown", () => { }), ).toBe(true); - expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], { - stdio: "ignore", - }); - expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], { - stdio: "ignore", - }); + expect(runTaskkill).toHaveBeenNthCalledWith( + 1, + expectedTaskkillPath(), + ["/PID", "12345", "/T"], + { + stdio: "ignore", + }, + ); + expect(runTaskkill).toHaveBeenNthCalledWith( + 2, + expectedTaskkillPath(), + ["/PID", "12345", "/T", "/F"], + { + stdio: "ignore", + }, + ); expect(killProcess).not.toHaveBeenCalled(); expect(child.kill).not.toHaveBeenCalled(); }); @@ -702,17 +727,27 @@ setInterval(() => {}, 1000); platform: "win32", runTaskkill, }); - expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], { - stdio: "ignore", - }); + expect(runTaskkill).toHaveBeenNthCalledWith( + 1, + expectedTaskkillPath(), + ["/PID", "12345", "/T"], + { + stdio: "ignore", + }, + ); signalProcessGroup(child, "SIGKILL", { platform: "win32", runTaskkill, }); - expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], { - stdio: "ignore", - }); + expect(runTaskkill).toHaveBeenNthCalledWith( + 2, + expectedTaskkillPath(), + ["/PID", "12345", "/T", "/F"], + { + stdio: "ignore", + }, + ); expect(child.kill).not.toHaveBeenCalled(); }); @@ -731,12 +766,22 @@ setInterval(() => {}, 1000); runTaskkill, }); - expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], { - stdio: "ignore", - }); - expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], { - stdio: "ignore", - }); + expect(runTaskkill).toHaveBeenNthCalledWith( + 1, + expectedTaskkillPath(), + ["/PID", "12345", "/T"], + { + stdio: "ignore", + }, + ); + expect(runTaskkill).toHaveBeenNthCalledWith( + 2, + expectedTaskkillPath(), + ["/PID", "12345", "/T", "/F"], + { + stdio: "ignore", + }, + ); expect(child.kill).not.toHaveBeenCalled(); }); diff --git a/test/scripts/measure-rpc-rtt.test.ts b/test/scripts/measure-rpc-rtt.test.ts index 56223ee37f63..e2f461cb1057 100644 --- a/test/scripts/measure-rpc-rtt.test.ts +++ b/test/scripts/measure-rpc-rtt.test.ts @@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process"; import { EventEmitter } from "node:events"; import { describe, expect, it, vi } from "vitest"; +import { resolveWindowsTaskkillPath } from "../../scripts/lib/windows-taskkill.mjs"; import { assertRpcSmokeResponse, cleanupTempRoot, @@ -16,6 +17,31 @@ import { waitForGatewayReady, } from "../../scripts/measure-rpc-rtt.mjs"; +function expectedTaskkillPath(): string { + return resolveWindowsTaskkillPath(); +} + +function restoreEnvValue(key: string, value: string | undefined): void { + if (value === undefined) { + delete process.env[key]; + return; + } + process.env[key] = value; +} + +function withDefaultWindowsSystemRoot(run: () => void): void { + const originalSystemRoot = process.env.SystemRoot; + const originalWindir = process.env.WINDIR; + try { + process.env.SystemRoot = "C:\\Windows"; + delete process.env.WINDIR; + run(); + } finally { + restoreEnvValue("SystemRoot", originalSystemRoot); + restoreEnvValue("WINDIR", originalWindir); + } +} + class FakeWebSocket extends EventEmitter { static OPEN = 1; static instances: FakeWebSocket[] = []; @@ -324,7 +350,7 @@ describe("scripts/measure-rpc-rtt.mjs", () => { expect(signalGatewayProcess(child, "SIGTERM", kill, { runTaskkill })).toBe(true); if (process.platform === "win32") { - expect(runTaskkill).toHaveBeenCalledWith("taskkill", ["/PID", "12345", "/T"], { + expect(runTaskkill).toHaveBeenCalledWith(expectedTaskkillPath(), ["/PID", "12345", "/T"], { stdio: "ignore", }); expect(child.kill).not.toHaveBeenCalled(); @@ -335,66 +361,90 @@ describe("scripts/measure-rpc-rtt.mjs", () => { }); it("signals Windows gateway process trees with taskkill", () => { - const child = Object.assign(new EventEmitter(), { - exitCode: null, - kill: vi.fn(), - pid: 12345, - signalCode: null, - }); - const kill = vi.fn(() => true); - const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 })); + withDefaultWindowsSystemRoot(() => { + const child = Object.assign(new EventEmitter(), { + exitCode: null, + kill: vi.fn(), + pid: 12345, + signalCode: null, + }); + const kill = vi.fn(() => true); + const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 })); - expect( - signalGatewayProcess(child, "SIGTERM", kill, { - platform: "win32", - runTaskkill, - }), - ).toBe(true); - expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], { - stdio: "ignore", - }); + expect( + signalGatewayProcess(child, "SIGTERM", kill, { + platform: "win32", + runTaskkill, + }), + ).toBe(true); + expect(runTaskkill).toHaveBeenNthCalledWith( + 1, + expectedTaskkillPath(), + ["/PID", "12345", "/T"], + { + stdio: "ignore", + }, + ); - expect( - signalGatewayProcess(child, "SIGKILL", kill, { - platform: "win32", - runTaskkill, - }), - ).toBe(true); - expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], { - stdio: "ignore", + expect( + signalGatewayProcess(child, "SIGKILL", kill, { + platform: "win32", + runTaskkill, + }), + ).toBe(true); + expect(runTaskkill).toHaveBeenNthCalledWith( + 2, + expectedTaskkillPath(), + ["/PID", "12345", "/T", "/F"], + { + stdio: "ignore", + }, + ); + expect(kill).not.toHaveBeenCalled(); + expect(child.kill).not.toHaveBeenCalled(); }); - expect(kill).not.toHaveBeenCalled(); - expect(child.kill).not.toHaveBeenCalled(); }); it("force-kills Windows gateway process trees when graceful taskkill fails", () => { - const child = Object.assign(new EventEmitter(), { - exitCode: null, - kill: vi.fn(), - pid: 12345, - signalCode: null, - }); - const kill = vi.fn(() => true); - const runTaskkill = vi - .fn() - .mockReturnValueOnce({ error: undefined, status: 1 }) - .mockReturnValueOnce({ error: undefined, status: 0 }); + withDefaultWindowsSystemRoot(() => { + const child = Object.assign(new EventEmitter(), { + exitCode: null, + kill: vi.fn(), + pid: 12345, + signalCode: null, + }); + const kill = vi.fn(() => true); + const runTaskkill = vi + .fn() + .mockReturnValueOnce({ error: undefined, status: 1 }) + .mockReturnValueOnce({ error: undefined, status: 0 }); - expect( - signalGatewayProcess(child, "SIGTERM", kill, { - platform: "win32", - runTaskkill, - }), - ).toBe(true); + expect( + signalGatewayProcess(child, "SIGTERM", kill, { + platform: "win32", + runTaskkill, + }), + ).toBe(true); - expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], { - stdio: "ignore", + expect(runTaskkill).toHaveBeenNthCalledWith( + 1, + expectedTaskkillPath(), + ["/PID", "12345", "/T"], + { + stdio: "ignore", + }, + ); + expect(runTaskkill).toHaveBeenNthCalledWith( + 2, + expectedTaskkillPath(), + ["/PID", "12345", "/T", "/F"], + { + stdio: "ignore", + }, + ); + expect(kill).not.toHaveBeenCalled(); + expect(child.kill).not.toHaveBeenCalled(); }); - expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], { - stdio: "ignore", - }); - expect(kill).not.toHaveBeenCalled(); - expect(child.kill).not.toHaveBeenCalled(); }); it("treats missing gateway process groups as already exited", () => { @@ -442,12 +492,22 @@ describe("scripts/measure-rpc-rtt.mjs", () => { await stopGateway(child, { killGraceMs: 1, killProcess: kill, runTaskkill }); if (process.platform === "win32") { - expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12346", "/T"], { - stdio: "ignore", - }); - expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12346", "/T", "/F"], { - stdio: "ignore", - }); + expect(runTaskkill).toHaveBeenNthCalledWith( + 1, + expectedTaskkillPath(), + ["/PID", "12346", "/T"], + { + stdio: "ignore", + }, + ); + expect(runTaskkill).toHaveBeenNthCalledWith( + 2, + expectedTaskkillPath(), + ["/PID", "12346", "/T", "/F"], + { + stdio: "ignore", + }, + ); expect(child.kill).not.toHaveBeenCalled(); } else { expect(kill).toHaveBeenNthCalledWith(1, -12346, 0); @@ -489,12 +549,22 @@ describe("scripts/measure-rpc-rtt.mjs", () => { }); if (process.platform === "win32") { - expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12350", "/T"], { - stdio: "ignore", - }); - expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12350", "/T", "/F"], { - stdio: "ignore", - }); + expect(runTaskkill).toHaveBeenNthCalledWith( + 1, + expectedTaskkillPath(), + ["/PID", "12350", "/T"], + { + stdio: "ignore", + }, + ); + expect(runTaskkill).toHaveBeenNthCalledWith( + 2, + expectedTaskkillPath(), + ["/PID", "12350", "/T", "/F"], + { + stdio: "ignore", + }, + ); expect(child.kill).not.toHaveBeenCalled(); } else { expect(kill).toHaveBeenCalledWith(-12350, "SIGKILL"); @@ -549,7 +619,7 @@ describe("scripts/measure-rpc-rtt.mjs", () => { processLike.emit("SIGTERM"); if (process.platform === "win32") { - expect(runTaskkill).toHaveBeenCalledWith("taskkill", ["/PID", "12348", "/T"], { + expect(runTaskkill).toHaveBeenCalledWith(expectedTaskkillPath(), ["/PID", "12348", "/T"], { stdio: "ignore", }); expect(child.kill).not.toHaveBeenCalled(); @@ -595,7 +665,7 @@ describe("scripts/measure-rpc-rtt.mjs", () => { processLike.emit("exit"); if (process.platform === "win32") { - expect(runTaskkill).toHaveBeenCalledWith("taskkill", ["/PID", "12349", "/T"], { + expect(runTaskkill).toHaveBeenCalledWith(expectedTaskkillPath(), ["/PID", "12349", "/T"], { stdio: "ignore", }); expect(child.kill).not.toHaveBeenCalled();