mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(windows): resolve process inspection tools
This commit is contained in:
+4
-1
@@ -4,6 +4,7 @@ import { createServer } from "node:net";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { resolveLsofCommandSync } from "../infra/ports-lsof.js";
|
||||
import { tryListenOnPort } from "../infra/ports-probe.js";
|
||||
import { getWindowsSystem32ExePath } from "../infra/windows-install-roots.js";
|
||||
import { resolvePositiveTimerTimeoutMs, resolveTimerTimeoutMs } from "../shared/number-coercion.js";
|
||||
import { sleep } from "../utils.js";
|
||||
|
||||
@@ -165,7 +166,9 @@ export function parseLsofOutput(output: string): PortProcess[] {
|
||||
export function listPortListeners(port: number): PortProcess[] {
|
||||
if (process.platform === "win32") {
|
||||
try {
|
||||
const out = execFileSync("netstat", ["-ano", "-p", "TCP"], { encoding: "utf-8" });
|
||||
const out = execFileSync(getWindowsSystem32ExePath("netstat.exe"), ["-ano", "-p", "TCP"], {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
const lines = out.split(/\r?\n/).filter(Boolean);
|
||||
const results: PortProcess[] = [];
|
||||
for (const line of lines) {
|
||||
|
||||
@@ -16,6 +16,7 @@ vi.mock("../infra/ports-probe.js", () => ({
|
||||
}));
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { getWindowsSystem32ExePath } from "../infra/windows-install-roots.js";
|
||||
import {
|
||||
forceFreePort,
|
||||
forceFreePortAndWait,
|
||||
@@ -301,6 +302,11 @@ describe("gateway --force helpers (Windows netstat path)", () => {
|
||||
it("parses PIDs from netstat output correctly", () => {
|
||||
(execFileSync as unknown as Mock).mockReturnValue(makeNetstatOutput(18789, 42, 99));
|
||||
expect(listPortListeners(18789)).toEqual<PortProcess[]>([{ pid: 42 }, { pid: 99 }]);
|
||||
expect(execFileSync).toHaveBeenCalledWith(
|
||||
getWindowsSystem32ExePath("netstat.exe"),
|
||||
["-ano", "-p", "TCP"],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
});
|
||||
|
||||
it("does not incorrectly match a port that is a substring (e.g. 80 vs 8080)", () => {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// Covers gateway process discovery across platform process listings.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mockProcessPlatform } from "../test-utils/vitest-spies.js";
|
||||
import {
|
||||
getWindowsPowerShellExePath,
|
||||
getWindowsSystem32ExePath,
|
||||
getWindowsWmicExePath,
|
||||
} from "./windows-install-roots.js";
|
||||
|
||||
const spawnSyncMock = vi.hoisted(() => vi.fn());
|
||||
const readFileSyncMock = vi.hoisted(() => vi.fn());
|
||||
@@ -130,6 +135,8 @@ describe("gateway-processes", () => {
|
||||
parseCmdScriptCommandLineMock.mockReturnValue(["node.exe", "gateway", "run"]);
|
||||
|
||||
expect(readGatewayProcessArgsSync(77)).toEqual(["node.exe", "gateway", "run"]);
|
||||
expect(spawnSyncMock.mock.calls[0]?.[0]).toBe(getWindowsPowerShellExePath());
|
||||
expect(spawnSyncMock.mock.calls[1]?.[0]).toBe(getWindowsWmicExePath());
|
||||
expect(parseCmdScriptCommandLineMock).toHaveBeenCalledWith("node.exe gateway run");
|
||||
});
|
||||
|
||||
@@ -177,6 +184,36 @@ describe("gateway-processes", () => {
|
||||
expect(findVerifiedGatewayListenerPidsOnPortSync(18789)).toEqual([200]);
|
||||
});
|
||||
|
||||
it("falls back from powershell to trusted netstat for windows listener pids", () => {
|
||||
setPlatform("win32");
|
||||
spawnSyncMock
|
||||
.mockReturnValueOnce({
|
||||
error: new Error("powershell missing"),
|
||||
status: null,
|
||||
stdout: "",
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
error: null,
|
||||
status: 0,
|
||||
stdout: [
|
||||
"Proto Local Address Foreign Address State PID",
|
||||
"TCP 0.0.0.0:18789 0.0.0.0:0 LISTENING 200",
|
||||
].join("\r\n"),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
error: null,
|
||||
status: 0,
|
||||
stdout: "node.exe gateway run",
|
||||
});
|
||||
parseCmdScriptCommandLineMock.mockReturnValue(["node.exe", "gateway", "run"]);
|
||||
isGatewayArgvMock.mockReturnValue(true);
|
||||
|
||||
expect(findVerifiedGatewayListenerPidsOnPortSync(18789)).toEqual([200]);
|
||||
expect(spawnSyncMock.mock.calls[0]?.[0]).toBe(getWindowsPowerShellExePath());
|
||||
expect(spawnSyncMock.mock.calls[1]?.[0]).toBe(getWindowsSystem32ExePath("netstat.exe"));
|
||||
expect(spawnSyncMock.mock.calls[2]?.[0]).toBe(getWindowsPowerShellExePath());
|
||||
});
|
||||
|
||||
it("formats pid lists as comma-separated output", () => {
|
||||
expect(formatGatewayPidList([1, 2, 3])).toBe("1, 2, 3");
|
||||
});
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
resetWindowsInstallRootsForTests,
|
||||
getWindowsCmdExePath,
|
||||
getWindowsInstallRoots,
|
||||
getWindowsPowerShellExePath,
|
||||
getWindowsProgramFilesRoots,
|
||||
getWindowsSystem32ExePath,
|
||||
getWindowsWmicExePath,
|
||||
normalizeWindowsInstallRoot,
|
||||
} from "./windows-install-roots.js";
|
||||
|
||||
@@ -172,12 +175,33 @@ describe("getWindowsProgramFilesRoots", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWindowsCmdExePath", () => {
|
||||
describe("Windows system executable helpers", () => {
|
||||
it("resolves cmd.exe from the trusted Windows system root", () => {
|
||||
expect(getWindowsCmdExePath({ SystemRoot: "D:\\Windows" })).toBe(
|
||||
"D:\\Windows\\System32\\cmd.exe",
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves trusted Windows process-inspection tools", () => {
|
||||
const env = { SystemRoot: "D:\\Windows" };
|
||||
|
||||
expect(getWindowsSystem32ExePath("netstat.exe", env)).toBe(
|
||||
"D:\\Windows\\System32\\netstat.exe",
|
||||
);
|
||||
expect(getWindowsPowerShellExePath(env)).toBe(
|
||||
"D:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
|
||||
);
|
||||
expect(getWindowsWmicExePath(env)).toBe("D:\\Windows\\System32\\wbem\\wmic.exe");
|
||||
});
|
||||
|
||||
it("rejects unsafe System32 executable names", () => {
|
||||
expect(() => getWindowsSystem32ExePath("..\\netstat.exe")).toThrow(
|
||||
/Invalid Windows System32 executable name/u,
|
||||
);
|
||||
expect(() => getWindowsSystem32ExePath("netstat")).toThrow(
|
||||
/Invalid Windows System32 executable name/u,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("locateWindowsRegExe", () => {
|
||||
|
||||
@@ -237,7 +237,38 @@ export function getWindowsProgramFilesRoots(
|
||||
export function getWindowsCmdExePath(
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
): string {
|
||||
return path.win32.join(getWindowsInstallRoots(env).systemRoot, "System32", "cmd.exe");
|
||||
return getWindowsSystem32ExePath("cmd.exe", env);
|
||||
}
|
||||
|
||||
export function getWindowsSystem32ExePath(
|
||||
executableName: string,
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
): string {
|
||||
if (
|
||||
path.win32.basename(executableName) !== executableName ||
|
||||
!/^[A-Za-z0-9_.-]+\.exe$/u.test(executableName)
|
||||
) {
|
||||
throw new Error(`Invalid Windows System32 executable name: ${executableName}`);
|
||||
}
|
||||
return path.win32.join(getWindowsInstallRoots(env).systemRoot, "System32", executableName);
|
||||
}
|
||||
|
||||
export function getWindowsPowerShellExePath(
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
): string {
|
||||
return path.win32.join(
|
||||
getWindowsInstallRoots(env).systemRoot,
|
||||
"System32",
|
||||
"WindowsPowerShell",
|
||||
"v1.0",
|
||||
"powershell.exe",
|
||||
);
|
||||
}
|
||||
|
||||
export function getWindowsWmicExePath(
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
): string {
|
||||
return path.win32.join(getWindowsInstallRoots(env).systemRoot, "System32", "wbem", "wmic.exe");
|
||||
}
|
||||
|
||||
export function resetWindowsInstallRootsForTests(
|
||||
|
||||
@@ -4,6 +4,11 @@ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/st
|
||||
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
|
||||
import { parseCmdScriptCommandLine } from "../daemon/cmd-argv.js";
|
||||
import { parseStrictPositiveInteger } from "./parse-finite-number.js";
|
||||
import {
|
||||
getWindowsPowerShellExePath,
|
||||
getWindowsSystem32ExePath,
|
||||
getWindowsWmicExePath,
|
||||
} from "./windows-install-roots.js";
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 5_000;
|
||||
|
||||
@@ -21,7 +26,7 @@ export type WindowsProcessArgsResult =
|
||||
|
||||
function readListeningPidsViaPowerShell(port: number, timeoutMs: number): number[] | null {
|
||||
const ps = spawnSync(
|
||||
"powershell",
|
||||
getWindowsPowerShellExePath(),
|
||||
[
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
@@ -71,7 +76,7 @@ export function readWindowsListeningPidsResultSync(
|
||||
if (powershellPids != null) {
|
||||
return { ok: true, pids: powershellPids };
|
||||
}
|
||||
const netstat = spawnSync("netstat", ["-ano", "-p", "tcp"], {
|
||||
const netstat = spawnSync(getWindowsSystem32ExePath("netstat.exe"), ["-ano", "-p", "tcp"], {
|
||||
encoding: "utf8",
|
||||
timeout: timeoutMs,
|
||||
windowsHide: true,
|
||||
@@ -115,7 +120,7 @@ export function readWindowsProcessArgsResultSync(
|
||||
timeoutMs = DEFAULT_TIMEOUT_MS,
|
||||
): WindowsProcessArgsResult {
|
||||
const powershell = spawnSync(
|
||||
"powershell",
|
||||
getWindowsPowerShellExePath(),
|
||||
[
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
@@ -132,7 +137,7 @@ export function readWindowsProcessArgsResultSync(
|
||||
return { ok: true, args: command ? parseCmdScriptCommandLine(command) : null };
|
||||
}
|
||||
const wmic = spawnSync(
|
||||
"wmic",
|
||||
getWindowsWmicExePath(),
|
||||
["process", "where", `ProcessId=${pid}`, "get", "CommandLine", "/value"],
|
||||
{
|
||||
encoding: "utf8",
|
||||
|
||||
Reference in New Issue
Block a user