openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
398 lines (397 loc) • 17.8 kB
JavaScript
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { d as resolveGatewayPort } from "./paths-mvMm5bYV.js";
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js";
import { r as theme } from "./theme-vjDs9tao.js";
import { a as OPENCLAW_WRAPPER_ENV_KEY, c as resolveOpenClawWrapperPath } from "./daemon-install-plan.shared-Dr7I1qWf.js";
import { n as buildGatewayInstallPlan } from "./auth-install-policy-O7nc7i9I.js";
import { f as stopSystemdService, t as findInstalledSystemdGatewayScope, u as restartSystemdService } from "./systemd-BXTM6gEb.js";
import { n as defaultRuntime } from "./runtime-B4lgFmsS.js";
import { t as DEFAULT_GATEWAY_DAEMON_RUNTIME } from "./daemon-runtime-C76za6vm.js";
import { d as readConfigFileSnapshotForWrite, l as readBestEffortConfig } from "./io-Gi7-pyU-.js";
import { t as parseDurationMs } from "./parse-duration-oxu_S07c.js";
import "./config-C9RxTsn1.js";
import { t as resolveGatewayInstallToken } from "./gateway-install-token-jVqLCOms.js";
import { h as repairLaunchAgentBootstrap, i as formatLaunchAgentGuiSessionError, l as launchAgentPlistExists } from "./launchd-FSKBDZ2p.js";
import { i as resolveGatewayService, n as formatGatewayServiceStartRepairIssues } from "./service-MPqQWJ3s.js";
import { i as signalVerifiedGatewayPidSync, n as formatGatewayPidList, t as findVerifiedGatewayListenerPidsOnPortSync } from "./gateway-processes-cAJXq3M3.js";
import { h as createNullWriter, l as renderGatewayServiceStartHints, s as parsePortFromArgs } from "./shared-BEkveSUA.js";
import { t as mergeInstallInvocationEnv } from "./install-D_sMTHzb.js";
import { n as isRestartEnabled } from "./commands.flags-Bx5KKfOp.js";
import { s as callGatewayCli } from "./call-B5-GYOlf.js";
import { a as probeGateway } from "./probe-2_flQlhR.js";
import { _ as writeGatewayRestartIntentSync } from "./restart-CiO1ILZU.js";
import { i as runServiceUninstall, n as runServiceStart, r as runServiceStop, t as runServiceRestart } from "./lifecycle-core-BDdRclCf.js";
import { a as renderGatewayPortHealthDiagnostics, c as waitForGatewayHealthyListener, l as waitForGatewayHealthyRestart, o as renderRestartDiagnostics, s as terminateStaleGatewayPids, t as DEFAULT_RESTART_HEALTH_ATTEMPTS } from "./restart-health-Bbw_vf1w.js";
//#region src/cli/daemon-cli/launchd-recovery.ts
const LAUNCH_AGENT_RECOVERY_MESSAGE = "Gateway LaunchAgent was installed but not loaded; re-bootstrapped launchd service.";
/** Re-bootstrap an installed but unloaded LaunchAgent after a daemon start/restart command. */
async function recoverInstalledLaunchAgent(params) {
if (process.platform !== "darwin") return null;
const env = params.env ?? process.env;
if (!await launchAgentPlistExists(env).catch(() => false)) return null;
const repaired = await repairLaunchAgentBootstrap({ env }).catch(() => ({
ok: false,
status: "bootstrap-failed"
}));
if (!repaired.ok) {
if (repaired.status === "gui-session-unavailable") {
const actionHint = params.result === "started" ? "openclaw gateway start" : "openclaw gateway restart";
throw new Error(formatLaunchAgentGuiSessionError({
detail: repaired.detail,
domain: repaired.domain,
actionHint
}));
}
return null;
}
return {
result: params.result,
loaded: true,
message: LAUNCH_AGENT_RECOVERY_MESSAGE
};
}
//#endregion
//#region src/cli/daemon-cli/start-repair.ts
/** Repair a loaded but stale Gateway service definition and report the start result. */
async function repairLoadedGatewayServiceForStart(params) {
const { snapshot: configSnapshot, writeOptions: configWriteOptions } = await readConfigFileSnapshotForWrite();
const cfg = configSnapshot.valid ? configSnapshot.sourceConfig : configSnapshot.config;
const existingEnvironment = params.state.command?.environment;
const installEnv = mergeInstallInvocationEnv({
env: process.env,
existingServiceEnv: existingEnvironment
});
const wrapperPath = await resolveOpenClawWrapperPath(installEnv[OPENCLAW_WRAPPER_ENV_KEY]);
const port = resolveGatewayPort(cfg);
const tokenResolution = await resolveGatewayInstallToken({
config: cfg,
configSnapshot,
configWriteOptions,
env: installEnv,
autoGenerateWhenMissing: true,
persistGeneratedToken: true
});
if (tokenResolution.unavailableReason) throw new Error(tokenResolution.unavailableReason);
const warnings = [formatGatewayServiceStartRepairIssues(params.issues), ...tokenResolution.warnings].filter((warning) => warning.trim().length > 0);
if (!params.json) {
defaultRuntime.log("Gateway service definition needs repair:");
for (const warning of warnings) defaultRuntime.log(`- ${warning}`);
}
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
env: installEnv,
port,
runtime: DEFAULT_GATEWAY_DAEMON_RUNTIME,
wrapperPath,
existingEnvironment,
config: cfg,
warn: (message) => {
warnings.push(message);
if (!params.json) defaultRuntime.log(`- ${message}`);
}
});
await params.service.install({
env: installEnv,
stdout: params.stdout,
programArguments,
workingDirectory,
environment
});
let loaded;
try {
loaded = await params.service.isLoaded({ env: installEnv });
} catch {
loaded = true;
}
return {
result: "started",
message: "Gateway service definition repaired and started. Reopen the Control UI with `openclaw dashboard` or copy a fresh auth URL with `openclaw dashboard --no-open`.",
warnings: warnings.length ? warnings : void 0,
loaded
};
}
//#endregion
//#region src/cli/daemon-cli/lifecycle.ts
const POST_RESTART_HEALTH_ATTEMPTS = DEFAULT_RESTART_HEALTH_ATTEMPTS;
const POST_RESTART_HEALTH_DELAY_MS = 500;
const WINDOWS_POST_RESTART_HEALTH_TIMEOUT_MS = 18e4;
function postRestartHealthAttempts() {
return process.platform === "win32" ? Math.ceil(WINDOWS_POST_RESTART_HEALTH_TIMEOUT_MS / POST_RESTART_HEALTH_DELAY_MS) : POST_RESTART_HEALTH_ATTEMPTS;
}
function formatRestartFailure(params) {
if (params.health.waitOutcome === "stopped-free") {
const elapsedSeconds = Math.max(1, Math.round((params.health.elapsedMs ?? 0) / 1e3));
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 ?? void 0;
const mergedEnv = {
...process.env,
...serviceEnv ?? void 0
};
return parsePortFromArgs(command?.programArguments) ?? resolveGatewayPort(await readBestEffortConfig(), mergedEnv);
}
function resolveGatewayPortFallback() {
return readBestEffortConfig().then((cfg) => resolveGatewayPort(cfg, process.env)).catch(() => resolveGatewayPort(void 0, process.env));
}
async function assertUnmanagedGatewayRestartEnabled(port) {
const cfg = await readBestEffortConfig().catch(() => void 0);
const probe = await probeGateway({
url: `${Boolean(cfg?.gateway?.tls?.enabled) ? "wss" : "ws"}://127.0.0.1:${port}`,
auth: {
token: normalizeOptionalString(process.env.OPENCLAW_GATEWAY_TOKEN),
password: normalizeOptionalString(process.env.OPENCLAW_GATEWAY_PASSWORD)
},
timeoutMs: 1e3
}).catch(() => null);
if (!probe?.ok) return;
if (!isRestartEnabled(probe.configSnapshot)) throw new Error("Gateway restart is disabled in the running gateway config (commands.restart=false); unmanaged SIGUSR1 restart would be ignored");
}
function resolveVerifiedGatewayListenerPids(port) {
return findVerifiedGatewayListenerPidsOnPortSync(port).filter((pid) => Number.isFinite(pid) && pid > 0);
}
async function handleSystemScopeSystemdGateway(action) {
if (process.platform !== "linux") return null;
const installed = await findInstalledSystemdGatewayScope(process.env).catch(() => null);
if (installed?.scope !== "system") return null;
const stdout = createNullWriter();
if (action === "stop") {
await stopSystemdService({
stdout,
env: process.env
});
return {
result: "stopped",
message: `Gateway stopped via system-scope systemd unit ${installed.unitName}.`
};
}
await restartSystemdService({
stdout,
env: process.env
});
return {
result: "restarted",
message: `Gateway restarted via system-scope systemd unit ${installed.unitName}.`
};
}
async function stopGatewayWithoutServiceManager(port) {
const managed = await handleSystemScopeSystemdGateway("stop");
if (managed) return managed;
const pids = resolveVerifiedGatewayListenerPids(port);
if (pids.length === 0) return null;
for (const pid of pids) signalVerifiedGatewayPidSync(pid, "SIGTERM");
return {
result: "stopped",
message: `Gateway stop signal sent to unmanaged process${pids.length === 1 ? "" : "es"} on port ${port}: ${formatGatewayPidList(pids)}.`
};
}
function resolveGatewayRestartIntentOptions(opts) {
if (opts.force && opts.wait !== void 0) throw new Error("--force cannot be combined with --wait");
if (opts.force) return { force: true };
if (opts.wait !== void 0) return { waitMs: parseDurationMs(opts.wait) };
}
function formatSafeRestartWarnings(result) {
if (result.preflight.blockers.length === 0) return;
return [result.preflight.summary];
}
async function requestSafeGatewayRestart(opts) {
if (opts.force) throw new Error("--safe cannot be combined with --force; omit --safe to force restart now");
if (opts.wait !== void 0) throw new Error("--safe cannot be combined with --wait; safe restart uses gateway deferral");
const skipDeferral = opts.skipDeferral === true;
const params = { reason: "gateway.restart.safe" };
if (skipDeferral) params.skipDeferral = true;
const result = await callGatewayCli({
method: "gateway.restart.request",
params,
timeoutMs: 1e4
});
const message = result.status === "coalesced" ? "safe restart request joined an existing pending gateway restart" : result.status === "deferred" ? "safe restart requested; gateway will restart after active work drains" : skipDeferral ? "safe restart requested; gateway bypassing active-work deferral" : "safe restart requested; gateway will restart momentarily";
const payload = {
ok: true,
result: result.status,
message,
preflight: result.preflight,
restart: result.restart,
warnings: formatSafeRestartWarnings(result)
};
if (opts.json) defaultRuntime.log(JSON.stringify(payload, null, 2));
else {
defaultRuntime.log(message);
if (result.preflight.blockers.length > 0) defaultRuntime.log(theme.warn(result.preflight.summary));
}
return true;
}
async function restartGatewayWithoutServiceManager(port, restartIntent) {
const managed = await handleSystemScopeSystemdGateway("restart");
if (managed) return managed;
await assertUnmanagedGatewayRestartEnabled(port);
const pids = resolveVerifiedGatewayListenerPids(port);
if (pids.length === 0) return null;
if (pids.length > 1) throw new Error(`multiple gateway processes are listening on port ${port}: ${formatGatewayPidList(pids)}; use "openclaw gateway status --deep" before retrying restart`);
writeGatewayRestartIntentSync({
targetPid: pids[0],
reason: "gateway.restart",
...restartIntent ? { intent: restartIntent } : {}
});
signalVerifiedGatewayPidSync(pids[0], "SIGUSR1");
return {
result: "restarted",
message: `Gateway restart signal sent to unmanaged process on port ${port}: ${pids[0]}.`
};
}
/** Uninstall the managed Gateway service after stopping it. */
async function runDaemonUninstall(opts = {}) {
return await runServiceUninstall({
serviceNoun: "Gateway",
service: resolveGatewayService(),
opts,
stopBeforeUninstall: true,
assertNotLoadedAfterUninstall: true
});
}
/** Start the managed Gateway service, repairing stale service definitions when possible. */
async function runDaemonStart(opts = {}) {
const service = resolveGatewayService();
return await runServiceStart({
serviceNoun: "Gateway",
service,
renderStartHints: renderGatewayServiceStartHints,
onNotLoaded: process.platform === "darwin" ? async () => await recoverInstalledLaunchAgent({ result: "started" }) : void 0,
repairLoadedService: async ({ json, stdout, state, issues }) => await repairLoadedGatewayServiceForStart({
service,
json,
stdout,
state,
issues
}),
opts
});
}
/** Stop the managed Gateway service or verified unmanaged listener fallback. */
async function runDaemonStop(opts = {}) {
const service = resolveGatewayService();
let gatewayPortPromise;
return await runServiceStop({
serviceNoun: "Gateway",
service,
opts,
stopWhenNotLoaded: process.platform === "darwin" && Boolean(opts.disable),
onNotLoaded: async () => {
gatewayPortPromise ??= resolveGatewayLifecyclePort(service).catch(() => resolveGatewayPortFallback());
return await stopGatewayWithoutServiceManager(await gatewayPortPromise);
}
});
}
/** Restart the Gateway service or a verified unmanaged listener, then prove health. */
async function runDaemonRestart(opts = {}) {
if (opts.skipDeferral && !opts.safe) throw new Error("--skip-deferral requires --safe");
if (opts.safe) return await requestSafeGatewayRestart(opts);
const json = Boolean(opts.json);
const service = resolveGatewayService();
let restartedWithoutServiceManager = false;
const restartIntent = resolveGatewayRestartIntentOptions(opts);
const restartPort = await resolveGatewayLifecyclePort(service).catch(() => resolveGatewayPortFallback());
const restartHealthAttempts = postRestartHealthAttempts();
const restartWaitMs = restartHealthAttempts * POST_RESTART_HEALTH_DELAY_MS;
const restartWaitSeconds = Math.round(restartWaitMs / 1e3);
return await runServiceRestart({
serviceNoun: "Gateway",
service,
renderStartHints: renderGatewayServiceStartHints,
opts: {
...opts,
...restartIntent ? { restartIntent } : {}
},
checkTokenDrift: true,
onNotLoaded: async () => {
if (process.platform === "darwin") {
const recovered = await recoverInstalledLaunchAgent({ result: "restarted" });
if (recovered) return recovered;
}
const handled = await restartGatewayWithoutServiceManager(restartPort, restartIntent);
if (handled) {
restartedWithoutServiceManager = true;
return handled;
}
return null;
},
postRestartCheck: async ({ warnings, fail, stdout }) => {
if (restartedWithoutServiceManager) {
const health = await waitForGatewayHealthyListener({
port: restartPort,
attempts: restartHealthAttempts,
delayMs: POST_RESTART_HEALTH_DELAY_MS
});
if (health.healthy) return;
const diagnostics = renderGatewayPortHealthDiagnostics(health);
const timeoutLine = `Timed out after ${restartWaitSeconds}s waiting for gateway port ${restartPort} to become healthy.`;
if (!json) {
defaultRuntime.log(theme.warn(timeoutLine));
for (const line of diagnostics) defaultRuntime.log(theme.muted(line));
} else {
warnings.push(timeoutLine);
warnings.push(...diagnostics);
}
fail(`Gateway restart timed out after ${restartWaitSeconds}s waiting for health checks.`, [formatCliCommand("openclaw gateway status --deep"), formatCliCommand("openclaw doctor")]);
throw new Error("unreachable after gateway restart health failure");
}
let health = await waitForGatewayHealthyRestart({
service,
port: restartPort,
attempts: restartHealthAttempts,
delayMs: POST_RESTART_HEALTH_DELAY_MS,
includeUnknownListenersAsStale: process.platform === "win32"
});
if (!health.healthy && health.staleGatewayPids.length > 0) {
const staleMsg = `Found stale gateway process(es): ${health.staleGatewayPids.join(", ")}.`;
warnings.push(staleMsg);
if (!json) {
defaultRuntime.log(theme.warn(staleMsg));
defaultRuntime.log(theme.muted("Stopping stale process(es) and retrying restart..."));
}
await terminateStaleGatewayPids(health.staleGatewayPids);
const retryRestart = await service.restart({
env: process.env,
stdout
});
if (retryRestart.outcome === "scheduled") return retryRestart;
health = await waitForGatewayHealthyRestart({
service,
port: restartPort,
attempts: restartHealthAttempts,
delayMs: POST_RESTART_HEALTH_DELAY_MS,
includeUnknownListenersAsStale: process.platform === "win32"
});
}
if (health.healthy) return;
const diagnostics = renderRestartDiagnostics(health);
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(failure.statusLine));
if (runningNoPortLine) defaultRuntime.log(theme.warn(runningNoPortLine));
for (const line of diagnostics) defaultRuntime.log(theme.muted(line));
} else {
warnings.push(failure.statusLine);
if (runningNoPortLine) warnings.push(runningNoPortLine);
warnings.push(...diagnostics);
}
fail(failure.failMessage, [formatCliCommand("openclaw gateway status --deep"), formatCliCommand("openclaw doctor")]);
throw new Error("unreachable after gateway restart failure");
}
});
}
//#endregion
export { recoverInstalledLaunchAgent as a, runDaemonUninstall as i, runDaemonStart as n, runDaemonStop as r, runDaemonRestart as t };