mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
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:
@@ -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') {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user