diff --git a/src/main/wsl-background-distro-gate.test.ts b/src/main/wsl-background-distro-gate.test.ts index 75b226a9034..c806e209b9e 100644 --- a/src/main/wsl-background-distro-gate.test.ts +++ b/src/main/wsl-background-distro-gate.test.ts @@ -14,6 +14,7 @@ import { isWslDistroRunningForBackgroundWork, noteWslDistroMayHaveStopped, noteWslDistrosRunning, + readSettledConfirmedRunningWslDistros, WSL_STOP_SETTLE_MS } from './wsl-background-distro-gate' import { @@ -82,6 +83,28 @@ describe('the stop settle window', () => { }) }) +describe('readSettledConfirmedRunningWslDistros', () => { + it('never answers from a probe that started inside the window, even one still in flight', async () => { + let finishStale: (distros: string[]) => void = () => {} + const probe = vi + .fn<() => Promise>() + .mockImplementationOnce(() => new Promise((resolve) => (finishStale = resolve))) + .mockResolvedValueOnce([]) + wslMocks.listRunningWslDistrosAsync.mockImplementation((options) => + resolveRunningWslDistros(probe, options) + ) + const stale = isWslDistroRunning('Ubuntu') + await vi.waitFor(() => expect(probe).toHaveBeenCalledOnce()) + + const settled = readSettledConfirmedRunningWslDistros('Ubuntu', performance.now()) + finishStale(['Ubuntu']) + + await expect(stale).resolves.toBe(true) + await expect(settled).resolves.toEqual({ status: 'confirmed', distros: [] }) + expect(probe).toHaveBeenCalledTimes(2) + }) +}) + describe('resolveRunningWslDistros startedAfter', () => { it('answers from a confirmed read that started after the anchor, and reads again for a later one', async () => { const probe = vi.fn(async () => ['Ubuntu']) diff --git a/src/main/wsl-background-distro-gate.ts b/src/main/wsl-background-distro-gate.ts index 457f9b6cf3e..fbea1a44d82 100644 --- a/src/main/wsl-background-distro-gate.ts +++ b/src/main/wsl-background-distro-gate.ts @@ -91,6 +91,16 @@ async function settledStartedAfter( return anchor } +/** A confirmed list from a probe started after `startedAfter` and after `distro`'s stop settled. */ +export async function readSettledConfirmedRunningWslDistros( + distro: string, + startedAfter?: number +): Promise { + return readConfirmedRunningWslDistros({ + startedAfter: await settledStartedAfter(distro, startedAfter) + }) +} + export type WslDistroRunningState = 'running' | 'stopped' | 'unverifiable' /** A failed read is 'unverifiable', never a stop; `retryUnverifiable` re-reads once past the failure backoff. */ diff --git a/src/main/wsl/wsl-orcad-liveness-state.ts b/src/main/wsl/wsl-orcad-liveness-state.ts index b295600f3dc..d06ccd2e79e 100644 --- a/src/main/wsl/wsl-orcad-liveness-state.ts +++ b/src/main/wsl/wsl-orcad-liveness-state.ts @@ -19,7 +19,11 @@ export type WslOrcadLivenessDependencies = { WslOrcadConnectionManager, 'ensure' | 'get' | 'publish' | 'close' | 'retireListener' > - readRunning: () => Promise + /** `afterExit`: answer only from a probe started once that exit's settle window has passed. */ + readRunning: (afterExit?: { + distro: string + exitedAt: number | null + }) => Promise readIdentity: (distro: string, orcadPid: number | null) => Promise openWatch: (connection: WslOrcadConnection) => Promise collectCensus: (connection: WslOrcadConnection) => Promise diff --git a/src/main/wsl/wsl-orcad-liveness-supervisor.test.ts b/src/main/wsl/wsl-orcad-liveness-supervisor.test.ts index 2720f26a4be..5f6667fcfa9 100644 --- a/src/main/wsl/wsl-orcad-liveness-supervisor.test.ts +++ b/src/main/wsl/wsl-orcad-liveness-supervisor.test.ts @@ -169,6 +169,7 @@ function harness(options: { visible?: boolean } = {}) { jumpClock: (ms: number) => { clock += ms }, + clock: () => clock, lastWatch: () => watches.at(-1)! } } @@ -403,6 +404,17 @@ describe('WslOrcadLivenessSupervisor', () => { expect(h.statuses.at(-1)).toEqual({ state: 'ready', census: null }) }) + it('re-reads after the settle only from a probe started once the exit has settled', async () => { + const h = harness() + await h.supervisor.connect('Ubuntu') + h.jumpClock(1_000) + const exitedAt = h.clock() + h.lastWatch().exit(1) + await h.advance(EXIT_SETTLE_MS) + // A list probe begun inside the window may still show the stopping distro; it must not answer. + expect(h.readRunning).toHaveBeenNthCalledWith(2, { distro: 'Ubuntu', exitedAt }) + }) + it('waits for a still-listed distro to settle after a failed exit before probing inside it', async () => { const h = harness() await h.supervisor.connect('Ubuntu') diff --git a/src/main/wsl/wsl-orcad-liveness-supervisor.ts b/src/main/wsl/wsl-orcad-liveness-supervisor.ts index cf769a4560f..f99e0ffa675 100644 --- a/src/main/wsl/wsl-orcad-liveness-supervisor.ts +++ b/src/main/wsl/wsl-orcad-liveness-supervisor.ts @@ -227,7 +227,7 @@ export class WslOrcadLivenessSupervisor { if (isWatchOrHolderExit(trigger) && confirmsRunning(running, entry.key)) { // Stopping distros can still appear running; wait before exec accidentally boots them again. await new Promise((resolve) => this.schedule(EXIT_SETTLE_MS, resolve)) - running = await this.deps.readRunning() + running = await this.deps.readRunning({ distro: entry.distro, exitedAt: entry.lastExitMs }) if (entry.generation !== generation) { return } diff --git a/src/main/wsl/wsl-orcad-liveness-wiring.ts b/src/main/wsl/wsl-orcad-liveness-wiring.ts index efe2580c669..4a60617dd20 100644 --- a/src/main/wsl/wsl-orcad-liveness-wiring.ts +++ b/src/main/wsl/wsl-orcad-liveness-wiring.ts @@ -9,14 +9,15 @@ import { requestOrcadTerminalCensus } from '../ssh/orcad-terminal-census-client' import { subscribeSystemPowerLifecycle } from '../system-power-lifecycle' import { deferUntilWslDistroRunning, - readConfirmedRunningWslDistros + readConfirmedRunningWslDistros, + readSettledConfirmedRunningWslDistros } from '../wsl-background-distro-gate' import type { WslDistroHolder } from './wsl-distro-holder' import { wslDistroHolderKey } from './wsl-distro-holder-record' import { readWslGuestIdentity } from './wsl-guest-process-identity' import type { WslOrcadConnection, WslOrcadConnectionManager } from './wsl-orcad-connection' import { defaultWslOrcadEnvironmentId } from './wsl-orcad-deploy' -import { confirmsRunning } from './wsl-orcad-liveness-evidence' +import { confirmsRunning, EXIT_SETTLE_MS } from './wsl-orcad-liveness-evidence' import { WslOrcadLivenessSupervisor } from './wsl-orcad-liveness-supervisor' import { createWslOrcadHostTransport } from './wsl-orcad-host-transport' import { openWslOrcadWatch } from './wsl-orcad-watch' @@ -42,7 +43,13 @@ export function createWslOrcadLivenessSupervisor(options: { }): WslOrcadLivenessSupervisor { return new WslOrcadLivenessSupervisor({ manager: options.manager, - readRunning: readConfirmedRunningWslDistros, + readRunning: (afterExit) => + afterExit + ? readSettledConfirmedRunningWslDistros( + afterExit.distro, + afterExit.exitedAt === null ? undefined : afterExit.exitedAt + EXIT_SETTLE_MS + ) + : readConfirmedRunningWslDistros(), readIdentity: (distro, orcadPid) => readWslGuestIdentity(transportFor(distro), orcadPid), openWatch: (connection) => openWslOrcadWatch(