Files
orca/src/main/ipc/ssh-host-server-connect.ts
T
NeilandClaude 52554b303f fix(ssh): keep desktop 2 connected with its live relay terminal when desktop 1 already moved the host (STA-10604) (#29625)
* fix(ssh): keep a host connected for its bridged relay terminals when another desktop already moved their project (STA-10604)

Desktop 2's move onto the server desktop 1 already runs refused to link a
project its own relay-era terminal still runs in, and the connect then failed
outright: this version has no relay to fall back to. The decision now keeps
that host connected and unmoved, starts the legacy relay bridge, and the old
tab reattaches its shell through it. The SSH host card names the terminals and
how to clear them; the next connect after they exit links the project.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ssh): release a bridged ssh: row pane's claim on detach too

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ssh): admit the bridged mark in the renderer, and remount bridged tabs even when the connect reply lands first

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ssh): keep the unverifiable line for a bridged host whose terminal can't be confirmed

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ssh): keep the linked-project terminal guidance neutral; only the live bridged line reassures

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 04:21:24 -07:00

140 lines
5.3 KiB
TypeScript

/** The connect path's managed-server step: decide the host's server, and publish a managed connect. */
import { getAppEnvironment } from '../../shared/app-environment'
import type {
SshConnectionState,
SshManagedServerLinkedProject,
SshManagedServerServingNote,
SshManagedServerUpdateNote,
SshTarget
} from '../../shared/ssh-types'
import type { HostServerOnConnectResult } from '../ssh/ssh-host-server-on-connect'
import { unservedHostServerMessage, unservedServerStatus } from '../ssh/ssh-host-unserved-status'
import {
clearSshHostServerStatus,
getSshHostServerStatus,
setSshHostServerStatus
} from '../ssh/ssh-host-server-status'
import { connectionManager, getCurrentMainWindow } from './ssh-ipc-context'
import { restoreManagedHostPortForwards } from './ssh-managed-port-forward-restore'
import { broadcastSshState, getPublicSshState } from './ssh-renderer-broadcast'
import { isAuthError } from '../ssh/ssh-connection-utils'
export async function decideHostServer(target: SshTarget): Promise<HostServerOnConnectResult> {
// Why lazy: the managed-server graph (deploy, migration, tunnel) loads only when a host connects.
const [{ resolveHostServerOnConnect }, { hostServerOnConnectDeps }] = await Promise.all([
import('../ssh/ssh-host-server-on-connect'),
import('./ssh-host-server-on-connect-wiring')
])
return await resolveHostServerOnConnect(
target,
hostServerOnConnectDeps(getAppEnvironment().getPath('userData'))
)
}
/** Rechecks a host another desktop's update held during this connect, until the fence clears. */
export function recheckWhenManagedFenceClears(target: SshTarget, environmentId: string): void {
void Promise.all([
import('../ssh/managed-server-fence-recheck'),
import('./ssh-host-server-on-connect-wiring')
]).then(([{ recheckFencedManagedServer, scheduleManagedServerFenceRecheck }, wiring]) => {
const deps = wiring.hostServerOnConnectDeps(getAppEnvironment().getPath('userData'))
scheduleManagedServerFenceRecheck(target.id, {
stillCurrent: () => {
const status = getSshHostServerStatus(target.id)
return (
connectionManager?.getState(target.id)?.status === 'connected' &&
status?.kind === 'managed' &&
status.environmentId === environmentId
)
},
recheck: () => recheckFencedManagedServer(target, environmentId, deps),
publish: (result) =>
publishManagedServerConnect(target.id, environmentId, result.update, result.serving)
})
})
}
/**
* A host only its managed server reaches failed to set up: leave 'connecting' and the 'setting
* up' status for the error, as a failed transport connect does.
*/
export function publishHostServerDecisionFailure(targetId: string, error: unknown): void {
const failure = error instanceof Error ? error : new Error(String(error))
clearSshHostServerStatus(targetId)
broadcastSshState(getCurrentMainWindow, targetId, {
targetId,
status: isAuthError(failure) ? 'auth-failed' : 'error',
error: failure.message,
reconnectAttempt: 0
})
}
export function publishManagedServerConnect(
targetId: string,
environmentId: string,
update?: SshManagedServerUpdateNote,
serving?: SshManagedServerServingNote,
linked?: SshManagedServerLinkedProject[]
): SshConnectionState {
const managedServer = {
kind: 'managed' as const,
environmentId,
...(update ? { update } : {}),
...(serving ? { serving } : {}),
...(linked ? { linked } : {})
}
setSshHostServerStatus(targetId, managedServer)
void restoreManagedHostPortForwards(targetId)
const state: SshConnectionState = {
...(connectionManager!.getState(targetId) ?? { targetId, reconnectAttempt: 0 }),
targetId,
status: 'connected',
error: null,
managedServer
}
broadcastSshState(getCurrentMainWindow, targetId, state)
return getPublicSshState(targetId) ?? state
}
/**
* COMPAT(legacy-relay-bridge): a host that can't move only because relay terminals run in it stays
* connected, so the bridge keeps those terminals working in their tabs until they exit.
*/
export function publishBridgedUnservedHostServer(
target: SshTarget,
decision: Extract<HostServerOnConnectResult, { route: 'unserved' }>
): SshConnectionState {
const managedServer = unservedServerStatus(decision)
setSshHostServerStatus(target.id, managedServer)
const state: SshConnectionState = {
...(connectionManager!.getState(target.id) ?? { targetId: target.id, reconnectAttempt: 0 }),
targetId: target.id,
status: 'connected',
error: null,
managedServer
}
broadcastSshState(getCurrentMainWindow, target.id, state)
return getPublicSshState(target.id) ?? state
}
/**
* A host the managed server doesn't serve: publish why and fail the connect. There is no fallback;
* the renderer translates the status, and this message is only its untranslated twin.
*/
export function publishUnservedHostServer(
target: SshTarget,
decision: Extract<HostServerOnConnectResult, { route: 'unserved' }>
): Error {
const managedServer = unservedServerStatus(decision)
setSshHostServerStatus(target.id, managedServer)
const failure = new Error(unservedHostServerMessage(decision))
broadcastSshState(getCurrentMainWindow, target.id, {
targetId: target.id,
status: 'error',
error: failure.message,
reconnectAttempt: 0,
managedServer
})
return failure
}