fix(wsl): flag-on liveness and post-launch WSL steps use settled running reads (E9) (#35433)

L1: the orcad liveness supervisor's heartbeat/verify reads, its recovery gate, the no-boot
reconnect check and the connector's passive probe now take a settled read of their own distro, so
a list probed while the distro is still stopping cannot admit a holder spawn, identity read or
reconnect that would boot it again.
L2: a Git cancel/timeout's guest kill, the WSL move's process table and each step of the Codex
session bridge chain check the distro first. A confirmed stop counts as the group gone / an empty
table / the end of the chain; an unanswered read proves nothing and spawns nothing.
This commit is contained in:
Neil
2026-10-28 17:32:29 -07:00
committed by GitHub
parent b685340243
commit bc6a5ec32e
15 changed files with 363 additions and 36 deletions
@@ -11,6 +11,8 @@ const { runWslProcessMock, stopping } = vi.hoisted(() => ({
stopping: { value: false }
}))
// The E9 running check has its own cases; these cover the chain once the distro runs.
vi.mock('../wsl/wsl-unc-access-gate', () => ({ assertWslUncPathRunning: async () => {} }))
vi.mock('../wsl/wsl-runner', () => ({
runWslProcess: runWslProcessMock
}))
@@ -225,4 +227,91 @@ describe('startWslCodexSessionBridgeInBackground', () => {
expect(openStateDb).not.toHaveBeenCalled()
expect(runWslProcessMock).not.toHaveBeenCalled()
})
it('links nothing in a distro that stopped after the launch that started the chain (E9)', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const openStateDb = vi.fn(async () => true)
await startWslCodexSessionBridgeInBackground(TARGET, {
openStateDb,
healPending: vi.fn(),
requireRunning: async () => {
throw new Error("WSL distro 'Ubuntu' is not running.")
}
})
expect(openStateDb).not.toHaveBeenCalled()
expect(runWslProcessMock).not.toHaveBeenCalled()
warn.mockRestore()
})
it('ends the index heal before its next batch once the distro stops (E9)', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
runWslProcessMock
.mockResolvedValueOnce(bridgeResult([NEWER]))
.mockResolvedValue(markerClearResult)
let running = true
let stoppedBeforeNextBatch: boolean | undefined
const healPending = vi.fn<typeof healPendingCodexAccountThreads>(
async (_home, _pending, options = {}) => {
running = false
await options
.afterBatch?.([{ threadId: '22222222-2222-4222-8222-222222222222', outcome: 'healed' }])
.catch(() => {})
stoppedBeforeNextBatch = options.shouldStop?.()
return {
outcome: 'stopped' as const,
healedThreads: 1,
missingThreads: 0,
failedThreads: 0
}
}
)
await startWslCodexSessionBridgeInBackground(TARGET, {
openStateDb: vi.fn(async () => true),
healPending,
requireRunning: async () => {
if (!running) {
throw new Error("WSL distro 'Ubuntu' is not running.")
}
}
})
expect(stoppedBeforeNextBatch).toBe(true)
// Only the link step ran; the marker clear after the stop never spawned.
expect(runWslProcessMock).toHaveBeenCalledTimes(1)
warn.mockRestore()
})
it('ends the index heal when a running-list probe sees the distro stop (E9)', async () => {
runWslProcessMock
.mockResolvedValueOnce(bridgeResult([NEWER]))
.mockResolvedValue(markerClearResult)
let emitStop: (stopped: string[]) => void = () => {}
let stopped: boolean | undefined
const healPending = vi.fn<typeof healPendingCodexAccountThreads>(
async (_home, _pending, options = {}) => {
emitStop(['ubuntu'])
stopped = options.shouldStop?.()
return {
outcome: 'stopped' as const,
healedThreads: 0,
missingThreads: 0,
failedThreads: 0
}
}
)
await startWslCodexSessionBridgeInBackground(TARGET, {
openStateDb: vi.fn(async () => true),
healPending,
requireRunning: async () => {},
subscribeStops: (listener) => {
emitStop = (distros) => listener(distros, [])
return () => {}
}
})
expect(stopped).toBe(true)
})
})
@@ -8,6 +8,8 @@ const { runWslProcessMock } = vi.hoisted(() => ({
runWslProcessMock: vi.fn()
}))
// The E9 running check has its own cases; these cover the chain once the distro runs.
vi.mock('../wsl/wsl-unc-access-gate', () => ({ assertWslUncPathRunning: async () => {} }))
vi.mock('../wsl/wsl-runner', () => ({
runWslProcess: runWslProcessMock
}))
+35 -6
View File
@@ -2,6 +2,8 @@ import { posix as pathPosix } from 'node:path'
import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path'
import { parseWslUncPath } from '../../shared/wsl-paths'
import { runWslProcess } from '../wsl/wsl-runner'
import { assertWslUncPathRunning } from '../wsl/wsl-unc-access-gate'
import { onRunningWslDistrosStopped } from '../wsl-running-distro-cache'
import { isCodexAccountSessionBridgeStopping } from './codex-account-session-bridge'
import {
createCodexAccountStateDb,
@@ -57,11 +59,16 @@ const WSL_SESSION_BRIDGE_MAX_OUTPUT_BYTES = 64 * 1024 * 1024
type WslBridgeDependencies = {
openStateDb: typeof createCodexAccountStateDb
healPending: typeof healPendingCodexAccountThreads
/** Throws unless the distro runs; each step outlives the launch that started the chain (E9). */
requireRunning: (distro: string) => Promise<void>
subscribeStops: typeof onRunningWslDistrosStopped
}
const defaultWslBridgeDependencies: WslBridgeDependencies = {
openStateDb: createCodexAccountStateDb,
healPending: healPendingCodexAccountThreads
healPending: healPendingCodexAccountThreads,
requireRunning: (distro) => assertWslUncPathRunning('', distro),
subscribeStops: onRunningWslDistrosStopped
}
const emptySummary: WslCodexSessionBridgeSummary = {
@@ -119,7 +126,10 @@ async function bridgeAndIndexWslCodexSessions(
const buildInvocation = (_codexHomePath: string, timeoutMs: number) =>
buildWslHealInvocation(target.distro, paths.managedHomePath, timeoutMs)
const homeKey = normalizeRuntimePathForComparison(target.managedCodexHomePath)
// Every wsl.exe step below re-checks: the user may stop the distro mid-chain, and a step would boot it.
const requireRunning = (): Promise<void> => dependencies.requireRunning(target.distro)
if (!homesWithCompleteIndex.has(homeKey)) {
await requireRunning()
// Why: linking before Codex's first open makes that open index everything,
// and every other Codex on the home gives up after 30s (#20669).
const complete = await dependencies.openStateDb(target.managedCodexHomePath, {
@@ -134,6 +144,7 @@ async function bridgeAndIndexWslCodexSessions(
if (isCodexAccountSessionBridgeStopping()) {
return
}
await requireRunning()
const summary = await syncWslCodexSessionsIntoManagedHome(target)
const pendingThreads = new Map<string, string>()
const markerByThreadId = new Map<string, string>()
@@ -149,6 +160,7 @@ async function bridgeAndIndexWslCodexSessions(
}
if (markersWithoutThread.length > 0) {
// Why: no thread id means thread/read can never settle it, so it would be reported forever.
await requireRunning()
await clearIndexPendingMarkers(target.distro, paths, markersWithoutThread).catch(
(error: unknown) => {
console.warn('[codex-session-bridge] Could not clear WSL index markers:', error)
@@ -156,12 +168,29 @@ async function bridgeAndIndexWslCodexSessions(
)
}
if (pendingThreads.size > 0 && !isCodexAccountSessionBridgeStopping()) {
await dependencies.healPending(target.managedCodexHomePath, pendingThreads, {
buildInvocation,
shouldStop: isCodexAccountSessionBridgeStopping,
afterBatch: (settled) =>
clearSettledIndexPendingMarkers(target.distro, paths, settled, markerByThreadId)
await requireRunning()
// Batches spawn synchronously from here on; a confirmed stop ends the heal before the next one.
const distroKey = target.distro.toLowerCase()
let distroStopped = false
const unsubscribe = dependencies.subscribeStops((stopped) => {
distroStopped ||= stopped.some((distro) => distro.toLowerCase() === distroKey)
})
try {
await dependencies.healPending(target.managedCodexHomePath, pendingThreads, {
buildInvocation,
shouldStop: () => distroStopped || isCodexAccountSessionBridgeStopping(),
afterBatch: async (settled) => {
// Also the check before the next batch: a refused read ends the heal there.
await requireRunning().catch((error: unknown) => {
distroStopped = true
throw error
})
await clearSettledIndexPendingMarkers(target.distro, paths, settled, markerByThreadId)
}
})
} finally {
unsubscribe()
}
}
if (summary.bridgeFailed) {
throw new Error(`WSL codex session bridge could not link every session for ${target.distro}`)
+11 -3
View File
@@ -308,7 +308,13 @@ describe('WSL direct Git reads', () => {
return command
}
const terminator = createMockChild()
queueMicrotask(() => terminator.emit('close', 0, null))
queueMicrotask(() => {
// The kill first confirms the distro still runs, so it cannot boot a stopped one (E9).
if (args?.includes('--running')) {
terminator.stdout.emit('data', Buffer.from(`${DISTRO}\n`))
}
terminator.emit('close', 0, null)
})
return terminator
})
const controller = new AbortController()
@@ -326,8 +332,10 @@ describe('WSL direct Git reads', () => {
await expect(pending).rejects.toMatchObject({ name: 'AbortError' })
expect(command.kill).not.toHaveBeenCalled()
expect(spawnMock.mock.calls[1]?.[1]?.join(' ')).toContain('kill -TERM')
expect(spawnMock.mock.calls[1]?.[1]).toContain('4321')
const kill = spawnMock.mock.calls.find(([, args]) =>
String(args?.join(' ')).includes('kill -TERM')
)
expect(kill?.[1]).toContain('4321')
})
})
@@ -1,9 +1,17 @@
import type { ChildProcess } from 'node:child_process'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as BackgroundDistroGate from '../wsl-background-distro-gate'
const { runWslProcessMock } = vi.hoisted(() => ({ runWslProcessMock: vi.fn() }))
const { runWslProcessMock, readWslDistroRunningStateMock } = vi.hoisted(() => ({
runWslProcessMock: vi.fn(),
readWslDistroRunningStateMock: vi.fn()
}))
vi.mock('../wsl/wsl-runner', () => ({ runWslProcess: runWslProcessMock }))
vi.mock('../wsl-background-distro-gate', async (importOriginal) => ({
...(await importOriginal<typeof BackgroundDistroGate>()),
readWslDistroRunningState: readWslDistroRunningStateMock
}))
import { createWslProcessGroupTermination } from './wsl-process-group-termination'
@@ -11,6 +19,8 @@ describe('WSL process-group termination', () => {
beforeEach(() => {
runWslProcessMock.mockReset()
runWslProcessMock.mockResolvedValue({ code: 0, timedOut: false })
// Windows CI runs the real gate; these cases kill in a running distro.
readWslDistroRunningStateMock.mockReset().mockResolvedValue('running')
})
it('wraps the guest command in a reported process group', () => {
@@ -92,4 +102,45 @@ describe('WSL process-group termination', () => {
await expect(termination.signal({} as ChildProcess)).resolves.toBe(true)
expect(runWslProcessMock.mock.calls[0]?.[0]?.args).toEqual(['4321'])
})
describe('on Windows (E9)', () => {
const originalPlatform = process.platform
beforeEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
})
afterEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the guest barrier never reads the host child.
const child = {} as ChildProcess
function reportedGroup(): ReturnType<typeof createWslProcessGroupTermination> {
const termination = createWslProcessGroupTermination('Ubuntu')
const wrapped = termination.wrapGuestArgs(['git', 'fetch']).join(' ')
const marker = wrapped.match(/(__ORCA_WSL_PROCESS_GROUP_[0-9a-f-]+__=)/)?.[1]
termination.observeStderr?.(Buffer.from(`${marker}4321\n`))
return termination
}
it('treats a group in a stopped distro as gone instead of booting it to kill', async () => {
readWslDistroRunningStateMock.mockResolvedValue('stopped')
await expect(reportedGroup().force(child)).resolves.toBe(true)
expect(runWslProcessMock).not.toHaveBeenCalled()
})
it('claims nothing when WSL cannot say whether the distro runs', async () => {
readWslDistroRunningStateMock.mockResolvedValue('unverifiable')
await expect(reportedGroup().signal(child)).resolves.toBe(false)
expect(runWslProcessMock).not.toHaveBeenCalled()
})
it('kills in a running distro as before', async () => {
readWslDistroRunningStateMock.mockResolvedValue('running')
await expect(reportedGroup().signal(child)).resolves.toBe(true)
expect(runWslProcessMock).toHaveBeenCalledOnce()
})
})
})
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto'
import type { ProcessTerminationBarrier } from '@orca/process-host/process-spec'
import { quotePosixShell } from '../../shared/shell-argument-quoting'
import { runWslProcess } from '../wsl/wsl-runner'
import { blockedWslDistroForPath } from '../wsl/wsl-unc-access-gate'
const GUEST_TERMINATION_ATTEMPTS = 40
const GUEST_TERMINATION_INTERVAL_SECONDS = '0.025'
@@ -32,6 +33,12 @@ export function createWslProcessGroupTermination(distro: string): WslProcessGrou
if (processGroupId === null) {
return false
}
// A cancel or timeout can land after the distro stopped; a kill would boot it again (E9).
// A confirmed stop already ended the group, while an unanswered read proves nothing.
const blocked = await blockedWslDistroForPath('', distro)
if (blocked) {
return blocked.state === 'stopped'
}
const script = [
'_orca_group=$1',
`kill -${signal} "-$_orca_group" 2>/dev/null || :`,
@@ -0,0 +1,56 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as BackgroundDistroGate from '../wsl-background-distro-gate'
const mocks = vi.hoisted(() => ({
runWslProcess: vi.fn(),
readWslDistroRunningState: vi.fn()
}))
vi.mock('./wsl-runner', () => ({ runWslProcess: mocks.runWslProcess }))
vi.mock('../wsl-background-distro-gate', async (importOriginal) => ({
...(await importOriginal<typeof BackgroundDistroGate>()),
readWslDistroRunningState: mocks.readWslDistroRunningState
}))
import { readWslGuestProcessTable } from './wsl-guest-process-table'
import { runAsWslUserAct } from './wsl-unc-access-gate'
describe('readWslGuestProcessTable never boots a stopped distro (E9)', () => {
const originalPlatform = process.platform
beforeEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
mocks.runWslProcess.mockReset().mockResolvedValue({
environmentResolved: true,
code: 0,
stdout: '',
stderr: '',
timedOut: false
})
mocks.readWslDistroRunningState.mockReset().mockResolvedValue('stopped')
})
afterEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
})
it('reports an empty table for a stopped distro without listing it', async () => {
await expect(readWslGuestProcessTable('Ubuntu')).resolves.toEqual([])
expect(mocks.runWslProcess).not.toHaveBeenCalled()
})
it('reports an unreadable table when WSL cannot say whether the distro runs', async () => {
mocks.readWslDistroRunningState.mockResolvedValue('unverifiable')
await expect(readWslGuestProcessTable('Ubuntu')).resolves.toBeNull()
expect(mocks.runWslProcess).not.toHaveBeenCalled()
})
it('lists a running distro, and a user move may list a stopped one', async () => {
mocks.readWslDistroRunningState.mockResolvedValue('running')
await readWslGuestProcessTable('Ubuntu')
expect(mocks.runWslProcess).toHaveBeenCalledTimes(1)
mocks.readWslDistroRunningState.mockResolvedValue('stopped')
await runAsWslUserAct('Debian', () => readWslGuestProcessTable('Debian'))
expect(mocks.runWslProcess).toHaveBeenCalledTimes(2)
})
})
+13 -4
View File
@@ -8,6 +8,7 @@ import {
type WslGuestProcessRow
} from '../persistence/moving-wsl-projects/wsl-move-guest-foreground'
import { runWslProcess } from './wsl-runner'
import { blockedWslDistroForPath } from './wsl-unc-access-gate'
const readsInFlight = new Map<string, Promise<WslGuestProcessRow[] | null>>()
@@ -19,7 +20,18 @@ export function readWslGuestProcessTable(distro: string): Promise<WslGuestProces
if (inFlight) {
return inFlight
}
const read = runWslProcess({
const read = readRunningGuestProcessTable(distro).finally(() => readsInFlight.delete(key))
readsInFlight.set(key, read)
return read
}
async function readRunningGuestProcessTable(distro: string): Promise<WslGuestProcessRow[] | null> {
// An automatic move must not boot the distro to list it (E9); a stopped one runs nothing.
const blocked = await blockedWslDistroForPath('', distro)
if (blocked) {
return blocked.state === 'stopped' ? [] : null
}
return runWslProcess({
distro,
script: WSL_GUEST_PANE_PROCESS_SCRIPT,
loginPath: 'none',
@@ -32,7 +44,4 @@ export function readWslGuestProcessTable(distro: string): Promise<WslGuestProces
: parseWslGuestPaneProcesses(result.stdout)
)
.catch(() => null)
.finally(() => readsInFlight.delete(key))
readsInFlight.set(key, read)
return read
}
+9 -2
View File
@@ -4,7 +4,7 @@ import type { WslRunningDistrosRead } from '../wsl-background-distro-gate'
import type * as TunnelIdentity from '../managed-server/lifecycle/orcad-managed-tunnel-identity'
const mocks = vi.hoisted(() => ({
readRunning: vi.fn<() => Promise<WslRunningDistrosRead>>(),
readRunning: vi.fn<(distro: string) => Promise<WslRunningDistrosRead>>(),
connect: vi.fn(),
stopServer: vi.fn(async () => ({ stopped: true })),
verify: vi.fn(),
@@ -22,7 +22,8 @@ vi.mock('./wsl-orcad-connection', async (importOriginal) => ({
}
}))
vi.mock('../wsl-background-distro-gate', () => ({
readConfirmedRunningWslDistros: mocks.readRunning,
// Settled per distro: a list taken while it stops still names it (E9).
readSettledConfirmedRunningWslDistros: mocks.readRunning,
listConfirmedRunningWslDistros: vi.fn(async () => [])
}))
vi.mock('../managed-server/lifecycle/orcad-managed-tunnel-identity', async (importOriginal) => ({
@@ -57,6 +58,7 @@ it('uses owned readiness without discovery and coalesces cold cleanup without wa
await expect(passive).resolves.toBe(false)
await expect(removal).resolves.toEqual({ stopped: true })
expect(mocks.readRunning).toHaveBeenCalledTimes(1)
expect(mocks.readRunning).toHaveBeenCalledWith('Ubuntu')
mocks.readRunning.mockResolvedValueOnce({ status: 'unavailable' })
await expect(connector.stopServer('Ubuntu', 'wsl:ubuntu')).resolves.toMatchObject({
stopped: false
@@ -120,6 +122,11 @@ it('a no-boot call joins a booting connect but stays no-boot itself (E9)', async
'Ubuntu',
expect.objectContaining({ requireRunning: expect.any(Function) })
)
// That check is a settled read, so a distro still stopping counts as stopped.
const [, ensureOptions] = mocks.managerEnsure.mock.lastCall ?? []
mocks.readRunning.mockClear().mockResolvedValueOnce({ status: 'confirmed', distros: ['Ubuntu'] })
await expect(ensureOptions?.requireRunning('Ubuntu')).resolves.toBe(true)
expect(mocks.readRunning).toHaveBeenCalledWith('Ubuntu')
finish()
await first
})
+5 -4
View File
@@ -1,7 +1,7 @@
import { WslOrcadBridgeActivity } from './wsl-orcad-bridge-activity'
import { listenWslOrcadStdioBridge } from './wsl-orcad-stdio-bridge'
import { createWslOrcadPairingRecovery } from './wsl-orcad-pairing-repair'
import { readConfirmedRunningWslDistros } from '../wsl-background-distro-gate'
import { readSettledConfirmedRunningWslDistros } from '../wsl-background-distro-gate'
import { parsePairingCode } from '../../shared/pairing'
import { pairedRuntimeConnectionFromOffer } from '../../shared/paired-runtime-connection'
import { verifyOrcadPairingIdentity } from '../managed-server/lifecycle/orcad-managed-tunnel-identity'
@@ -100,9 +100,10 @@ export function createWslHostConnector(options: {
string,
{ promise: Promise<WslOrcadConnection>; noBoot: boolean; intent: boolean }
>()
// Read right before a no-boot reconnect spawns anything in the distro (E9).
// Read right before a no-boot reconnect spawns anything in the distro (E9); settled, since a
// list taken while the distro stops still names it.
const requireRunning = async (distro: string): Promise<boolean> =>
confirmsRunning(await readConfirmedRunningWslDistros(), wslDistroHolderKey(distro))
confirmsRunning(await readSettledConfirmedRunningWslDistros(distro), wslDistroHolderKey(distro))
const coldProbes = new Map<
string,
{
@@ -117,7 +118,7 @@ export function createWslHostConnector(options: {
connection: WslOrcadConnection | null
): Promise<Reachability> => {
// Bridge dialing spawns wsl.exe; passive checks must first prove the distro runs.
const running = await readConfirmedRunningWslDistros()
const running = await readSettledConfirmedRunningWslDistros(distro)
if (running.status === 'unavailable') {
return 'unavailable'
}
+6 -3
View File
@@ -19,10 +19,13 @@ export type WslOrcadLivenessDependencies = {
WslOrcadConnectionManager,
'ensure' | 'get' | 'publish' | 'close' | 'retireListener'
>
/** `afterExit`: answer only from a probe started once that exit's settle window has passed. */
readRunning: (afterExit?: {
/**
* A settled read for `distro`: never a list probed while it may still be stopping (E9). With
* `exitedAt`, also only a probe started once that exit's settle window has passed.
*/
readRunning: (target: {
distro: string
exitedAt: number | null
exitedAt?: number | null
}) => Promise<WslRunningDistrosRead>
readIdentity: (distro: string, orcadPid: number | null) => Promise<WslGuestIdentity | null>
openWatch: (connection: WslOrcadConnection) => Promise<WslOrcadWatch>
@@ -73,7 +73,9 @@ function harness(options: { visible?: boolean } = {}) {
return connection
}
)
const readRunning = vi.fn(async () => state.running)
const readRunning = vi.fn(
async (_target: { distro: string; exitedAt?: number | null }) => state.running
)
const requireRunning = vi.fn(async (_distro: string) => true)
const readIdentity = vi.fn(async () => state.identity)
const collectCensus = vi.fn(async () => CENSUS)
@@ -343,6 +345,10 @@ describe('WslOrcadLivenessSupervisor', () => {
for (const [, opts] of recoveryCalls) {
expect(opts).toMatchObject({ requireRunning: h.requireRunning })
}
// Its backoff gate reads too: every read names the distro, so the wiring can settle it.
for (const [target] of h.readRunning.mock.calls) {
expect(target).toMatchObject({ distro: 'Ubuntu' })
}
})
it('reports ready itself when a user connect supersedes a recovery', async () => {
@@ -427,6 +433,15 @@ describe('WslOrcadLivenessSupervisor', () => {
expect(h.readRunning).toHaveBeenNthCalledWith(2, { distro: 'Ubuntu', exitedAt })
})
it('asks for a settled read of its own distro on every check, not a bare list (E9)', async () => {
const h = harness()
await h.supervisor.connect('Ubuntu')
h.lastWatch().exit(1)
await h.settle()
// The wiring answers a distro-scoped read only from a probe past that distro's stop settle.
expect(h.readRunning).toHaveBeenNthCalledWith(1, { distro: 'Ubuntu' })
})
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')
@@ -219,7 +219,7 @@ export class WslOrcadLivenessSupervisor {
private async run(entry: Supervised, trigger: Trigger): Promise<void> {
const generation = entry.generation
let running = await this.deps.readRunning()
let running = await this.deps.readRunning({ distro: entry.distro })
if (entry.generation !== generation) {
return
}
@@ -325,7 +325,7 @@ export class WslOrcadLivenessSupervisor {
ensure: (options) => this.deps.manager.ensure(entry.distro, { ...options, requireRunning }),
collectCensus: this.deps.collectCensus,
arm: (connection) => this.arm(entry, connection),
readRunning: this.deps.readRunning
readRunning: () => this.deps.readRunning({ distro: entry.distro })
},
reason
)
@@ -0,0 +1,50 @@
import { beforeEach, expect, it, vi } from 'vitest'
import type { WslOrcadLivenessDependencies } from './wsl-orcad-liveness-state'
const mocks = vi.hoisted(() => {
const captured: { deps: WslOrcadLivenessDependencies | null } = { deps: null }
return { readSettled: vi.fn(), captured }
})
vi.mock('../wsl-background-distro-gate', () => ({
deferUntilWslDistroRunning: vi.fn(),
readSettledConfirmedRunningWslDistros: mocks.readSettled
}))
vi.mock('./wsl-orcad-liveness-supervisor', () => ({
WslOrcadLivenessSupervisor: class {
constructor(deps: WslOrcadLivenessDependencies) {
mocks.captured.deps = deps
}
}
}))
import { createWslOrcadLivenessSupervisor } from './wsl-orcad-liveness-wiring'
import { EXIT_SETTLE_MS } from './wsl-orcad-liveness-evidence'
beforeEach(() => {
mocks.readSettled.mockReset().mockResolvedValue({ status: 'confirmed', distros: ['Ubuntu'] })
createWslOrcadLivenessSupervisor({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the captured deps under test never touch these.
manager: {} as never,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the captured deps under test never touch these.
bridgeActivity: {} as never,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the captured deps under test never touch these.
holder: {} as never,
hasUserVisibleWork: () => false
})
})
it('reads a settled running list for heartbeats and recovery, not one probed mid-stop (E9)', async () => {
await mocks.captured.deps?.readRunning({ distro: 'Ubuntu' })
expect(mocks.readSettled).toHaveBeenLastCalledWith('Ubuntu', undefined)
await mocks.captured.deps?.readRunning({ distro: 'Ubuntu', exitedAt: 1_000 })
expect(mocks.readSettled).toHaveBeenLastCalledWith('Ubuntu', 1_000 + EXIT_SETTLE_MS)
})
it('checks a no-boot reconnect against a settled read of its own distro (E9)', async () => {
await expect(mocks.captured.deps?.requireRunning?.('Ubuntu')).resolves.toBe(true)
expect(mocks.readSettled).toHaveBeenLastCalledWith('Ubuntu')
mocks.readSettled.mockResolvedValue({ status: 'confirmed', distros: [] })
await expect(mocks.captured.deps?.requireRunning?.('Ubuntu')).resolves.toBe(false)
})
+9 -9
View File
@@ -13,7 +13,6 @@ import { requestOrcadTerminalCensus } from '../ssh/orcad-terminal-census-client'
import { subscribeSystemPowerLifecycle } from '../system-power-lifecycle'
import {
deferUntilWslDistroRunning,
readConfirmedRunningWslDistros,
readSettledConfirmedRunningWslDistros
} from '../wsl-background-distro-gate'
import type { WslDistroHolder } from './wsl-distro-holder'
@@ -48,13 +47,11 @@ export function createWslOrcadLivenessSupervisor(options: {
}): WslOrcadLivenessSupervisor {
return new WslOrcadLivenessSupervisor({
manager: options.manager,
readRunning: (afterExit) =>
afterExit
? readSettledConfirmedRunningWslDistros(
afterExit.distro,
afterExit.exitedAt === null ? undefined : afterExit.exitedAt + EXIT_SETTLE_MS
)
: readConfirmedRunningWslDistros(),
readRunning: ({ distro, exitedAt }) =>
readSettledConfirmedRunningWslDistros(
distro,
exitedAt === null || exitedAt === undefined ? undefined : exitedAt + EXIT_SETTLE_MS
),
readIdentity: (distro, orcadPid) => readWslGuestIdentity(transportFor(distro), orcadPid),
openWatch: (connection) =>
probeThenWatchWslOrcadBridges(
@@ -70,7 +67,10 @@ export function createWslOrcadLivenessSupervisor(options: {
hasUserVisibleWork: options.hasUserVisibleWork,
deferUntilRunning: deferUntilWslDistroRunning,
requireRunning: async (distro) =>
confirmsRunning(await readConfirmedRunningWslDistros(), wslDistroHolderKey(distro)),
confirmsRunning(
await readSettledConfirmedRunningWslDistros(distro),
wslDistroHolderKey(distro)
),
ensureHolder: (distro) => options.holder.ensureHeld(distro),
subscribePower: subscribeSystemPowerLifecycle
})