mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-06 01:55:37 +00:00
fix(scripts): resolve taskkill from system32
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
// Runs child commands with process-group signal forwarding and Windows shell normalization.
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { constants as osConstants } from "node:os";
|
||||
import path from "node:path";
|
||||
import { buildCmdExeCommandLine } from "../windows-cmd-helpers.mjs";
|
||||
import { resolveWindowsTaskkillPath } from "./windows-taskkill.mjs";
|
||||
|
||||
const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
||||
const FORCE_KILL_DELAY_MS = 5_000;
|
||||
const DEFAULT_WINDOWS_SYSTEM_ROOT = "C:\\Windows";
|
||||
const managedChildren = new Set();
|
||||
const signalHandlers = new Map();
|
||||
|
||||
@@ -72,46 +71,6 @@ export function terminateManagedChild(
|
||||
child.kill(signal);
|
||||
}
|
||||
|
||||
function getEnvValueCaseInsensitive(env, expectedKey) {
|
||||
const direct = env[expectedKey];
|
||||
if (direct !== undefined) {
|
||||
return direct;
|
||||
}
|
||||
const expected = expectedKey.toUpperCase();
|
||||
const actualKey = Object.keys(env).find((key) => key.toUpperCase() === expected);
|
||||
return actualKey ? env[actualKey] : undefined;
|
||||
}
|
||||
|
||||
function normalizeWindowsSystemRoot(raw) {
|
||||
const trimmed = raw?.trim();
|
||||
if (
|
||||
!trimmed ||
|
||||
trimmed.includes("\0") ||
|
||||
trimmed.includes("\r") ||
|
||||
trimmed.includes("\n") ||
|
||||
trimmed.includes(";")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const normalized = path.win32.normalize(trimmed);
|
||||
if (!path.win32.isAbsolute(normalized) || normalized.startsWith("\\\\")) {
|
||||
return null;
|
||||
}
|
||||
const parsed = path.win32.parse(normalized);
|
||||
if (!/^[A-Za-z]:\\$/.test(parsed.root) || normalized.length <= parsed.root.length) {
|
||||
return null;
|
||||
}
|
||||
return normalized.replace(/[\\/]+$/, "");
|
||||
}
|
||||
|
||||
function resolveWindowsTaskkillPath(env = process.env) {
|
||||
const systemRoot =
|
||||
normalizeWindowsSystemRoot(getEnvValueCaseInsensitive(env, "SystemRoot")) ??
|
||||
normalizeWindowsSystemRoot(getEnvValueCaseInsensitive(env, "WINDIR")) ??
|
||||
DEFAULT_WINDOWS_SYSTEM_ROOT;
|
||||
return path.win32.join(systemRoot, "System32", "taskkill.exe");
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a child command while forwarding termination signals to the managed process group.
|
||||
*
|
||||
|
||||
44
scripts/lib/windows-taskkill.mjs
Normal file
44
scripts/lib/windows-taskkill.mjs
Normal file
@@ -0,0 +1,44 @@
|
||||
// Resolves the Windows taskkill binary without trusting PATH.
|
||||
import path from "node:path";
|
||||
|
||||
const DEFAULT_WINDOWS_SYSTEM_ROOT = "C:\\Windows";
|
||||
|
||||
function getEnvValueCaseInsensitive(env, expectedKey) {
|
||||
const direct = env[expectedKey];
|
||||
if (direct !== undefined) {
|
||||
return direct;
|
||||
}
|
||||
const expected = expectedKey.toUpperCase();
|
||||
const actualKey = Object.keys(env).find((key) => key.toUpperCase() === expected);
|
||||
return actualKey ? env[actualKey] : undefined;
|
||||
}
|
||||
|
||||
function normalizeWindowsSystemRoot(raw) {
|
||||
const trimmed = raw?.trim();
|
||||
if (
|
||||
!trimmed ||
|
||||
trimmed.includes("\0") ||
|
||||
trimmed.includes("\r") ||
|
||||
trimmed.includes("\n") ||
|
||||
trimmed.includes(";")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const normalized = path.win32.normalize(trimmed);
|
||||
if (!path.win32.isAbsolute(normalized) || normalized.startsWith("\\\\")) {
|
||||
return null;
|
||||
}
|
||||
const parsed = path.win32.parse(normalized);
|
||||
if (!/^[A-Za-z]:\\$/.test(parsed.root) || normalized.length <= parsed.root.length) {
|
||||
return null;
|
||||
}
|
||||
return normalized.replace(/[\\/]+$/, "");
|
||||
}
|
||||
|
||||
export function resolveWindowsTaskkillPath(env = process.env) {
|
||||
const systemRoot =
|
||||
normalizeWindowsSystemRoot(getEnvValueCaseInsensitive(env, "SystemRoot")) ??
|
||||
normalizeWindowsSystemRoot(getEnvValueCaseInsensitive(env, "WINDIR")) ??
|
||||
DEFAULT_WINDOWS_SYSTEM_ROOT;
|
||||
return path.win32.join(systemRoot, "System32", "taskkill.exe");
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Runs a command with inline KEY=value assignments while preserving signal behavior.
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { resolveWindowsTaskkillPath } from "./lib/windows-taskkill.mjs";
|
||||
|
||||
const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/u;
|
||||
const USAGE = "Usage: node scripts/run-with-env.mjs KEY=value [KEY=value ...] -- command [args...]";
|
||||
@@ -104,16 +105,17 @@ export function signalRunWithEnvChild(
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1089,6 +1089,10 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
|
||||
["scripts/lib/local-heavy-check-runtime.mjs", ["test/scripts/local-heavy-check-runtime.test.ts"]],
|
||||
["scripts/lib/kova-report-gate.mjs", ["test/scripts/kova-report-gate.test.ts"]],
|
||||
["scripts/lib/managed-child-process.mjs", ["test/scripts/managed-child-process.test.ts"]],
|
||||
[
|
||||
"scripts/lib/windows-taskkill.mjs",
|
||||
["test/scripts/managed-child-process.test.ts", "test/scripts/run-with-env.test.ts"],
|
||||
],
|
||||
[
|
||||
"scripts/lib/local-build-metadata.mjs",
|
||||
[
|
||||
|
||||
@@ -12,6 +12,29 @@ import {
|
||||
signalRunWithEnvChild,
|
||||
} from "../../scripts/run-with-env.mjs";
|
||||
|
||||
const taskkillPath = path.win32.join("C:\\Windows", "System32", "taskkill.exe");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitFor(predicate: () => boolean, label: string, timeoutMs = 3_000): Promise<void> {
|
||||
const startedAt = Date.now();
|
||||
while (!predicate()) {
|
||||
@@ -160,52 +183,56 @@ describe("run-with-env", () => {
|
||||
});
|
||||
|
||||
it("signals Windows wrapped command trees with taskkill", () => {
|
||||
const child = {
|
||||
kill: vi.fn(),
|
||||
pid: 12345,
|
||||
};
|
||||
const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 }));
|
||||
withDefaultWindowsSystemRoot(() => {
|
||||
const child = {
|
||||
kill: vi.fn(),
|
||||
pid: 12345,
|
||||
};
|
||||
const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 }));
|
||||
|
||||
signalRunWithEnvChild(child, "SIGTERM", {
|
||||
platform: "win32",
|
||||
runTaskkill,
|
||||
});
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
signalRunWithEnvChild(child, "SIGTERM", {
|
||||
platform: "win32",
|
||||
runTaskkill,
|
||||
});
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(1, taskkillPath, ["/PID", "12345", "/T"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
|
||||
signalRunWithEnvChild(child, "SIGKILL", {
|
||||
platform: "win32",
|
||||
runTaskkill,
|
||||
signalRunWithEnvChild(child, "SIGKILL", {
|
||||
platform: "win32",
|
||||
runTaskkill,
|
||||
});
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(2, taskkillPath, ["/PID", "12345", "/T", "/F"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
expect(child.kill).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
expect(child.kill).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("force-kills Windows wrapped command trees when graceful taskkill fails", () => {
|
||||
const child = {
|
||||
kill: vi.fn(),
|
||||
pid: 12345,
|
||||
};
|
||||
const runTaskkill = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce({ error: undefined, status: 1 })
|
||||
.mockReturnValueOnce({ error: undefined, status: 0 });
|
||||
withDefaultWindowsSystemRoot(() => {
|
||||
const child = {
|
||||
kill: vi.fn(),
|
||||
pid: 12345,
|
||||
};
|
||||
const runTaskkill = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce({ error: undefined, status: 1 })
|
||||
.mockReturnValueOnce({ error: undefined, status: 0 });
|
||||
|
||||
signalRunWithEnvChild(child, "SIGTERM", {
|
||||
platform: "win32",
|
||||
runTaskkill,
|
||||
});
|
||||
signalRunWithEnvChild(child, "SIGTERM", {
|
||||
platform: "win32",
|
||||
runTaskkill,
|
||||
});
|
||||
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], {
|
||||
stdio: "ignore",
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(1, taskkillPath, ["/PID", "12345", "/T"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(2, taskkillPath, ["/PID", "12345", "/T", "/F"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
expect(child.kill).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
expect(child.kill).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32").each(["SIGTERM", "SIGHUP", "SIGINT"] as const)(
|
||||
|
||||
Reference in New Issue
Block a user