fix: preserve Windows scheduled task restart/install behavior (#59335) (thanks @tmimmanuel)

* fix(daemon): preserve Windows Task Scheduler settings on reinstall and exit early on failed restart

* fix(daemon): add test coverage for Create/Change paths, fix early exit grace period

* fix(daemon): fix startup-fallback tests for new isRegisteredScheduledTask call

* fix(daemon): report early restart failure accurately

* fix: preserve Windows scheduled task restart/install behavior (#59335) (thanks @tmimmanuel)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
tmimmanuel
2026-04-04 05:16:00 +02:00
committed by GitHub
parent ff0c1b57a7
commit 0fef95b17d
9 changed files with 328 additions and 19 deletions

View File

@@ -19,6 +19,7 @@ import {
import {
DEFAULT_RESTART_HEALTH_ATTEMPTS,
DEFAULT_RESTART_HEALTH_DELAY_MS,
type GatewayRestartSnapshot,
renderGatewayPortHealthDiagnostics,
renderRestartDiagnostics,
terminateStaleGatewayPids,
@@ -31,6 +32,25 @@ import type { DaemonLifecycleOptions } from "./types.js";
const POST_RESTART_HEALTH_ATTEMPTS = DEFAULT_RESTART_HEALTH_ATTEMPTS;
const POST_RESTART_HEALTH_DELAY_MS = DEFAULT_RESTART_HEALTH_DELAY_MS;
function formatRestartFailure(params: {
health: GatewayRestartSnapshot;
port: number;
timeoutSeconds: number;
}): { statusLine: string; failMessage: string } {
if (params.health.waitOutcome === "stopped-free") {
const elapsedSeconds = Math.max(1, Math.round((params.health.elapsedMs ?? 0) / 1000));
return {
statusLine: `Gateway restart failed after ${elapsedSeconds}s: service stayed stopped and port ${params.port} stayed free.`,
failMessage: `Gateway restart failed after ${elapsedSeconds}s: service stayed stopped and health checks never came up.`,
};
}
return {
statusLine: `Timed out after ${params.timeoutSeconds}s waiting for gateway port ${params.port} to become healthy.`,
failMessage: `Gateway restart timed out after ${params.timeoutSeconds}s waiting for health checks.`,
};
}
async function resolveGatewayLifecyclePort(service = resolveGatewayService()) {
const command = await service.readCommand(process.env).catch(() => null);
const serviceEnv = command?.environment ?? undefined;
@@ -234,13 +254,17 @@ export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promi
}
const diagnostics = renderRestartDiagnostics(health);
const timeoutLine = `Timed out after ${restartWaitSeconds}s waiting for gateway port ${restartPort} to become healthy.`;
const failure = formatRestartFailure({
health,
port: restartPort,
timeoutSeconds: restartWaitSeconds,
});
const runningNoPortLine =
health.runtime.status === "running" && health.portUsage.status === "free"
? `Gateway process is running but port ${restartPort} is still free (startup hang/crash loop or very slow VM startup).`
: null;
if (!json) {
defaultRuntime.log(theme.warn(timeoutLine));
defaultRuntime.log(theme.warn(failure.statusLine));
if (runningNoPortLine) {
defaultRuntime.log(theme.warn(runningNoPortLine));
}
@@ -248,14 +272,14 @@ export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promi
defaultRuntime.log(theme.muted(line));
}
} else {
warnings.push(timeoutLine);
warnings.push(failure.statusLine);
if (runningNoPortLine) {
warnings.push(runningNoPortLine);
}
warnings.push(...diagnostics);
}
fail(`Gateway restart timed out after ${restartWaitSeconds}s waiting for health checks.`, [
fail(failure.failMessage, [
formatCliCommand("openclaw gateway status --deep"),
formatCliCommand("openclaw doctor"),
]);