fix(gateway): recognize Windows gateway listeners via PowerShell

This commit is contained in:
Peter Steinberger
2026-04-29 15:25:03 +01:00
parent 4bd6dd77ef
commit 34d11d5757
3 changed files with 103 additions and 4 deletions

View File

@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
- CLI/status: keep default text `openclaw status --usage` on metadata-only channel scans unless `--deep` or `--all` is set, and send stray `openclaw tools --help` through the precomputed root-help fast path so latency-triage commands avoid plugin/runtime cold loads before printing. Refs #73477 and #74220. Thanks @oromeis and @NianJiuZst.
- Agents/diagnostics: trace embedded-run startup and preparation stage timings before model I/O, and warn only on severe slow stages, so Docker/VPS latency reports can identify whether plugin loading, auth/model resolution, tool inventory, bootstrap, MCP/LSP, resource loading, or stream setup is dominating pre-run latency without noisy normal logs. Refs #73428. Thanks @Dimaoggg, @quangtran88, and @Heyvhuang.
- Gateway/clients: wait for the event loop to become responsive before opening Gateway WebSocket RPC/probe/client connections while charging that readiness wait to caller timeouts, so Windows deferred module-evaluation stalls no longer turn healthy loopback gateways into false handshake timeouts across status, TUI, ACP, MCP, node-host, and plugin client paths. Refs #74279 and #48270. Thanks @wongcode and @joost-heijden.
- Gateway/Windows: read listener command lines via PowerShell before falling back to `wmic`, so restart health can recognize OpenClaw listeners on modern Windows installs and avoid long anonymous-port waits. Refs #74280. Thanks @zym951223.
- Plugins/runtime-deps: memoize packaged bundled runtime dist-mirror preparation after the first successful pass while keeping source-checkout mirrors refreshable, so constrained Docker/VPS installs avoid repeated root scans before chat turns. Refs #73428, #73421, #73532, and #73477. Thanks @Dimaoggg, @oromeis, @oadiazp, @jmfraga, @bstanbury, @antoniusfelix, and @jkobject.
- Channels/Discord: treat bare numeric outbound targets that match the effective Discord DM allowlist as user DMs while preserving account-specific legacy `dm.allowFrom` precedence over inherited root `allowFrom`. (#74303) Thanks @Squirbie.
- Control UI: make the chat sidebar split divider focusable, keyboard-resizable, ARIA-described, and pointer-event based so sidebar resizing works without a mouse. Thanks @BunsDev.

View File

@@ -250,7 +250,20 @@ async function resolveWindowsImageName(pid: number): Promise<string | undefined>
}
async function resolveWindowsCommandLine(pid: number): Promise<string | undefined> {
const res = await runCommandSafe([
const powershell = await runCommandSafe([
"powershell",
"-NoProfile",
"-Command",
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}" | Select-Object -ExpandProperty CommandLine)`,
]);
if (powershell.code === 0) {
const value = powershell.stdout.trim();
if (value) {
return value;
}
}
const wmic = await runCommandSafe([
"wmic",
"process",
"where",
@@ -259,10 +272,10 @@ async function resolveWindowsCommandLine(pid: number): Promise<string | undefine
"CommandLine",
"/value",
]);
if (res.code !== 0) {
if (wmic.code !== 0) {
return undefined;
}
for (const rawLine of res.stdout.split(/\r?\n/)) {
for (const rawLine of wmic.stdout.split(/\r?\n/)) {
const line = rawLine.trim();
if (!normalizeLowercaseStringOrEmpty(line).startsWith("commandline=")) {
continue;

View File

@@ -1,5 +1,5 @@
import net from "node:net";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { stripAnsi } from "../terminal/ansi.js";
const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn());
@@ -14,6 +14,14 @@ let handlePortError: typeof import("./ports.js").handlePortError;
let PortInUseError: typeof import("./ports.js").PortInUseError;
const describeUnix = process.platform === "win32" ? describe.skip : describe;
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
function setPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, "platform", {
value: platform,
configurable: true,
});
}
async function listenServer(
server: net.Server,
@@ -53,6 +61,12 @@ beforeEach(() => {
runCommandWithTimeoutMock.mockReset();
});
afterEach(() => {
if (originalPlatformDescriptor) {
Object.defineProperty(process, "platform", originalPlatformDescriptor);
}
});
describe("ports helpers", () => {
it("ensurePortAvailable rejects when port busy", async () => {
const server = net.createServer();
@@ -183,3 +197,74 @@ describeUnix("inspectPortUsage", () => {
}
});
});
describe("inspectPortUsage on Windows", () => {
it("uses PowerShell process command lines to classify OpenClaw listeners", async () => {
setPlatform("win32");
runCommandWithTimeoutMock.mockImplementation(async (argv: string[]) => {
const [command] = argv;
if (command === "netstat") {
return {
stdout: " TCP 127.0.0.1:18789 0.0.0.0:0 LISTENING 4242\r\n",
stderr: "",
code: 0,
};
}
if (command === "tasklist") {
return { stdout: "Image Name: node.exe\r\n", stderr: "", code: 0 };
}
if (command === "powershell") {
return {
stdout:
'"C:\\Program Files\\nodejs\\node.exe" C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\openclaw\\dist\\index.js gateway run\r\n',
stderr: "",
code: 0,
};
}
return { stdout: "", stderr: "", code: 1 };
});
const result = await inspectPortUsage(18789);
expect(result.status).toBe("busy");
expect(result.listeners).toHaveLength(1);
expect(result.listeners[0]?.command).toBe("node.exe");
expect(result.listeners[0]?.commandLine).toContain("openclaw");
expect(result.hints.some((hint) => hint.includes("Gateway already running locally"))).toBe(
true,
);
});
it("falls back to wmic when PowerShell cannot read the command line", async () => {
setPlatform("win32");
runCommandWithTimeoutMock.mockImplementation(async (argv: string[]) => {
const [command] = argv;
if (command === "netstat") {
return {
stdout: " TCP 127.0.0.1:18789 0.0.0.0:0 LISTENING 4242\r\n",
stderr: "",
code: 0,
};
}
if (command === "tasklist") {
return { stdout: "Image Name: node.exe\r\n", stderr: "", code: 0 };
}
if (command === "powershell") {
return { stdout: "", stderr: "access denied", code: 1 };
}
if (command === "wmic") {
return {
stdout: "CommandLine=node.exe C:\\openclaw\\dist\\index.js gateway run\r\n",
stderr: "",
code: 0,
};
}
return { stdout: "", stderr: "", code: 1 };
});
const result = await inspectPortUsage(18789);
expect(result.listeners[0]?.commandLine).toContain("openclaw");
expect(runCommandWithTimeoutMock.mock.calls.some(([argv]) => argv[0] === "wmic")).toBe(true);
});
});