From c3810346f9451e4ef7089f0fc94bd9c0f902e60b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 7 Mar 2026 21:42:25 -0800 Subject: [PATCH] CLI: avoid false update restart failures without listener attribution (#39508) --- src/cli/daemon-cli/restart-health.test.ts | 22 ++++++++++++++++++++++ src/cli/daemon-cli/restart-health.ts | 18 +++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/cli/daemon-cli/restart-health.test.ts b/src/cli/daemon-cli/restart-health.test.ts index 88f8aa524c6..0202f591cc2 100644 --- a/src/cli/daemon-cli/restart-health.test.ts +++ b/src/cli/daemon-cli/restart-health.test.ts @@ -198,4 +198,26 @@ describe("inspectGatewayRestart", () => { expect(snapshot.healthy).toBe(true); }); + + it("treats busy ports with unavailable listener details as healthy when runtime is running", async () => { + const service = { + readRuntime: vi.fn(async () => ({ status: "running", pid: 8000 })), + } as unknown as GatewayService; + + inspectPortUsage.mockResolvedValue({ + port: 18789, + status: "busy", + listeners: [], + hints: [ + "Port is in use but process details are unavailable (install lsof or run as an admin user).", + ], + errors: ["Error: spawn lsof ENOENT"], + }); + + const { inspectGatewayRestart } = await import("./restart-health.js"); + const snapshot = await inspectGatewayRestart({ service, port: 18789 }); + + expect(snapshot.healthy).toBe(true); + expect(probeGateway).not.toHaveBeenCalled(); + }); }); diff --git a/src/cli/daemon-cli/restart-health.ts b/src/cli/daemon-cli/restart-health.ts index fb0ecf0c12f..00b6b4e98b3 100644 --- a/src/cli/daemon-cli/restart-health.ts +++ b/src/cli/daemon-cli/restart-health.ts @@ -28,6 +28,16 @@ export type GatewayPortHealthSnapshot = { healthy: boolean; }; +function hasListenerAttributionGap(portUsage: PortUsage): boolean { + if (portUsage.status !== "busy" || portUsage.listeners.length > 0) { + return false; + } + if (portUsage.errors?.length) { + return true; + } + return portUsage.hints.some((hint) => hint.includes("process details are unavailable")); +} + function listenerOwnedByRuntimePid(params: { listener: PortUsage["listeners"][number]; runtimePid: number; @@ -131,11 +141,13 @@ export async function inspectGatewayRestart(params: { : []; const running = runtime.status === "running"; const runtimePid = runtime.pid; + const listenerAttributionGap = hasListenerAttributionGap(portUsage); const ownsPort = runtimePid != null - ? portUsage.listeners.some((listener) => listenerOwnedByRuntimePid({ listener, runtimePid })) - : gatewayListeners.length > 0 || - (portUsage.status === "busy" && portUsage.listeners.length === 0); + ? portUsage.listeners.some((listener) => + listenerOwnedByRuntimePid({ listener, runtimePid }), + ) || listenerAttributionGap + : gatewayListeners.length > 0 || listenerAttributionGap; let healthy = running && ownsPort; if (!healthy && running && portUsage.status === "busy") { try {