mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 01:00:44 +00:00
121 lines
4.0 KiB
TypeScript
121 lines
4.0 KiB
TypeScript
import { spawn, type ChildProcess } from "node:child_process";
|
|
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
|
import { isContainerEnvironment } from "./container-environment.js";
|
|
import { formatErrorMessage } from "./errors.js";
|
|
import { triggerOpenClawRestart } from "./restart.js";
|
|
import { detectRespawnSupervisor } from "./supervisor-markers.js";
|
|
|
|
type RespawnMode = "spawned" | "supervised" | "disabled" | "failed";
|
|
|
|
type GatewayRespawnResult = {
|
|
mode: RespawnMode;
|
|
pid?: number;
|
|
detail?: string;
|
|
};
|
|
|
|
type GatewayUpdateRespawnResult = GatewayRespawnResult & {
|
|
child?: ChildProcess;
|
|
};
|
|
|
|
function isTruthy(value: string | undefined): boolean {
|
|
const normalized = normalizeOptionalLowercaseString(value);
|
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
}
|
|
|
|
function spawnDetachedGatewayProcess(): { child: ChildProcess; pid?: number } {
|
|
const args = [...process.execArgv, ...process.argv.slice(1)];
|
|
const child = spawn(process.execPath, args, {
|
|
env: process.env,
|
|
detached: true,
|
|
stdio: "inherit",
|
|
});
|
|
child.unref();
|
|
return { child, pid: child.pid ?? undefined };
|
|
}
|
|
|
|
/**
|
|
* Attempt to restart this process with a fresh PID.
|
|
* - 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
|
|
*/
|
|
export function restartGatewayProcessWithFreshPid(): GatewayRespawnResult {
|
|
if (isTruthy(process.env.OPENCLAW_NO_RESPAWN)) {
|
|
return { mode: "disabled" };
|
|
}
|
|
const supervisor = detectRespawnSupervisor(process.env);
|
|
if (supervisor) {
|
|
// On macOS launchd, exit cleanly and let KeepAlive relaunch the service.
|
|
// Avoid detached kickstart/start handoffs here so restart timing stays tied
|
|
// to launchd's native supervision rather than a second helper process.
|
|
if (supervisor === "schtasks") {
|
|
const restart = triggerOpenClawRestart();
|
|
if (!restart.ok) {
|
|
return {
|
|
mode: "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",
|
|
};
|
|
}
|
|
if (isContainerEnvironment()) {
|
|
return {
|
|
mode: "disabled",
|
|
detail: "container: use in-process restart to keep PID 1 alive",
|
|
};
|
|
}
|
|
|
|
try {
|
|
const { pid } = spawnDetachedGatewayProcess();
|
|
return { mode: "spawned", pid };
|
|
} catch (err) {
|
|
const detail = formatErrorMessage(err);
|
|
return { mode: "failed", detail };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update restarts must replace the OS process so the new code runs from a
|
|
* fresh module graph after package files have changed on disk.
|
|
*
|
|
* Unlike the generic restart path, update mode allows detached respawn on
|
|
* unmanaged Windows installs because there is no safe in-process fallback once
|
|
* the installed package contents have been replaced.
|
|
*/
|
|
export function respawnGatewayProcessForUpdate(): GatewayUpdateRespawnResult {
|
|
if (isTruthy(process.env.OPENCLAW_NO_RESPAWN)) {
|
|
return { mode: "disabled", detail: "OPENCLAW_NO_RESPAWN" };
|
|
}
|
|
const supervisor = detectRespawnSupervisor(process.env);
|
|
if (supervisor) {
|
|
if (supervisor === "schtasks") {
|
|
const restart = triggerOpenClawRestart();
|
|
if (!restart.ok) {
|
|
return {
|
|
mode: "failed",
|
|
detail: restart.detail ?? `${restart.method} restart failed`,
|
|
};
|
|
}
|
|
}
|
|
return { mode: "supervised" };
|
|
}
|
|
try {
|
|
const { child, pid } = spawnDetachedGatewayProcess();
|
|
return { mode: "spawned", pid, child };
|
|
} catch (err) {
|
|
return {
|
|
mode: "failed",
|
|
detail: formatErrorMessage(err),
|
|
};
|
|
}
|
|
}
|