Fix windows serve wsl barrier (#8559)
* fix(startup): bound serve WSL reconciliation wait
* test(startup): cover WSL barrier fail-open on early reconciliation rejection
Co-authored-by: Orca <help@stably.ai>
* feat(serve): surface managed WSL reconciliation status to headless clients
Expose reconciliation state ('pending'|'settled'|'failed') in the
orca_server_ready payload and a wsl-cli-barrier startup milestone, so
headless/SSH agents can tell the fail-open barrier outlived its budget
and a WSL PTY launch may still race an un-migrated registration.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
6091995e75
commit
13ed697cc2
|
|
@ -229,6 +229,10 @@ const recoveryReloadInFlight = createWebContentsTimedFlag()
|
|||
let firstWindowStartupServicesReady: Promise<void> = Promise.resolve()
|
||||
let managedWslCliReconciliationReady: Promise<void> = Promise.resolve()
|
||||
let managedWslCliStartupBarrierReady: Promise<void> = Promise.resolve()
|
||||
// Why: the serve barrier fails open at its budget, so headless clients need the
|
||||
// reconciliation state at ready time to know a WSL PTY launch may still race an
|
||||
// un-migrated registration. 'settled' covers the off-Windows no-op fast path.
|
||||
let managedWslCliReconciliationStatus: 'pending' | 'settled' | 'failed' = 'settled'
|
||||
// Why: GPU child crashes clustered right after launch indicate a broken driver;
|
||||
// track them so Orca can move this build onto software rendering.
|
||||
const gpuLaunchTimeMs = Date.now()
|
||||
|
|
@ -1384,6 +1388,9 @@ async function printServeReady(options: ServeOptions): Promise<void> {
|
|||
type: 'orca_server_ready',
|
||||
runtimeId: runtime.getRuntimeId(),
|
||||
endpoint,
|
||||
// Why: the WSL reconciliation barrier fails open, so 'pending' warns clients
|
||||
// it outlived the startup budget and a WSL PTY launch may still race a repair.
|
||||
managedWslCliReconciliation: managedWslCliReconciliationStatus,
|
||||
pairing: pairing.available
|
||||
? {
|
||||
url: pairing.pairingUrl,
|
||||
|
|
@ -1610,6 +1617,7 @@ app.whenReady().then(async () => {
|
|||
|
||||
// Why: managed WSL launchers live outside the Windows app bundle, so keep
|
||||
// their launcher and bridge contract synchronized across app updates.
|
||||
managedWslCliReconciliationStatus = 'pending'
|
||||
managedWslCliReconciliationReady = reconcileManagedWslCliRegistrations({
|
||||
isPackaged: app.isPackaged,
|
||||
userDataPath: getCanonicalUserDataPath(),
|
||||
|
|
@ -1625,8 +1633,10 @@ app.whenReady().then(async () => {
|
|||
console.log(`[wsl-cli] Repaired managed registration in ${result.distro}.`)
|
||||
}
|
||||
}
|
||||
managedWslCliReconciliationStatus = 'settled'
|
||||
})
|
||||
.catch((error) => {
|
||||
managedWslCliReconciliationStatus = 'failed'
|
||||
console.warn(
|
||||
'[wsl-cli] Managed registration reconciliation discovery failed:',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
|
|
@ -2090,9 +2100,13 @@ app.whenReady().then(async () => {
|
|||
}
|
||||
|
||||
if (serveOptions) {
|
||||
// Why: headless serve has no renderer startup barrier, so settle managed
|
||||
// WSL command reconciliation before exposing its runtime transport.
|
||||
await managedWslCliReconciliationReady
|
||||
// Why: give managed WSL launchers a brief chance to migrate before headless
|
||||
// PTYs become reachable without letting slow repairs withhold all RPC readiness.
|
||||
logStartupMilestone('wsl-cli-barrier-start')
|
||||
await managedWslCliStartupBarrierReady
|
||||
logStartupMilestone('wsl-cli-barrier-resolved', {
|
||||
reconciliation: managedWslCliReconciliationStatus
|
||||
})
|
||||
await startServeAgentHookServer()
|
||||
registerHeadlessPtyRuntime(
|
||||
runtime,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs'
|
|||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('desktop startup ordering', () => {
|
||||
describe('startup ordering', () => {
|
||||
it('passes the startup barrier into PTY handlers without blocking window creation', () => {
|
||||
const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8')
|
||||
const attachStart = source.indexOf('attachMainWindowServices(')
|
||||
|
|
@ -24,7 +24,7 @@ describe('desktop startup ordering', () => {
|
|||
expect(Math.max(rpcStartIndex, legacyRpcStartIndex)).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
|
||||
it('shows the desktop window without waiting for WSL registration reconciliation', () => {
|
||||
it('bounds WSL reconciliation before serve RPC while leaving desktop startup independent', () => {
|
||||
const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8')
|
||||
const barrierStart = source.indexOf("ipcMain.handle('app:awaitFirstWindowStartupServices'")
|
||||
const barrierEnd = source.indexOf("ipcMain.handle(\n 'app:startupDiagnostic'", barrierStart)
|
||||
|
|
@ -43,12 +43,32 @@ describe('desktop startup ordering', () => {
|
|||
expect(serveStart).toBeGreaterThan(reconciliationStart)
|
||||
expect(serveEnd).toBeGreaterThan(serveStart)
|
||||
expect(desktopWindowStart).toBeGreaterThan(reconciliationStart)
|
||||
expect(serveStartup).toContain('await managedWslCliReconciliationReady')
|
||||
expect(serveStartup).toContain('await managedWslCliStartupBarrierReady')
|
||||
expect(serveStartup).not.toContain('await managedWslCliReconciliationReady')
|
||||
expect(serveStartup.indexOf('await managedWslCliStartupBarrierReady')).toBeLessThan(
|
||||
serveStartup.indexOf('await runtimeRpc.start()')
|
||||
)
|
||||
expect(desktopStartup).not.toContain('await managedWslCliReconciliationReady')
|
||||
expect(barrier).toContain('managedWslCliStartupBarrierReady')
|
||||
expect(barrier).not.toContain('managedWslCliReconciliationReady')
|
||||
})
|
||||
|
||||
it('exposes managed WSL reconciliation status to headless serve clients and diagnostics', () => {
|
||||
const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8')
|
||||
|
||||
// Why: the barrier fails open, so the serve-ready payload must carry the
|
||||
// reconciliation state and the bounded wait must be traceable via a milestone.
|
||||
const readyStart = source.indexOf("type: 'orca_server_ready'")
|
||||
const readyEnd = source.indexOf('pairing: pairing.available', readyStart)
|
||||
const readyPayload = source.slice(readyStart, readyEnd)
|
||||
expect(readyPayload).toContain('managedWslCliReconciliation: managedWslCliReconciliationStatus')
|
||||
|
||||
expect(source).toContain("managedWslCliReconciliationStatus = 'pending'")
|
||||
expect(source).toContain("managedWslCliReconciliationStatus = 'settled'")
|
||||
expect(source).toContain("managedWslCliReconciliationStatus = 'failed'")
|
||||
expect(source).toContain("logStartupMilestone('wsl-cli-barrier-resolved'")
|
||||
})
|
||||
|
||||
it('does not run the rate-limit quota fetch before the first window can show results', () => {
|
||||
const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8')
|
||||
const attachIndex = source.indexOf('rateLimits.attach(window)')
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ describe('createWslCliReconciliationStartupBarrier', () => {
|
|||
}
|
||||
})
|
||||
|
||||
it('fails open when reconciliation exceeds the startup budget', async () => {
|
||||
it('fails open immediately when reconciliation rejects before the budget expires', async () => {
|
||||
vi.useFakeTimers()
|
||||
let resolveReconciliation!: () => void
|
||||
let rejectReconciliation!: (error: Error) => void
|
||||
|
||||
try {
|
||||
const reconciliation = new Promise<void>((resolve) => {
|
||||
resolveReconciliation = resolve
|
||||
const reconciliation = new Promise<void>((_resolve, reject) => {
|
||||
rejectReconciliation = reject
|
||||
})
|
||||
const barrier = createWslCliReconciliationStartupBarrier(reconciliation)
|
||||
let barrierSettled = false
|
||||
|
|
@ -44,38 +44,73 @@ describe('createWslCliReconciliationStartupBarrier', () => {
|
|||
barrierSettled = true
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(WSL_CLI_RECONCILIATION_STARTUP_BUDGET_MS - 1)
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
expect(barrierSettled).toBe(false)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
// Why: a fast WSL discovery failure should release the barrier via its catch
|
||||
// branch without waiting out the budget, and must clear the pending timer.
|
||||
rejectReconciliation(new Error('WSL discovery failed'))
|
||||
await expect(barrier).resolves.toBeUndefined()
|
||||
resolveReconciliation()
|
||||
await reconciliation
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves reconciliation running after the startup budget expires', async () => {
|
||||
it('lets serve reach RPC readiness at budget while reconciliation remains pending', async () => {
|
||||
vi.useFakeTimers()
|
||||
let resolveWork!: () => void
|
||||
let completed = false
|
||||
let resolveReconciliation!: () => void
|
||||
let reconciliationCompleted = false
|
||||
|
||||
try {
|
||||
const work = new Promise<void>((resolve) => {
|
||||
resolveWork = resolve
|
||||
const reconciliation = new Promise<void>((resolve) => {
|
||||
resolveReconciliation = resolve
|
||||
}).then(() => {
|
||||
completed = true
|
||||
reconciliationCompleted = true
|
||||
})
|
||||
const barrier = createWslCliReconciliationStartupBarrier(work, { timeoutMs: 10 })
|
||||
const barrier = createWslCliReconciliationStartupBarrier(reconciliation)
|
||||
let rpcReady = false
|
||||
const serveRpcReadiness = barrier.then(() => {
|
||||
rpcReady = true
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(WSL_CLI_RECONCILIATION_STARTUP_BUDGET_MS - 1)
|
||||
expect(rpcReady).toBe(false)
|
||||
expect(reconciliationCompleted).toBe(false)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
await expect(serveRpcReadiness).resolves.toBeUndefined()
|
||||
expect(rpcReady).toBe(true)
|
||||
expect(reconciliationCompleted).toBe(false)
|
||||
|
||||
resolveReconciliation()
|
||||
await reconciliation
|
||||
expect(reconciliationCompleted).toBe(true)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves eventual reconciliation error reporting after the budget expires', async () => {
|
||||
vi.useFakeTimers()
|
||||
let rejectReconciliation!: (error: Error) => void
|
||||
const reportedErrors: string[] = []
|
||||
|
||||
try {
|
||||
const reconciliation = new Promise<void>((_resolve, reject) => {
|
||||
rejectReconciliation = reject
|
||||
}).catch((error) => {
|
||||
reportedErrors.push(error instanceof Error ? error.message : String(error))
|
||||
})
|
||||
const barrier = createWslCliReconciliationStartupBarrier(reconciliation, { timeoutMs: 10 })
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
await expect(barrier).resolves.toBeUndefined()
|
||||
expect(completed).toBe(false)
|
||||
expect(reportedErrors).toEqual([])
|
||||
|
||||
resolveWork()
|
||||
await work
|
||||
expect(completed).toBe(true)
|
||||
rejectReconciliation(new Error('WSL discovery failed'))
|
||||
await reconciliation
|
||||
expect(reportedErrors).toEqual(['WSL discovery failed'])
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ type WslCliReconciliationStartupBarrierOptions = {
|
|||
}
|
||||
|
||||
/**
|
||||
* Briefly gates restored terminals while managed WSL registrations reconcile.
|
||||
* Briefly gates terminal startup while managed WSL registrations reconcile.
|
||||
*/
|
||||
export function createWslCliReconciliationStartupBarrier(
|
||||
reconciliation: Promise<unknown>,
|
||||
|
|
@ -22,8 +22,8 @@ export function createWslCliReconciliationStartupBarrier(
|
|||
}
|
||||
})
|
||||
|
||||
// Why: reconciliation may outlive a slow or unavailable WSL distro; restored
|
||||
// terminals should wait briefly without turning WSL discovery into an app hang.
|
||||
// Why: reconciliation may outlive a slow or unavailable WSL distro; terminal
|
||||
// startup should wait briefly without turning WSL discovery into an app hang.
|
||||
return Promise.race([
|
||||
settled,
|
||||
new Promise<void>((resolve) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue