fix(desktop): resolve v2 registry SSH terminals from the bootstrap pool key

The terminal pane's registry-scoped lookup keys sshConnections with
backendScopeKey(connectionId, profile), but the pool's single writer
publishes every tunnel under its per-profile bootstrap key with the
registry connection stamped on the entry. The lookup can never hit, so
a v2 registry SSH terminal pane stays 'pending' forever (#97345).

Fall back to the entry stamped with the window's connection id - the
same identity match managedSshScopeRole applies to pool entries - and
return the writer's actual scope so downstream getSshConnectionState
and teardown keep working.
This commit is contained in:
liuhao1024
2026-08-29 02:30:31 +08:00
committed by kshitij
parent dde7ed42b3
commit 2afd17a9d7
3 changed files with 59 additions and 2 deletions
+17 -2
View File
@@ -411,7 +411,11 @@ import { isHermesOwnedVenvDaemon } from './venv-holder-select'
import { fetchMarketplaceThemes, searchMarketplaceThemes } from './vscode-marketplace'
import { createWakeIndicatorWindowController } from './wake-indicator-window'
import { enumerateWindowsFrontToBack, enumerationFailed, readWindowBelow } from './window-below'
import { registrySshScopeForWindowRoute, WindowConnectionRouteRegistry } from './window-connection-route'
import {
registrySshPoolScopeByConnectionId,
registrySshScopeForWindowRoute,
WindowConnectionRouteRegistry
} from './window-connection-route'
import { createWindowOpenHandler } from './window-open-policy'
import { installWindowRendererLifecycle } from './window-renderer-lifecycle'
import { createWindowRevealController } from './window-reveal'
@@ -10315,7 +10319,18 @@ function activeSshTerminalTarget(webContentsId?: number) {
const state = sshConnections.get(scope)
return state && state.ssh ? { ssh: state.ssh, scope } : 'pending'
if (state && state.ssh) {
return { ssh: state.ssh, scope }
}
// The pool's single writer publishes under the per-profile bootstrap key
// while stamping the entry with its registry connection id (#97345), so a
// composite-key miss must still resolve the live tunnel by that identity
// instead of reporting 'pending' forever.
const pooledScope = registrySshPoolScopeByConnectionId(sshConnections, windowRoute.connectionId)
const pooledState = pooledScope === null ? null : sshConnections.get(pooledScope)
return pooledState && pooledState.ssh ? { ssh: pooledState.ssh, scope: pooledScope } : 'pending'
}
const profile = windowRoute?.profile ?? primaryProfileKey()
@@ -4,6 +4,7 @@ import { test } from 'vitest'
import {
normalizeWindowConnectionRoute,
registrySshPoolScopeByConnectionId,
registrySshScopeForWindowRoute,
WindowConnectionRouteRegistry
} from './window-connection-route'
@@ -120,3 +121,20 @@ test('uses the canonical default profile scope when a registry SSH route has no
'conn:source-b::default'
)
})
test('recovers the bootstrap pool key a registry SSH tunnel was published under', () => {
const pool = new Map<string, any>([['', { registryConnectionId: 'source-b', ssh: { alive: true } }]])
assert.equal(registrySshPoolScopeByConnectionId(pool, 'source-b'), '')
})
test('does not match another connection, an unlabelled entry, or a torn-down tunnel', () => {
const pool = new Map<string, any>([
['research', { registryConnectionId: 'source-a', ssh: { alive: true } }],
['', { registryConnectionId: '', ssh: { alive: true } }],
['worker', { registryConnectionId: 'source-b' }]
])
assert.equal(registrySshPoolScopeByConnectionId(pool, 'source-b'), null)
assert.equal(registrySshPoolScopeByConnectionId(pool, 'source-c'), null)
})
@@ -40,6 +40,30 @@ export function registrySshScopeForWindowRoute(
return backendScopeKey(route.connectionId, route.profile)
}
export interface RegistrySshPoolEntry {
registryConnectionId?: null | string
ssh?: unknown
}
// The sshConnections pool has a single writer that publishes every tunnel under
// its per-profile bootstrap key while stamping the entry with the registry
// connection that owns it. A registry-scoped lookup under the composite
// backendScopeKey can therefore miss a live tunnel; this recovers the writer's
// actual key from the stamped identity, the same match managedSshScopeRole
// applies to pool entries.
export function registrySshPoolScopeByConnectionId(
entries: Iterable<readonly [string, RegistrySshPoolEntry | undefined]>,
connectionId: string
): null | string {
for (const [scope, entry] of entries) {
if (entry?.ssh && entry.registryConnectionId === connectionId) {
return scope
}
}
return null
}
export class WindowConnectionRouteRegistry {
private readonly routes = new Map<number, WindowConnectionRoute>()