fix(desktop): bound the boot descriptor wait so a dead primary cannot strand the registry restore
Follow-up to the #95007 partial cherry: waitForInitialConnection() was an unbounded listen on $connection — a primary that never publishes its descriptor (spawn failure, dead SSH target) would strand initializeConnectionsRegistry() forever and the last-used source would never be restored. Bound it with the codebase's withTimeout helper (same pattern as the sibling SWITCH_* call sites in this file): after 45s (the primary spawn budget) the restore proceeds exactly as it did before the wait existed, and the listener is torn down either way. Regression test: boot restore proceeds after the deadline with the descriptor never arriving. Original-PR: #95007
This commit is contained in:
@@ -745,6 +745,30 @@ describe('selectConnection', () => {
|
||||
expect($showAllProfiles.get()).toBe(true)
|
||||
})
|
||||
|
||||
it('boot restore proceeds after the descriptor wait deadline (bounded wait)', async () => {
|
||||
// A primary that never publishes (spawn failure, dead SSH target) must
|
||||
// not strand the registry restore forever: after the deadline the restore
|
||||
// runs exactly as it did before the wait existed.
|
||||
vi.useFakeTimers()
|
||||
|
||||
try {
|
||||
list.mockResolvedValueOnce({ ...registry, lastUsed: 'homelab', launchMode: 'last-used' })
|
||||
|
||||
const restoring = initializeConnectionsRegistry()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_000)
|
||||
expect(ensureGatewayAgent).not.toHaveBeenCalled()
|
||||
|
||||
// Descriptor never arrives; deadline elapses.
|
||||
await vi.advanceTimersByTimeAsync(60_000)
|
||||
await restoring
|
||||
|
||||
expect(ensureGatewayAgent).toHaveBeenCalledWith('homelab', 'default', expect.anything())
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('a user-initiated source switch still collapses "All profiles"', async () => {
|
||||
setConnectionsRegistry(registry)
|
||||
$connection.set({ connectionId: 'local', mode: 'local' })
|
||||
|
||||
@@ -32,6 +32,10 @@ const LAST_PROFILE_STORAGE_KEY = 'hermes.desktop.lastProfileByConnection'
|
||||
const SWITCH_DIAL_TIMEOUT_MS = 20_000
|
||||
const SWITCH_COMMIT_TIMEOUT_MS = 20_000
|
||||
const SWITCH_REMEMBER_TIMEOUT_MS = 5_000
|
||||
// Matches the primary spawn budget: a healthy cold boot publishes well within
|
||||
// this; anything longer means the primary is not coming and the registry
|
||||
// restore should stop waiting for it.
|
||||
const BOOT_DESCRIPTOR_WAIT_TIMEOUT_MS = 45_000
|
||||
|
||||
export { $connectionsRegistry } from '@/store/connection-registry-state'
|
||||
|
||||
@@ -141,22 +145,41 @@ async function rememberConnection(connectionId: string): Promise<void> {
|
||||
* source needs a secondary dial. Otherwise a remote primary can be opened a
|
||||
* second time through the registry while the identical primary SSH backend is
|
||||
* still publishing its connection identity.
|
||||
*
|
||||
* Bounded: a primary that never publishes (spawn failure, dead SSH target)
|
||||
* must not strand the registry restore forever — after the deadline the
|
||||
* restore proceeds exactly as it did before this wait existed. The listener
|
||||
* is always torn down so a late descriptor can't leak a dangling resolver.
|
||||
*/
|
||||
function waitForInitialConnection(): Promise<void> {
|
||||
if ($connection.get()) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const unlisten = $connection.listen(connection => {
|
||||
let unlisten: (() => void) | undefined
|
||||
|
||||
const published = new Promise<void>(resolve => {
|
||||
unlisten = $connection.listen(connection => {
|
||||
if (!connection) {
|
||||
return
|
||||
}
|
||||
|
||||
unlisten()
|
||||
unlisten?.()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
return withTimeout(
|
||||
published,
|
||||
BOOT_DESCRIPTOR_WAIT_TIMEOUT_MS,
|
||||
'Timed out waiting for the primary connection descriptor'
|
||||
).catch(error => {
|
||||
unlisten?.()
|
||||
|
||||
if (!isTimeoutError(error)) {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user