fix(desktop): don't latch remote backend boot failures so remote gateway reconnect recovers (#65756)
This commit is contained in:
parent
dfb76d36d5
commit
3f199f5c51
|
|
@ -0,0 +1,23 @@
|
|||
import assert from 'node:assert/strict'
|
||||
|
||||
import { test } from 'vitest'
|
||||
|
||||
import { shouldLatchBackendStartFailure } from './backend-start-failure'
|
||||
|
||||
test('latches a LOCAL backend failure so the install-retry loop is broken', () => {
|
||||
assert.equal(shouldLatchBackendStartFailure({ attemptedRemote: false }), true)
|
||||
})
|
||||
|
||||
test('never latches a REMOTE failure so recovery stays retryable without a restart', () => {
|
||||
// A lapsed OAuth session / mint timeout / host briefly unreachable across a
|
||||
// laptop sleep must not wedge the app: the next connect has to re-attempt and
|
||||
// re-mint against the refreshed session.
|
||||
assert.equal(shouldLatchBackendStartFailure({ attemptedRemote: true }), false)
|
||||
})
|
||||
|
||||
test('the two branches are mutually exclusive (a failure either latches or stays retryable)', () => {
|
||||
for (const attemptedRemote of [true, false]) {
|
||||
const latched = shouldLatchBackendStartFailure({ attemptedRemote })
|
||||
assert.equal(latched, !attemptedRemote)
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* backend-start-failure.ts
|
||||
*
|
||||
* Decides whether a failed primary-backend boot should *latch* into
|
||||
* `backendStartFailure`. A latched failure makes every subsequent
|
||||
* startHermes() re-throw the cached error without re-attempting the connect —
|
||||
* the right behavior for a LOCAL backend so the renderer's retry loop can't
|
||||
* restart a broken install over and over.
|
||||
*
|
||||
* It is the WRONG behavior for a REMOTE backend. A remote connect can fail for
|
||||
* transient reasons — a lapsed OAuth access-token cookie (the gateway rotates a
|
||||
* fresh one from the live refresh-token cookie on the next request), a
|
||||
* ws-ticket mint that timed out mid sleep/wake, or a host that was briefly
|
||||
* unreachable across a laptop sleep. There is no child process whose 'exit'
|
||||
* handler would clear the cache, so a latched remote failure sticks until the
|
||||
* whole app is quit and relaunched: reconnect, "Sign out & sign in" (which only
|
||||
* reloads the renderer), and the wake-recovery revalidate path all keep hitting
|
||||
* the same stale error. Not latching lets the very next connect re-mint a
|
||||
* ticket against the (now refreshed) session and self-heal.
|
||||
*
|
||||
* Extracted as a dependency-free pure predicate so the invariant is testable
|
||||
* without booting Electron or reading main.ts source text.
|
||||
*/
|
||||
|
||||
export interface BackendStartFailureContext {
|
||||
/**
|
||||
* True when the boot that just failed was resolving/dialing a REMOTE (or
|
||||
* cloud) primary backend rather than spawning a local child.
|
||||
*/
|
||||
attemptedRemote: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a startHermes() failure should latch into `backendStartFailure`.
|
||||
* Latch local failures (prevent install-restart loops); never latch remote
|
||||
* failures (they are transient and must stay retryable so recovery paths work
|
||||
* without an app restart).
|
||||
*/
|
||||
export function shouldLatchBackendStartFailure(context: BackendStartFailureContext): boolean {
|
||||
return !context.attemptedRemote
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ import { createBackendConnectionState } from './backend-connection-state'
|
|||
import { buildDesktopBackendEnv, normalizeHermesHomeRoot } from './backend-env'
|
||||
import { canImportHermesCli, verifyHermesCli } from './backend-probes'
|
||||
import { waitForDashboardPortAnnouncement } from './backend-ready'
|
||||
import { shouldLatchBackendStartFailure } from './backend-start-failure'
|
||||
import { detectRemoteDisplay, isWindowsBinaryPathInWsl, isWslEnvironment } from './bootstrap-platform'
|
||||
import { runBootstrap } from './bootstrap-runner'
|
||||
import {
|
||||
|
|
@ -6171,6 +6172,16 @@ function globalRemoteActive() {
|
|||
return modeIsRemoteLike(readDesktopConnectionConfig().mode)
|
||||
}
|
||||
|
||||
// True when the PRIMARY profile's backend resolves to a remote/cloud host —
|
||||
// i.e. resolveRemoteBackend(primaryProfileKey()) would return a descriptor
|
||||
// rather than null. Mirrors that function's precedence (per-profile override →
|
||||
// env → global) so a startHermes() failure can be classified as remote (never
|
||||
// latch — transient, must stay retryable) vs local (latch to break install
|
||||
// loops) BEFORE the throwing resolve/mint runs.
|
||||
function primaryBackendIsRemote() {
|
||||
return Boolean(profileHasRemoteOverride(primaryProfileKey())) || globalRemoteActive()
|
||||
}
|
||||
|
||||
// GET a profile's resolved backend (remote pool or local primary), parsed JSON.
|
||||
async function fetchJsonForProfile(profile, path) {
|
||||
return requestJsonForProfile(profile, path, 'GET')
|
||||
|
|
@ -6738,10 +6749,17 @@ async function startHermes() {
|
|||
|
||||
const connectionAttempt = backendConnectionState.startAttempt()
|
||||
|
||||
// Classify this boot BEFORE the throwing resolve/mint runs: a remote failure
|
||||
// must NOT latch (it's transient — see shouldLatchBackendStartFailure), while
|
||||
// a local failure latches to break install-restart loops.
|
||||
let attemptedRemote = primaryBackendIsRemote()
|
||||
|
||||
const connectionPromise = (async () => {
|
||||
await advanceBootProgress('backend.resolve', 'Resolving Hermes backend', 8)
|
||||
// Resolve for the desktop's primary profile so a per-profile remote
|
||||
// override on the active profile is honored (falls back to env / global).
|
||||
// Re-read once resolved so the classification tracks the value actually used.
|
||||
attemptedRemote = primaryBackendIsRemote()
|
||||
const remote = await resolveRemoteBackend(primaryProfileKey())
|
||||
|
||||
if (remote) {
|
||||
|
|
@ -6947,7 +6965,14 @@ async function startHermes() {
|
|||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
backendStartFailure = error instanceof Error ? error : new Error(message)
|
||||
// Only latch LOCAL boot failures. A remote failure (lapsed session / mint
|
||||
// timeout / host briefly unreachable across sleep) is transient and has no
|
||||
// child 'exit' handler to clear the cache — latching it would wedge the app
|
||||
// on "session expired" until a full restart, defeating reconnect, the
|
||||
// "Sign out & sign in" reload, and the wake-recovery revalidate path.
|
||||
if (shouldLatchBackendStartFailure({ attemptedRemote })) {
|
||||
backendStartFailure = error instanceof Error ? error : new Error(message)
|
||||
}
|
||||
updateBootProgress(
|
||||
{
|
||||
error: message,
|
||||
|
|
|
|||
Loading…
Reference in New Issue