feat(wsl): detached distro holder re-adopted from a pid file (WSL-via-orcad C3) (#27800)

* feat(wsl): detached distro holder re-adopted from a pid file (WSL-via-orcad C3)

* fix(wsl): observe an early holder exit from spawn, not after the record write

* fix(wsl): release polls through a mid-exit unverifiable read until the holder's exit is proven
This commit is contained in:
Neil
2026-10-12 23:59:13 -07:00
committed by GitHub
parent d31f37c2dd
commit 099bb76501
4 changed files with 783 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
import { buildWslExecArgs } from '../../shared/wsl-login-shell-command'
/** `$0` of the holder's shell; the token that identifies our holder in a process command line. */
export const WSL_DISTRO_HOLDER_MARKER = 'orca-wsl-distro-holder'
// Why a loop of short sleeps: busybox `sleep` (Alpine) rejects `infinity`, and one huge
// duration depends on how each `sleep` implementation parses it.
const HOLDER_SCRIPT = 'while :; do sleep 3600; done'
/** `wsl.exe` argv for a holder: only a Windows-side WSL client keeps a distro from idling out. */
export function buildWslDistroHolderArgs(distro: string): string[] {
return buildWslExecArgs(distro, ['/bin/sh', '-c', HOLDER_SCRIPT, WSL_DISTRO_HOLDER_MARKER])
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
/** Whether a Windows command line is our holder for `distro` (distro names are case-insensitive). */
export function isWslDistroHolderCommandLine(commandLine: string, distro: string): boolean {
if (!commandLine.includes(WSL_DISTRO_HOLDER_MARKER) || !/wsl(\.exe)?"?\s/i.test(commandLine)) {
return false
}
const distroArg = new RegExp(`(^|\\s)-d\\s+("?)${escapeRegExp(distro)}\\2(\\s|$)`, 'i')
return distroArg.test(commandLine)
}
+149
View File
@@ -0,0 +1,149 @@
import { mkdir, readdir, readFile, rm } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import {
durableWriteTempPath,
removeStaleDurableWriteTempFiles,
writeFileDurable
} from '../durable-file-write'
/**
* One file per distro under `<userData>/wsl-distro-holders/`. Survives a full Orca quit on
* purpose: the holder does too, and the next launch re-adopts it from this record.
*/
export type WslDistroHolderRecord = {
version: 1
distro: string
pid: number
/** Process creation time (Unix ms) from the process table; null when the table could not say. */
creationTimeMs: number | null
startedAtMs: number
}
const HOLDER_DIRECTORY = 'wsl-distro-holders'
const RECORD_SUFFIX = '.json'
export function wslDistroHolderKey(distro: string): string {
return distro.trim().toLowerCase()
}
export function wslDistroHolderDirectory(userDataPath: string): string {
return join(userDataPath, HOLDER_DIRECTORY)
}
function recordPath(userDataPath: string, distro: string): string {
return join(
wslDistroHolderDirectory(userDataPath),
`${encodeURIComponent(wslDistroHolderKey(distro))}${RECORD_SUFFIX}`
)
}
function isPositiveInteger(value: unknown): value is number {
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0
}
export function parseWslDistroHolderRecord(text: string): WslDistroHolderRecord | null {
let parsed: unknown
try {
parsed = JSON.parse(text)
} catch {
return null
}
if (typeof parsed !== 'object' || parsed === null) {
return null
}
const fields = new Map<string, unknown>(Object.entries(parsed))
const version = fields.get('version')
const distro = fields.get('distro')
const pid = fields.get('pid')
const creationTimeMs = fields.get('creationTimeMs')
const startedAtMs = fields.get('startedAtMs')
if (
version !== 1 ||
typeof distro !== 'string' ||
wslDistroHolderKey(distro) === '' ||
!isPositiveInteger(pid) ||
!(creationTimeMs === null || isPositiveInteger(creationTimeMs)) ||
!isPositiveInteger(startedAtMs)
) {
return null
}
return { version, distro, pid, creationTimeMs, startedAtMs }
}
export type WslDistroHolderRecordRead =
| { status: 'absent' }
| { status: 'present'; record: WslDistroHolderRecord }
/** Present but not ours to parse, or not readable: the holder it named cannot be identified. */
| { status: 'corrupt' }
| { status: 'unreadable' }
export async function readWslDistroHolderRecord(
userDataPath: string,
distro: string
): Promise<WslDistroHolderRecordRead> {
let text: string
try {
text = await readFile(recordPath(userDataPath, distro), 'utf8')
} catch (error) {
return isMissingFileError(error) ? { status: 'absent' } : { status: 'unreadable' }
}
const record = parseWslDistroHolderRecord(text)
if (!record || wslDistroHolderKey(record.distro) !== wslDistroHolderKey(distro)) {
return { status: 'corrupt' }
}
return { status: 'present', record }
}
export async function writeWslDistroHolderRecord(
userDataPath: string,
record: WslDistroHolderRecord
): Promise<void> {
const finalPath = recordPath(userDataPath, record.distro)
await mkdir(dirname(finalPath), { recursive: true })
await writeFileDurable(durableWriteTempPath(finalPath), finalPath, `${JSON.stringify(record)}\n`)
}
export async function removeWslDistroHolderRecord(
userDataPath: string,
distro: string
): Promise<void> {
await rm(recordPath(userDataPath, distro), { force: true })
}
/**
* Distro names that have a record file; sweeps temp files a crashed writer left behind.
* Why decode the file name: the record may be corrupt, and its file is still owed a verdict.
*/
export async function listWslDistroHolderRecordDistros(userDataPath: string): Promise<string[]> {
const directory = wslDistroHolderDirectory(userDataPath)
let entries: string[]
try {
entries = await readdir(directory)
} catch (error) {
if (isMissingFileError(error)) {
return []
}
throw error
}
const distros: string[] = []
for (const entry of entries) {
if (!entry.endsWith(RECORD_SUFFIX)) {
continue
}
const path = join(directory, entry)
await removeStaleDurableWriteTempFiles(path, { minimumAgeMs: 60_000 }).catch(() => {})
try {
const distro = decodeURIComponent(entry.slice(0, -RECORD_SUFFIX.length))
if (wslDistroHolderKey(distro) !== '') {
distros.push(distro)
}
} catch {
await rm(path, { force: true })
}
}
return distros
}
function isMissingFileError(error: unknown): boolean {
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT'
}
+307
View File
@@ -0,0 +1,307 @@
import { EventEmitter } from 'node:events'
import { mkdtemp, readdir, readFile, rm, writeFile, mkdir } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { WindowsProcessEvidence } from '../daemon/daemon-incarnation-evidence-types'
import {
buildWslDistroHolderArgs,
isWslDistroHolderCommandLine,
WSL_DISTRO_HOLDER_MARKER
} from './wsl-distro-holder-command'
import {
createWslDistroHolder,
type WslDistroHolderChild,
type WslDistroHolderDependencies
} from './wsl-distro-holder'
import { wslDistroHolderDirectory, type WslDistroHolderRecord } from './wsl-distro-holder-record'
const HOLDER_COMMAND = (distro: string): string =>
`C:\\Windows\\System32\\wsl.exe -d ${distro} --exec /bin/sh -c "while :; do sleep 3600; done" ${WSL_DISTRO_HOLDER_MARKER}`
type FakeProcess = { commandLine: string | null; startedAtMs: number | null }
class FakeHost {
processes = new Map<number, FakeProcess>()
running = new Set<string>()
tableFails = false
nextPid = 4000
spawned: string[] = []
killed: number[] = []
children: (EventEmitter & { pid?: number; unref: () => void })[] = []
query = async (pid: number): Promise<WindowsProcessEvidence> => {
if (this.tableFails) {
return { status: 'unavailable' }
}
const row = this.processes.get(pid)
return row ? { status: 'present', ...row } : { status: 'missing' }
}
spawn = (distro: string): WslDistroHolderChild => {
const pid = this.nextPid++
this.spawned.push(distro)
const child = Object.assign(new EventEmitter(), { pid, unref: vi.fn() })
this.children.push(child)
this.processes.set(pid, { commandLine: HOLDER_COMMAND(distro), startedAtMs: 1_000 + pid })
this.running.add(distro.toLowerCase())
queueMicrotask(() => child.emit('spawn'))
return child
}
kill = (pid: number): void => {
this.killed.push(pid)
this.processes.delete(pid)
}
}
let userDataPath: string
let host: FakeHost
function makeHolder(overrides: Partial<WslDistroHolderDependencies> = {}) {
return createWslDistroHolder({
userDataPath,
spawnHolder: host.spawn,
queryProcess: host.query,
killProcess: host.kill,
listRunningDistros: async () => [...host.running],
sleep: async () => {},
...overrides
})
}
async function writeRecord(record: WslDistroHolderRecord): Promise<void> {
const directory = wslDistroHolderDirectory(userDataPath)
await mkdir(directory, { recursive: true })
await writeFile(
join(directory, `${encodeURIComponent(record.distro.toLowerCase())}.json`),
JSON.stringify(record)
)
}
async function recordFiles(): Promise<string[]> {
return readdir(wslDistroHolderDirectory(userDataPath)).catch(() => [])
}
function record(overrides: Partial<WslDistroHolderRecord> = {}): WslDistroHolderRecord {
return {
version: 1,
distro: 'Alpine',
pid: 77,
creationTimeMs: 5_000,
startedAtMs: 5_001,
...overrides
}
}
beforeEach(async () => {
userDataPath = await mkdtemp(join(tmpdir(), 'wsl-distro-holder-'))
host = new FakeHost()
})
afterEach(async () => {
await rm(userDataPath, { recursive: true, force: true })
})
describe('holder command line', () => {
it('runs a busybox-safe loop through --exec and names the distro', () => {
expect(buildWslDistroHolderArgs('Alpine')).toEqual([
'-d',
'Alpine',
'--exec',
'/bin/sh',
'-c',
'while :; do sleep 3600; done',
WSL_DISTRO_HOLDER_MARKER
])
})
it('matches only our holder for the same distro', () => {
expect(isWslDistroHolderCommandLine(HOLDER_COMMAND('Alpine'), 'alpine')).toBe(true)
expect(isWslDistroHolderCommandLine(HOLDER_COMMAND('Alpine-2'), 'Alpine')).toBe(false)
expect(isWslDistroHolderCommandLine('wsl.exe -d Alpine --exec sleep 9', 'Alpine')).toBe(false)
expect(
isWslDistroHolderCommandLine(`node x.js -d Alpine ${WSL_DISTRO_HOLDER_MARKER}`, 'Alpine')
).toBe(false)
})
})
describe('createWslDistroHolder', () => {
it('spawns a detached-style holder, records it, and waits for the distro to run', async () => {
const holder = makeHolder()
const result = await holder.ensureHeld('Alpine')
expect(result.outcome).toBe('spawned')
expect(host.children[0]?.unref).toHaveBeenCalled()
const saved = JSON.parse(
await readFile(join(wslDistroHolderDirectory(userDataPath), 'alpine.json'), 'utf8')
)
expect(saved).toMatchObject({ version: 1, distro: 'Alpine', pid: 4000, creationTimeMs: 5_000 })
expect(await holder.holderState('Alpine')).toBe('live')
})
it('single-flights concurrent ensures for one distro', async () => {
const holder = makeHolder()
const [a, b] = await Promise.all([holder.ensureHeld('Alpine'), holder.ensureHeld('alpine')])
expect(a).toBe(b)
expect(host.spawned).toEqual(['Alpine'])
expect((await holder.ensureHeld('Alpine')).outcome).toBe('adopted')
expect(host.spawned).toHaveLength(1)
})
it('adopts a valid holder left by a previous run without spawning another', async () => {
host.processes.set(77, { commandLine: HOLDER_COMMAND('Alpine'), startedAtMs: 5_000 })
await writeRecord(record())
const holder = makeHolder()
expect(await holder.adoptHolders()).toEqual([{ distro: 'alpine', state: 'live' }])
expect((await holder.ensureHeld('Alpine')).outcome).toBe('adopted')
expect(host.spawned).toEqual([])
})
it('rejects a reused pid (creation time differs) and drops the record', async () => {
host.processes.set(77, { commandLine: HOLDER_COMMAND('Alpine'), startedAtMs: 9_999 })
await writeRecord(record())
const holder = makeHolder()
expect(await holder.adoptHolders()).toEqual([{ distro: 'alpine', state: 'exited' }])
expect(await recordFiles()).toEqual([])
expect(host.killed).toEqual([])
})
it('rejects a pid whose command line is not our holder for that distro', async () => {
host.processes.set(77, { commandLine: HOLDER_COMMAND('Ubuntu'), startedAtMs: 5_000 })
await writeRecord(record())
expect(await makeHolder().adoptHolders()).toEqual([{ distro: 'alpine', state: 'exited' }])
expect(await recordFiles()).toEqual([])
})
it('cleans up stale and corrupt records', async () => {
await writeRecord(record({ distro: 'Gone', pid: 12 }))
const directory = wslDistroHolderDirectory(userDataPath)
await writeFile(join(directory, 'broken.json'), '{nope')
const holder = makeHolder()
const adopted = await holder.adoptHolders()
expect(adopted.sort((a, b) => a.distro.localeCompare(b.distro))).toEqual([
{ distro: 'broken', state: 'exited' },
{ distro: 'gone', state: 'exited' }
])
expect(await recordFiles()).toEqual([])
})
it('keeps the record and reports unverifiable when the process table fails', async () => {
host.processes.set(77, { commandLine: HOLDER_COMMAND('Alpine'), startedAtMs: 5_000 })
await writeRecord(record())
host.tableFails = true
const holder = makeHolder()
expect(await holder.adoptHolders()).toEqual([{ distro: 'alpine', state: 'unverifiable' }])
expect(await holder.holderState('Alpine')).toBe('unverifiable')
expect((await holder.ensureHeld('Alpine')).outcome).toBe('unverifiable')
expect(await holder.release('Alpine')).toBe('unverifiable')
expect(host.spawned).toEqual([])
expect(host.killed).toEqual([])
expect(await recordFiles()).toEqual(['alpine.json'])
})
it('reports unverifiable when the command line is unreadable even with a matching pid', async () => {
host.processes.set(77, { commandLine: null, startedAtMs: 5_000 })
await writeRecord(record())
expect(await makeHolder().holderState('Alpine')).toBe('unverifiable')
})
it('release kills a verified holder and removes its record', async () => {
const holder = makeHolder()
await holder.ensureHeld('Alpine')
expect(await holder.release('Alpine')).toBe('exited')
expect(host.killed).toEqual([4000])
expect(await recordFiles()).toEqual([])
expect(await holder.holderState('Alpine')).toBe('exited')
})
it('release never kills a pid that is no longer our holder', async () => {
host.processes.set(77, { commandLine: 'C:\\Windows\\notepad.exe', startedAtMs: 5_000 })
await writeRecord(record())
expect(await makeHolder().release('Alpine')).toBe('exited')
expect(host.killed).toEqual([])
})
it('fails and cleans up when the holder exits before its record is written', async () => {
const holder = makeHolder({
listRunningDistros: async () => [],
spawnHolder: (distro) => {
const child = host.spawn(distro)
// Right after 'spawn', before the process-table lookup resolves (a bad distro name).
queueMicrotask(() => {
host.processes.delete(child.pid ?? 0)
child.emit('exit', 1, null)
})
return child
},
bootTimeoutMs: 1_000_000
})
await expect(holder.ensureHeld('Missing')).rejects.toThrow(/exited before the distro started/)
expect(await recordFiles()).toEqual([])
})
it('stops the holder when the distro never starts', async () => {
let clock = 0
const holder = makeHolder({
listRunningDistros: async () => [],
now: () => (clock += 1_000),
bootTimeoutMs: 3_000
})
await expect(holder.ensureHeld('Alpine')).rejects.toThrow(/did not start/)
expect(host.killed).toEqual([4000])
expect(await recordFiles()).toEqual([])
})
it('release keeps polling through an unverifiable window until the exit is proven', async () => {
const holder = makeHolder()
await holder.ensureHeld('Alpine')
// Mid-exit: the pid is still listed but its command line is already unreadable.
let midExitReads = 2
host.kill = (pid) => {
host.killed.push(pid)
host.processes.set(pid, { commandLine: null, startedAtMs: 5_000 })
}
const query = host.query
host.query = async (pid) => {
if (host.killed.includes(pid) && midExitReads-- <= 0) {
host.processes.delete(pid)
}
return query(pid)
}
const released = createWslDistroHolder({
userDataPath,
queryProcess: (pid) => host.query(pid),
killProcess: (pid) => host.kill(pid),
sleep: async () => {}
})
expect(await released.release('Alpine')).toBe('exited')
expect(midExitReads).toBeLessThan(0)
expect(await recordFiles()).toEqual([])
expect(await holder.holderState('Alpine')).toBe('exited')
})
it('release reports unverifiable and keeps the record when the exit is never proven', async () => {
const holder = makeHolder()
await holder.ensureHeld('Alpine')
let clock = 0
const released = makeHolder({
killProcess: (pid) => {
host.killed.push(pid)
host.processes.set(pid, { commandLine: null, startedAtMs: 5_000 })
},
now: () => (clock += 1_000)
})
expect(await released.release('Alpine')).toBe('unverifiable')
expect(host.killed).toEqual([4000])
expect(await recordFiles()).toEqual(['alpine.json'])
})
it('serializes release behind an in-flight ensure', async () => {
const holder = makeHolder()
const ensure = holder.ensureHeld('Alpine')
const release = holder.release('Alpine')
await ensure
expect(await release).toBe('exited')
expect(host.killed).toEqual([4000])
})
})
+301
View File
@@ -0,0 +1,301 @@
import type { EventEmitter } from 'node:events'
import { spawnProcess } from '@orca/process-host'
import { queryWindowsProcess } from '../daemon/daemon-process-inspection'
import type { WindowsProcessEvidence } from '../daemon/daemon-incarnation-evidence-types'
import { listConfirmedRunningWslDistros } from '../wsl-background-distro-gate'
import { resolveWslInteropSpawnCwd } from '../wsl-interop-spawn-directory'
import { buildWslDistroHolderArgs, isWslDistroHolderCommandLine } from './wsl-distro-holder-command'
import {
listWslDistroHolderRecordDistros,
readWslDistroHolderRecord,
removeWslDistroHolderRecord,
wslDistroHolderKey,
writeWslDistroHolderRecord,
type WslDistroHolderRecord
} from './wsl-distro-holder-record'
import { resolveWslExecutablePath } from './wsl-executable-path'
/** Holder liveness, in the execution-host vocabulary: lookup failure is never `exited`. */
export type WslDistroHolderState = 'live' | 'exited' | 'unverifiable'
export type WslDistroHolderEnsureResult =
| { outcome: 'adopted'; record: WslDistroHolderRecord }
| { outcome: 'spawned'; record: WslDistroHolderRecord }
/** The recorded holder could not be checked; kept rather than doubled (it may be live). */
| { outcome: 'unverifiable'; record: WslDistroHolderRecord }
/** The slice of a ChildProcess the holder reads; `spawn`, `error` and `exit` events. */
export type WslDistroHolderChild = EventEmitter & { readonly pid?: number; unref(): void }
export type WslDistroHolderAdoption = { distro: string; state: WslDistroHolderState }
export type WslDistroHolderDependencies = {
userDataPath: string
/** Test seam; production spawns `wsl.exe` detached through process-host. */
spawnHolder?: (distro: string) => WslDistroHolderChild
queryProcess?: (pid: number) => Promise<WindowsProcessEvidence>
killProcess?: (pid: number) => void
/** Must never boot a distro: `wsl --list --running` only. */
listRunningDistros?: () => Promise<string[]>
now?: () => number
sleep?: (ms: number) => Promise<void>
/** How long a spawned holder has to bring its distro up (a cold boot can take ~15 s). */
bootTimeoutMs?: number
}
export type WslDistroHolder = {
/** User-initiated only: the one path allowed to boot a stopped distro. */
ensureHeld(distro: string): Promise<WslDistroHolderEnsureResult>
/** Startup: keep holders a previous Orca left running, drop records that are provably stale. */
adoptHolders(): Promise<WslDistroHolderAdoption[]>
/** Explicit stop only; the distro then idles out on WSL's own schedule. */
release(distro: string): Promise<WslDistroHolderState>
holderState(distro: string): Promise<WslDistroHolderState>
}
const DEFAULT_BOOT_TIMEOUT_MS = 60_000
const BOOT_POLL_INTERVAL_MS = 250
const RELEASE_EXIT_TIMEOUT_MS = 5_000
function spawnDetachedHolder(distro: string): WslDistroHolderChild {
// Why detached: an attached child sits in libuv's kill-on-close job and dies with Orca, and the
// holder must outlive a full quit. A `taskkill /T` of Orca's tree still reaches it, as for the daemon.
return spawnProcess({
program: resolveWslExecutablePath(),
args: buildWslDistroHolderArgs(distro),
// Why: outlives every worktree, so it must not pin one as its Win32 cwd.
cwd: resolveWslInteropSpawnCwd(),
env: { ...process.env, WSL_UTF8: '1' },
detached: true,
stdio: 'ignore'
})
}
/** Pid + creation time + command line must all agree; any one contradicting means pid reuse. */
export function classifyWslDistroHolderProcess(
record: WslDistroHolderRecord,
evidence: WindowsProcessEvidence
): WslDistroHolderState {
if (evidence.status === 'unavailable') {
return 'unverifiable'
}
if (evidence.status === 'missing') {
return 'exited'
}
const timeKnown = record.creationTimeMs !== null && evidence.startedAtMs !== null
if (timeKnown && record.creationTimeMs !== evidence.startedAtMs) {
return 'exited'
}
if (!evidence.commandLine) {
return 'unverifiable'
}
if (!isWslDistroHolderCommandLine(evidence.commandLine, record.distro)) {
return 'exited'
}
// A record written without a creation time never had one to compare; the command line decides.
return timeKnown || record.creationTimeMs === null ? 'live' : 'unverifiable'
}
export function createWslDistroHolder(dependencies: WslDistroHolderDependencies): WslDistroHolder {
const { userDataPath } = dependencies
const spawnHolder = dependencies.spawnHolder ?? spawnDetachedHolder
const queryProcess = dependencies.queryProcess ?? ((pid: number) => queryWindowsProcess(pid))
const killProcess = dependencies.killProcess ?? ((pid: number) => process.kill(pid))
const listRunningDistros = dependencies.listRunningDistros ?? listConfirmedRunningWslDistros
const now = dependencies.now ?? Date.now
const sleep =
dependencies.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))
const bootTimeoutMs = dependencies.bootTimeoutMs ?? DEFAULT_BOOT_TIMEOUT_MS
const tails = new Map<string, Promise<unknown>>()
const inFlightEnsures = new Map<string, Promise<WslDistroHolderEnsureResult>>()
// Ensure, release and adoption of one distro never interleave; other distros run freely.
function serialize<T>(distro: string, task: () => Promise<T>): Promise<T> {
const key = wslDistroHolderKey(distro)
const run = (tails.get(key) ?? Promise.resolve()).then(task, task)
const tail = run.catch(() => {})
tails.set(key, tail)
void tail.then(() => {
if (tails.get(key) === tail) {
tails.delete(key)
}
})
return run
}
async function recordedHolderState(
distro: string
): Promise<{ state: WslDistroHolderState; record: WslDistroHolderRecord | null }> {
const read = await readWslDistroHolderRecord(userDataPath, distro)
if (read.status === 'absent') {
return { state: 'exited', record: null }
}
if (read.status === 'unreadable') {
return { state: 'unverifiable', record: null }
}
if (read.status === 'corrupt') {
// Nothing in it can identify a process, so it holds nothing.
await removeWslDistroHolderRecord(userDataPath, distro)
return { state: 'exited', record: null }
}
const state = classifyWslDistroHolderProcess(read.record, await queryProcess(read.record.pid))
if (state === 'exited') {
await removeWslDistroHolderRecord(userDataPath, distro)
}
return { state, record: read.record }
}
type HolderExit = { code: number | null; signal: NodeJS.Signals | null }
async function waitForDistroRunning(
distro: string,
observedExit: () => HolderExit | null
): Promise<void> {
const deadline = now() + bootTimeoutMs
const key = wslDistroHolderKey(distro)
for (;;) {
const exit = observedExit()
if (exit) {
throw new Error(
`WSL holder for ${distro} exited before the distro started (code ${exit.code}, signal ${exit.signal})`
)
}
const running = await listRunningDistros()
if (running.some((candidate) => wslDistroHolderKey(candidate) === key)) {
return
}
if (now() >= deadline) {
throw new Error(`WSL distro ${distro} did not start within ${bootTimeoutMs} ms`)
}
await sleep(BOOT_POLL_INTERVAL_MS)
}
}
async function spawnAndRecord(distro: string): Promise<WslDistroHolderRecord> {
const child = spawnHolder(distro)
// Listen from the start: a bad distro name makes wsl.exe exit before the record is written.
const observed: { exit: HolderExit | null } = { exit: null }
const onExit = (code: number | null, signal: NodeJS.Signals | null): void => {
observed.exit = { code, signal }
}
child.once('exit', onExit)
try {
await new Promise<void>((resolve, reject) => {
child.once('spawn', resolve)
child.once('error', reject)
})
// The holder must never keep this process alive; it is the other way around.
child.unref()
const pid = child.pid
if (pid === undefined) {
throw new Error(`WSL holder for ${distro} started without a pid`)
}
const evidence = await queryProcess(pid)
const record: WslDistroHolderRecord = {
version: 1,
distro,
pid,
creationTimeMs: evidence.status === 'present' ? evidence.startedAtMs : null,
startedAtMs: now()
}
await writeWslDistroHolderRecord(userDataPath, record)
try {
await waitForDistroRunning(distro, () => observed.exit)
} catch (error) {
await stopRecordedHolder(record)
throw error
}
return record
} finally {
child.off('exit', onExit)
}
}
async function stopRecordedHolder(record: WslDistroHolderRecord): Promise<WslDistroHolderState> {
let state = classifyWslDistroHolderProcess(record, await queryProcess(record.pid))
if (state === 'live') {
try {
// wsl.exe re-launches itself as a child with the same argv; ending the recorded parent ends both (WSL 2.7.10).
killProcess(record.pid)
} catch {
// Raced its own exit, or denied; the re-check below decides.
}
const deadline = now() + RELEASE_EXIT_TIMEOUT_MS
for (;;) {
state = classifyWslDistroHolderProcess(record, await queryProcess(record.pid))
// A holder mid-exit can read unverifiable (pid present, command line gone); only `exited` is proof.
if (state === 'exited' || now() >= deadline) {
break
}
await sleep(BOOT_POLL_INTERVAL_MS)
}
}
if (state === 'exited') {
await removeWslDistroHolderRecord(userDataPath, record.distro)
}
return state
}
return {
ensureHeld(distro) {
const key = wslDistroHolderKey(distro)
if (!key) {
return Promise.reject(new Error('A WSL distro name is required'))
}
const existing = inFlightEnsures.get(key)
if (existing) {
return existing
}
const pending = serialize(distro, async (): Promise<WslDistroHolderEnsureResult> => {
const { state, record } = await recordedHolderState(distro)
if (record && state === 'live') {
return { outcome: 'adopted', record }
}
if (record && state === 'unverifiable') {
return { outcome: 'unverifiable', record }
}
if (state === 'unverifiable') {
throw new Error(`The WSL holder record for ${distro} could not be read`)
}
return { outcome: 'spawned', record: await spawnAndRecord(distro) }
}).finally(() => {
if (inFlightEnsures.get(key) === pending) {
inFlightEnsures.delete(key)
}
})
inFlightEnsures.set(key, pending)
return pending
},
async adoptHolders() {
const distros = await listWslDistroHolderRecordDistros(userDataPath)
return Promise.all(
distros.map((distro) =>
serialize(distro, async () => ({
distro,
state: (await recordedHolderState(distro)).state
}))
)
)
},
release(distro) {
return serialize(distro, async () => {
const read = await readWslDistroHolderRecord(userDataPath, distro)
if (read.status === 'unreadable') {
return 'unverifiable'
}
if (read.status !== 'present') {
await removeWslDistroHolderRecord(userDataPath, distro)
return 'exited'
}
return stopRecordedHolder(read.record)
})
},
holderState(distro) {
return serialize(distro, async () => (await recordedHolderState(distro)).state)
}
}
}