fix(rpc): resolve taskkill from system32 in script probes

This commit is contained in:
Vincent Koc
2026-06-21 09:10:48 +02:00
parent fc8542b377
commit 0a7b009647
4 changed files with 212 additions and 93 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
});

View File

@@ -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();