mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:50:43 +00:00
fix(gateway): recognize Windows gateway listeners via PowerShell
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user