openclaw / src /cli /daemon-cli /lifecycle-safe-restart.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
f778c12 verified
Raw History Blame Contribute Delete
4.85 kB
import { GATEWAY_SERVER_CAPS } from "../../../packages/gateway-protocol/src/index.js";
import { theme } from "../../../packages/terminal-core/src/theme.js";
import { refreshLegacySystemdServiceMetadata } from "../../daemon/systemd.js";
import { callGatewayCli } from "../../gateway/call.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { resolveGatewayServiceMutationError } from "../../infra/gateway-supervision.js";
import type { SafeGatewayRestartRequestResult } from "../../infra/restart-coordinator.js";
import type { GatewayRestartIntent } from "../../infra/restart-intent.js";
import { defaultRuntime, writeRuntimeJson } from "../../runtime.js";
import { parseDurationMs } from "../parse-duration.js";
import { appendGatewayLifecycleAudit } from "./lifecycle-audit.js";
import type { DaemonLifecycleOptions } from "./types.js";
const SAFE_RESTART_METADATA_REFRESH_TIMEOUT_MS = 5_000;
function formatSafeRestartWarnings(result: SafeGatewayRestartRequestResult): string[] | undefined {
return result.preflight.blockers.length === 0 ? undefined : [result.preflight.summary];
}
export function resolveGatewayRestartIntentOptions(
opts: DaemonLifecycleOptions,
): GatewayRestartIntent | undefined {
if (opts.force && opts.wait !== undefined) {
throw new Error("--force cannot be combined with --wait");
}
if (opts.force) {
return { force: true };
}
return opts.wait === undefined ? undefined : { waitMs: parseDurationMs(opts.wait) };
}
/** Request an OpenClaw-aware restart through the running Gateway. */
type SafeRestartTarget = { pid: number; ownerId: string; port: number };
export async function runSafeGatewayRestart(
opts: DaemonLifecycleOptions,
target?: SafeRestartTarget,
): Promise<boolean> {
if (opts.force) {
throw new Error("--safe cannot be combined with --force; omit --safe to force restart now");
}
if (opts.wait !== undefined) {
throw new Error("--safe cannot be combined with --wait; safe restart uses gateway deferral");
}
const skipDeferral = opts.skipDeferral === true;
const params: {
reason: string;
safe?: true;
skipDeferral?: true;
target?: SafeRestartTarget;
} = { reason: "gateway.restart.safe" };
if (target) {
params.safe = true;
params.target = {
pid: target.pid,
ownerId: target.ownerId,
port: target.port,
};
}
if (skipDeferral) {
params.skipDeferral = true;
}
if (process.platform === "linux") {
const reportRefreshError = (error: unknown) => {
defaultRuntime.error(
theme.warn(
`Warning: legacy systemd metadata was not refreshed: ${formatErrorMessage(error)}`,
),
);
};
const mutationError = resolveGatewayServiceMutationError(
"refresh legacy systemd service metadata",
process.env,
);
if (mutationError) {
reportRefreshError(mutationError);
} else {
// Definition maintenance is best effort. Keep a wedged systemd manager from
// suppressing the separately bounded Gateway restart request below.
await refreshLegacySystemdServiceMetadata(
process.env,
SAFE_RESTART_METADATA_REFRESH_TIMEOUT_MS,
).catch(reportRefreshError);
}
}
const result = await callGatewayCli<SafeGatewayRestartRequestResult>({
method: "gateway.restart.request",
params,
...(target
? {
ignoreEnvUrlOverride: true,
localPortOverride: target.port,
requiredCapabilities: [GATEWAY_SERVER_CAPS.GATEWAY_RESTART_TARGET_SAFE],
}
: {}),
timeoutMs: 10_000,
});
if (target && result.restart.pid !== target.pid) {
throw new Error("invalid safe restart acknowledgement");
}
appendGatewayLifecycleAudit({
action: "restart",
source: "safe-rpc",
mode: result.status,
pid: result.restart.pid,
});
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 " +
"(bounded wait; may force after the timeout expires)"
: skipDeferral
? "safe restart requested; gateway bypassing active-work deferral; " +
"shutdown may still wait for pending replies to drain"
: "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) {
writeRuntimeJson(defaultRuntime, payload);
} else {
defaultRuntime.log(message);
if (result.preflight.blockers.length > 0) {
defaultRuntime.log(theme.warn(result.preflight.summary));
}
}
return true;
}