fix: restart Windows gateway via Scheduled Task (#38825) (#38825)

This commit is contained in:
Ayaan Zaidi
2026-03-07 18:00:38 +05:30
committed by GitHub
parent 26c9796736
commit 05c240fad6
12 changed files with 371 additions and 54 deletions

View File

@@ -229,6 +229,7 @@ Docs: https://docs.openclaw.ai
- Sessions/bootstrap cache rollover invalidation: clear cached workspace bootstrap snapshots whenever an existing `sessionKey` rolls to a new `sessionId` across auto-reply, command, and isolated cron session resolvers, so `AGENTS.md`/`MEMORY.md`/`USER.md` updates are reloaded after daily, idle, or forced session resets instead of staying stale until gateway restart. (#38494) Thanks @LivingInDrm.
- Gateway/Telegram polling health monitor: skip stale-socket restarts for Telegram long-polling channels and thread channel identity through shared health evaluation so polling connections are not restarted on the WebSocket stale-socket heuristic. (#38395) Thanks @ql-wade and @Takhoffman.
- Daemon/systemd fresh-install probe: check for OpenClaw's managed user unit before running `systemctl --user is-enabled`, so first-time Linux installs no longer fail on generic missing-unit probe errors. (#38819) Thanks @adaHubble.
- Gateway/Windows restart supervision: relaunch task-managed gateways through Scheduled Task with quoted helper-script command paths, distinguish restart-capable supervisors per platform, and stop orphaned Windows gateway children during self-restart. (#38825) Thanks @obviyus.
## 2026.3.2

View File

@@ -75,7 +75,9 @@ export async function runGatewayLoop(params: {
`full process restart failed (${respawn.detail ?? "unknown error"}); falling back to in-process restart`,
);
} else {
gatewayLog.info("restart mode: in-process restart (OPENCLAW_NO_RESPAWN)");
gatewayLog.info(
`restart mode: in-process restart (${respawn.detail ?? "OPENCLAW_NO_RESPAWN"})`,
);
}
if (hadLock && !(await reacquireLockForInProcessRestart())) {
return;

View File

@@ -298,11 +298,25 @@ describe("restart-helper", () => {
await runRestartScript(scriptPath);
expect(spawn).toHaveBeenCalledWith("cmd.exe", ["/c", scriptPath], {
expect(spawn).toHaveBeenCalledWith("cmd.exe", ["/d", "/s", "/c", scriptPath], {
detached: true,
stdio: "ignore",
});
expect(mockChild.unref).toHaveBeenCalled();
});
it("quotes cmd.exe /c paths with metacharacters on Windows", async () => {
Object.defineProperty(process, "platform", { value: "win32" });
const scriptPath = "C:\\Temp\\me&(ow)\\fake-script.bat";
const mockChild = { unref: vi.fn() };
vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
await runRestartScript(scriptPath);
expect(spawn).toHaveBeenCalledWith("cmd.exe", ["/d", "/s", "/c", `"${scriptPath}"`], {
detached: true,
stdio: "ignore",
});
});
});
});

View File

@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { DEFAULT_GATEWAY_PORT } from "../../config/paths.js";
import { quoteCmdScriptArg } from "../../daemon/cmd-argv.js";
import {
resolveGatewayLaunchAgentLabel,
resolveGatewaySystemdServiceName,
@@ -161,7 +162,7 @@ del "%~f0"
export async function runRestartScript(scriptPath: string): Promise<void> {
const isWindows = process.platform === "win32";
const file = isWindows ? "cmd.exe" : "/bin/sh";
const args = isWindows ? ["/c", scriptPath] : [scriptPath];
const args = isWindows ? ["/d", "/s", "/c", quoteCmdScriptArg(scriptPath)] : [scriptPath];
const child = spawn(file, args, {
detached: true,

View File

@@ -278,6 +278,7 @@ describe("buildServiceEnvironment", () => {
expect(env.OPENCLAW_SERVICE_KIND).toBe("gateway");
expect(typeof env.OPENCLAW_SERVICE_VERSION).toBe("string");
expect(env.OPENCLAW_SYSTEMD_UNIT).toBe("openclaw-gateway.service");
expect(env.OPENCLAW_WINDOWS_TASK_NAME).toBe("OpenClaw Gateway");
if (process.platform === "darwin") {
expect(env.OPENCLAW_LAUNCHD_LABEL).toBe("ai.openclaw.gateway");
}
@@ -305,6 +306,7 @@ describe("buildServiceEnvironment", () => {
port: 18789,
});
expect(env.OPENCLAW_SYSTEMD_UNIT).toBe("openclaw-gateway-work.service");
expect(env.OPENCLAW_WINDOWS_TASK_NAME).toBe("OpenClaw Gateway (work)");
if (process.platform === "darwin") {
expect(env.OPENCLAW_LAUNCHD_LABEL).toBe("ai.openclaw.work");
}

View File

@@ -6,6 +6,7 @@ import {
GATEWAY_SERVICE_MARKER,
resolveGatewayLaunchAgentLabel,
resolveGatewaySystemdServiceName,
resolveGatewayWindowsTaskName,
NODE_SERVICE_KIND,
NODE_SERVICE_MARKER,
NODE_WINDOWS_TASK_SCRIPT_NAME,
@@ -262,6 +263,7 @@ export function buildServiceEnvironment(params: {
OPENCLAW_GATEWAY_TOKEN: token,
OPENCLAW_LAUNCHD_LABEL: resolvedLaunchdLabel,
OPENCLAW_SYSTEMD_UNIT: systemdUnit,
OPENCLAW_WINDOWS_TASK_NAME: resolveGatewayWindowsTaskName(profile),
OPENCLAW_SERVICE_MARKER: GATEWAY_SERVICE_MARKER,
OPENCLAW_SERVICE_KIND: GATEWAY_SERVICE_KIND,
OPENCLAW_SERVICE_VERSION: VERSION,

View File

@@ -67,11 +67,14 @@ describe("restartGatewayProcessWithFreshPid", () => {
expect(spawnMock).not.toHaveBeenCalled();
});
it("returns supervised when launchd/systemd hints are present", () => {
it("returns supervised when launchd hints are present on macOS", () => {
clearSupervisorHints();
setPlatform("darwin");
process.env.LAUNCH_JOB_LABEL = "ai.openclaw.gateway";
triggerOpenClawRestartMock.mockReturnValue({ ok: true, method: "launchctl" });
const result = restartGatewayProcessWithFreshPid();
expect(result.mode).toBe("supervised");
expect(triggerOpenClawRestartMock).toHaveBeenCalledOnce();
expect(spawnMock).not.toHaveBeenCalled();
});
@@ -110,6 +113,7 @@ describe("restartGatewayProcessWithFreshPid", () => {
it("spawns detached child with current exec argv", () => {
delete process.env.OPENCLAW_NO_RESPAWN;
clearSupervisorHints();
setPlatform("linux");
process.execArgv = ["--import", "tsx"];
process.argv = ["/usr/local/bin/node", "/repo/dist/index.js", "gateway", "run"];
spawnMock.mockReturnValue({ pid: 4242, unref: vi.fn() });
@@ -134,23 +138,68 @@ describe("restartGatewayProcessWithFreshPid", () => {
it("returns supervised when OPENCLAW_SYSTEMD_UNIT is set", () => {
clearSupervisorHints();
setPlatform("linux");
process.env.OPENCLAW_SYSTEMD_UNIT = "openclaw-gateway.service";
const result = restartGatewayProcessWithFreshPid();
expect(result.mode).toBe("supervised");
expect(spawnMock).not.toHaveBeenCalled();
});
it("returns supervised when OPENCLAW_SERVICE_MARKER is set", () => {
it("returns supervised when OpenClaw gateway task markers are set on Windows", () => {
clearSupervisorHints();
process.env.OPENCLAW_SERVICE_MARKER = "gateway";
setPlatform("win32");
process.env.OPENCLAW_SERVICE_MARKER = "openclaw";
process.env.OPENCLAW_SERVICE_KIND = "gateway";
triggerOpenClawRestartMock.mockReturnValue({ ok: true, method: "schtasks" });
const result = restartGatewayProcessWithFreshPid();
expect(result.mode).toBe("supervised");
expect(triggerOpenClawRestartMock).toHaveBeenCalledOnce();
expect(spawnMock).not.toHaveBeenCalled();
});
it("keeps generic service markers out of non-Windows supervisor detection", () => {
clearSupervisorHints();
setPlatform("linux");
process.env.OPENCLAW_SERVICE_MARKER = "openclaw";
process.env.OPENCLAW_SERVICE_KIND = "gateway";
spawnMock.mockReturnValue({ pid: 4242, unref: vi.fn() });
const result = restartGatewayProcessWithFreshPid();
expect(result).toEqual({ mode: "spawned", pid: 4242 });
expect(triggerOpenClawRestartMock).not.toHaveBeenCalled();
});
it("returns disabled on Windows without Scheduled Task markers", () => {
clearSupervisorHints();
setPlatform("win32");
const result = restartGatewayProcessWithFreshPid();
expect(result.mode).toBe("disabled");
expect(result.detail).toContain("Scheduled Task");
expect(spawnMock).not.toHaveBeenCalled();
});
it("ignores node task script hints for gateway restart detection on Windows", () => {
clearSupervisorHints();
setPlatform("win32");
process.env.OPENCLAW_TASK_SCRIPT = "C:\\openclaw\\node.cmd";
process.env.OPENCLAW_TASK_SCRIPT_NAME = "node.cmd";
process.env.OPENCLAW_SERVICE_MARKER = "openclaw";
process.env.OPENCLAW_SERVICE_KIND = "node";
const result = restartGatewayProcessWithFreshPid();
expect(result.mode).toBe("disabled");
expect(triggerOpenClawRestartMock).not.toHaveBeenCalled();
expect(spawnMock).not.toHaveBeenCalled();
});
it("returns failed when spawn throws", () => {
delete process.env.OPENCLAW_NO_RESPAWN;
clearSupervisorHints();
setPlatform("linux");
spawnMock.mockImplementation(() => {
throw new Error("spawn failed");

View File

@@ -1,6 +1,6 @@
import { spawn } from "node:child_process";
import { triggerOpenClawRestart } from "./restart.js";
import { hasSupervisorHint } from "./supervisor-markers.js";
import { detectRespawnSupervisor } from "./supervisor-markers.js";
type RespawnMode = "spawned" | "supervised" | "disabled" | "failed";
@@ -18,13 +18,9 @@ function isTruthy(value: string | undefined): boolean {
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
}
function isLikelySupervisedProcess(env: NodeJS.ProcessEnv = process.env): boolean {
return hasSupervisorHint(env);
}
/**
* Attempt to restart this process with a fresh PID.
* - supervised environments (launchd/systemd): caller should exit and let supervisor restart
* - supervised environments (launchd/systemd/schtasks): caller should exit and let supervisor restart
* - OPENCLAW_NO_RESPAWN=1: caller should keep in-process restart behavior (tests/dev)
* - otherwise: spawn detached child with current argv/execArgv, then caller exits
*/
@@ -32,20 +28,27 @@ export function restartGatewayProcessWithFreshPid(): GatewayRespawnResult {
if (isTruthy(process.env.OPENCLAW_NO_RESPAWN)) {
return { mode: "disabled" };
}
if (isLikelySupervisedProcess(process.env)) {
// On macOS under launchd, actively kickstart the supervised service to
// bypass ThrottleInterval delays for intentional restarts.
if (process.platform === "darwin" && process.env.OPENCLAW_LAUNCHD_LABEL?.trim()) {
const supervisor = detectRespawnSupervisor(process.env);
if (supervisor) {
if (supervisor === "launchd" || supervisor === "schtasks") {
const restart = triggerOpenClawRestart();
if (!restart.ok) {
return {
mode: "failed",
detail: restart.detail ?? "launchctl kickstart failed",
detail: restart.detail ?? `${restart.method} restart failed`,
};
}
}
return { mode: "supervised" };
}
if (process.platform === "win32") {
// Detached respawn is unsafe on Windows without an identified Scheduled Task:
// the child becomes orphaned if the original process exits.
return {
mode: "disabled",
detail: "win32: detached respawn unsupported without Scheduled Task markers",
};
}
try {
const args = [...process.execArgv, ...process.argv.slice(1)];

View File

@@ -7,10 +7,11 @@ import {
} from "../daemon/constants.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { cleanStaleGatewayProcessesSync, findGatewayPidsOnPortSync } from "./restart-stale-pids.js";
import { relaunchGatewayScheduledTask } from "./windows-task-restart.js";
export type RestartAttempt = {
ok: boolean;
method: "launchctl" | "systemd" | "supervisor";
method: "launchctl" | "systemd" | "schtasks" | "supervisor";
detail?: string;
tried?: string[];
};
@@ -296,36 +297,41 @@ export function triggerOpenClawRestart(): RestartAttempt {
cleanStaleGatewayProcessesSync();
const tried: string[] = [];
if (process.platform !== "darwin") {
if (process.platform === "linux") {
const unit = normalizeSystemdUnit(
process.env.OPENCLAW_SYSTEMD_UNIT,
process.env.OPENCLAW_PROFILE,
);
const userArgs = ["--user", "restart", unit];
tried.push(`systemctl ${userArgs.join(" ")}`);
const userRestart = spawnSync("systemctl", userArgs, {
encoding: "utf8",
timeout: SPAWN_TIMEOUT_MS,
});
if (!userRestart.error && userRestart.status === 0) {
return { ok: true, method: "systemd", tried };
}
const systemArgs = ["restart", unit];
tried.push(`systemctl ${systemArgs.join(" ")}`);
const systemRestart = spawnSync("systemctl", systemArgs, {
encoding: "utf8",
timeout: SPAWN_TIMEOUT_MS,
});
if (!systemRestart.error && systemRestart.status === 0) {
return { ok: true, method: "systemd", tried };
}
const detail = [
`user: ${formatSpawnDetail(userRestart)}`,
`system: ${formatSpawnDetail(systemRestart)}`,
].join("; ");
return { ok: false, method: "systemd", detail, tried };
if (process.platform === "linux") {
const unit = normalizeSystemdUnit(
process.env.OPENCLAW_SYSTEMD_UNIT,
process.env.OPENCLAW_PROFILE,
);
const userArgs = ["--user", "restart", unit];
tried.push(`systemctl ${userArgs.join(" ")}`);
const userRestart = spawnSync("systemctl", userArgs, {
encoding: "utf8",
timeout: SPAWN_TIMEOUT_MS,
});
if (!userRestart.error && userRestart.status === 0) {
return { ok: true, method: "systemd", tried };
}
const systemArgs = ["restart", unit];
tried.push(`systemctl ${systemArgs.join(" ")}`);
const systemRestart = spawnSync("systemctl", systemArgs, {
encoding: "utf8",
timeout: SPAWN_TIMEOUT_MS,
});
if (!systemRestart.error && systemRestart.status === 0) {
return { ok: true, method: "systemd", tried };
}
const detail = [
`user: ${formatSpawnDetail(userRestart)}`,
`system: ${formatSpawnDetail(systemRestart)}`,
].join("; ");
return { ok: false, method: "systemd", detail, tried };
}
if (process.platform === "win32") {
return relaunchGatewayScheduledTask(process.env);
}
if (process.platform !== "darwin") {
return {
ok: false,
method: "supervisor",

View File

@@ -1,20 +1,52 @@
export const SUPERVISOR_HINT_ENV_VARS = [
// macOS launchd
const LAUNCHD_SUPERVISOR_HINT_ENV_VARS = [
"LAUNCH_JOB_LABEL",
"LAUNCH_JOB_NAME",
// OpenClaw service env markers
"OPENCLAW_LAUNCHD_LABEL",
] as const;
const SYSTEMD_SUPERVISOR_HINT_ENV_VARS = [
"OPENCLAW_SYSTEMD_UNIT",
"OPENCLAW_SERVICE_MARKER",
// Linux systemd
"INVOCATION_ID",
"SYSTEMD_EXEC_PID",
"JOURNAL_STREAM",
] as const;
export function hasSupervisorHint(env: NodeJS.ProcessEnv = process.env): boolean {
return SUPERVISOR_HINT_ENV_VARS.some((key) => {
const WINDOWS_TASK_SUPERVISOR_HINT_ENV_VARS = ["OPENCLAW_WINDOWS_TASK_NAME"] as const;
export const SUPERVISOR_HINT_ENV_VARS = [
...LAUNCHD_SUPERVISOR_HINT_ENV_VARS,
...SYSTEMD_SUPERVISOR_HINT_ENV_VARS,
...WINDOWS_TASK_SUPERVISOR_HINT_ENV_VARS,
"OPENCLAW_SERVICE_MARKER",
"OPENCLAW_SERVICE_KIND",
] as const;
export type RespawnSupervisor = "launchd" | "systemd" | "schtasks";
function hasAnyHint(env: NodeJS.ProcessEnv, keys: readonly string[]): boolean {
return keys.some((key) => {
const value = env[key];
return typeof value === "string" && value.trim().length > 0;
});
}
export function detectRespawnSupervisor(
env: NodeJS.ProcessEnv = process.env,
platform: NodeJS.Platform = process.platform,
): RespawnSupervisor | null {
if (platform === "darwin") {
return hasAnyHint(env, LAUNCHD_SUPERVISOR_HINT_ENV_VARS) ? "launchd" : null;
}
if (platform === "linux") {
return hasAnyHint(env, SYSTEMD_SUPERVISOR_HINT_ENV_VARS) ? "systemd" : null;
}
if (platform === "win32") {
if (hasAnyHint(env, WINDOWS_TASK_SUPERVISOR_HINT_ENV_VARS)) {
return "schtasks";
}
const marker = env.OPENCLAW_SERVICE_MARKER?.trim();
const serviceKind = env.OPENCLAW_SERVICE_KIND?.trim();
return marker && serviceKind === "gateway" ? "schtasks" : null;
}
return null;
}

View File

@@ -0,0 +1,133 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { captureFullEnv } from "../test-utils/env.js";
const spawnMock = vi.hoisted(() => vi.fn());
const resolvePreferredOpenClawTmpDirMock = vi.hoisted(() => vi.fn(() => os.tmpdir()));
vi.mock("node:child_process", () => ({
spawn: (...args: unknown[]) => spawnMock(...args),
}));
vi.mock("./tmp-openclaw-dir.js", () => ({
resolvePreferredOpenClawTmpDir: () => resolvePreferredOpenClawTmpDirMock(),
}));
import { relaunchGatewayScheduledTask } from "./windows-task-restart.js";
const envSnapshot = captureFullEnv();
const createdScriptPaths = new Set<string>();
const createdTmpDirs = new Set<string>();
function decodeCmdPathArg(value: string): string {
const trimmed = value.trim();
const withoutQuotes =
trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
return withoutQuotes.replace(/\^!/g, "!").replace(/%%/g, "%");
}
afterEach(() => {
envSnapshot.restore();
spawnMock.mockReset();
resolvePreferredOpenClawTmpDirMock.mockReset();
resolvePreferredOpenClawTmpDirMock.mockReturnValue(os.tmpdir());
for (const scriptPath of createdScriptPaths) {
try {
fs.unlinkSync(scriptPath);
} catch {
// Best-effort cleanup for temp helper scripts created in tests.
}
}
createdScriptPaths.clear();
for (const tmpDir of createdTmpDirs) {
try {
fs.rmSync(tmpDir, { recursive: true, force: true });
} catch {
// Best-effort cleanup for test temp roots.
}
}
createdTmpDirs.clear();
});
describe("relaunchGatewayScheduledTask", () => {
it("writes a detached schtasks relaunch helper", () => {
const unref = vi.fn();
let seenCommandArg = "";
spawnMock.mockImplementation((_file: string, args: string[]) => {
seenCommandArg = args[3];
createdScriptPaths.add(decodeCmdPathArg(args[3]));
return { unref };
});
const result = relaunchGatewayScheduledTask({ OPENCLAW_PROFILE: "work" });
expect(result).toMatchObject({
ok: true,
method: "schtasks",
tried: expect.arrayContaining(['schtasks /Run /TN "OpenClaw Gateway (work)"']),
});
expect(result.tried).toContain(`cmd.exe /d /s /c ${seenCommandArg}`);
expect(spawnMock).toHaveBeenCalledWith(
"cmd.exe",
["/d", "/s", "/c", expect.any(String)],
expect.objectContaining({
detached: true,
stdio: "ignore",
windowsHide: true,
}),
);
expect(unref).toHaveBeenCalledOnce();
const scriptPath = [...createdScriptPaths][0];
expect(scriptPath).toBeTruthy();
const script = fs.readFileSync(scriptPath, "utf8");
expect(script).toContain("timeout /t 1 /nobreak >nul");
expect(script).toContain('schtasks /Run /TN "OpenClaw Gateway (work)" >nul 2>&1');
expect(script).toContain('del "%~f0" >nul 2>&1');
});
it("prefers OPENCLAW_WINDOWS_TASK_NAME overrides", () => {
spawnMock.mockImplementation((_file: string, args: string[]) => {
createdScriptPaths.add(decodeCmdPathArg(args[3]));
return { unref: vi.fn() };
});
relaunchGatewayScheduledTask({
OPENCLAW_PROFILE: "work",
OPENCLAW_WINDOWS_TASK_NAME: "OpenClaw Gateway (custom)",
});
const scriptPath = [...createdScriptPaths][0];
const script = fs.readFileSync(scriptPath, "utf8");
expect(script).toContain('schtasks /Run /TN "OpenClaw Gateway (custom)" >nul 2>&1');
});
it("returns failed when the helper cannot be spawned", () => {
spawnMock.mockImplementation(() => {
throw new Error("spawn failed");
});
const result = relaunchGatewayScheduledTask({ OPENCLAW_PROFILE: "work" });
expect(result.ok).toBe(false);
expect(result.method).toBe("schtasks");
expect(result.detail).toContain("spawn failed");
});
it("quotes the cmd /c script path when temp paths contain metacharacters", () => {
const unref = vi.fn();
const metacharTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw&(restart)-"));
createdTmpDirs.add(metacharTmpDir);
resolvePreferredOpenClawTmpDirMock.mockReturnValue(metacharTmpDir);
spawnMock.mockReturnValue({ unref });
relaunchGatewayScheduledTask({ OPENCLAW_PROFILE: "work" });
expect(spawnMock).toHaveBeenCalledWith(
"cmd.exe",
["/d", "/s", "/c", expect.stringMatching(/^".*&.*"$/)],
expect.any(Object),
);
});
});

View File

@@ -0,0 +1,72 @@
import { spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { quoteCmdScriptArg } from "../daemon/cmd-argv.js";
import { resolveGatewayWindowsTaskName } from "../daemon/constants.js";
import type { RestartAttempt } from "./restart.js";
import { resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir.js";
const TASK_RESTART_RETRY_LIMIT = 12;
const TASK_RESTART_RETRY_DELAY_SEC = 1;
function resolveWindowsTaskName(env: NodeJS.ProcessEnv): string {
const override = env.OPENCLAW_WINDOWS_TASK_NAME?.trim();
if (override) {
return override;
}
return resolveGatewayWindowsTaskName(env.OPENCLAW_PROFILE);
}
function buildScheduledTaskRestartScript(taskName: string): string {
const quotedTaskName = quoteCmdScriptArg(taskName);
return [
"@echo off",
"setlocal",
"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`,
"goto retry",
":cleanup",
'del "%~f0" >nul 2>&1',
].join("\r\n");
}
export function relaunchGatewayScheduledTask(env: NodeJS.ProcessEnv = process.env): RestartAttempt {
const taskName = resolveWindowsTaskName(env);
const scriptPath = path.join(
resolvePreferredOpenClawTmpDir(),
`openclaw-schtasks-restart-${randomUUID()}.cmd`,
);
const quotedScriptPath = quoteCmdScriptArg(scriptPath);
try {
fs.writeFileSync(scriptPath, `${buildScheduledTaskRestartScript(taskName)}\r\n`, "utf8");
const child = spawn("cmd.exe", ["/d", "/s", "/c", quotedScriptPath], {
detached: true,
stdio: "ignore",
windowsHide: true,
});
child.unref();
return {
ok: true,
method: "schtasks",
tried: [`schtasks /Run /TN "${taskName}"`, `cmd.exe /d /s /c ${quotedScriptPath}`],
};
} catch (err) {
try {
fs.unlinkSync(scriptPath);
} catch {
// Best-effort cleanup; keep the original restart failure.
}
return {
ok: false,
method: "schtasks",
detail: err instanceof Error ? err.message : String(err),
tried: [`schtasks /Run /TN "${taskName}"`],
};
}
}