'use strict' /** * update-gate.ts * * Pure, dependency-injected gate that parks local backend spawns while an * in-app update is running (#73822, #50238). * * Two independent signals mean "an update owns the venv right now": * * - the on-disk marker (`HERMES_HOME/.hermes-update-in-progress`), written * by the updater — and by the desktop itself just before hand-off — and * - the in-process `updateInFlight` flag, true for the whole * `applyUpdates()` critical section. * * The marker alone is NOT enough (#73822): `applyUpdates` kills its own * backend early (`releaseBackendLock`) but only writes the marker AFTER the * Windows venv-blocker scan. Killing the backend drops the renderer's * WebSocket, the renderer reconnects within ~1s, and a marker-only gate * happily spawns a fresh backend inside the update's own critical section — * which `scanVenvBlockers` then reports as a blocker, aborting every update * attempt forever. Consulting the flag closes that window. On the success * path the marker is written BEFORE the flag clears in `applyUpdates`' * `finally`, so there is no instant where both signals are false and a * waiter could slip through mid-update. */ export type UpdateGateReason = 'marker' | 'update-in-flight' | null export interface UpdateGateDeps { /** True when a live on-disk update marker exists (see update-marker.ts). */ hasLiveMarker: () => boolean /** True while this process is inside applyUpdates()' critical section. */ isUpdateInFlight: () => boolean } /** Why the gate is closed right now, or null when it is open. */ export function updateGateReason(deps: UpdateGateDeps): UpdateGateReason { if (deps.hasLiveMarker()) { return 'marker' } if (deps.isUpdateInFlight()) { return 'update-in-flight' } return null } export type UpdateClearanceOutcome = 'clear' | 'finished' | 'timeout' export interface WaitForUpdateClearanceOptions { timeoutMs: number pollMs: number /** Invoked once per poll while parked (boot progress / logging). */ onWaitTick?: (reason: Exclude) => void | Promise now?: () => number sleep?: (ms: number) => Promise } /** * Park until no update signal remains, or the deadline passes. * * Returns 'clear' when the gate was already open (no wait happened), * 'finished' when it opened during the wait, and 'timeout' when the deadline * expired with the gate still closed (callers proceed anyway — matching the * long-standing marker-gate behavior, since a wedged updater must not brick * the app forever). */ export async function waitForUpdateClearance( deps: UpdateGateDeps, options: WaitForUpdateClearanceOptions ): Promise { const now = options.now || Date.now const sleep = options.sleep || (ms => new Promise(r => setTimeout(r, ms))) let reason = updateGateReason(deps) if (!reason) { return 'clear' } const deadline = now() + options.timeoutMs while (reason && now() < deadline) { if (options.onWaitTick) { await options.onWaitTick(reason) } await sleep(options.pollMs) reason = updateGateReason(deps) } return reason ? 'timeout' : 'finished' }