Skip to content

Commit 9a31bc8

Browse files
andyk-msBradGroux
authored andcommitted
fix(windows): prevent restart race from duplicate schtasks /Run
When CLI runs 'openclaw gateway restart', it does schtasks /End then schtasks /Run. Meanwhile the dying gateway fires a self-restart script (relaunchGatewayScheduledTask) that also retries schtasks /Run up to 12 times. Both succeed, creating duplicate gateway windows. Fix: the self-restart .cmd script now checks if the task is already running before attempting schtasks /Run. If another restart (CLI) beat it there, the script exits cleanly instead of spawning a duplicate. Fixes #52044
1 parent 53efd63 commit 9a31bc8

3 files changed

Lines changed: 35 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai
6565
- Plugin skills/Windows: publish plugin-provided skill directories as junctions on Windows so standard users without Developer Mode can register plugin skills without symlink EPERM failures. Fixes #77958. (#77971) Thanks @hclsys and @jarro.
6666
- Shell env/Windows: hide the login-shell environment probe child window so gateway startup and shell-env refreshes do not flash a console on Windows. Fixes #78159. (#78266) Thanks @BradGroux.
6767
- MS Teams: surface blocked Bot Framework egress by logging JWKS fetch network failures and adding a Bot Connector send hint for transport-level reply failures. Fixes #77674. (#78081) Thanks @Beandon13.
68+
- Windows/restart: skip duplicate scheduled-task `/Run` calls when the gateway task is already running, using a locale-stable PowerShell task-state probe before retrying. Fixes #52044. (#52487) Thanks @andyk-ms.
6869
- Gateway/sessions: fast-path already-qualified model refs while building session-list rows so `openclaw sessions` and Control UI session lists avoid heavyweight model resolution on large stores. (#77902) Thanks @ragesaq.
6970
- Contributor PRs: remind external contributors to redact private information like IP addresses, API keys, phone numbers, and non-public endpoints from real behavior proof. Thanks @pashpashpash.
7071
- ACP bridge: relay Gateway exec approval prompts from active ACP turns to the ACP client's `session/request_permission` handler before resolving the Gateway approval. Thanks @amknight.

src/infra/windows-task-restart.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,13 @@ describe("relaunchGatewayScheduledTask", () => {
120120
expect(script).toContain(
121121
'openclaw restart attempt source=windows-task-handoff target="OpenClaw Gateway (work)"',
122122
);
123+
expect(script).toContain(
124+
`powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "(Get-ScheduledTask -TaskName 'OpenClaw Gateway (work)' -ErrorAction SilentlyContinue).State" 2>nul | findstr /I /C:"Running" >nul 2>&1`,
125+
);
123126
expect(script).toContain('schtasks /Run /TN "OpenClaw Gateway (work)" >>');
127+
expect(script.indexOf("powershell.exe -NoProfile")).toBeLessThan(
128+
script.indexOf('schtasks /Run /TN "OpenClaw Gateway (work)"'),
129+
);
124130
expect(script).toContain('del "%~f0" >nul 2>&1');
125131
});
126132

@@ -140,6 +146,23 @@ describe("relaunchGatewayScheduledTask", () => {
140146
expect(script).toContain('schtasks /Run /TN "OpenClaw Gateway (custom)" >>');
141147
});
142148

149+
it("escapes custom task names in the PowerShell running-task probe", () => {
150+
spawnMock.mockImplementation((_file: string, args: string[]) => {
151+
createdScriptPaths.add(decodeCmdPathArg(args[3]));
152+
return { unref: vi.fn() };
153+
});
154+
155+
relaunchGatewayScheduledTask({
156+
OPENCLAW_WINDOWS_TASK_NAME: "OpenClaw Gateway (Bob's work)",
157+
});
158+
159+
const scriptPath = [...createdScriptPaths][0];
160+
const script = fs.readFileSync(scriptPath, "utf8");
161+
expect(script).toContain(
162+
"-Command \"(Get-ScheduledTask -TaskName 'OpenClaw Gateway (Bob''s work)' -ErrorAction SilentlyContinue).State\"",
163+
);
164+
});
165+
143166
it("returns failed when the helper cannot be spawned", () => {
144167
spawnMock.mockImplementation(() => {
145168
throw new Error("spawn failed");

src/infra/windows-task-restart.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import { resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir.js";
1313
const TASK_RESTART_RETRY_LIMIT = 12;
1414
const TASK_RESTART_RETRY_DELAY_SEC = 1;
1515

16+
function quotePowerShellSingleQuotedLiteral(value: string): string {
17+
return `'${value.replace(/'/g, "''")}'`;
18+
}
19+
1620
function resolveWindowsTaskName(env: NodeJS.ProcessEnv): string {
1721
const override = env.OPENCLAW_WINDOWS_TASK_NAME?.trim();
1822
if (override) {
@@ -29,6 +33,10 @@ function buildScheduledTaskRestartScript(params: {
2933
}): string {
3034
const { quotedLogPath, setupLines, taskName, taskScriptPath } = params;
3135
const quotedTaskName = quoteCmdScriptArg(taskName);
36+
const queryTaskStateCommand = `(Get-ScheduledTask -TaskName ${quotePowerShellSingleQuotedLiteral(
37+
taskName,
38+
)} -ErrorAction SilentlyContinue).State`;
39+
const quotedQueryTaskStateCommand = quoteCmdScriptArg(queryTaskStateCommand);
3240
const lines = [
3341
"@echo off",
3442
"setlocal",
@@ -40,6 +48,9 @@ function buildScheduledTaskRestartScript(params: {
4048
":retry",
4149
`timeout /t ${TASK_RESTART_RETRY_DELAY_SEC} /nobreak >nul`,
4250
"set /a attempts+=1",
51+
// Avoid racing with another restart path that already started the scheduled task.
52+
`powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command ${quotedQueryTaskStateCommand} 2>nul | findstr /I /C:"Running" >nul 2>&1`,
53+
"if not errorlevel 1 goto cleanup",
4354
`schtasks /Run /TN ${quotedTaskName} >> ${quotedLogPath} 2>&1`,
4455
"if not errorlevel 1 goto cleanup",
4556
`if %attempts% GEQ ${TASK_RESTART_RETRY_LIMIT} goto fallback`,

0 commit comments

Comments
 (0)