From 0e61a1d0caf4b3edf8ec2768c8fc71dd54676174 Mon Sep 17 00:00:00 2001 From: imechZhangLY Date: Sun, 5 Apr 2026 16:21:57 +0800 Subject: [PATCH] fix: windows restart fallback when scheduled task is unregistered (#58943) (thanks @imechZhangLY) * fix(infra): windows-task-restart fallback to startup entry when schtasks task is unregistered * fix code style problem * use /min for startup fallback and assert schtasks pre-check in test * fix: windows restart fallback when scheduled task is unregistered (#58943) (thanks @imechZhangLY) --------- Co-authored-by: Luyao Zhang Co-authored-by: Ayaan Zaidi --- CHANGELOG.md | 1 + src/infra/windows-task-restart.test.ts | 37 ++++++++++++++++++++++++++ src/infra/windows-task-restart.ts | 34 ++++++++++++++++++----- 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4ab407d3b4..042b5711e5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -132,6 +132,7 @@ Docs: https://docs.openclaw.ai - Android/Talk Mode: restore voice replies on gateway-backed talk mode sessions by updating embedded runner transport overrides to the current agent transport API. (#61214) Thanks @obviyus. - Amazon Bedrock/aws-sdk auth: stop injecting the fake `AWS_PROFILE` apiKey marker when no AWS auth env vars exist, so instance-role and other default-chain setups keep working without poisoning provider config. (#61194) Thanks @wirjo. - Providers/Google: add model-level `cacheRetention` support for direct Gemini system prompts by creating, reusing, and refreshing `cachedContents` automatically on Google AI Studio runs. (#51372) Thanks @rafaelmariano-glitch. +- Windows/restart: fall back to the installed Startup-entry launcher when the scheduled task was never registered, so `/restart` can relaunch the gateway on Windows setups where `schtasks` install fell back during onboarding. (#58943) Thanks @imechZhangLY. ## 2026.4.2 diff --git a/src/infra/windows-task-restart.test.ts b/src/infra/windows-task-restart.test.ts index 54140607cf1..6519ce9e3aa 100644 --- a/src/infra/windows-task-restart.test.ts +++ b/src/infra/windows-task-restart.test.ts @@ -6,6 +6,12 @@ import { captureFullEnv } from "../test-utils/env.js"; const spawnMock = vi.hoisted(() => vi.fn()); const resolvePreferredOpenClawTmpDirMock = vi.hoisted(() => vi.fn(() => os.tmpdir())); +const resolveTaskScriptPathMock = vi.hoisted(() => + vi.fn((env: Record) => { + const home = env.USERPROFILE || env.HOME || os.homedir(); + return path.join(home, ".openclaw", "gateway.cmd"); + }), +); vi.mock("node:child_process", async () => { const { mockNodeBuiltinModule } = await import("../../test/helpers/node-builtin-mocks.js"); @@ -19,6 +25,9 @@ vi.mock("node:child_process", async () => { vi.mock("./tmp-openclaw-dir.js", () => ({ resolvePreferredOpenClawTmpDir: () => resolvePreferredOpenClawTmpDirMock(), })); +vi.mock("../daemon/schtasks.js", () => ({ + resolveTaskScriptPath: (env: Record) => resolveTaskScriptPathMock(env), +})); type WindowsTaskRestartModule = typeof import("./windows-task-restart.js"); @@ -64,6 +73,11 @@ describe("relaunchGatewayScheduledTask", () => { spawnMock.mockReset(); resolvePreferredOpenClawTmpDirMock.mockReset(); resolvePreferredOpenClawTmpDirMock.mockReturnValue(os.tmpdir()); + resolveTaskScriptPathMock.mockReset(); + resolveTaskScriptPathMock.mockImplementation((env: Record) => { + const home = env.USERPROFILE || env.HOME || os.homedir(); + return path.join(home, ".openclaw", "gateway.cmd"); + }); }); it("writes a detached schtasks relaunch helper", () => { @@ -145,4 +159,27 @@ describe("relaunchGatewayScheduledTask", () => { expect.any(Object), ); }); + + it("includes startup fallback", () => { + const taskScriptDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-state-")); + createdTmpDirs.add(taskScriptDir); + const taskScriptPath = path.join(taskScriptDir, "gateway.cmd"); + fs.writeFileSync(taskScriptPath, "@echo off\r\nrem placeholder\r\n", "utf8"); + resolveTaskScriptPathMock.mockReturnValue(taskScriptPath); + + spawnMock.mockImplementation((_file: string, args: string[]) => { + createdScriptPaths.add(decodeCmdPathArg(args[3])); + return { unref: vi.fn() }; + }); + + const result = relaunchGatewayScheduledTask({ OPENCLAW_PROFILE: "work" }); + + expect(result.ok).toBe(true); + const scriptPath = [...createdScriptPaths][0]; + 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(taskScriptPath); + }); }); diff --git a/src/infra/windows-task-restart.ts b/src/infra/windows-task-restart.ts index 147a88bac41..584cd10bfeb 100644 --- a/src/infra/windows-task-restart.ts +++ b/src/infra/windows-task-restart.ts @@ -4,6 +4,7 @@ import fs from "node:fs"; import path from "node:path"; import { quoteCmdScriptArg } from "../daemon/cmd-argv.js"; import { resolveGatewayWindowsTaskName } from "../daemon/constants.js"; +import { resolveTaskScriptPath } from "../daemon/schtasks.js"; import type { RestartAttempt } from "./restart.js"; import { resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir.js"; @@ -18,33 +19,52 @@ function resolveWindowsTaskName(env: NodeJS.ProcessEnv): string { return resolveGatewayWindowsTaskName(env.OPENCLAW_PROFILE); } -function buildScheduledTaskRestartScript(taskName: string): string { +function buildScheduledTaskRestartScript( + taskName: string, + taskScriptPath?: string, +): string { const quotedTaskName = quoteCmdScriptArg(taskName); - return [ + const lines = [ "@echo off", "setlocal", + `schtasks /Query /TN ${quotedTaskName} >nul 2>&1`, + "if errorlevel 1 goto fallback", "set /a attempts=0", ":retry", `timeout /t ${TASK_RESTART_RETRY_DELAY_SEC} /nobreak >nul`, "set /a attempts+=1", `schtasks /Run /TN ${quotedTaskName} >nul 2>&1`, "if not errorlevel 1 goto cleanup", - `if %attempts% GEQ ${TASK_RESTART_RETRY_LIMIT} goto cleanup`, + `if %attempts% GEQ ${TASK_RESTART_RETRY_LIMIT} goto fallback`, "goto retry", - ":cleanup", - 'del "%~f0" >nul 2>&1', - ].join("\r\n"); + ":fallback", + ]; + if (taskScriptPath) { + const quotedScript = quoteCmdScriptArg(taskScriptPath); + lines.push( + `if exist ${quotedScript} (`, + ` start "" /min cmd.exe /d /c ${quotedScript}`, + ")", + ); + } + lines.push(":cleanup", 'del "%~f0" >nul 2>&1'); + return lines.join("\r\n"); } export function relaunchGatewayScheduledTask(env: NodeJS.ProcessEnv = process.env): RestartAttempt { const taskName = resolveWindowsTaskName(env); + const taskScriptPath = resolveTaskScriptPath(env); const scriptPath = path.join( resolvePreferredOpenClawTmpDir(), `openclaw-schtasks-restart-${randomUUID()}.cmd`, ); const quotedScriptPath = quoteCmdScriptArg(scriptPath); try { - fs.writeFileSync(scriptPath, `${buildScheduledTaskRestartScript(taskName)}\r\n`, "utf8"); + fs.writeFileSync( + scriptPath, + `${buildScheduledTaskRestartScript(taskName, taskScriptPath)}\r\n`, + "utf8", + ); const child = spawn("cmd.exe", ["/d", "/s", "/c", quotedScriptPath], { detached: true, stdio: "ignore",