Skip to content

Commit d1e5f4b

Browse files
committed
fix(update): bound Windows scheduled task stop
1 parent 3ad2997 commit d1e5f4b

3 files changed

Lines changed: 241 additions & 78 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai
7676

7777
### Fixes
7878

79+
- CLI/update: guard Windows scheduled-task stops by state and timeout so auto-update restart cannot hang indefinitely on `schtasks /End` before stale-listener cleanup. Fixes #69970. Thanks @yangswld and @sherlock-huang.
7980
- Gateway/install: refresh loaded gateway service installs when the current service embeds stale gateway auth instead of returning already-installed, avoiding LaunchAgent token-mismatch loops after token rotation. Fixes #70752. Thanks @hyspacex.
8081
- Update: ignore bundled plugin `.openclaw-install-stage` directories during global install verification and packaged dist pruning so leftover runtime-dep staging files do not turn successful updates into `unexpected packaged dist file` failures. Fixes #71752. Thanks @waynegault.
8182
- Node runtime: keep node-host retry timers alive across Gateway restarts and exit on terminal credential pauses so supervised nodes do not become silent zombies. Fixes #69800. Thanks @meroli28.

src/cli/update-cli/restart-helper.test.ts

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -69,36 +69,44 @@ exit 0
6969
}
7070

7171
function expectWindowsRestartWaitOrdering(content: string, port = 18789) {
72-
const endCommand = 'schtasks /End /TN "';
73-
const pollAttemptsInit = "set /a attempts=0";
74-
const pollLabel = ":wait_for_port_release";
75-
const pollAttemptIncrement = "set /a attempts+=1";
76-
const pollNetstatCheck = `netstat -ano | findstr /R /C:":${port} .*LISTENING" >nul`;
77-
const forceKillLabel = ":force_kill_listener";
78-
const forceKillCommand = "taskkill /F /PID %%P >>";
79-
const portReleasedLabel = ":port_released";
80-
const runCommand = 'schtasks /Run /TN "';
81-
const endIndex = content.indexOf(endCommand);
82-
const attemptsInitIndex = content.indexOf(pollAttemptsInit, endIndex);
83-
const pollLabelIndex = content.indexOf(pollLabel, attemptsInitIndex);
84-
const pollAttemptIncrementIndex = content.indexOf(pollAttemptIncrement, pollLabelIndex);
85-
const pollNetstatCheckIndex = content.indexOf(pollNetstatCheck, pollAttemptIncrementIndex);
86-
const forceKillLabelIndex = content.indexOf(forceKillLabel, pollNetstatCheckIndex);
87-
const forceKillCommandIndex = content.indexOf(forceKillCommand, forceKillLabelIndex);
88-
const portReleasedLabelIndex = content.indexOf(portReleasedLabel, forceKillCommandIndex);
89-
const runIndex = content.indexOf(runCommand, portReleasedLabelIndex);
90-
91-
expect(endIndex).toBeGreaterThanOrEqual(0);
92-
expect(attemptsInitIndex).toBeGreaterThan(endIndex);
93-
expect(pollLabelIndex).toBeGreaterThan(attemptsInitIndex);
94-
expect(pollAttemptIncrementIndex).toBeGreaterThan(pollLabelIndex);
95-
expect(pollNetstatCheckIndex).toBeGreaterThan(pollAttemptIncrementIndex);
96-
expect(forceKillLabelIndex).toBeGreaterThan(pollNetstatCheckIndex);
97-
expect(forceKillCommandIndex).toBeGreaterThan(forceKillLabelIndex);
98-
expect(portReleasedLabelIndex).toBeGreaterThan(forceKillCommandIndex);
99-
expect(runIndex).toBeGreaterThan(portReleasedLabelIndex);
72+
const stateCheck = "$taskState = Get-OpenClawScheduledTaskState -TaskName $taskName";
73+
const runningGuard = 'if ($taskState -eq "Running")';
74+
const endCommand =
75+
'Invoke-OpenClawSchtasksWithTimeout -Arguments @("/End", "/TN", $taskName) -TimeoutSeconds 10';
76+
const skipEndLog = "openclaw restart skipped schtasks end";
77+
const pollLoop = "for ($attempt = 1; $attempt -le 10; $attempt++)";
78+
const pollCall = `Get-OpenClawListenerPids -Port $port`;
79+
const forceKillBranch = "if ($attempt -eq 10)";
80+
const forceKillCommand = "Stop-Process -Id $listenerPid -Force";
81+
const runCommand =
82+
'Invoke-OpenClawSchtasksWithTimeout -Arguments @("/Run", "/TN", $taskName) -TimeoutSeconds 30';
83+
const portAssignment = `$port = ${port}`;
84+
const stateCheckIndex = content.indexOf(stateCheck);
85+
const runningGuardIndex = content.indexOf(runningGuard, stateCheckIndex);
86+
const endIndex = content.indexOf(endCommand, runningGuardIndex);
87+
const skipEndLogIndex = content.indexOf(skipEndLog, endIndex);
88+
const portAssignmentIndex = content.indexOf(portAssignment);
89+
const pollLoopIndex = content.indexOf(pollLoop, skipEndLogIndex);
90+
const pollCallIndex = content.indexOf(pollCall, pollLoopIndex);
91+
const forceKillBranchIndex = content.indexOf(forceKillBranch, pollCallIndex);
92+
const forceKillCommandIndex = content.indexOf(forceKillCommand, forceKillBranchIndex);
93+
const runIndex = content.indexOf(runCommand, forceKillCommandIndex);
94+
95+
expect(stateCheckIndex).toBeGreaterThanOrEqual(0);
96+
expect(runningGuardIndex).toBeGreaterThan(stateCheckIndex);
97+
expect(endIndex).toBeGreaterThan(runningGuardIndex);
98+
expect(skipEndLogIndex).toBeGreaterThan(endIndex);
99+
expect(portAssignmentIndex).toBeGreaterThanOrEqual(0);
100+
expect(pollLoopIndex).toBeGreaterThan(skipEndLogIndex);
101+
expect(pollCallIndex).toBeGreaterThan(pollLoopIndex);
102+
expect(forceKillBranchIndex).toBeGreaterThan(pollCallIndex);
103+
expect(forceKillCommandIndex).toBeGreaterThan(forceKillBranchIndex);
104+
expect(runIndex).toBeGreaterThan(forceKillCommandIndex);
100105

101106
expect(content).not.toContain("timeout /t 3 /nobreak >nul");
107+
expect(content).not.toContain("findstr");
108+
expect(content).not.toContain("netstat -ano |");
109+
expect(content).not.toContain("schtasks /End /TN");
102110
}
103111

