2026-02-21 18:02:05 +01:00
|
|
|
import { loadConfig, resolveGatewayPort } from "../../config/config.js";
|
2026-01-14 01:08:15 +00:00
|
|
|
import { resolveGatewayService } from "../../daemon/service.js";
|
2026-02-21 18:02:05 +01:00
|
|
|
import { defaultRuntime } from "../../runtime.js";
|
|
|
|
|
import { theme } from "../../terminal/theme.js";
|
|
|
|
|
import { formatCliCommand } from "../command-format.js";
|
2026-02-14 14:00:34 +00:00
|
|
|
import {
|
|
|
|
|
runServiceRestart,
|
|
|
|
|
runServiceStart,
|
|
|
|
|
runServiceStop,
|
|
|
|
|
runServiceUninstall,
|
|
|
|
|
} from "./lifecycle-core.js";
|
2026-02-21 18:02:05 +01:00
|
|
|
import {
|
|
|
|
|
renderRestartDiagnostics,
|
|
|
|
|
terminateStaleGatewayPids,
|
|
|
|
|
waitForGatewayHealthyRestart,
|
|
|
|
|
} from "./restart-health.js";
|
|
|
|
|
import { parsePortFromArgs, renderGatewayServiceStartHints } from "./shared.js";
|
2026-02-18 01:34:35 +00:00
|
|
|
import type { DaemonLifecycleOptions } from "./types.js";
|
2026-01-14 01:08:15 +00:00
|
|
|
|
2026-02-21 18:02:05 +01:00
|
|
|
const POST_RESTART_HEALTH_ATTEMPTS = 8;
|
|
|
|
|
const POST_RESTART_HEALTH_DELAY_MS = 450;
|
|
|
|
|
|
|
|
|
|
async function resolveGatewayRestartPort() {
|
|
|
|
|
const service = resolveGatewayService();
|
|
|
|
|
const command = await service.readCommand(process.env).catch(() => null);
|
|
|
|
|
const serviceEnv = command?.environment ?? undefined;
|
|
|
|
|
const mergedEnv = {
|
|
|
|
|
...(process.env as Record<string, string | undefined>),
|
|
|
|
|
...(serviceEnv ?? undefined),
|
|
|
|
|
} as NodeJS.ProcessEnv;
|
|
|
|
|
|
|
|
|
|
const portFromArgs = parsePortFromArgs(command?.programArguments);
|
|
|
|
|
return portFromArgs ?? resolveGatewayPort(loadConfig(), mergedEnv);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:40:35 +00:00
|
|
|
export async function runDaemonUninstall(opts: DaemonLifecycleOptions = {}) {
|
2026-02-14 14:00:34 +00:00
|
|
|
return await runServiceUninstall({
|
|
|
|
|
serviceNoun: "Gateway",
|
|
|
|
|
service: resolveGatewayService(),
|
|
|
|
|
opts,
|
|
|
|
|
stopBeforeUninstall: true,
|
|
|
|
|
assertNotLoadedAfterUninstall: true,
|
2026-01-16 05:40:35 +00:00
|
|
|
});
|
2026-01-14 01:08:15 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:40:35 +00:00
|
|
|
export async function runDaemonStart(opts: DaemonLifecycleOptions = {}) {
|
2026-02-14 14:00:34 +00:00
|
|
|
return await runServiceStart({
|
|
|
|
|
serviceNoun: "Gateway",
|
|
|
|
|
service: resolveGatewayService(),
|
|
|
|
|
renderStartHints: renderGatewayServiceStartHints,
|
|
|
|
|
opts,
|
2026-01-16 05:40:35 +00:00
|
|
|
});
|
2026-01-14 01:08:15 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 05:40:35 +00:00
|
|
|
export async function runDaemonStop(opts: DaemonLifecycleOptions = {}) {
|
2026-02-14 14:00:34 +00:00
|
|
|
return await runServiceStop({
|
|
|
|
|
serviceNoun: "Gateway",
|
|
|
|
|
service: resolveGatewayService(),
|
|
|
|
|
opts,
|
2026-01-16 05:40:35 +00:00
|
|
|
});
|
2026-01-14 01:08:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-21 17:45:06 +00:00
|
|
|
* Restart the gateway service service.
|
2026-01-14 01:08:15 +00:00
|
|
|
* @returns `true` if restart succeeded, `false` if the service was not loaded.
|
|
|
|
|
* Throws/exits on check or restart failures.
|
|
|
|
|
*/
|
2026-01-16 05:40:35 +00:00
|
|
|
export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promise<boolean> {
|
2026-02-21 18:02:05 +01:00
|
|
|
const json = Boolean(opts.json);
|
|
|
|
|
const service = resolveGatewayService();
|
|
|
|
|
const restartPort = await resolveGatewayRestartPort().catch(() =>
|
|
|
|
|
resolveGatewayPort(loadConfig(), process.env),
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-14 14:00:34 +00:00
|
|
|
return await runServiceRestart({
|
|
|
|
|
serviceNoun: "Gateway",
|
2026-02-21 18:02:05 +01:00
|
|
|
service,
|
2026-02-14 14:00:34 +00:00
|
|
|
renderStartHints: renderGatewayServiceStartHints,
|
|
|
|
|
opts,
|
2026-02-17 08:44:07 -05:00
|
|
|
checkTokenDrift: true,
|
2026-02-21 18:02:05 +01:00
|
|
|
postRestartCheck: async ({ warnings, fail, stdout }) => {
|
|
|
|
|
let health = await waitForGatewayHealthyRestart({
|
|
|
|
|
service,
|
|
|
|
|
port: restartPort,
|
|
|
|
|
attempts: POST_RESTART_HEALTH_ATTEMPTS,
|
|
|
|
|
delayMs: POST_RESTART_HEALTH_DELAY_MS,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
await service.restart({ env: process.env, stdout });
|
|
|
|
|
health = await waitForGatewayHealthyRestart({
|
|
|
|
|
service,
|
|
|
|
|
port: restartPort,
|
|
|
|
|
attempts: POST_RESTART_HEALTH_ATTEMPTS,
|
|
|
|
|
delayMs: POST_RESTART_HEALTH_DELAY_MS,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (health.healthy) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const diagnostics = renderRestartDiagnostics(health);
|
|
|
|
|
if (!json) {
|
|
|
|
|
defaultRuntime.log(theme.warn("Gateway did not become healthy after restart."));
|
|
|
|
|
for (const line of diagnostics) {
|
|
|
|
|
defaultRuntime.log(theme.muted(line));
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
warnings.push(...diagnostics);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fail("Gateway restart failed health checks.", [
|
|
|
|
|
formatCliCommand("openclaw gateway status --probe --deep"),
|
|
|
|
|
formatCliCommand("openclaw doctor"),
|
|
|
|
|
]);
|
|
|
|
|
},
|
2026-02-14 14:00:34 +00:00
|
|
|
});
|
2026-01-14 01:08:15 +00:00
|
|
|
}
|