diff --git a/src/codex/commands.ts b/src/codex/commands.ts index d9f0ede6..570b710f 100644 --- a/src/codex/commands.ts +++ b/src/codex/commands.ts @@ -168,7 +168,7 @@ export function collectCodexStatus(fsPort: HeadroomFs, paths: LayoutPaths, isRun usageSnapshotPath: codexUsageSnapshotPath(), usageSnapshotExists: fsPort.readFileUtf8(codexUsageSnapshotPath()) !== undefined, logPath: paths.frontdoorLogPath, - logExists: fsPort.readFileUtf8(paths.frontdoorLogPath) !== undefined, + logExists: fsPort.lstat(paths.frontdoorLogPath) !== undefined, }; } diff --git a/src/frontdoor/server.ts b/src/frontdoor/server.ts index de02c1d8..4a1147ed 100644 --- a/src/frontdoor/server.ts +++ b/src/frontdoor/server.ts @@ -15,6 +15,10 @@ const HEALTH_TIMEOUT_MS = 2_000; const HEALTH_START_BUDGET_MS = 5_000; const HEALTH_RETRY_MS = 50; +/** How many attempts listenFrontDoor makes to bind preferredPort before falling back to an ephemeral port. */ +const PREFERRED_PORT_MAX_ATTEMPTS = 6; +const PREFERRED_PORT_RETRY_MS = 50; + /** A front-door listener: plain HTTP for the direct listener only headroom reaches, HTTPS for the provider listener every provider session's child connects to. */ export type FrontDoorServer = http.Server | https.Server; @@ -166,17 +170,26 @@ export async function listenFrontDoor(server: FrontDoorServer, options: Readonly resolve(bound); }); }); - let bound: number; - if (preferredPort !== undefined) { - try { - bound = await tryListen(preferredPort); - } catch { - // The sticky port was taken between generations; any free port will do, and the supervisor logs the move. - bound = await tryListen(0); + const bindListener = async (): Promise => { + if (preferredPort === undefined) { + return await tryListen(0); } - } else { - bound = await tryListen(0); - } + for (let attempt = 1; attempt <= PREFERRED_PORT_MAX_ATTEMPTS; attempt += 1) { + try { + return await tryListen(preferredPort); + } catch { + if (attempt === PREFERRED_PORT_MAX_ATTEMPTS) { + // The sticky port was taken between generations; any free port will do, and the supervisor logs the move. + return await tryListen(0); + } + await new Promise((resolve) => { + setTimeout(resolve, PREFERRED_PORT_RETRY_MS); + }); + } + } + return await tryListen(0); + }; + const bound = await bindListener(); options.onBound?.(bound); if (options.onError !== undefined) { server.on("error", options.onError); diff --git a/src/frontdoor/status.ts b/src/frontdoor/status.ts index c0ddb327..ad3632a7 100644 --- a/src/frontdoor/status.ts +++ b/src/frontdoor/status.ts @@ -42,7 +42,7 @@ export function collectFrontDoorStatus(fsPort: HeadroomFs, socketTrust: Headroom sessions: listFrontDoorSessions(fsPort, paths.frontdoorSessionsDir).map((session) => ({ ...session, alive: isRunning(session.pid) })), headroomSocket: headroomSocketTarget(fsPort, socketTrust, paths), logPath: paths.frontdoorLogPath, - logExists: fsPort.readFileUtf8(paths.frontdoorLogPath) !== undefined, + logExists: fsPort.lstat(paths.frontdoorLogPath) !== undefined, }; } diff --git a/src/headroom/commands.ts b/src/headroom/commands.ts index 25c96308..61102f3f 100644 --- a/src/headroom/commands.ts +++ b/src/headroom/commands.ts @@ -101,7 +101,7 @@ export function collectHeadroomStatus(fsPort: HeadroomFs, paths: LayoutPaths, pr settings, settingsDrifted: state.settingsHash !== undefined && state.settingsHash !== hashSettings(settings), logPath: paths.headroomLogPath, - logExists: fsPort.readFileUtf8(paths.headroomLogPath) !== undefined, + logExists: fsPort.lstat(paths.headroomLogPath) !== undefined, }; } diff --git a/src/headroom/state.ts b/src/headroom/state.ts index 58317577..3e8c7017 100644 --- a/src/headroom/state.ts +++ b/src/headroom/state.ts @@ -45,7 +45,7 @@ export type HeadroomState = z.infer; /** The filesystem operations headroom coordination needs: the same shape as `FarmFs`, injected so every decision over state, sessions, and the start lock runs against an in-memory fake in tests. */ export type HeadroomFs = Pick< FarmFs, - "mkdirp" | "mkdirPrivate" | "readFileUtf8" | "writeFileUtf8" | "writeFilePrivate" | "writeFileExclusive" | "readdir" | "removeRecursive" + "lstat" | "mkdirp" | "mkdirPrivate" | "readFileUtf8" | "writeFileUtf8" | "writeFilePrivate" | "writeFileExclusive" | "readdir" | "removeRecursive" >; /**