104112
beforeEach(() => {
@@ -296,21 +304,25 @@ exit 0
296304
await cleanupScript(scriptPath);
297305
});
298306

299-
it("creates a schtasks restart script on Windows", async () => {
307+
it("creates a guarded schtasks restart script on Windows", async () => {
300308
Object.defineProperty(process, "platform", { value: "win32" });
301309

302310
const { scriptPath, content } = await prepareAndReadScript({
303311
OPENCLAW_PROFILE: "default",
304312
});
305-
expect(scriptPath.endsWith(".bat")).toBe(true);
313+
expect(scriptPath.endsWith(".cmd")).toBe(true);
306314
expect(content).toContain("@echo off");
315+
expect(content).toContain("powershell -NoProfile -ExecutionPolicy Bypass -Command");
316+
expect(content).not.toContain("-File");
317+
expect(content).toContain('$ErrorActionPreference = "Continue"');
307318
expect(content).toContain("gateway-restart.log");
308-
expect(content).toContain("openclaw restart attempt source=update target=OpenClaw Gateway");
309-
expect(content).toContain('schtasks /End /TN "OpenClaw Gateway"');
310-
expect(content).toContain('schtasks /Run /TN "OpenClaw Gateway" >>');
319+
expect(content).toContain("$taskName = 'OpenClaw Gateway'");
320+
expect(content).toContain("function Invoke-OpenClawSchtasksWithTimeout");
321+
expect(content).toContain("function Get-OpenClawScheduledTaskState");
322+
expect(content).toContain("Get-ScheduledTask -TaskName $TaskName");
323+
expect(content).toContain("openclaw restart skipped schtasks end");
311324
expectWindowsRestartWaitOrdering(content);
312-
// Batch self-cleanup
313-
expect(content).toContain('del "%~f0"');
325+
expect(content).toContain('del "%~f0" >nul 2>&1');
314326
await cleanupScript(scriptPath);
315327
});
316328

@@ -321,8 +333,11 @@ exit 0
321333
OPENCLAW_PROFILE: "default",
322334
OPENCLAW_WINDOWS_TASK_NAME: "OpenClaw Gateway (custom)",
323335
});
324-
expect(content).toContain('schtasks /End /TN "OpenClaw Gateway (custom)"');
325-
expect(content).toContain('schtasks /Run /TN "OpenClaw Gateway (custom)"');
336+
expect(content).toContain("$taskName = 'OpenClaw Gateway (custom)'");
337+
expect(content).toContain("Get-OpenClawScheduledTaskState -TaskName $taskName");
338+
expect(content).toContain(
339+
'Invoke-OpenClawSchtasksWithTimeout -Arguments @("/End", "/TN", $taskName) -TimeoutSeconds 10',
340+
);
326341
expectWindowsRestartWaitOrdering(content);
327342
await cleanupScript(scriptPath);
328343
});
@@ -337,10 +352,10 @@ exit 0
337352
},
338353
customPort,
339354
);
340-
expect(content).toContain(`netstat -ano | findstr /R /C:":${customPort} .*LISTENING" >nul`);
341-
expect(content).toContain(
342-
`for /f "tokens=5" %%P in ('netstat -ano ^| findstr /R /C:":${customPort} .*LISTENING"') do (`,
343-
);
355+
expect(content).toContain(`$port = ${customPort}`);
356+
expect(content).toContain("Get-NetTCPConnection -LocalPort $Port -State Listen");
357+
expect(content).toContain("& netstat.exe -ano -p tcp");
358+
expect(content).not.toContain("findstr");
344359
expectWindowsRestartWaitOrdering(content, customPort);
345360
await cleanupScript(scriptPath);
346361
});
@@ -371,7 +386,7 @@ exit 0
371386
const { scriptPath, content } = await prepareAndReadScript({
372387
OPENCLAW_PROFILE: "production",
373388
});
374-
expect(content).toContain('schtasks /End /TN "OpenClaw Gateway (production)"');
389+
expect(content).toContain("$taskName = 'OpenClaw Gateway (production)'");
375390
expectWindowsRestartWaitOrdering(content);
376391
await cleanupScript(scriptPath);
377392
});

0 commit comments

Comments
 (0)