From 3263ca2af66aad0ed88fa6b1742e022bc589e734 Mon Sep 17 00:00:00 2001 From: Jaime Marques Date: Sun, 23 Aug 2026 20:20:38 +0100 Subject: [PATCH] fix(desktop): reuse migrated primary SSH backend Avoid opening a second SSH lifecycle when a migrated registry request targets the same primary/default backend already booted through the legacy route. Compare effective SSH configuration for representation-only drift, treat empty and default as the same root profile, and keep named profiles isolated. --- .../electron/connection-registry.test.ts | 143 ++++++++++++++++++ apps/desktop/electron/connection-registry.ts | 48 ++++++ apps/desktop/electron/main.ts | 114 +++++++++++--- .../electron/primary-backend-startup.test.ts | 24 +++ .../electron/primary-backend-startup.ts | 10 ++ 5 files changed, 320 insertions(+), 19 deletions(-) diff --git a/apps/desktop/electron/connection-registry.test.ts b/apps/desktop/electron/connection-registry.test.ts index 79eb04eb75..4204c936c8 100644 --- a/apps/desktop/electron/connection-registry.test.ts +++ b/apps/desktop/electron/connection-registry.test.ts @@ -32,6 +32,7 @@ import { removeConnection, resolvedConnectionId, resolveRegistryLocalRoute, + reuseMatchingPrimarySshBackend, setConnectionLaunchMode, setLastUsedConnection, setPrimaryConnection, @@ -59,6 +60,148 @@ test('labelSlug kebab-cases and never returns empty for non-empty input', () => assert.equal(labelSlug('!!!'), 'connection') }) +test('matching primary/default SSH route reuses the existing descriptor once', async () => { + const registry = migrateV1ToRegistry({ + mode: 'ssh', + remote: { mode: 'ssh', host: 'build-host', user: 'alice' }, + profiles: {} + }) + + const source = registry.connections.find(connection => connection.id === registry.primary) + + const descriptor = { + mode: 'remote' as const, + remoteKind: 'ssh' as const, + ssh: { + effectiveConfigFingerprint: 'same-effective-config', + host: 'build-host', + keyPath: '~/.ssh/id_ed25519', + remoteProfile: 'default', + user: 'alice' + } + } + + let ensureCalls = 0 + let fingerprintCalls = 0 + + assert.equal(source?.kind, 'ssh') + assert.equal( + await reuseMatchingPrimarySshBackend({ + connectionId: registry.primary, + effectiveFingerprint: async () => { + fingerprintCalls += 1 + + return 'same-effective-config' + }, + ensurePrimary: async () => { + ensureCalls += 1 + + return descriptor + }, + profile: 'default', + registry, + source: source! + }), + descriptor + ) + assert.equal(ensureCalls, 1) + assert.equal(fingerprintCalls, 1) +}) + +test('non-default or non-primary SSH routes do not resolve the primary backend', async () => { + const registry = migrateV1ToRegistry({ + mode: 'ssh', + remote: { mode: 'ssh', host: 'build-host', user: 'alice' }, + profiles: {} + }) + + const source = registry.connections.find(connection => connection.id === registry.primary)! + let ensureCalls = 0 + + const opts = { + effectiveFingerprint: async () => 'same', + ensurePrimary: async () => { + ensureCalls += 1 + + return { mode: 'remote' as const, remoteKind: 'ssh' as const } + }, + registry, + source + } + + assert.equal( + await reuseMatchingPrimarySshBackend({ ...opts, connectionId: registry.primary, profile: 'researcher' }), + null + ) + assert.equal( + await reuseMatchingPrimarySshBackend({ ...opts, connectionId: LOCAL_CONNECTION_ID, profile: 'default' }), + null + ) + assert.equal(ensureCalls, 0) +}) + +test('primary SSH reuse rejects a descriptor with different effective dialing config', async () => { + const registry = migrateV1ToRegistry({ + mode: 'ssh', + remote: { mode: 'ssh', host: 'build-host', user: 'alice' }, + profiles: {} + }) + + const source = registry.connections.find(connection => connection.id === registry.primary)! + + assert.equal( + await reuseMatchingPrimarySshBackend({ + connectionId: registry.primary, + effectiveFingerprint: async () => 'registry-config', + ensurePrimary: async () => ({ + mode: 'remote', + remoteKind: 'ssh', + ssh: { + effectiveConfigFingerprint: 'active-config', + host: 'other-host', + remoteProfile: '' + } + }), + profile: 'default', + registry, + source + }), + null + ) +}) + +test('primary SSH reuse rejects a descriptor with a different remote Hermes path', async () => { + const registry = migrateV1ToRegistry({ + mode: 'ssh', + remote: { mode: 'ssh', host: 'build-host', remoteHermesPath: '/srv/hermes', user: 'alice' }, + profiles: {} + }) + + const source = registry.connections.find(connection => connection.id === registry.primary)! + + assert.equal( + await reuseMatchingPrimarySshBackend({ + connectionId: registry.primary, + effectiveFingerprint: async () => 'same-effective-config', + ensurePrimary: async () => ({ + mode: 'remote', + remoteKind: 'ssh', + ssh: { + effectiveConfigFingerprint: 'same-effective-config', + host: 'build-host', + remoteHermesPath: '/opt/hermes', + remoteProfile: '', + user: 'alice' + } + }), + profile: 'default', + registry, + source + }), + null + ) +}) + test('resolvedConnectionId identifies local and migrated remote descriptors', () => { const registry = migrateV1ToRegistry({ mode: 'local', diff --git a/apps/desktop/electron/connection-registry.ts b/apps/desktop/electron/connection-registry.ts index 01ec43d644..92058749fe 100644 --- a/apps/desktop/electron/connection-registry.ts +++ b/apps/desktop/electron/connection-registry.ts @@ -185,6 +185,7 @@ export interface RegistryLocalRoute { } export interface ResolvedConnectionSshDescriptor { + effectiveConfigFingerprint?: string host?: string keyPath?: string port?: number @@ -333,6 +334,53 @@ export function resolvedConnectionId( return matchingConnectionId(registry, route, 'unique') ?? null } +export interface ReuseMatchingPrimarySshBackendOptions { + connectionId: null | string | undefined + effectiveFingerprint: (source: RegistryConnection) => Promise + ensurePrimary: () => Promise + profile: null | string | undefined + registry: ConnectionRegistry + source: RegistryConnection +} + +/** + * Reuse the already-booted v1 window SSH backend only when its actual dialing + * identity matches the registry primary. Guards run before either async + * dependency so secondary profiles and sources never bootstrap the primary. + */ +export async function reuseMatchingPrimarySshBackend({ + connectionId, + effectiveFingerprint, + ensurePrimary, + profile, + registry, + source +}: ReuseMatchingPrimarySshBackendOptions): Promise { + const id = String(connectionId ?? '').trim() + const profileKey = String(profile ?? '').trim() || 'default' + + if (profileKey !== 'default' || !id || id !== registry.primary || source.id !== id || source.kind !== 'ssh') { + return null + } + + const sourceFingerprint = String(await effectiveFingerprint(source)).trim() + const descriptor = await ensurePrimary() + const activeSsh = descriptor.mode === 'remote' && descriptor.remoteKind === 'ssh' ? descriptor.ssh : null + const rootProfile = (value: unknown) => String(value || '').trim() || 'default' + + if ( + !sourceFingerprint || + !activeSsh || + sourceFingerprint !== String(activeSsh.effectiveConfigFingerprint || '').trim() || + String(source.remoteHermesPath || '').trim() !== String(activeSsh.remoteHermesPath || '').trim() || + rootProfile(source.remoteProfile) !== rootProfile(activeSsh.remoteProfile) + ) { + return null + } + + return descriptor +} + function normalizedSshTarget(route: { host?: unknown; port?: unknown; user?: unknown }): null | string { const ssh = normalizeSshConfig({ ...route, mode: 'ssh' }) diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index c40263be9c..a38f15ed7d 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -137,6 +137,7 @@ import { removeConnection, resolvedConnectionId, resolveRegistryLocalRoute, + reuseMatchingPrimarySshBackend, setConnectionLaunchMode, setLastUsedConnection, setPrimaryConnection, @@ -9669,7 +9670,7 @@ async function reachablePreviewUrl(webContentsId: number, rawUrl: string): Promi } } -function effectiveSshConfigFingerprint(sshConfig) { +async function effectiveSshConfigFingerprint(sshConfig) { const ssh = process.platform === 'win32' ? path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'OpenSSH', 'ssh.exe') @@ -9686,14 +9687,14 @@ function effectiveSshConfigFingerprint(sshConfig) { } args.push('--', sshConfig.user ? `${sshConfig.user}@${sshConfig.host}` : sshConfig.host) - const output = execFileSync(ssh, args, { encoding: 'utf8', timeout: 10_000, windowsHide: true }) + const output = await execText(ssh, args, { timeout: 10_000 }) return crypto.createHash('sha256').update(output).digest('hex') } -async function bootstrapSshConnection(profile, sshConfig, reuseToken, source) { +async function bootstrapSshConnection(profile, sshConfig, reuseToken, source, resolvedEffectiveFingerprint?) { const scope = sshScopeKey(profile) - const effectiveConfigFingerprint = effectiveSshConfigFingerprint(sshConfig) + const effectiveConfigFingerprint = resolvedEffectiveFingerprint || (await effectiveSshConfigFingerprint(sshConfig)) const resolvedConfig = { ...sshConfig, effectiveConfigFingerprint } const fingerprint = sshConfigFingerprint(scope, resolvedConfig) @@ -9835,7 +9836,19 @@ async function bootstrapSshConnectionInner(profile, sshConfig, reuseToken, sourc result.ownershipId ) - return { ...connection, remoteHermesVersion: result.hermesVersion || '' } + return { + ...connection, + remoteHermesVersion: result.hermesVersion || '', + ssh: { + effectiveConfigFingerprint: sshConfig.effectiveConfigFingerprint, + host: sshConfig.host, + keyPath: sshConfig.keyPath, + port: sshConfig.port, + remoteHermesPath: sshConfig.remoteHermesPath, + remoteProfile: sshConfig.remoteProfile, + user: sshConfig.user + } + } } function persistSshConnectionToken(profile, source, token) { @@ -10492,6 +10505,64 @@ async function ensureRegistryBackend(connectionId, profile) { throw new Error(`No connection with id "${id}".`) } + const profileKey = String(profile ?? '').trim() || 'default' + let resolvedRegistrySshConfig + let registryEffectiveFingerprintPromise: null | Promise = null + + const resolveRegistrySshConfig = () => { + if (source.kind !== 'ssh') { + return null + } + + if (!resolvedRegistrySshConfig) { + resolvedRegistrySshConfig = normalizeSshConfig({ + mode: 'ssh', + host: source.host, + user: source.user, + port: source.port, + keyPath: source.keyPath, + remoteHermesPath: source.remoteHermesPath, + remoteProfile: source.remoteProfile || (profileKey === 'default' ? '' : profileKey) + }) + } + + return resolvedRegistrySshConfig + } + + const resolveRegistryEffectiveFingerprint = () => { + if (!registryEffectiveFingerprintPromise) { + const sshConfig = resolveRegistrySshConfig() + + registryEffectiveFingerprintPromise = sshConfig + ? effectiveSshConfigFingerprint(sshConfig) + : Promise.reject(new Error(`SSH connection "${source.label}" has no host configured.`)) + } + + return registryEffectiveFingerprintPromise + } + + // The v2 registry is migrated from (but intentionally coexists with) the + // v1 primary connection config. Reuse the already-booted primary descriptor + // when both identities match; otherwise a default-profile registry request + // opens a second SSH dashboard under a different scope and the competing + // lifecycle probes repeatedly tear down each other's tunnel. + const primary = await reuseMatchingPrimarySshBackend({ + connectionId: id, + effectiveFingerprint: resolveRegistryEffectiveFingerprint, + ensurePrimary: () => ensureBackend(profile), + profile, + registry, + source + }) + + if (primary) { + return { + ...primary, + profile: profileKey, + connectionId: id + } + } + if (source.kind === 'local') { // The registry's 'local' entry means THIS machine's runtime — always. // ensureBackend() follows the v1 routing table, which resolves to a @@ -10502,8 +10573,6 @@ async function ensureRegistryBackend(connectionId, profile) { // the v1 route is genuinely local; otherwise spawn/reuse a forced-local // child pooled under the composite 'conn:local::' key so it // can't collide with the v1 remote descriptor cached at the bare key. - const profileKey = String(profile ?? '').trim() || 'default' - profileDeletionGate.assertCanStart(profileKey) const localRoute = resolveRegistryLocalRoute(profileKey, { @@ -10613,7 +10682,14 @@ async function ensureRegistryBackend(connectionId, profile) { remoteBaseUrl: null } - entry.connectionPromise = connectRegistryBackend(source, profile, key, entry).catch(error => { + entry.connectionPromise = connectRegistryBackend( + source, + profile, + key, + entry, + resolveRegistrySshConfig(), + source.kind === 'ssh' ? resolveRegistryEffectiveFingerprint() : null + ).catch(error => { if (backendPool.get(key) === entry) { backendPool.delete(key) } @@ -10629,7 +10705,14 @@ async function ensureRegistryBackend(connectionId, profile) { // Dial a non-local registry connection for one profile. Never spawns a local // child (entry.process stays null — stopPoolBackend/evict already tolerate // that shape from remote per-profile overrides). -async function connectRegistryBackend(source, profile, key, poolEntry) { +async function connectRegistryBackend( + source, + profile, + key, + poolEntry, + resolvedSshConfig?, + resolvedEffectiveFingerprint?: null | Promise +) { const profileKey = String(profile ?? '').trim() || 'default' if (source.kind === 'ssh') { @@ -10637,15 +10720,7 @@ async function connectRegistryBackend(source, profile, key, poolEntry) { // pair owns its own tunnel + remote dashboard; the profile that re-homes // the REMOTE process is the entry's remoteProfile or the requested one — // never the composite string. - const sshConfig = normalizeSshConfig({ - mode: 'ssh', - host: source.host, - user: source.user, - port: source.port, - keyPath: source.keyPath, - remoteHermesPath: source.remoteHermesPath, - remoteProfile: source.remoteProfile || (profileKey === 'default' ? '' : profileKey) - }) + const sshConfig = resolvedSshConfig if (!sshConfig) { throw new Error(`SSH connection "${source.label}" has no host configured.`) @@ -10655,7 +10730,8 @@ async function connectRegistryBackend(source, profile, key, poolEntry) { key, sshConfig, decryptDesktopSecret(source.token), - `registry:${source.id}` + `registry:${source.id}`, + resolvedEffectiveFingerprint ? await resolvedEffectiveFingerprint : undefined ) poolEntry.remoteBaseUrl = connection.baseUrl diff --git a/apps/desktop/electron/primary-backend-startup.test.ts b/apps/desktop/electron/primary-backend-startup.test.ts index a9f01371a5..fb3d488c5b 100644 --- a/apps/desktop/electron/primary-backend-startup.test.ts +++ b/apps/desktop/electron/primary-backend-startup.test.ts @@ -48,6 +48,30 @@ test('primary remote descriptor preserves a resolved registry connection id', () assert.equal(connection.isFullscreen, false) }) +test('primary remote descriptor preserves the effective SSH dialing identity', () => { + const ssh = { + effectiveConfigFingerprint: 'effective-config', + host: 'build-host', + remoteHermesPath: '/srv/hermes', + remoteProfile: 'default', + user: 'alice' + } + + const connection = createPrimaryRemoteConnection( + { + baseUrl: 'http://127.0.0.1:49152', + remoteKind: 'ssh', + ssh, + token: 'secret', + wsUrl: 'ws://127.0.0.1:49152/api/ws' + }, + [], + {} + ) + + assert.equal(connection.ssh, ssh) +}) + test('primary remote descriptor keeps legacy unregistered routes unqualified', () => { const connection = createPrimaryRemoteConnection( { diff --git a/apps/desktop/electron/primary-backend-startup.ts b/apps/desktop/electron/primary-backend-startup.ts index 3336298244..c105b7f3d6 100644 --- a/apps/desktop/electron/primary-backend-startup.ts +++ b/apps/desktop/electron/primary-backend-startup.ts @@ -20,6 +20,15 @@ interface ResolvedPrimaryRemote { remoteHost?: string remoteKind?: 'cloud' | 'ssh' | 'url' source?: string + ssh?: { + effectiveConfigFingerprint?: string + host?: string + keyPath?: string + port?: number + remoteHermesPath?: string + remoteProfile?: string + user?: string + } token: unknown wsUrl: string } @@ -43,6 +52,7 @@ export function createPrimaryRemoteConnection( remoteKind: remote.remoteKind, remoteHermesVersion: remote.remoteHermesVersion, ...(remote.connectionId ? { connectionId: remote.connectionId } : {}), + ...(remote.ssh ? { ssh: remote.ssh } : {}), token: remote.token, wsUrl: remote.wsUrl, logs,