fix: launch Windows startup gateway directly

This commit is contained in:
Peter Steinberger
2026-04-21 08:02:58 +01:00
parent c197b3fef4
commit fccb2b8ace
4 changed files with 57 additions and 25 deletions

View File

@@ -47,7 +47,7 @@ BUILD_LOCK_DIR="${TMPDIR:-/tmp}/openclaw-parallels-build.lock"
TIMEOUT_INSTALL_SITE_S=420
TIMEOUT_INSTALL_TGZ_S=420
TIMEOUT_INSTALL_REGISTRY_S=420
TIMEOUT_UPDATE_DEV_S=300
TIMEOUT_UPDATE_DEV_S="${OPENCLAW_PARALLELS_MACOS_UPDATE_DEV_TIMEOUT_S:-600}"
TIMEOUT_VERIFY_S=60
TIMEOUT_ONBOARD_S=180
TIMEOUT_GATEWAY_S=180

View File

@@ -41,7 +41,7 @@ BUILD_LOCK_DIR="${TMPDIR:-/tmp}/openclaw-parallels-build.lock"
TIMEOUT_SNAPSHOT_S=240
TIMEOUT_GIT_SETUP_S=1200
TIMEOUT_INSTALL_S=420
TIMEOUT_UPDATE_S=300
TIMEOUT_UPDATE_S="${OPENCLAW_PARALLELS_WINDOWS_UPDATE_TIMEOUT_S:-1800}"
TIMEOUT_UPDATE_POLL_GRACE_S=60
TIMEOUT_VERIFY_S=120
TIMEOUT_ONBOARD_S=600

View File

@@ -2,7 +2,6 @@ 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 { quoteCmdScriptArg } from "./cmd-argv.js";
import "./test-helpers/schtasks-base-mocks.js";
import {
inspectPortUsage,
@@ -91,11 +90,25 @@ async function writeStartupFallbackEntry(env: Record<string, string>) {
return startupEntryPath;
}
function expectStartupFallbackSpawn(env: Record<string, string>) {
expect(spawn).toHaveBeenCalledWith(
"cmd.exe",
["/d", "/s", "/c", quoteCmdScriptArg(resolveTaskScriptPath(env))],
expect.objectContaining({ detached: true, stdio: "ignore", windowsHide: true }),
function expectStartupFallbackSpawn() {
expect(spawn).toHaveBeenCalled();
const calls = spawn.mock.calls as unknown as Array<
[string, readonly string[], Record<string, unknown>]
>;
const lastCall = calls[calls.length - 1];
if (!lastCall) {
throw new Error("expected gateway launch spawn call");
}
const [executable, args, options] = lastCall;
expect(executable).not.toBe("cmd.exe");
expect(args).toEqual(expect.arrayContaining(["--port", "18789"]));
expect(options).toEqual(
expect.objectContaining({
detached: true,
env: expect.objectContaining({ OPENCLAW_GATEWAY_PORT: "18789" }),
stdio: "ignore",
windowsHide: true,
}),
);
}
@@ -197,11 +210,7 @@ describe("Windows startup fallback", () => {
expect(result.scriptPath).toBe(resolveTaskScriptPath(env));
expect(startupScript).toContain('start "" /min cmd.exe /d /c');
expect(startupScript).toContain("gateway.cmd");
expect(spawn).toHaveBeenCalledWith(
"cmd.exe",
["/d", "/s", "/c", quoteCmdScriptArg(resolveTaskScriptPath(env))],
expect.objectContaining({ detached: true, stdio: "ignore", windowsHide: true }),
);
expectStartupFallbackSpawn();
expect(childUnref).toHaveBeenCalled();
expect(printed).toContain("Installed Windows login item");
});
@@ -216,7 +225,7 @@ describe("Windows startup fallback", () => {
await installGatewayScheduledTask(env);
await expect(fs.access(resolveStartupEntryPath(env))).resolves.toBeUndefined();
expectStartupFallbackSpawn(env);
expectStartupFallbackSpawn();
});
});
@@ -231,18 +240,18 @@ describe("Windows startup fallback", () => {
await installGatewayScheduledTask(env);
await expect(fs.access(resolveStartupEntryPath(env))).resolves.toBeUndefined();
expectStartupFallbackSpawn(env);
expectStartupFallbackSpawn();
});
});
it("launches the task script directly when schtasks /Run is accepted but never starts the task", async () => {
it("launches through the Startup-style launcher when schtasks /Run is accepted but never starts the task", async () => {
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
fastForwardTaskStartWait();
addAcceptedRunNeverStartsResponses();
await installGatewayScheduledTask(env);
expectStartupFallbackSpawn(env);
expectStartupFallbackSpawn();
});
});
@@ -388,6 +397,7 @@ describe("Windows startup fallback", () => {
{ code: 0, stdout: "", stderr: "" },
{ code: 1, stdout: "", stderr: "not found" },
]);
await writeGatewayScript(env);
await writeStartupFallbackEntry(env);
inspectPortUsage.mockResolvedValue({
port: 18789,
@@ -401,7 +411,7 @@ describe("Windows startup fallback", () => {
outcome: "completed",
});
expectGatewayTermination(5151);
expectStartupFallbackSpawn(env);
expectStartupFallbackSpawn();
});
});
@@ -432,7 +442,7 @@ describe("Windows startup fallback", () => {
outcome: "completed",
});
expectStartupFallbackSpawn(env);
expectStartupFallbackSpawn();
});
});

