mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-25 16:13:31 +00:00
fix(cli): handle scheduled gateway restarts consistently
This commit is contained in:
@@ -40,11 +40,12 @@ vi.mock("../../runtime.js", () => ({
|
||||
}));
|
||||
|
||||
let runServiceRestart: typeof import("./lifecycle-core.js").runServiceRestart;
|
||||
let runServiceStart: typeof import("./lifecycle-core.js").runServiceStart;
|
||||
let runServiceStop: typeof import("./lifecycle-core.js").runServiceStop;
|
||||
|
||||
describe("runServiceRestart token drift", () => {
|
||||
beforeAll(async () => {
|
||||
({ runServiceRestart, runServiceStop } = await import("./lifecycle-core.js"));
|
||||
({ runServiceRestart, runServiceStart, runServiceStop } = await import("./lifecycle-core.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -196,4 +197,21 @@ describe("runServiceRestart token drift", () => {
|
||||
expect(payload.result).toBe("scheduled");
|
||||
expect(payload.message).toBe("restart scheduled, gateway will restart momentarily");
|
||||
});
|
||||
|
||||
it("emits scheduled when service start routes through a scheduled restart", async () => {
|
||||
service.restart.mockResolvedValue({ outcome: "scheduled" });
|
||||
|
||||
await runServiceStart({
|
||||
serviceNoun: "Gateway",
|
||||
service,
|
||||
renderStartHints: () => [],
|
||||
opts: { json: true },
|
||||
});
|
||||
|
||||
expect(service.isLoaded).toHaveBeenCalledTimes(1);
|
||||
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
|
||||
const payload = JSON.parse(jsonLine ?? "{}") as { result?: string; message?: string };
|
||||
expect(payload.result).toBe("scheduled");
|
||||
expect(payload.message).toBe("restart scheduled, gateway will restart momentarily");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { formatConfigIssueLines } from "../../config/issue-format.js";
|
||||
import { resolveIsNixMode } from "../../config/paths.js";
|
||||
import { checkTokenDrift } from "../../daemon/service-audit.js";
|
||||
import type { GatewayServiceRestartResult } from "../../daemon/service-types.js";
|
||||
import { describeGatewayServiceRestart } from "../../daemon/service.js";
|
||||
import type { GatewayService } from "../../daemon/service.js";
|
||||
import { renderSystemdUnavailableHints } from "../../daemon/systemd-hints.js";
|
||||
import { isSystemdUserServiceAvailable } from "../../daemon/systemd.js";
|
||||
@@ -224,7 +225,20 @@ export async function runServiceStart(params: {
|
||||
}
|
||||
|
||||
try {
|
||||
await params.service.restart({ env: process.env, stdout });
|
||||
const restartResult = await params.service.restart({ env: process.env, stdout });
|
||||
const restartStatus = describeGatewayServiceRestart(params.serviceNoun, restartResult);
|
||||
if (restartStatus.scheduled) {
|
||||
emit({
|
||||
ok: true,
|
||||
result: restartStatus.daemonActionResult,
|
||||
message: restartStatus.message,
|
||||
service: buildDaemonServiceSnapshot(params.service, loaded),
|
||||
});
|
||||
if (!json) {
|
||||
defaultRuntime.log(restartStatus.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
const hints = params.renderStartHints();
|
||||
fail(`${params.serviceNoun} start failed: ${String(err)}`, hints);
|
||||
@@ -318,7 +332,7 @@ export async function runServiceRestart(params: {
|
||||
renderStartHints: () => string[];
|
||||
opts?: DaemonLifecycleOptions;
|
||||
checkTokenDrift?: boolean;
|
||||
postRestartCheck?: (ctx: RestartPostCheckContext) => Promise<void>;
|
||||
postRestartCheck?: (ctx: RestartPostCheckContext) => Promise<GatewayServiceRestartResult | void>;
|
||||
onNotLoaded?: (ctx: NotLoadedActionContext) => Promise<NotLoadedActionResult | null>;
|
||||
}): Promise<boolean> {
|
||||
const json = Boolean(params.opts?.json);
|
||||
@@ -407,22 +421,38 @@ export async function runServiceRestart(params: {
|
||||
if (loaded) {
|
||||
restartResult = await params.service.restart({ env: process.env, stdout });
|
||||
}
|
||||
if (restartResult.outcome === "scheduled") {
|
||||
const message = `restart scheduled, ${params.serviceNoun.toLowerCase()} will restart momentarily`;
|
||||
let restartStatus = describeGatewayServiceRestart(params.serviceNoun, restartResult);
|
||||
if (restartStatus.scheduled) {
|
||||
emit({
|
||||
ok: true,
|
||||
result: "scheduled",
|
||||
message,
|
||||
result: restartStatus.daemonActionResult,
|
||||
message: restartStatus.message,
|
||||
service: buildDaemonServiceSnapshot(params.service, loaded),
|
||||
warnings: warnings.length ? warnings : undefined,
|
||||
});
|
||||
if (!json) {
|
||||
defaultRuntime.log(message);
|
||||
defaultRuntime.log(restartStatus.message);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (params.postRestartCheck) {
|
||||
await params.postRestartCheck({ json, stdout, warnings, fail });
|
||||
const postRestartResult = await params.postRestartCheck({ json, stdout, warnings, fail });
|
||||
if (postRestartResult) {
|
||||
restartStatus = describeGatewayServiceRestart(params.serviceNoun, postRestartResult);
|
||||
if (restartStatus.scheduled) {
|
||||
emit({
|
||||
ok: true,
|
||||
result: restartStatus.daemonActionResult,
|
||||
message: restartStatus.message,
|
||||
service: buildDaemonServiceSnapshot(params.service, loaded),
|
||||
warnings: warnings.length ? warnings : undefined,
|
||||
});
|
||||
if (!json) {
|
||||
defaultRuntime.log(restartStatus.message);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let restarted = loaded;
|
||||
if (loaded) {
|
||||
|
||||
@@ -132,6 +132,7 @@ describe("runDaemonRestart health checks", () => {
|
||||
programArguments: ["openclaw", "gateway", "--port", "18789"],
|
||||
environment: {},
|
||||
});
|
||||
service.restart.mockResolvedValue({ outcome: "completed" });
|
||||
|
||||
runServiceRestart.mockImplementation(async (params: RestartParams) => {
|
||||
const fail = (message: string, hints?: string[]) => {
|
||||
@@ -204,6 +205,25 @@ describe("runDaemonRestart health checks", () => {
|
||||
expect(waitForGatewayHealthyRestart).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("skips stale-pid retry health checks when the retry restart is only scheduled", async () => {
|
||||
const unhealthy: RestartHealthSnapshot = {
|
||||
healthy: false,
|
||||
staleGatewayPids: [1993],
|
||||
runtime: { status: "stopped" },
|
||||
portUsage: { port: 18789, status: "busy", listeners: [], hints: [] },
|
||||
};
|
||||
waitForGatewayHealthyRestart.mockResolvedValueOnce(unhealthy);
|
||||
terminateStaleGatewayPids.mockResolvedValue([1993]);
|
||||
service.restart.mockResolvedValueOnce({ outcome: "scheduled" });
|
||||
|
||||
const result = await runDaemonRestart({ json: true });
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(terminateStaleGatewayPids).toHaveBeenCalledWith([1993]);
|
||||
expect(service.restart).toHaveBeenCalledTimes(1);
|
||||
expect(waitForGatewayHealthyRestart).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("fails restart when gateway remains unhealthy", async () => {
|
||||
const unhealthy: RestartHealthSnapshot = {
|
||||
healthy: false,
|
||||
|
||||
@@ -286,7 +286,10 @@ export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promi
|
||||
}
|
||||
|
||||
await terminateStaleGatewayPids(health.staleGatewayPids);
|
||||
await service.restart({ env: process.env, stdout });
|
||||
const retryRestart = await service.restart({ env: process.env, stdout });
|
||||
if (retryRestart.outcome === "scheduled") {
|
||||
return retryRestart;
|
||||
}
|
||||
health = await waitForGatewayHealthyRestart({
|
||||
service,
|
||||
port: restartPort,
|
||||
|
||||
Reference in New Issue
Block a user