mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(windows): resolve cmd handoff path
This commit is contained in:
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { getWindowsCmdExePath } from "../../infra/windows-install-roots.js";
|
||||
import { prepareRestartScript, runRestartScript } from "./restart-helper.js";
|
||||
|
||||
vi.mock("node:child_process", async () => {
|
||||
@@ -616,7 +617,7 @@ exit 0
|
||||
|
||||
await runRestartScript(scriptPath);
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith("cmd.exe", ["/d", "/s", "/c", scriptPath], {
|
||||
expect(spawn).toHaveBeenCalledWith(getWindowsCmdExePath(), ["/d", "/s", "/c", scriptPath], {
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
windowsHide: true,
|
||||
@@ -633,11 +634,15 @@ exit 0
|
||||
|
||||
await runRestartScript(scriptPath);
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith("cmd.exe", ["/d", "/s", "/c", `"${scriptPath}"`], {
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
windowsHide: true,
|
||||
});
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
getWindowsCmdExePath(),
|
||||
["/d", "/s", "/c", `"${scriptPath}"`],
|
||||
{
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
windowsHide: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("does not throw when spawn fails synchronously", async () => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
resolveGatewayRestartLogPath,
|
||||
shellEscapeRestartLogValue,
|
||||
} from "../../daemon/restart-logs.js";
|
||||
import { getWindowsCmdExePath } from "../../infra/windows-install-roots.js";
|
||||
|
||||
/**
|
||||
* Shell-escape a string for embedding in single-quoted shell arguments.
|
||||
@@ -404,7 +405,7 @@ exit $status
|
||||
*/
|
||||
export async function runRestartScript(scriptPath: string): Promise<void> {
|
||||
const isWindows = process.platform === "win32";
|
||||
const file = isWindows ? "cmd.exe" : "/bin/sh";
|
||||
const file = isWindows ? getWindowsCmdExePath() : "/bin/sh";
|
||||
const args = isWindows ? ["/d", "/s", "/c", quoteCmdScriptArg(scriptPath)] : [scriptPath];
|
||||
|
||||
try {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { parseStrictInteger, parseStrictPositiveInteger } from "../infra/parse-f
|
||||
import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
|
||||
import { cleanStaleGatewayProcessesSync } from "../infra/restart-stale-pids.js";
|
||||
import { parseTcpPort } from "../infra/tcp-port.js";
|
||||
import { getWindowsCmdExePath } from "../infra/windows-install-roots.js";
|
||||
import {
|
||||
GATEWAY_LAUNCH_AGENT_LABEL,
|
||||
GATEWAY_SERVICE_KIND,
|
||||
@@ -287,7 +288,7 @@ async function execLaunchctl(
|
||||
args: string[],
|
||||
): Promise<{ stdout: string; stderr: string; code: number }> {
|
||||
const isWindows = process.platform === "win32";
|
||||
const file = isWindows ? (process.env.ComSpec ?? "cmd.exe") : "launchctl";
|
||||
const file = isWindows ? getWindowsCmdExePath() : "launchctl";
|
||||
const fileArgs = isWindows ? ["/d", "/s", "/c", "launchctl", ...args] : args;
|
||||
return await execFileUtf8(file, fileArgs, isWindows ? { windowsHide: true } : {});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getWindowsCmdExePath } from "../infra/windows-install-roots.js";
|
||||
import "./test-helpers/schtasks-base-mocks.js";
|
||||
import {
|
||||
inspectPortUsage,
|
||||
@@ -292,7 +293,7 @@ describe("Windows startup fallback", () => {
|
||||
const startupEntryPath = resolveStartupEntryPath(env);
|
||||
const startupScript = await fs.readFile(startupEntryPath, "utf8");
|
||||
expect(result.scriptPath).toBe(resolveTaskScriptPath(env));
|
||||
expect(startupScript).toContain('start "" /min cmd.exe /d /c');
|
||||
expect(startupScript).toContain(`start "" /min ${getWindowsCmdExePath()} /d /c`);
|
||||
expect(startupScript).toContain("gateway.cmd");
|
||||
expectStartupFallbackSpawn();
|
||||
expect(childUnref).toHaveBeenCalled();
|
||||
|
||||
@@ -9,7 +9,7 @@ import { isGatewayArgv } from "../infra/gateway-process-argv.js";
|
||||
import { findVerifiedGatewayListenerPidsOnPortSync } from "../infra/gateway-processes.js";
|
||||
import { inspectPortUsage } from "../infra/ports.js";
|
||||
import { parseTcpPort } from "../infra/tcp-port.js";
|
||||
import { getWindowsInstallRoots } from "../infra/windows-install-roots.js";
|
||||
import { getWindowsCmdExePath, getWindowsInstallRoots } from "../infra/windows-install-roots.js";
|
||||
import { killProcessTree } from "../process/kill-tree.js";
|
||||
import { sleep } from "../utils.js";
|
||||
import { parseCmdScriptCommandLine, quoteCmdScriptArg } from "./cmd-argv.js";
|
||||
@@ -414,7 +414,8 @@ function buildTaskScript({
|
||||
}
|
||||
|
||||
function renderStartupLaunchCommand(scriptPath: string): string {
|
||||
return `start "" /min cmd.exe /d /c ${quoteCmdScriptArg(scriptPath)}`;
|
||||
const cmdExePath = quoteCmdScriptArg(getWindowsCmdExePath());
|
||||
return `start "" /min ${cmdExePath} /d /c ${quoteCmdScriptArg(scriptPath)}`;
|
||||
}
|
||||
|
||||
function buildStartupLauncherScript(params: { description?: string; scriptPath: string }): string {
|
||||
@@ -497,7 +498,7 @@ async function launchFallbackTaskScript(env: GatewayServiceEnv): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const child = spawn("cmd.exe", ["/d", "/c", scriptPath], {
|
||||
const child = spawn(getWindowsCmdExePath(), ["/d", "/c", scriptPath], {
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
windowsHide: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Detects and decodes Windows console output encodings.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
import { getWindowsCmdExePath } from "./windows-install-roots.js";
|
||||
|
||||
const WINDOWS_CODEPAGE_ENCODING_MAP: Record<number, string> = {
|
||||
65001: "utf-8",
|
||||
@@ -49,7 +50,7 @@ export function resolveWindowsConsoleEncoding(): string | null {
|
||||
return cachedWindowsConsoleEncoding;
|
||||
}
|
||||
try {
|
||||
const result = spawnSync("cmd.exe", ["/d", "/s", "/c", "chcp"], {
|
||||
const result = spawnSync(getWindowsCmdExePath(), ["/d", "/s", "/c", "chcp"], {
|
||||
windowsHide: true,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
|
||||
@@ -3,6 +3,7 @@ import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
privateTestApi,
|
||||
resetWindowsInstallRootsForTests,
|
||||
getWindowsCmdExePath,
|
||||
getWindowsInstallRoots,
|
||||
getWindowsProgramFilesRoots,
|
||||
normalizeWindowsInstallRoot,
|
||||
@@ -171,6 +172,14 @@ describe("getWindowsProgramFilesRoots", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWindowsCmdExePath", () => {
|
||||
it("resolves cmd.exe from the trusted Windows system root", () => {
|
||||
expect(getWindowsCmdExePath({ SystemRoot: "D:\\Windows" })).toBe(
|
||||
"D:\\Windows\\System32\\cmd.exe",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("locateWindowsRegExe", () => {
|
||||
it("uses the fixed Windows system reg.exe candidate", () => {
|
||||
expect(privateTestApi.getWindowsRegExeCandidates()).toEqual(["C:\\Windows\\System32\\reg.exe"]);
|
||||
|
||||
@@ -234,6 +234,12 @@ export function getWindowsProgramFilesRoots(
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getWindowsCmdExePath(
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
): string {
|
||||
return path.win32.join(getWindowsInstallRoots(env).systemRoot, "System32", "cmd.exe");
|
||||
}
|
||||
|
||||
export function resetWindowsInstallRootsForTests(
|
||||
overrides: WindowsInstallRootsTestOverrides = {},
|
||||
): void {
|
||||
|
||||
@@ -4,6 +4,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { captureFullEnv } from "../test-utils/env.js";
|
||||
import { getWindowsCmdExePath } from "./windows-install-roots.js";
|
||||
|
||||
const spawnMock = vi.hoisted(() => vi.fn());
|
||||
const resolvePreferredOpenClawTmpDirMock = vi.hoisted(() => vi.fn(() => os.tmpdir()));
|
||||
@@ -100,13 +101,14 @@ describe("relaunchGatewayScheduledTask", () => {
|
||||
});
|
||||
|
||||
const result = relaunchGatewayScheduledTask({ OPENCLAW_PROFILE: "work" });
|
||||
const cmdExePath = getWindowsCmdExePath();
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.method).toBe("schtasks");
|
||||
expect(result.tried).toContain('schtasks /Run /TN "OpenClaw Gateway (work)"');
|
||||
expect(result.tried).toContain(`cmd.exe /d /s /c ${seenCommandArg}`);
|
||||
expect(result.tried).toContain(`${cmdExePath} /d /s /c ${seenCommandArg}`);
|
||||
const spawnCall = requireFirstMockCall(spawnMock, "restart helper spawn");
|
||||
expect(spawnCall[0]).toBe("cmd.exe");
|
||||
expect(spawnCall[0]).toBe(cmdExePath);
|
||||
expect(spawnCall[1]).toStrictEqual(["/d", "/s", "/c", seenCommandArg]);
|
||||
expect(spawnCall[2]).toStrictEqual({
|
||||
detached: true,
|
||||
@@ -200,7 +202,7 @@ describe("relaunchGatewayScheduledTask", () => {
|
||||
if (typeof commandArg !== "string") {
|
||||
throw new Error("expected quoted restart helper path");
|
||||
}
|
||||
expect(spawnCall[0]).toBe("cmd.exe");
|
||||
expect(spawnCall[0]).toBe(getWindowsCmdExePath());
|
||||
expect(commandArgs).toStrictEqual(["/d", "/s", "/c", commandArg]);
|
||||
expect(commandArg.startsWith('"')).toBe(true);
|
||||
expect(commandArg.endsWith('"')).toBe(true);
|
||||
@@ -231,7 +233,7 @@ describe("relaunchGatewayScheduledTask", () => {
|
||||
const script = fs.readFileSync(scriptPath, "utf8");
|
||||
expect(script).toContain(`schtasks /Query /TN`);
|
||||
expect(script).toContain(":fallback");
|
||||
expect(script).toContain(`start "" /min cmd.exe /d /c`);
|
||||
expect(script).toContain(`start "" /min ${getWindowsCmdExePath()} /d /c`);
|
||||
expect(script).toContain(taskScriptPath);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { resolveTaskScriptPath } from "../daemon/schtasks.js";
|
||||
import { formatErrorMessage } from "./errors.js";
|
||||
import type { RestartAttempt } from "./restart.types.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir.js";
|
||||
import { getWindowsCmdExePath } from "./windows-install-roots.js";
|
||||
|
||||
const TASK_RESTART_RETRY_LIMIT = 12;
|
||||
const TASK_RESTART_RETRY_DELAY_SEC = 1;
|
||||
@@ -61,7 +62,12 @@ function buildScheduledTaskRestartScript(params: {
|
||||
];
|
||||
if (taskScriptPath) {
|
||||
const quotedScript = quoteCmdScriptArg(taskScriptPath);
|
||||
lines.push(`if exist ${quotedScript} (`, ` start "" /min cmd.exe /d /c ${quotedScript}`, ")");
|
||||
const quotedCmd = quoteCmdScriptArg(getWindowsCmdExePath());
|
||||
lines.push(
|
||||
`if exist ${quotedScript} (`,
|
||||
` start "" /min ${quotedCmd} /d /c ${quotedScript}`,
|
||||
")",
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
":cleanup",
|
||||
@@ -91,7 +97,8 @@ export function relaunchGatewayScheduledTask(env: NodeJS.ProcessEnv = process.en
|
||||
})}\r\n`,
|
||||
"utf8",
|
||||
);
|
||||
const child = spawn("cmd.exe", ["/d", "/s", "/c", quotedScriptPath], {
|
||||
const cmdExePath = getWindowsCmdExePath();
|
||||
const child = spawn(cmdExePath, ["/d", "/s", "/c", quotedScriptPath], {
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
windowsHide: true,
|
||||
@@ -100,7 +107,7 @@ export function relaunchGatewayScheduledTask(env: NodeJS.ProcessEnv = process.en
|
||||
return {
|
||||
ok: true,
|
||||
method: "schtasks",
|
||||
tried: [`schtasks /Run /TN "${taskName}"`, `cmd.exe /d /s /c ${quotedScriptPath}`],
|
||||
tried: [`schtasks /Run /TN "${taskName}"`, `${cmdExePath} /d /s /c ${quotedScriptPath}`],
|
||||
};
|
||||
} catch (err) {
|
||||
try {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
import { getWindowsInstallRoots } from "../infra/windows-install-roots.js";
|
||||
import { getWindowsCmdExePath } from "../infra/windows-install-roots.js";
|
||||
|
||||
const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>%\r\n]/;
|
||||
|
||||
@@ -41,7 +41,7 @@ export function resolveTrustedWindowsCmdExe(platform: NodeJS.Platform = process.
|
||||
if (platform !== "win32") {
|
||||
return "cmd.exe";
|
||||
}
|
||||
return path.win32.join(getWindowsInstallRoots().systemRoot, "System32", "cmd.exe");
|
||||
return getWindowsCmdExePath();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user