diff --git a/src/main/wsl/wsl-orcad-connect-state-snapshot.test.ts b/src/main/wsl/wsl-orcad-connect-state-snapshot.test.ts index e6a4d47e561..292a9e870d8 100644 --- a/src/main/wsl/wsl-orcad-connect-state-snapshot.test.ts +++ b/src/main/wsl/wsl-orcad-connect-state-snapshot.test.ts @@ -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() diff --git a/src/main/wsl/wsl-orcad-connect-state-snapshot.ts b/src/main/wsl/wsl-orcad-connect-state-snapshot.ts index 20115563534..ebbb3a4d234 100644 --- a/src/main/wsl/wsl-orcad-connect-state-snapshot.ts +++ b/src/main/wsl/wsl-orcad-connect-state-snapshot.ts @@ -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 => { 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') { diff --git a/src/main/wsl/wsl-orcad-deploy.test.ts b/src/main/wsl/wsl-orcad-deploy.test.ts index e5066ea6258..2286da3acc9 100644 --- a/src/main/wsl/wsl-orcad-deploy.test.ts +++ b/src/main/wsl/wsl-orcad-deploy.test.ts @@ -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 () => {