mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-26 14:03:32 +00:00
@@ -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");
|
||||
|
||||
@@ -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)];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
133
src/infra/windows-task-restart.test.ts
Normal file
133
src/infra/windows-task-restart.test.ts
Normal 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),
|
||||
);
|
||||
});
|
||||
});
|
||||
72
src/infra/windows-task-restart.ts
Normal file
72
src/infra/windows-task-restart.ts
Normal 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}"`],
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user