openclaw / src /cli /daemon-cli /lifecycle-unmanaged.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
f778c12 verified
Raw
History Blame Contribute Delete
6.81 kB
import { expectDefined } from "@openclaw/normalization-core";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { isRestartEnabled } from "../../config/commands.flags.js";
import { readBestEffortConfig } from "../../config/config.js";
import { callGatewayCli } from "../../gateway/call.js";
import { probeGateway } from "../../gateway/probe.js";
import {
isSameGatewayLockIdentity,
readActiveGatewayLockIdentity,
} from "../../infra/gateway-lock.js";
import {
readGatewayOwnerLease,
type GatewayOwnerLeaseIdentity,
} from "../../infra/gateway-owner-lease.js";
import {
findVerifiedGatewayListenerPidsOnPortSync,
formatGatewayPidList,
signalVerifiedGatewayPidSync,
} from "../../infra/gateway-processes.js";
import {
clearGatewayRestartIntentSync,
type GatewayRestartIntent,
writeGatewayRestartIntentSync,
} from "../../infra/restart-intent.js";
import { appendGatewayLifecycleAudit } from "./lifecycle-audit.js";
async function assertUnmanagedGatewayRestartEnabled(port: number): Promise<void> {
const cfg = await readBestEffortConfig({ observe: false }).catch(() => undefined);
const scheme = cfg?.gateway?.tls?.enabled ? "wss" : "ws";
const probe = await probeGateway({
url: `${scheme}://127.0.0.1:${port}`,
auth: {
token: normalizeOptionalString(process.env.OPENCLAW_GATEWAY_TOKEN),
password: normalizeOptionalString(process.env.OPENCLAW_GATEWAY_PASSWORD),
},
timeoutMs: 1_000,
}).catch(() => null);
if (!probe?.ok) {
return;
}
if (
isRecord(probe.configSnapshot) &&
!isRestartEnabled({ commands: probe.configSnapshot.commands })
) {
throw new Error(
"Gateway restart is disabled in the running gateway config (commands.restart=false); unmanaged SIGUSR1 restart would be ignored",
);
}
}
export function resolveVerifiedGatewayListenerPids(port: number): number[] {
return findVerifiedGatewayListenerPidsOnPortSync(port).filter(
(pid): pid is number => Number.isFinite(pid) && pid > 0,
);
}
export async function signalGatewayRestart(
port: number,
params: {
restartIntent?: GatewayRestartIntent;
enforceRestartConfig: boolean;
processLabel: string;
requireLockIdentity?: boolean;
auditSource: "cli" | "supervisor";
ownerLease?: GatewayOwnerLeaseIdentity;
env?: NodeJS.ProcessEnv;
},
) {
if (params.enforceRestartConfig) {
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`,
);
}
const pid = expectDefined(pids[0], "pids entry at 0");
if (params.ownerLease && params.ownerLease.pid !== pid) {
throw new Error(
`Port ${port} is owned by pid ${pid}, not the recorded foreground Gateway pid ${params.ownerLease.pid}; refusing restart`,
);
}
const isWindows = process.platform === "win32";
const requiresTargetedDelivery = params.requireLockIdentity === true || isWindows;
const previousLockIdentity = requiresTargetedDelivery
? await readActiveGatewayLockIdentity({ env: params.env })
: undefined;
if (
requiresTargetedDelivery &&
(!previousLockIdentity ||
previousLockIdentity.pid !== pid ||
previousLockIdentity.port !== port)
) {
throw new Error(
`gateway lock identity does not match the verified listener on port ${port}; refusing an ambiguous restart`,
);
}
const intentWritten = previousLockIdentity?.ownerId
? false
: writeGatewayRestartIntentSync({
targetPid: pid,
reason: "gateway.restart",
...(params.restartIntent ? { intent: params.restartIntent } : {}),
});
if (requiresTargetedDelivery && !previousLockIdentity?.ownerId && !intentWritten) {
throw new Error("failed to persist the gateway restart intent");
}
try {
if (previousLockIdentity) {
const currentLockIdentity = await readActiveGatewayLockIdentity({ env: params.env });
if (
!currentLockIdentity ||
!isSameGatewayLockIdentity(previousLockIdentity, currentLockIdentity)
) {
throw new Error(
`gateway lock owner changed before the restart request could be delivered on port ${port}`,
);
}
}
if (params.ownerLease) {
const current = readGatewayOwnerLease({ env: params.env });
if (
!current ||
current.state !== "live" ||
current.owner !== params.ownerLease.owner ||
current.pid !== pid ||
current.startedAt !== params.ownerLease.startedAt ||
current.mode !== "foreground" ||
current.port !== port ||
previousLockIdentity?.ownerId !== current.owner
) {
throw new Error(`Foreground Gateway owner changed before restart on port ${port}`);
}
}
if (previousLockIdentity?.ownerId) {
const result = await callGatewayCli<{ pid: number }>({
method: "gateway.restart.request",
params: {
reason: "gateway.restart",
target: {
pid,
ownerId: previousLockIdentity.ownerId,
port,
},
...(params.restartIntent ? { restartIntent: params.restartIntent } : {}),
},
localPortOverride: port,
ignoreEnvUrlOverride: true,
timeoutMs: 10_000,
});
expectDefined(result.pid === pid ? result : undefined, "invalid restart acknowledgement");
} else if (isWindows) {
// Gateways started before lock owner IDs were introduced do not understand the
// targeted payload. The exact loopback port plus the revalidated legacy lock is
// the strongest available target; the PID-bound persisted intent carries options.
await callGatewayCli({
method: "gateway.restart.request",
params: {
reason: "gateway.restart",
skipDeferral: true,
},
localPortOverride: port,
ignoreEnvUrlOverride: true,
timeoutMs: 10_000,
});
} else {
signalVerifiedGatewayPidSync(pid, "SIGUSR1");
}
} catch (err) {
if (intentWritten) {
clearGatewayRestartIntentSync();
}
throw err;
}
appendGatewayLifecycleAudit({
action: "restart",
source: params.auditSource,
mode: previousLockIdentity?.ownerId || isWindows ? "rpc" : "sigusr1",
pid,
});
return {
result: "restarted" as const,
pid,
previousLockIdentity,
message: `Gateway restart request sent to ${params.processLabel} process on port ${port}: ${pid}.`,
};
}