fix(wsl): fall back from a failed connect snapshot, and hint only from its own record read (#35663)

A snapshot command that rejects now falls back to the per-file reads
instead of failing the connect. A snapshot whose record section was
unreadable gives no hints even when the fallback read answered. Pins the
held-fence and pending-journal cases at the connect level, and notes that
snapshot raws drop trailing newlines.
This commit is contained in:
Neil
2026-10-28 21:43:47 -07:00
committed by GitHub
parent 66501d1d37
commit dd29fc73f9
3 changed files with 50 additions and 12 deletions
@@ -103,7 +103,7 @@ describe.skipIf(process.platform === 'win32')('WSL connect state snapshot', () =
// The plain read refuses an oversized file too (exit 65), so the connect still fails closed.
await expect(reader.readActivationRecord(target())).rejects.toThrow()
expect(mockExec).toHaveBeenCalledTimes(2)
expect(reader.snapshot()?.activation.state).toBe('unreadable')
expect(reader.snapshot()).toBeNull()
})
it('falls back to the plain record read when the answer has another shape', async () => {
@@ -118,6 +118,32 @@ describe.skipIf(process.platform === 'win32')('WSL connect state snapshot', () =
})
})
describe.skipIf(process.platform === 'win32')('WSL connect state snapshot fallbacks', () => {
it('falls back to the plain read when the snapshot command itself fails', async () => {
write('.orca-remote/orcad-active.json', serializeOrcadActivationRecord(ACTIVE))
mockExec.mockImplementationOnce(async () => {
throw new Error('transport closed')
})
const reader = createWslOrcadConnectStateReader()
await expect(reader.readActivationRecord(target())).resolves.toEqual(ACTIVE)
expect(reader.snapshot()).toBeNull()
})
it('gives no hints when only the fallback read could answer the record', async () => {
write('.orca-remote/orcad-active.json', serializeOrcadActivationRecord(ACTIVE))
mockExec.mockImplementationOnce(
async () =>
'UNREADABLE\n__ORCA_WSL_CONNECT_STATE_END__\nABSENT\n__ORCA_WSL_CONNECT_STATE_END__\n' +
'ABSENT\n__ORCA_WSL_CONNECT_STATE_END__\nOPEN\n'
)
const reader = createWslOrcadConnectStateReader()
await expect(reader.readActivationRecord(target())).resolves.toEqual(ACTIVE)
expect(reader.snapshot()).toBeNull()
})
})
describe('parseWslOrcadConnectState', () => {
it('rejects a truncated answer', () => {
expect(parseWslOrcadConnectState('ABSENT\n__ORCA_WSL_CONNECT_STATE_END__\n')).toBeNull()
@@ -65,6 +65,7 @@ export function wslOrcadConnectStateCommand(
paths: SnapshotPaths
): string {
// PRESENT only when the whole file was read; a failed or oversized read is UNREADABLE.
// `$(cat)` drops trailing newlines, so raws are not byte-exact; every consumer parses JSON.
const read = (path: string): string => {
const file = quotePosixShell(path)
return (
@@ -126,7 +127,9 @@ export function parseWslOrcadConnectState(output: string): WslOrcadConnectStateS
export function createWslOrcadConnectStateReader() {
let snapshot: WslOrcadConnectStateSnapshot | null = null
return {
snapshot: (): WslOrcadConnectStateSnapshot | null => snapshot,
// No hints when the record came from the fallback read rather than the snapshot.
snapshot: (): WslOrcadConnectStateSnapshot | null =>
snapshot?.activation.state === 'unreadable' ? null : snapshot,
readActivationRecord: async (target: {
conn: OrcadHostTransport
host: RemoteHostPlatform
@@ -136,9 +139,12 @@ export function createWslOrcadConnectStateReader() {
}): Promise<OrcadActivationRecord> => {
if (!isWindowsRemoteHost(target.host)) {
const paths = snapshotPaths(target.host, target.remoteHome, target.userDataDir)
snapshot = parseWslOrcadConnectState(
await execOrcadRemote(target, wslOrcadConnectStateCommand(target.host, paths))
)
// A failed snapshot only costs the per-file reads; an aborted signal stops those too.
const output = await execOrcadRemote(
target,
wslOrcadConnectStateCommand(target.host, paths)
).catch(() => null)
snapshot = output === null ? null : parseWslOrcadConnectState(output)
}
const activation = snapshot?.activation
if (activation?.state === 'ok') {
+13 -7
View File
@@ -107,14 +107,16 @@ function transport(target: WslOrcadHostTarget): OrcadHostTransport {
return { getTarget: () => target } as unknown as OrcadHostTransport
}
const section = (text: string) => `${text}\n__ORCA_WSL_CONNECT_STATE_END__\n`
let snapshotAnswer = ''
function launch(kernel = '5.15.167.4-microsoft-standard-WSL2', options = {}) {
mocks.exec.mockImplementation(async (_conn: unknown, command: string) => {
if (command.includes('uname -r')) {
mocks.steps.push('uname')
return `__ORCA_WSL_KERNEL__ ${kernel}\n__ORCA_WSL_HOME__ /home/u\n\n__ORCA_REMOTE_PLATFORM__ Linux x86_64\n\n__ORCA_WSL_LIBC__\nldd (GNU libc) 2.39\n`
}
const end = '\n__ORCA_WSL_CONNECT_STATE_END__\n'
return command.includes(end.trim()) ? `ABSENT${end}ABSENT${end}ABSENT${end}OPEN\n` : '4242\n'
return command.includes('CONNECT_STATE_END') ? snapshotAnswer : '4242\n'
})
return deployAndLaunchWslOrcad('Ubuntu-24.04', {
holder: holder(),
@@ -194,7 +196,12 @@ describe('deployAndLaunchWslOrcad', () => {
)
})
it('hands its one connect-state read to recovery, adoption and deploy as hints', async () => {
it.each([
['nothing pending', 'ABSENT', 'OPEN', { transactionAbsent: true, fenceOpen: true }],
['a held fence', 'ABSENT', 'LOCKED', { transactionAbsent: true, fenceOpen: false }],
['a pending journal', 'PRESENT\n{}', 'OPEN', { transactionAbsent: false, fenceOpen: true }]
])('passes its connect-state read (%s) on as hints', async (_, journal, fence, preRead) => {
snapshotAnswer = `${section('ABSENT')}${section(journal)}${section('ABSENT')}${fence}\n`
const resolve = mocks.resolveContext.getMockImplementation()
mocks.resolveContext.mockImplementationOnce(async (target, connection, signal, h, known) => {
const context = await resolve?.(target, connection, signal, h, known)
@@ -202,11 +209,10 @@ describe('deployAndLaunchWslOrcad', () => {
return { ...context, activationRecord: await known.readActivationRecord(at) }
})
await launch()
const idle = { transactionAbsent: true, fenceOpen: true }
expect(mocks.recover).toHaveBeenCalledWith(expect.anything(), expect.objectContaining(idle))
// Recovery reads the host unless both are idle; deploy re-probes a fence seen held.
expect(mocks.recover.mock.calls[0][1]).toMatchObject(preRead)
expect(mocks.adopt.mock.calls[0][3]).toEqual({ state: 'absent' })
const record = emptyOrcadActivationRecord()
expect(mocks.deploy.mock.calls[0][0].preRead).toEqual({ record, fenceOpen: true })
expect(mocks.deploy.mock.calls[0][0].preRead.fenceOpen).toBe(preRead.fenceOpen)
})
it('applies the SSH census and downgrade admission to the first deploy', async () => {