ci(wsl): real WSL2 lane for WSL via orcad (C10) (#27921)

* ci(wsl): real WSL2 lane for orcad deploy, bridge, liveness and holder (WSL-via-orcad C10)

* ci(wsl): upload the template's dot-directories; save the rootfs cache on failure

* ci(wsl): update the runner's WSL to 3.0.1+ before asserting WSLInterop survives

* test(wsl): let a released holder settle before asserting it exited

* ci(wsl): install a pinned, cached WSL 3.0.1 MSI instead of wsl --update

* test(wsl): keep the lane's survival seconds typed in its assertion message
This commit is contained in:
Neil
2026-10-13 07:09:02 -07:00
committed by GitHub
parent aa7afabb1d
commit fc7dff8ce1
2 changed files with 695 additions and 0 deletions
+220
View File
@@ -0,0 +1,220 @@
name: WSL orcad E2E
# WSL via orcad (step C10) on real WSL2 distros: a GitHub-hosted windows-2025 runner imports a
# pinned Ubuntu 24.04 rootfs twice (systemd off and on) and runs
# src/main/wsl/wsl-orcad-real-distro-lane.test.ts against both: deploy and launch, the wsl.exe
# stdio bridge, runtime RPC, liveness recovery, holder survival and teardown. Not a required check.
# Hosted WSL2 was proven in #27783: WSL 2.7.14, import 13-18 s, cold boot 8-14 s.
on:
pull_request:
paths:
- "src/main/wsl/**"
- "src/main/ssh/orcad-*"
- "src/main/ssh/remote-install-transport.ts"
- ".github/workflows/wsl-orcad-e2e.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: wsl-orcad-e2e-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
# wsl.exe prints UTF-16LE to pipes unless told otherwise.
WSL_UTF8: "1"
ROOTFS_URL: https://releases.ubuntu.com/24.04.4/ubuntu-24.04.4-wsl-amd64.wsl
ROOTFS_SHA256: 9b2f7730dc68227dd04a9f3e5eab86ad85caf556b8606ad94f1f29ff5c4fd3f5
WSL_MSI_URL: https://github.com/microsoft/WSL/releases/download/3.0.1/wsl.3.0.1.0.x64.msi
WSL_MSI_SHA256: 28b1a0d013640a2ac95898ea705fa186e5b4ff767a1c1b49257161bc106599c6
PLAIN_DISTRO: orca-lane-plain
SYSTEMD_DISTRO: orca-lane-systemd
jobs:
# The distro is linux-x64-glibc; its node-pty slot only builds on Linux, and nothing else is needed.
template:
name: Build the linux-x64-glibc orcad template
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: node
- name: Build the template
run: |
pnpm build:orcad-prebuilds
pnpm build:orcad-prebuilds --require-slots linux-x64-glibc
pnpm build:orcad
node config/scripts/build-orcad-template.mjs --targets linux-x64-glibc
- uses: actions/upload-artifact@v7
with:
name: wsl-orcad-template
path: out/orcad-template/
# The template carries dot-directories (`.claude-plugin`) its manifest checksums.
include-hidden-files: true
retention-days: 1
if-no-files-found: error
wsl2:
name: WSL2 distros (systemd off and on)
needs: template
runs-on: windows-2025
timeout-minutes: 45
env:
ORCA_BACKGROUND_LAUNCH: "1"
ORCA_WSL_ORCAD_LANE_RECEIPTS: ${{ github.workspace }}\test-results\wsl-orcad-lane
defaults:
run:
shell: pwsh
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
# The image ships WSL 2.7.14, where stopping a systemd distro drops the VM-wide WSLInterop
# entry (microsoft/WSL#13885, fixed in 3.0.1). The lane asserts the fixed behavior, so it
# installs a pinned 3.0.1 MSI; `wsl --update` asks the GitHub API and gets rate-limited (403).
- name: Restore the pinned WSL package
id: wsl-msi-cache
uses: actions/cache/restore@v5
with:
path: ${{ runner.temp }}/wsl-msi
key: wsl-orcad-msi-${{ env.WSL_MSI_SHA256 }}
- name: Install WSL 3.0.1
timeout-minutes: 8
run: |
"ImageOS=$env:ImageOS ImageVersion=$env:ImageVersion"
wsl.exe --version
$dir = Join-Path $env:RUNNER_TEMP 'wsl-msi'
New-Item -ItemType Directory -Force $dir | Out-Null
$msi = Join-Path $dir 'wsl.msi'
if (-not (Test-Path $msi)) { & "$env:WINDIR\System32\curl.exe" --fail --location --silent --show-error --output $msi $env:WSL_MSI_URL }
if ((Get-FileHash $msi -Algorithm SHA256).Hash.ToLowerInvariant() -ne $env:WSL_MSI_SHA256) {
Remove-Item $msi; throw 'WSL package checksum mismatch'
}
$install = Start-Process msiexec.exe -ArgumentList @('/i', $msi, '/qn', '/norestart', '/l*v', (Join-Path $env:RUNNER_TEMP 'wsl-msi.log')) -Wait -PassThru
if ($install.ExitCode -notin 0, 3010) { throw "WSL install failed: $($install.ExitCode)" }
wsl.exe --shutdown
$version = [version](((wsl.exe --version) -join "`n") -replace '(?s)^\D*?(\d+\.\d+\.\d+).*$', '$1')
wsl.exe --version
if ($version -lt [version]'3.0.1') { throw "WSL $version is older than 3.0.1" }
- name: Save the pinned WSL package
if: steps.wsl-msi-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ${{ runner.temp }}/wsl-msi
key: wsl-orcad-msi-${{ env.WSL_MSI_SHA256 }}
- name: Restore the pinned rootfs
id: rootfs-cache
uses: actions/cache/restore@v5
with:
path: ${{ runner.temp }}/wsl-rootfs
key: wsl-orcad-rootfs-${{ env.ROOTFS_SHA256 }}
# Runs beside the dependency install; the lane waits for it below.
- name: Import both distros as WSL2
id: import
background: true
timeout-minutes: 10
run: |
$cache = Join-Path $env:RUNNER_TEMP 'wsl-rootfs'
New-Item -ItemType Directory -Force $cache | Out-Null
$rootfs = Join-Path $cache 'ubuntu.wsl'
if (-not (Test-Path $rootfs)) { Invoke-WebRequest $env:ROOTFS_URL -OutFile $rootfs }
if ((Get-FileHash $rootfs -Algorithm SHA256).Hash.ToLowerInvariant() -ne $env:ROOTFS_SHA256) {
Remove-Item $rootfs; throw 'rootfs checksum mismatch'
}
foreach ($d in @(@{ Name = $env:PLAIN_DISTRO; Systemd = 'false' }, @{ Name = $env:SYSTEMD_DISTRO; Systemd = 'true' })) {
$sw = [Diagnostics.Stopwatch]::StartNew()
# No WSL1 fallback: the lane exists to prove WSL2.
wsl.exe --import $d.Name (Join-Path $env:RUNNER_TEMP $d.Name) $rootfs --version 2
if ($LASTEXITCODE -ne 0) { throw "WSL2 import of $($d.Name) failed: $LASTEXITCODE" }
"$($d.Name) import_ms=$($sw.ElapsedMilliseconds)"
if (-not ((wsl.exe --list --verbose) -match "$($d.Name)\s+\S+\s+2\s*$")) { throw "$($d.Name) is not a WSL2 distro" }
# A non-root default user, as on a real install; git for the lane's repo.
$setup = "set -e; id orca >/dev/null 2>&1 || useradd -m -s /bin/bash orca; " +
"printf '[boot]\nsystemd=$($d.Systemd)\n[user]\ndefault=orca\n' > /etc/wsl.conf; " +
"command -v git >/dev/null || (apt-get update -qq && apt-get install -y -qq git)"
wsl.exe -d $d.Name -u root --exec sh -c $setup
if ($LASTEXITCODE -ne 0) { throw "provisioning $($d.Name) failed: $LASTEXITCODE" }
wsl.exe --terminate $d.Name
$sw.Restart()
wsl.exe -d $d.Name --exec sh -c 'echo "user=$(id -un) pid1=$(ps -p 1 -o comm=) kernel=$(uname -r)"'
if ($LASTEXITCODE -ne 0) { throw "first boot of $($d.Name) failed: $LASTEXITCODE" }
"$($d.Name) boot_ms=$($sw.ElapsedMilliseconds)"
wsl.exe --terminate $d.Name
}
wsl.exe --list --verbose
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: node
- uses: actions/download-artifact@v8
with:
name: wsl-orcad-template
path: ${{ runner.temp }}/orcad-template
- wait: import
# Saved even when the lane fails; the import step verified its checksum.
- name: Save the pinned rootfs
if: steps.rootfs-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ${{ runner.temp }}/wsl-rootfs
key: wsl-orcad-rootfs-${{ env.ROOTFS_SHA256 }}
- name: "Phase 1: deploy, RPC, kill orcad, terminate distro"
timeout-minutes: 20
env:
ORCA_RUN_WSL_ORCAD_LANE: "1"
ORCA_WSL_ORCAD_LANE_PHASE: launch
ORCA_ORCAD_TEMPLATE_PATH: ${{ runner.temp }}/orcad-template
run: |
New-Item -ItemType Directory -Force $env:ORCA_WSL_ORCAD_LANE_RECEIPTS | Out-Null
node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/wsl/wsl-orcad-real-distro-lane.test.ts --reporter=verbose 2>&1 | Tee-Object (Join-Path $env:ORCA_WSL_ORCAD_LANE_RECEIPTS 'launch.log')
if ($global:LASTEXITCODE -ne 0) { throw "launch phase failed: $global:LASTEXITCODE" }
# A new process: the holders must have kept both distros Running since phase 1 exited.
- name: "Phase 2: holders survive, reconnect, teardown"
timeout-minutes: 10
env:
ORCA_RUN_WSL_ORCAD_LANE: "1"
ORCA_WSL_ORCAD_LANE_PHASE: survive
ORCA_ORCAD_TEMPLATE_PATH: ${{ runner.temp }}/orcad-template
run: |
node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/wsl/wsl-orcad-real-distro-lane.test.ts --reporter=verbose 2>&1 | Tee-Object (Join-Path $env:ORCA_WSL_ORCAD_LANE_RECEIPTS 'survive.log')
if ($global:LASTEXITCODE -ne 0) { throw "survive phase failed: $global:LASTEXITCODE" }
# Belt and braces for the runner: a missing VM-wide interop entry is restored, then fails.
- name: WSLInterop is intact
if: ${{ !cancelled() && steps.import.outcome == 'success' }}
timeout-minutes: 3
run: |
$state = wsl.exe -d $env:PLAIN_DISTRO -u root --exec sh -c 'if [ -e /proc/sys/fs/binfmt_misc/WSLInterop ]; then echo present; else echo :WSLInterop:M::MZ::/init:P > /proc/sys/fs/binfmt_misc/register && echo restored; fi'
"WSLInterop: $state"
wsl.exe --terminate $env:PLAIN_DISTRO
if ("$state".Trim() -ne 'present') { throw "WSLInterop was missing ($state)" }
- name: Collect orcad logs
if: failure()
timeout-minutes: 3
run: |
$out = $env:ORCA_WSL_ORCAD_LANE_RECEIPTS
New-Item -ItemType Directory -Force $out | Out-Null
foreach ($d in @($env:PLAIN_DISTRO, $env:SYSTEMD_DISTRO)) {
wsl.exe -d $d --exec sh -c 'for f in $(find "$HOME/.orca-remote" -name "*.log*" -type f 2>/dev/null); do echo "=== $f"; tail -n 400 "$f"; done' > (Join-Path $out "$d-orcad-logs.txt") 2>&1
}
wsl.exe --list --verbose
- name: Summary
if: always()
run: |
$dir = $env:ORCA_WSL_ORCAD_LANE_RECEIPTS
$lines = @("### WSL orcad lane — $env:ImageOS $env:ImageVersion", '')
foreach ($phase in 'launch', 'survive') {
$file = Join-Path $dir "$phase.json"
if (Test-Path $file) { $lines += "#### $phase", '```json', (Get-Content $file -Raw), '```' }
}
$lines | Add-Content $env:GITHUB_STEP_SUMMARY
- uses: actions/upload-artifact@v7
if: always()
with:
name: wsl-orcad-lane-receipts
path: test-results/wsl-orcad-lane/
retention-days: 7
if-no-files-found: ignore
@@ -0,0 +1,475 @@
// WSL via orcad against real WSL2 distros (step C10): deploy and launch (C4), connect over the
// wsl.exe stdio bridge (C5), drive runtime RPC through it, and prove liveness (C6) on one
// systemd-off and one systemd-on distro. Two phases, each its own process, so the holder is
// proven to outlive the process that spawned it:
// launch - deploy, RPC, kill orcad => relaunch + census, `wsl --terminate` => terminals exited
// survive - >= 60 s after launch exited: distros still Running, holders adopted live, reconnect,
// teardown leaves no bridge or holder processes, WSLInterop intact
// .github/workflows/wsl-orcad-e2e.yml imports the distros and runs both phases.
//
// Run: ORCA_RUN_WSL_ORCAD_LANE=1 ORCA_WSL_ORCAD_LANE_PHASE=launch|survive pnpm test <this file>
import { randomBytes } from 'node:crypto'
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
vi.mock('electron', () => ({ app: { getAppPath: () => process.cwd() } }))
import { runProcess } from '@orca/process-host'
import { setAppEnvironment } from '../../shared/app-environment'
import { ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES } from '../../shared/electron-remote-runtime-client-capabilities'
import { pairedRuntimeConnectionFromOffer } from '../../shared/paired-runtime-connection'
import { parsePairingCode } from '../../shared/pairing'
import { sendRemoteRuntimeRequest } from '../../shared/remote-runtime-client'
import { buildWslExecArgs } from '../../shared/wsl-login-shell-command'
import { requestOrcadTerminalCensus } from '../ssh/orcad-terminal-census-client'
import { execCommand } from '../ssh/ssh-relay-exec-command'
import { readConfirmedRunningWslDistros } from '../wsl-background-distro-gate'
import { readWindowsProcessTableFresh } from '../windows/windows-process-table'
import { resolveWslInteropSpawnCwd } from '../wsl-interop-spawn-directory'
import { createWslDistroHolder, type WslDistroHolder } from './wsl-distro-holder'
import { WSL_DISTRO_HOLDER_MARKER } from './wsl-distro-holder-command'
import { resolveWslExecutablePath } from './wsl-executable-path'
import { wslDistroHolderKey } from './wsl-distro-holder-record'
import { WslOrcadConnectionManager, type WslOrcadConnection } from './wsl-orcad-connection'
import type { WslOrcadConnectionStatus } from './wsl-orcad-connection-status'
import { defaultWslOrcadEnvironmentId } from './wsl-orcad-deploy'
import { createWslOrcadHostTransport } from './wsl-orcad-host-transport'
import type { WslOrcadLivenessSupervisor } from './wsl-orcad-liveness-supervisor'
import { createWslOrcadLivenessSupervisor } from './wsl-orcad-liveness-wiring'
const RUN = process.env.ORCA_RUN_WSL_ORCAD_LANE === '1'
const PHASE = process.env.ORCA_WSL_ORCAD_LANE_PHASE ?? 'launch'
const PLAIN = process.env.ORCA_WSL_ORCAD_LANE_PLAIN_DISTRO ?? 'orca-lane-plain'
const SYSTEMD = process.env.ORCA_WSL_ORCAD_LANE_SYSTEMD_DISTRO ?? 'orca-lane-systemd'
const DISTROS = [
{ name: PLAIN, systemd: false },
{ name: SYSTEMD, systemd: true }
]
const WORK_DIR = process.env.ORCA_WSL_ORCAD_LANE_DIR ?? join(tmpdir(), 'orca-wsl-orcad-lane')
const USER_DATA = join(WORK_DIR, 'userData')
const STATE_FILE = join(WORK_DIR, 'launch-state.json')
const RECEIPT_DIR = process.env.ORCA_WSL_ORCAD_LANE_RECEIPTS ?? join(WORK_DIR, 'receipts')
const HOLDER_SURVIVAL_MS = 60_000
const DISTRO_TIMEOUT_MS = 12 * 60_000
const BRIDGE_PATTERN = '[o]rcadStdioBridge'
const WSL_INTEROP = '/proc/sys/fs/binfmt_misc/WSLInterop'
type LaunchState = {
launchEndedAtMs: number
orcadPids: Record<string, number | null>
}
type Receipt = Record<string, unknown>
const receipt: Receipt = { phase: PHASE }
const interopChecks: Receipt[] = []
receipt.wslInterop = interopChecks
const statuses = new Map<string, WslOrcadConnectionStatus[]>()
let visibleWork = true
let holder: WslDistroHolder
let manager: WslOrcadConnectionManager
let supervisor: WslOrcadLivenessSupervisor | null = null
function installLaneAppEnvironment(): void {
const { version } = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8'))
setAppEnvironment({
getPath: (name) => (name === 'userData' ? USER_DATA : tmpdir()),
getAppPath: () => process.cwd(),
getVersion: () => version,
isPackaged: () => false,
onWillQuit: () => {},
exit: () => {},
getAppMetrics: () => []
})
}
function statusLog(distro: string): WslOrcadConnectionStatus[] {
const key = wslDistroHolderKey(distro)
const log = statuses.get(key) ?? []
statuses.set(key, log)
return log
}
function transportFor(distro: string) {
return createWslOrcadHostTransport({
target: {
kind: 'wsl',
id: defaultWslOrcadEnvironmentId(distro),
distro,
label: distro
}
})
}
/** As the distro's default user, through the same transport orcad's deploy uses. */
function guest(distro: string, command: string): Promise<string> {
return execCommand(transportFor(distro), command, { timeoutMs: 60_000 })
}
async function wslExe(args: string[], timeoutMs = 60_000) {
const result = await runProcess({
program: resolveWslExecutablePath(),
args,
cwd: resolveWslInteropSpawnCwd(),
env: { ...process.env, WSL_UTF8: '1' },
timeoutMs
})
return {
code: result.code,
output: `${result.stdout}${result.stderr}`.trim()
}
}
function asRoot(distro: string, script: string) {
return wslExe(buildWslExecArgs(distro, ['sh', '-c', script], { user: 'root' }))
}
/** WSL < 3.0.1 lost this VM-wide entry when a systemd distro stopped (microsoft/WSL#13885). */
async function assertWslInteropIntact(viaDistro: string, label: string): Promise<void> {
const probe = await asRoot(
viaDistro,
`if [ -e ${WSL_INTEROP} ]; then echo present; else ` +
`echo ':WSLInterop:M::MZ::/init:P' > /proc/sys/fs/binfmt_misc/register && echo restored; fi`
)
interopChecks.push({ label, viaDistro, result: probe.output })
// Restored above so later jobs on this runner are not poisoned; the lane still fails.
expect(probe.output, `WSLInterop ${label}`).toBe('present')
const ver = await guest(viaDistro, '/mnt/c/Windows/System32/cmd.exe /c ver')
expect(ver).toMatch(/Windows/u)
}
async function waitFor<T>(
what: string,
read: () => T | null | undefined | Promise<T | null | undefined>,
timeoutMs: number,
intervalMs = 100
): Promise<T> {
const deadline = performance.now() + timeoutMs
for (;;) {
const value = await read()
if (value !== null && value !== undefined) {
return value
}
if (performance.now() > deadline) {
throw new Error(`Timed out after ${timeoutMs} ms waiting for ${what}`)
}
await new Promise((resolve) => setTimeout(resolve, intervalMs))
}
}
function waitForStatus<S extends WslOrcadConnectionStatus['state']>(
distro: string,
since: number,
state: S,
timeoutMs: number
): Promise<Extract<WslOrcadConnectionStatus, { state: S }>> {
return waitFor(
`${distro} to report ${state} (saw ${JSON.stringify(statusLog(distro).slice(since))})`,
() =>
statusLog(distro)
.slice(since)
.find((status): status is Extract<WslOrcadConnectionStatus, { state: S }> => {
return status.state === state
}),
timeoutMs
)
}
function pairedConnection(connection: WslOrcadConnection) {
const offer = parsePairingCode(connection.pairingOffer)
if (!offer) {
throw new Error(`Unparseable pairing offer for ${connection.distro}`)
}
return pairedRuntimeConnectionFromOffer(offer)
}
async function rpc<T>(connection: WslOrcadConnection, method: string, params?: unknown) {
const response = await sendRemoteRuntimeRequest<T>(
pairedConnection(connection),
method,
params,
30_000,
undefined,
undefined,
ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES
)
if (!response.ok) {
throw new Error(`${method} failed: ${response.error.code}: ${response.error.message}`)
}
return response.result
}
async function runningDistros(): Promise<string[]> {
const running = await readConfirmedRunningWslDistros()
if (running.status !== 'confirmed') {
throw new Error('wsl --list --running was unavailable')
}
return running.distros.map(wslDistroHolderKey)
}
/** Windows-side wsl.exe children still carrying a bridge or a holder for this lane's distros. */
async function laneWindowsProcesses(): Promise<string[]> {
const names = DISTROS.map((d) => d.name.toLowerCase())
return (await readWindowsProcessTableFresh())
.filter((row) => /^wsl(host)?\.exe$/iu.test(row.name))
.map((row) => row.command)
.filter((command) => names.some((name) => command.toLowerCase().includes(name)))
.filter(
(command) =>
command.includes('orcadStdioBridge') || command.includes(WSL_DISTRO_HOLDER_MARKER)
)
}
/** A closed bridge's guest half ends on stdin EOF, a moment after the Windows side. */
async function expectNoGuestBridges(distro: string): Promise<void> {
const count = async () =>
Number.parseInt(await guest(distro, `pgrep -fc '${BRIDGE_PATTERN}' || true`), 10) || 0
const left = await waitFor(
`bridges in ${distro} to exit`,
async () => ((await count()) === 0 ? 0 : null),
15_000,
500
).catch(count)
expect(left, `bridges left in ${distro}`).toBe(0)
}
function createLaneManager(): void {
holder = createWslDistroHolder({
userDataPath: USER_DATA,
onHolderExit: (distro, exit) => supervisor?.noteHolderExit(distro, exit)
})
manager = new WslOrcadConnectionManager({
holder,
onChange: (distro, _connection, status) => {
statusLog(distro).push(status)
console.log(`[lane] ${distro} ${JSON.stringify(status)}`)
}
})
}
/** status, repo add on a fresh `git init`, terminal create, and its output read back. */
async function proveRuntimeRpc(connection: WslOrcadConnection, distro: string): Promise<Receipt> {
const status = await rpc<{ runtimeId?: string }>(connection, 'status.get')
expect(status.runtimeId).toEqual(expect.any(String))
const repoPath = (
await guest(
distro,
'set -e; d="$HOME/wsl-orcad-lane-repo"; rm -rf "$d"; mkdir -p "$d"; cd "$d"; ' +
'git init -q -b main; git -c user.email=lane@orca.test -c user.name=lane ' +
'commit -q --allow-empty -m seed; printf %s "$d"'
)
).trim()
const added = await rpc<{ repo: { id: string } }>(connection, 'repo.add', {
path: repoPath
})
expect(added.repo.id).toEqual(expect.any(String))
const created = await rpc<{ terminal: { handle: string } }>(connection, 'terminal.create', {
worktree: `path:${repoPath}`
})
const handle = created.terminal.handle
expect(handle).toEqual(expect.any(String))
const nonce = randomBytes(6).toString('hex')
// printf splits the token so the echoed input line never matches it.
await rpc(connection, 'terminal.send', {
terminal: handle,
text: `printf 'WSL_LANE_%s\\n' ${nonce}`,
enter: true
})
await waitFor(
`terminal output token in ${distro}`,
async () => {
const read = await rpc<{ terminal: { tail?: unknown[] } }>(connection, 'terminal.read', {
terminal: handle
})
return (read.terminal.tail ?? []).map(String).join('\n').includes(`WSL_LANE_${nonce}`)
? true
: null
},
60_000,
500
)
return {
runtimeId: status.runtimeId,
repoId: added.repo.id,
terminal: handle
}
}
async function launchPhase(distro: string, systemd: boolean): Promise<Receipt> {
const result: Receipt = {}
visibleWork = true
if (!supervisor) {
throw new Error('supervisor missing')
}
let started = performance.now()
const connection = await supervisor.connect(distro)
result.connectMs = Math.round(performance.now() - started)
result.orcadVersion = connection.fullVersion
expect(connection.pid).toEqual(expect.any(Number))
const pid1 = (await guest(distro, 'ps -p 1 -o comm=')).trim()
result.pid1 = pid1
if (systemd) {
expect(pid1).toBe('systemd')
} else {
expect(pid1).not.toBe('systemd')
}
expect(await runningDistros()).toContain(wslDistroHolderKey(distro))
result.rpc = await proveRuntimeRpc(connection, distro)
// orcad killed: the watch sees it, the verdict is orcad-exited, and recovery relaunches it.
let since = statusLog(distro).length
started = performance.now()
await guest(distro, `kill -9 ${connection.pid}; true`)
const recovering = await waitForStatus(distro, since, 'reconnecting', 30_000)
expect(recovering.reason).toBe('orcad-exited')
result.killDetectMs = Math.round(performance.now() - started)
const ready = await waitForStatus(distro, since, 'ready', 180_000)
result.killRecoverMs = Math.round(performance.now() - started)
expect(ready.census).not.toBeNull()
result.census = ready.census
const relaunched = manager.get(distro)
if (!relaunched) {
throw new Error(`${distro} reported ready without a connection`)
}
expect(relaunched.pid).toEqual(expect.any(Number))
expect(relaunched.pid).not.toBe(connection.pid)
await rpc(relaunched, 'status.get')
// `wsl --terminate`: nothing user-visible, so the verdict stands and nothing reboots it.
visibleWork = false
since = statusLog(distro).length
started = performance.now()
const terminated = await wslExe(['--terminate', distro])
expect(terminated.code, terminated.output).toBe(0)
const stopped = await waitForStatus(distro, since, 'distro-stopped', 30_000)
result.terminateDetectMs = Math.round(performance.now() - started)
expect(stopped).toMatchObject({ terminals: 'exited', recovering: false })
expect(await runningDistros()).not.toContain(wslDistroHolderKey(distro))
if (systemd) {
await assertWslInteropIntact(PLAIN, `after terminating ${distro}`)
}
// A user connect is the one path allowed to boot it again.
visibleWork = true
started = performance.now()
const rebooted = await supervisor.connect(distro)
result.rebootConnectMs = Math.round(performance.now() - started)
await rpc(rebooted, 'status.get')
if (systemd) {
await assertWslInteropIntact(distro, `after rebooting ${distro}`)
}
result.orcadPid = rebooted.pid
return result
}
async function survivePhase(): Promise<void> {
const state: LaunchState = JSON.parse(readFileSync(STATE_FILE, 'utf8'))
const waitMs = state.launchEndedAtMs + HOLDER_SURVIVAL_MS - Date.now()
if (waitMs > 0) {
await new Promise((resolve) => setTimeout(resolve, waitMs))
}
const secondsSinceLaunchExited = Math.round((Date.now() - state.launchEndedAtMs) / 1000)
receipt.secondsSinceLaunchExited = secondsSinceLaunchExited
// Only the holders keep them up: no other wsl.exe client survived the launch process.
const running = await runningDistros()
for (const { name } of DISTROS) {
expect(running, `${name} Running ${secondsSinceLaunchExited} s later`).toContain(
wslDistroHolderKey(name)
)
}
const adopted = await holder.adoptHolders()
receipt.adopted = adopted
for (const { name } of DISTROS) {
expect(adopted).toContainEqual({ distro: name, state: 'live' })
}
for (const { name } of DISTROS) {
const connection = await manager.ensure(name)
await rpc(connection, 'status.get')
receipt[`${name}.census`] = await requestOrcadTerminalCensus(pairedConnection(connection), 0)
}
// Teardown: bridges closed, holders released, distros terminated.
await manager.dispose()
for (const { name } of DISTROS) {
await expectNoGuestBridges(name)
}
const released: Receipt = {}
receipt.released = released
for (const { name } of DISTROS) {
// A holder caught mid-exit can read `unverifiable` (no command line yet no exit); it must settle.
released[name] = await holder.release(name)
await waitFor(
`the ${name} holder to exit`,
async () => ((await holder.holderState(name)) === 'exited' ? true : null),
10_000,
250
)
}
// The systemd distro stops while the plain one still runs, so the interop entry is shared.
expect((await wslExe(['--terminate', SYSTEMD])).code).toBe(0)
await assertWslInteropIntact(PLAIN, `after final terminate of ${SYSTEMD}`)
expect((await wslExe(['--terminate', PLAIN])).code).toBe(0)
const leftovers = await waitFor(
'lane wsl.exe processes to exit',
async () => {
const rows = await laneWindowsProcesses()
return rows.length === 0 ? rows : null
},
15_000,
500
).catch(async () => laneWindowsProcesses())
expect(leftovers, 'wsl.exe bridge/holder processes after teardown').toEqual([])
}
describe.runIf(RUN && process.platform === 'win32')('WSL orcad on real WSL2 distros', () => {
beforeAll(async () => {
mkdirSync(USER_DATA, { recursive: true })
mkdirSync(RECEIPT_DIR, { recursive: true })
installLaneAppEnvironment()
createLaneManager()
receipt.wslVersion = (await wslExe(['--version'])).output
})
afterAll(async () => {
await supervisor?.dispose().catch(() => {})
await manager?.dispose().catch(() => {})
writeFileSync(join(RECEIPT_DIR, `${PHASE}.json`), `${JSON.stringify(receipt, null, 2)}\n`)
})
it.runIf(PHASE === 'launch')(
'deploys, serves RPC, recovers a killed orcad and reports a terminated distro',
async () => {
supervisor = createWslOrcadLivenessSupervisor({
manager,
holder,
hasUserVisibleWork: () => visibleWork
})
const pids: LaunchState['orcadPids'] = {}
for (const { name, systemd } of DISTROS) {
const result = await launchPhase(name, systemd)
receipt[name] = result
pids[name] = typeof result.orcadPid === 'number' ? result.orcadPid : null
}
// Bridges and watches go; the holders must not.
await supervisor.dispose()
supervisor = null
await manager.dispose()
for (const { name } of DISTROS) {
await expectNoGuestBridges(name)
}
const state: LaunchState = {
launchEndedAtMs: Date.now(),
orcadPids: pids
}
writeFileSync(STATE_FILE, JSON.stringify(state))
},
DISTROS.length * DISTRO_TIMEOUT_MS
)
it.runIf(PHASE === 'survive')(
'keeps both distros held after the launching process exited, then tears down cleanly',
survivePhase,
DISTRO_TIMEOUT_MS
)
})