mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-11 09:11:13 +00:00
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 <zhangluyao@microsoft.com> Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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<string, string | undefined>) => {
|
||||
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<string, string | undefined>) => 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<string, string | undefined>) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user