From 34d11d57579d05e0e7aa1ebff4bd1d1804d7d2b0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 29 Apr 2026 15:25:03 +0100 Subject: [PATCH] fix(gateway): recognize Windows gateway listeners via PowerShell --- CHANGELOG.md | 1 + src/infra/ports-inspect.ts | 19 +++++++-- src/infra/ports.test.ts | 87 +++++++++++++++++++++++++++++++++++++- 3 files changed, 103 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c656ed998eb..e63de094bcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/infra/ports-inspect.ts b/src/infra/ports-inspect.ts index 6789f99ed93..faed621d630 100644 --- a/src/infra/ports-inspect.ts +++ b/src/infra/ports-inspect.ts @@ -250,7 +250,20 @@ async function resolveWindowsImageName(pid: number): Promise } async function resolveWindowsCommandLine(pid: number): Promise { - 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 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); + }); +});