diff --git a/scripts/e2e/parallels-macos-smoke.sh b/scripts/e2e/parallels-macos-smoke.sh index 3c38aa5973c..a35cd321a36 100644 --- a/scripts/e2e/parallels-macos-smoke.sh +++ b/scripts/e2e/parallels-macos-smoke.sh @@ -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 diff --git a/scripts/e2e/parallels-windows-smoke.sh b/scripts/e2e/parallels-windows-smoke.sh index ef7552f79a1..4443ec2f8e6 100644 --- a/scripts/e2e/parallels-windows-smoke.sh +++ b/scripts/e2e/parallels-windows-smoke.sh @@ -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 diff --git a/src/daemon/schtasks.startup-fallback.test.ts b/src/daemon/schtasks.startup-fallback.test.ts index fd4b7176933..18ea4f5d5e6 100644 --- a/src/daemon/schtasks.startup-fallback.test.ts +++ b/src/daemon/schtasks.startup-fallback.test.ts @@ -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) { return startupEntryPath; } -function expectStartupFallbackSpawn(env: Record) { - 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] + >; + 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(); }); }); diff --git a/src/daemon/schtasks.ts b/src/daemon/schtasks.ts index 93c7020a7bb..5e85514e553 100644 --- a/src/daemon/schtasks.ts +++ b/src/daemon/schtasks.ts @@ -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 { + 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, [