Skip to content

Commit c381034

Browse files
authored
CLI: avoid false update restart failures without listener attribution (openclaw#39508)
1 parent e0f80cf commit c381034

File tree

2 files changed

+37
-3
lines changed

2 files changed

+37
-3
lines changed

src/cli/daemon-cli/restart-health.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,26 @@ describe("inspectGatewayRestart", () => {
198198

199199
expect(snapshot.healthy).toBe(true);
200200
});
201+
202+
it("treats busy ports with unavailable listener details as healthy when runtime is running", async () => {
203+
const service = {
204+
readRuntime: vi.fn(async () => ({ status: "running", pid: 8000 })),
205+
} as unknown as GatewayService;
206+
207+
inspectPortUsage.mockResolvedValue({
208+
port: 18789,
209+
status: "busy",
210+
listeners: [],
211+
hints: [
212+
"Port is in use but process details are unavailable (install lsof or run as an admin user).",
213+
],
214+
errors: ["Error: spawn lsof ENOENT"],
215+
});
216+
217+
const { inspectGatewayRestart } = await import("./restart-health.js");
218+
const snapshot = await inspectGatewayRestart({ service, port: 18789 });
219+
220+
expect(snapshot.healthy).toBe(true);
221+
expect(probeGateway).not.toHaveBeenCalled();
222+
});
201223
});

src/cli/daemon-cli/restart-health.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ export type GatewayPortHealthSnapshot = {
2828
healthy: boolean;
2929
};
3030

31+
function hasListenerAttributionGap(portUsage: PortUsage): boolean {
32+
if (portUsage.status !== "busy" || portUsage.listeners.length > 0) {
33+
return false;
34+
}
35+
if (portUsage.errors?.length) {
36+
return true;
37+
}
38+
return portUsage.hints.some((hint) => hint.includes("process details are unavailable"));
39+
}
40+
3141
function listenerOwnedByRuntimePid(params: {
3242
listener: PortUsage["listeners"][number];
3343
runtimePid: number;
@@ -131,11 +141,13 @@ export async function inspectGatewayRestart(params: {
131141
: [];
132142
const running = runtime.status === "running";
133143
const runtimePid = runtime.pid;
144+
const listenerAttributionGap = hasListenerAttributionGap(portUsage);
134145
const ownsPort =
135146
runtimePid != null
136-
? portUsage.listeners.some((listener) => listenerOwnedByRuntimePid({ listener, runtimePid }))
137-
: gatewayListeners.length > 0 ||
138-
(portUsage.status === "busy" && portUsage.listeners.length === 0);
147+
? portUsage.listeners.some((listener) =>
148+
listenerOwnedByRuntimePid({ listener, runtimePid }),
149+
) || listenerAttributionGap
150+
: gatewayListeners.length > 0 || listenerAttributionGap;
139151
let healthy = running && ownsPort;
140152
if (!healthy && running && portUsage.status === "busy") {
141153
try {

0 commit comments

Comments
 (0)