File size: 4,845 Bytes
f778c12 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | 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;
}
|