fix(orchestration): keep recovery off renderer startup barrier

This commit is contained in:
OrcaWin
2026-07-28 21:59:57 -07:00
parent 9009241a7a
commit 285dd7885e
4 changed files with 136 additions and 7 deletions
+10 -7
View File
@@ -137,6 +137,7 @@ import {
import { maybeRedirectAppImageCliLaunch } from './startup/appimage-cli-redirect'
import { maybeRedirectPackagedCliEntryLaunch } from './startup/packaged-cli-entry-redirect'
import { startFirstWindowStartupServices } from './startup/first-window-startup-services'
import { recoverLegacyWorkerTerminalsForRendererStartup } from './startup/legacy-worker-renderer-recovery'
import { createWslCliReconciliationStartupBarrier } from './startup/wsl-cli-reconciliation-startup-barrier'
import { getDevInstanceIdentity } from './startup/dev-instance-identity'
import { hydrateShellPath, mergePathSegments } from './startup/hydrate-shell-path'
@@ -728,15 +729,17 @@ if (hasSingleInstanceLock) {
headlessBrowserDisplayAvailable = ensureVirtualDisplayForHeadlessServe({ isServeMode })
}
ipcMain.handle('app:awaitFirstWindowStartupServices', async () => {
// Why: restored WSL terminals get a bounded chance to receive launcher repairs before window rendering proceeds.
await Promise.all([
ipcMain.handle('app:awaitFirstWindowStartupServices', () =>
recoverLegacyWorkerTerminalsForRendererStartup({
firstWindowStartupServicesReady,
managedWslCliStartupBarrierReady,
localPtyProviderStartupReady,
managedWslCliStartupBarrierReady
])
await runtime?.reconcileLegacyWorkerTerminals({ materializeRenderer: true })
})
reconcile: () => runtime?.reconcileLegacyWorkerTerminals({ materializeRenderer: true }),
onDeferredRecoveryError: (error) => {
console.warn('[orchestration] legacy worker provider-ready recovery failed', error)
}
})
)
// Why: the renderer pulls this once its ui:openSettings listener attaches, so a Settings request queued before mount isn't lost.
ipcMain.handle('ui:consumePendingOpenSettings', (event) =>
@@ -73,6 +73,11 @@ describe('startup ordering', () => {
expect(desktopStartup).not.toContain('await managedWslCliReconciliationReady')
expect(barrier).toContain('managedWslCliStartupBarrierReady')
expect(barrier).not.toContain('managedWslCliReconciliationReady')
expect(barrier).toContain('recoverLegacyWorkerTerminalsForRendererStartup({')
expect(barrier).toContain('localPtyProviderStartupReady,')
expect(barrier).toContain(
'reconcile: () => runtime?.reconcileLegacyWorkerTerminals({ materializeRenderer: true })'
)
})
it('exposes managed WSL reconciliation status to headless serve clients and diagnostics', () => {
@@ -0,0 +1,92 @@
import { describe, expect, it, vi } from 'vitest'
import { recoverLegacyWorkerTerminalsForRendererStartup } from './legacy-worker-renderer-recovery'
describe('legacy worker renderer recovery', () => {
it('hydrates after bounded barriers while provider startup remains pending', async () => {
let resolveFirstWindow!: () => void
let resolveWslBarrier!: () => void
let resolveProvider!: () => void
const firstWindowReady = new Promise<void>((resolve) => {
resolveFirstWindow = resolve
})
const wslBarrierReady = new Promise<void>((resolve) => {
resolveWslBarrier = resolve
})
const providerReady = new Promise<void>((resolve) => {
resolveProvider = resolve
})
const reconcile = vi.fn().mockResolvedValue(undefined)
const startup = recoverLegacyWorkerTerminalsForRendererStartup({
firstWindowStartupServicesReady: firstWindowReady,
managedWslCliStartupBarrierReady: wslBarrierReady,
localPtyProviderStartupReady: providerReady,
reconcile,
onDeferredRecoveryError: vi.fn()
})
resolveFirstWindow()
await Promise.resolve()
expect(reconcile).not.toHaveBeenCalled()
resolveWslBarrier()
await startup
expect(reconcile).toHaveBeenCalledTimes(1)
resolveProvider()
await vi.waitFor(() => expect(reconcile).toHaveBeenCalledTimes(2))
})
it('retries after initial recovery when the provider is already ready', async () => {
const reconcile = vi.fn().mockResolvedValue(undefined)
await recoverLegacyWorkerTerminalsForRendererStartup({
firstWindowStartupServicesReady: Promise.resolve(),
managedWslCliStartupBarrierReady: Promise.resolve(),
localPtyProviderStartupReady: Promise.resolve(),
reconcile,
onDeferredRecoveryError: vi.fn()
})
await vi.waitFor(() => expect(reconcile).toHaveBeenCalledTimes(2))
})
it('contains provider startup rejection after initial recovery', async () => {
const providerError = new Error('provider failed')
const reconcile = vi.fn().mockResolvedValue(undefined)
let reportError!: (error: unknown) => void
const reportedError = new Promise<unknown>((resolve) => {
reportError = resolve
})
await recoverLegacyWorkerTerminalsForRendererStartup({
firstWindowStartupServicesReady: Promise.resolve(),
managedWslCliStartupBarrierReady: Promise.resolve(),
localPtyProviderStartupReady: Promise.reject(providerError),
reconcile,
onDeferredRecoveryError: reportError
})
await expect(reportedError).resolves.toBe(providerError)
expect(reconcile).toHaveBeenCalledTimes(1)
})
it('contains deferred recovery rejection', async () => {
const recoveryError = new Error('recovery failed')
const reconcile = vi.fn().mockResolvedValueOnce(undefined).mockRejectedValueOnce(recoveryError)
let reportError!: (error: unknown) => void
const reportedError = new Promise<unknown>((resolve) => {
reportError = resolve
})
await recoverLegacyWorkerTerminalsForRendererStartup({
firstWindowStartupServicesReady: Promise.resolve(),
managedWslCliStartupBarrierReady: Promise.resolve(),
localPtyProviderStartupReady: Promise.resolve(),
reconcile,
onDeferredRecoveryError: reportError
})
await expect(reportedError).resolves.toBe(recoveryError)
expect(reconcile).toHaveBeenCalledTimes(2)
})
})
@@ -0,0 +1,29 @@
type LegacyWorkerRendererRecoveryOptions = {
firstWindowStartupServicesReady: Promise<void>
managedWslCliStartupBarrierReady: Promise<void>
localPtyProviderStartupReady: Promise<void>
reconcile: () => Promise<unknown> | undefined
onDeferredRecoveryError: (error: unknown) => void
}
export async function recoverLegacyWorkerTerminalsForRendererStartup(
options: LegacyWorkerRendererRecoveryOptions
): Promise<void> {
const providerStartupResult = options.localPtyProviderStartupReady.then(
() => ({ ok: true as const }),
(error: unknown) => ({ ok: false as const, error })
)
await Promise.all([
options.firstWindowStartupServicesReady,
options.managedWslCliStartupBarrierReady
])
await options.reconcile()
void providerStartupResult
.then(async (result) => {
if (!result.ok) {
throw result.error
}
await options.reconcile()
})
.catch(options.onDeferredRecoveryError)
}