diff --git a/.secrets.baseline b/.secrets.baseline index efc80e24554..3123a9ec76a 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -11317,7 +11317,7 @@ "filename": "extensions/voice-call/src/config.test.ts", "hashed_secret": "62207a469ec2fdcfc7d66b04c2980ac1501acbf0", "is_verified": false, - "line_number": 39 + "line_number": 44 } ], "extensions/voice-call/src/providers/telnyx.test.ts": [ @@ -13011,5 +13011,5 @@ } ] }, - "generated_at": "2026-03-08T02:36:42Z" + "generated_at": "2026-03-08T03:31:44Z" } diff --git a/extensions/acpx/src/runtime.test.ts b/extensions/acpx/src/runtime.test.ts index 4c975bd1d51..53fc3c1f8a3 100644 --- a/extensions/acpx/src/runtime.test.ts +++ b/extensions/acpx/src/runtime.test.ts @@ -21,6 +21,7 @@ beforeAll(async () => { allowPluginLocalInstall: false, installCommand: "n/a", cwd: process.cwd(), + mcpServers: {}, permissionMode: "approve-reads", nonInteractivePermissions: "fail", strictWindowsCmdWrapper: true, diff --git a/src/cli/daemon-cli/lifecycle.test.ts b/src/cli/daemon-cli/lifecycle.test.ts index eee0513a024..3f0ed6d531c 100644 --- a/src/cli/daemon-cli/lifecycle.test.ts +++ b/src/cli/daemon-cli/lifecycle.test.ts @@ -223,8 +223,16 @@ describe("runDaemonRestart health checks", () => { }); it("signals an unmanaged gateway process on stop", async () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const killSpy = vi.spyOn(process, "kill").mockImplementation(() => true); findGatewayPidsOnPortSync.mockReturnValue([4200, 4200, 4300]); + mockSpawnSync.mockReturnValue({ + error: null, + status: 0, + stdout: + 'CommandLine="C:\\\\Program Files\\\\OpenClaw\\\\openclaw.exe" gateway --port 18789\r\n', + stderr: "", + }); runServiceStop.mockImplementation(async (params: { onNotLoaded?: () => Promise }) => { await params.onNotLoaded?.(); }); @@ -237,8 +245,16 @@ describe("runDaemonRestart health checks", () => { }); it("signals a single unmanaged gateway process on restart", async () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const killSpy = vi.spyOn(process, "kill").mockImplementation(() => true); findGatewayPidsOnPortSync.mockReturnValue([4200]); + mockSpawnSync.mockReturnValue({ + error: null, + status: 0, + stdout: + 'CommandLine="C:\\\\Program Files\\\\OpenClaw\\\\openclaw.exe" gateway --port 18789\r\n', + stderr: "", + }); runServiceRestart.mockImplementation( async (params: RestartParams & { onNotLoaded?: () => Promise }) => { await params.onNotLoaded?.(); @@ -266,7 +282,15 @@ describe("runDaemonRestart health checks", () => { }); it("fails unmanaged restart when multiple gateway listeners are present", async () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); findGatewayPidsOnPortSync.mockReturnValue([4200, 4300]); + mockSpawnSync.mockReturnValue({ + error: null, + status: 0, + stdout: + 'CommandLine="C:\\\\Program Files\\\\OpenClaw\\\\openclaw.exe" gateway --port 18789\r\n', + stderr: "", + }); runServiceRestart.mockImplementation( async (params: RestartParams & { onNotLoaded?: () => Promise }) => { await params.onNotLoaded?.(); diff --git a/src/cli/daemon-cli/lifecycle.ts b/src/cli/daemon-cli/lifecycle.ts index 1ca321b0d16..cd54df8035a 100644 --- a/src/cli/daemon-cli/lifecycle.ts +++ b/src/cli/daemon-cli/lifecycle.ts @@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process"; import fsSync from "node:fs"; import { isRestartEnabled } from "../../config/commands.js"; import { readBestEffortConfig, resolveGatewayPort } from "../../config/config.js"; +import { parseCmdScriptCommandLine } from "../../daemon/cmd-argv.js"; import { resolveGatewayService } from "../../daemon/service.js"; import { probeGateway } from "../../gateway/probe.js"; import { findGatewayPidsOnPortSync } from "../../infra/restart.js"; @@ -52,6 +53,25 @@ function parseProcCmdline(raw: string): string[] { .filter(Boolean); } +function extractWindowsCommandLine(raw: string): string | null { + const lines = raw + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + for (const line of lines) { + if (!line.toLowerCase().startsWith("commandline=")) { + continue; + } + const value = line.slice("commandline=".length).trim(); + return value || null; + } + return lines.find((line) => line.toLowerCase() !== "commandline") ?? null; +} + +function stripExecutableExtension(value: string): string { + return value.replace(/\.(bat|cmd|exe)$/i, ""); +} + function isGatewayArgv(args: string[]): boolean { const normalized = args.map(normalizeProcArg); if (!normalized.includes("gateway")) { @@ -69,7 +89,7 @@ function isGatewayArgv(args: string[]): boolean { return true; } - const exe = normalized[0] ?? ""; + const exe = stripExecutableExtension(normalized[0] ?? ""); return exe.endsWith("/openclaw") || exe === "openclaw" || exe.endsWith("/openclaw-gateway"); } @@ -92,6 +112,21 @@ function readGatewayProcessArgsSync(pid: number): string[] | null { const command = ps.stdout.trim(); return command ? command.split(/\s+/) : null; } + if (process.platform === "win32") { + const wmic = spawnSync( + "wmic", + ["process", "where", `ProcessId=${pid}`, "get", "CommandLine", "/value"], + { + encoding: "utf8", + timeout: 1000, + }, + ); + if (wmic.error || wmic.status !== 0) { + return null; + } + const command = extractWindowsCommandLine(wmic.stdout); + return command ? parseCmdScriptCommandLine(command) : null; + } return null; }