View File

@@ -270,6 +270,10 @@ function buildTaskScript({
return `${lines.join("\r\n")}\r\n`;
}
function renderStartupLaunchCommand(scriptPath: string): string {
return `start "" /min cmd.exe /d /c ${quoteCmdScriptArg(scriptPath)}`;
}
function buildStartupLauncherScript(params: { description?: string; scriptPath: string }): string {
const lines = ["@echo off"];
const trimmedDescription = params.description?.trim();
@@ -277,7 +281,7 @@ function buildStartupLauncherScript(params: { description?: string; scriptPath:
assertNoCmdLineBreak(trimmedDescription, "Startup launcher description");
lines.push(`rem ${trimmedDescription}`);
}
lines.push(`start "" /min cmd.exe /d /c ${quoteCmdScriptArg(params.scriptPath)}`);
lines.push(renderStartupLaunchCommand(params.scriptPath));
return `${lines.join("\r\n")}\r\n`;
}
@@ -309,8 +313,26 @@ async function isRegisteredScheduledTask(env: GatewayServiceEnv): Promise<boolea
return res.code === 0;
}
function launchFallbackTaskScript(scriptPath: string): void {
const child = spawn("cmd.exe", ["/d", "/s", "/c", quoteCmdScriptArg(scriptPath)], {
async function launchFallbackTaskScript(env: GatewayServiceEnv): Promise<void> {
const scriptPath = resolveTaskScriptPath(env);
const command = await readScheduledTaskCommand(env);
if (command?.programArguments.length) {
const [executable, ...args] = command.programArguments;
const child = spawn(executable, args, {
cwd: command.workingDirectory || undefined,
detached: true,
env: {
...process.env,
...command.environment,
},
stdio: "ignore",
windowsHide: true,
});
child.unref();
return;
}
const child = spawn("cmd.exe", ["/d", "/c", scriptPath], {
detached: true,
stdio: "ignore",
windowsHide: true,
@@ -563,7 +585,7 @@ async function restartStartupEntry(
if (typeof runtime.pid === "number" && runtime.pid > 0) {
await terminateGatewayProcessTree(runtime.pid, 300);
}
launchFallbackTaskScript(resolveTaskScriptPath(env));
await launchFallbackTaskScript(env);
stdout.write(`${formatLine("Restarted Windows login item", resolveTaskName(env))}\n`);
return { outcome: "completed" };
}
@@ -784,7 +806,7 @@ async function runScheduledTaskOrThrow(params: {
) {
return;
}
launchFallbackTaskScript(params.scriptPath);
await launchFallbackTaskScript(params.env);
}
async function activateScheduledTask(params: {
@@ -831,7 +853,7 @@ async function activateScheduledTask(params: {
scriptPath: params.scriptPath,
});
await fs.writeFile(startupEntryPath, launcher, "utf8");
launchFallbackTaskScript(params.scriptPath);
await launchFallbackTaskScript(params.env);
writeFormattedLines(
params.stdout,
[