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.
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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<string>
|
||||
ensurePrimary: () => Promise<ResolvedConnectionDescriptor>
|
||||
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<null | ResolvedConnectionDescriptor> {
|
||||
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' })
|
||||
|
||||
|
||||
@@ -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<string> = 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::<profile>' 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<string>
|
||||
) {
|
||||
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
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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<State extends object>(
|
||||
remoteKind: remote.remoteKind,
|
||||
remoteHermesVersion: remote.remoteHermesVersion,
|
||||
...(remote.connectionId ? { connectionId: remote.connectionId } : {}),
|
||||
...(remote.ssh ? { ssh: remote.ssh } : {}),
|
||||
token: remote.token,
|
||||
wsUrl: remote.wsUrl,
|
||||
logs,
|
||||
|
||||
Reference in New Issue
Block a user