Skip to content

Commit 45fc88c

Browse files
committed
fix(gateway): health check timeout when lsof is not installed
The `ownsPort` check in `inspectGatewayRestart` only verified port ownership via `portUsage.listeners.some(...)` when `runtimePid` was known. When `lsof` is not installed, `inspectPortUsage` returns `{ status: "busy", listeners: [] }` because `checkPortInUse` detects the port is occupied but cannot enumerate listeners. `.some()` on an empty array always returns `false`, so `ownsPort` was always `false`, causing the health-check loop to spin for the full 60 s timeout on every restart. Add the same `(status === "busy" && listeners.length === 0)` fallback that already existed in the `runtimePid == null` branch so that a running service with a known PID is treated as the port owner when listener enumeration is unavailable.
1 parent 0b3bbfe commit 45fc88c

File tree

2 files changed

+27
-1
lines changed

2 files changed

+27
-1
lines changed

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,26 @@ describe("inspectGatewayRestart", () => {
123123
expect(snapshot.staleGatewayPids).toEqual([]);
124124
});
125125

126+
it("treats port as owned when runtime pid is known but listeners are empty (e.g. lsof missing)", async () => {
127+
const service = {
128+
readRuntime: vi.fn(async () => ({ status: "running", pid: 40977 })),
129+
} as unknown as GatewayService;
130+
131+
inspectPortUsage.mockResolvedValue({
132+
port: 18789,
133+
status: "busy",
134+
listeners: [],
135+
hints: [],
136+
errors: ["Error: spawn lsof ENOENT"],
137+
});
138+
139+
const { inspectGatewayRestart } = await import("./restart-health.js");
140+
const snapshot = await inspectGatewayRestart({ service, port: 18789 });
141+
142+
expect(snapshot.healthy).toBe(true);
143+
expect(snapshot.staleGatewayPids).toEqual([]);
144+
});
145+
126146
it("does not treat known non-gateway listeners as stale in fallback mode", async () => {
127147
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
128148
classifyPortListener.mockReturnValue("ssh");

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,15 @@ export async function inspectGatewayRestart(params: {
7474
: [];
7575
const running = runtime.status === "running";
7676
const runtimePid = runtime.pid;
77+
// When the port is busy but no listeners are available (e.g. lsof not installed),
78+
// optimistically assume the running service owns the port since we cannot
79+
// distinguish ownership without process enumeration tools.
7780
const ownsPort =
7881
runtimePid != null
79-
? portUsage.listeners.some((listener) => listenerOwnedByRuntimePid({ listener, runtimePid }))
82+
? portUsage.listeners.some((listener) =>
83+
listenerOwnedByRuntimePid({ listener, runtimePid }),
84+
) ||
85+
(portUsage.status === "busy" && portUsage.listeners.length === 0)
8086
: gatewayListeners.length > 0 ||
8187
(portUsage.status === "busy" && portUsage.listeners.length === 0);
8288
const healthy = running && ownsPort;

0 commit comments

Comments
 (0)