fix: harden external restart integration

This commit is contained in:
Shakker
2026-07-16 17:01:39 +01:00
committed by Shakker
parent fadd5cfd22
commit 332b522f90
3 changed files with 53 additions and 7 deletions
+1 -1
View File
@@ -620,7 +620,7 @@ export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promi
const handled = await restartGatewayWithoutServiceManager(unmanagedPort, restartIntent);
if (handled) {
restartedWithoutServiceManager = true;
if ("previousLockIdentity" in handled && handled.previousLockIdentity) {
if ("pid" in handled && handled.previousLockIdentity) {
unmanagedPreviousLockIdentity = handled.previousLockIdentity;
const healthWait = await resolveRestartListenerHealthWait(restartIntent);
unmanagedRestartHealthAttempts = healthWait.attempts;
@@ -2,6 +2,7 @@
// response payloads returned by gateway.restart.request.
import { expectDefined } from "@openclaw/normalization-core";
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { restartHandlers } from "./restart.js";
@@ -185,6 +186,49 @@ describe("gateway.restart.request handler", () => {
});
});
it.each([0.5, -1, MAX_TIMER_TIMEOUT_MS + 1, Number.MAX_SAFE_INTEGER])(
"rejects an invalid targeted restart wait of %s ms",
async (waitMs) => {
const respond = await invokeRestartRequest({
reason: "operator",
target: {
pid: process.pid,
ownerId: "gateway-owner",
port: 18_789,
},
restartIntent: { waitMs },
});
expect(requestGatewayRestartWithSignalAdmission).not.toHaveBeenCalled();
expect(respond).toHaveBeenCalledWith(false, undefined, {
code: "INVALID_REQUEST",
message: "invalid targeted gateway restart intent",
});
},
);
it("accepts the maximum timer-safe targeted restart wait", async () => {
const respond = await invokeRestartRequest({
reason: "operator",
target: {
pid: process.pid,
ownerId: "gateway-owner",
port: 18_789,
},
restartIntent: { waitMs: MAX_TIMER_TIMEOUT_MS },
});
expect(requestGatewayRestartWithSignalAdmission).toHaveBeenCalledWith("operator", {
reason: "operator",
waitMs: MAX_TIMER_TIMEOUT_MS,
});
expect(respond).toHaveBeenCalledWith(true, {
ok: true,
status: "emitted",
pid: process.pid,
});
});
it("backs off before an emoji that crosses the reason limit", async () => {
mockScheduledRestart({ safe: true, summary: "safe to restart now" });
+8 -6
View File
@@ -1,4 +1,5 @@
// Gateway RPC handlers for safe gateway restart requests and preflight state.
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js";
import { readActiveGatewayLockIdentity } from "../../infra/gateway-lock.js";
@@ -6,10 +7,8 @@ import {
createSafeGatewayRestartPreflight,
requestSafeGatewayRestart,
} from "../../infra/restart-coordinator.js";
import {
type GatewayRestartIntent,
requestGatewayRestartWithSignalAdmission,
} from "../../infra/restart.js";
import type { GatewayRestartIntent } from "../../infra/restart-intent.js";
import { requestGatewayRestartWithSignalAdmission } from "../../infra/restart.js";
import type { GatewayRequestHandlers } from "./types.js";
function isRestartRequestParams(value: unknown): value is Record<string, unknown> {
@@ -74,8 +73,11 @@ function parseTargetedRestartIntent(
const raw = (value ?? {}) as { force?: unknown; waitMs?: unknown };
const force = raw.force === true;
const waitMs =
typeof raw.waitMs === "number" && Number.isFinite(raw.waitMs) && raw.waitMs >= 0
? Math.floor(raw.waitMs)
typeof raw.waitMs === "number" &&
Number.isSafeInteger(raw.waitMs) &&
raw.waitMs >= 0 &&
raw.waitMs <= MAX_TIMER_TIMEOUT_MS
? raw.waitMs
: undefined;
if (
(raw.force !== undefined && typeof raw.force !== "boolean") ||