mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 09:07:40 +00:00
* fix(ssh): re-check a macOS host's ended launch-session warning so another desktop's restart can clear it The ended verdict was cached for the whole SSH session, so when another desktop restarted the server under a live session, this desktop kept the warning. It now expires like every other verdict: a live re-probe clears the note, and an unanswered one keeps the proven verdict. Co-Authored-By: Claude <noreply@anthropic.com> * fix(ssh): only a live probe clears a proven launch-session end, and a failed recheck republishes it Co-Authored-By: Claude <noreply@anthropic.com> * fix(ssh): a failed launch-session recheck restores the warning only for the same live connection Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
120 lines
4.5 KiB
TypeScript
120 lines
4.5 KiB
TypeScript
/** After a managed connect or restore: flags a macOS host whose server outlived its launching SSH session. */
|
|
import { getAppEnvironment } from '../../shared/app-environment'
|
|
import { listEnvironments } from '../../shared/runtime-environment-store'
|
|
import { readManagedOrcadLaunchSshSession } from '../ssh/orcad-managed-launch-session'
|
|
import { knownSshHostPlatform } from '../ssh/ssh-host-platform-memo'
|
|
import { getSshHostServerStatus } from '../ssh/ssh-host-server-status'
|
|
import { connectionManager } from './ssh-ipc-context'
|
|
import {
|
|
clearLaunchSessionNote,
|
|
publishHostServerStatus
|
|
} from './runtime-environment-managed-tunnel'
|
|
|
|
// Once per SSH session: a restore runs on every runtime call, and the answer only changes when
|
|
// a session ends, which the next session (a new connection or reconnect generation) checks.
|
|
// The ended verdict is reapplied so a status a later connect replaces gets the warning back.
|
|
// Every verdict expires: another desktop can end its launching session, or restart the server.
|
|
const checked = new WeakMap<object, { generation: number; ended: boolean; at: number }>()
|
|
const RECHECK_MS = 10 * 60_000
|
|
|
|
/** Never blocks or fails the caller; anything but a proven exit shows nothing. */
|
|
export async function noteEndedLaunchSshSession(
|
|
targetId: string,
|
|
environmentId: string,
|
|
now: () => number = Date.now
|
|
): Promise<void> {
|
|
// Why the memo: Linux and Windows hosts cost no extra round trip; the tunnel build fills it.
|
|
if (knownSshHostPlatform(targetId)?.os !== 'darwin') {
|
|
return
|
|
}
|
|
const connection = connectionManager?.getConnection(targetId)
|
|
if (!connection) {
|
|
return
|
|
}
|
|
const generation = connection.getConnectGeneration()
|
|
const prior = checked.get(connection)
|
|
const sameSession = prior?.generation === generation
|
|
if (sameSession && now() - prior.at < RECHECK_MS) {
|
|
if (prior.ended) {
|
|
flag(targetId, environmentId)
|
|
}
|
|
return
|
|
}
|
|
const pending = { generation, ended: false, at: now() }
|
|
checked.set(connection, pending)
|
|
// Only a live probe disproves a proven end; unanswered or unknown keeps it.
|
|
const provenEnded = sameSession && prior.ended
|
|
const markEnded = (): void => {
|
|
checked.set(connection, { generation, ended: true, at: now() })
|
|
flag(targetId, environmentId)
|
|
}
|
|
// Why: a disconnect, reconnect, unlink or relaunch during the probe makes its answer stale.
|
|
const stale = (): boolean =>
|
|
connectionManager?.getConnection(targetId) !== connection ||
|
|
connection.getConnectGeneration() !== generation ||
|
|
checked.get(connection) !== pending
|
|
try {
|
|
const session = await readManagedOrcadLaunchSshSession(
|
|
getAppEnvironment().getPath('userData'),
|
|
environmentId
|
|
)
|
|
if (stale()) {
|
|
return
|
|
}
|
|
const status = getSshHostServerStatus(targetId)
|
|
if (status?.kind === 'setting-up') {
|
|
// A start or update is still deciding; a later call checks again.
|
|
checked.delete(connection)
|
|
return
|
|
}
|
|
if (session === 'exited' || (session !== 'live' && provenEnded)) {
|
|
markEnded()
|
|
} else if (session === 'live' && launchSessionNoted(targetId, environmentId)) {
|
|
clearLaunchSessionNote(targetId, environmentId)
|
|
}
|
|
} catch (error) {
|
|
if (provenEnded && !stale()) {
|
|
markEnded()
|
|
}
|
|
console.warn('[ssh] Could not check which SSH session started the managed server:', error)
|
|
}
|
|
}
|
|
|
|
function launchSessionNoted(targetId: string, environmentId: string): boolean {
|
|
const status = getSshHostServerStatus(targetId)
|
|
return (
|
|
status?.kind === 'managed' &&
|
|
status.environmentId === environmentId &&
|
|
status.launchSessionEnded === true
|
|
)
|
|
}
|
|
|
|
/** A restored host may have no status yet; any other server decision is left alone. */
|
|
function flag(targetId: string, environmentId: string): void {
|
|
const userDataPath = getAppEnvironment().getPath('userData')
|
|
if (!listEnvironments(userDataPath).some((entry) => entry.id === environmentId)) {
|
|
return
|
|
}
|
|
const status = getSshHostServerStatus(targetId)
|
|
if (
|
|
!status ||
|
|
(status.kind === 'managed' &&
|
|
status.environmentId === environmentId &&
|
|
!status.serving &&
|
|
!status.launchSessionEnded)
|
|
) {
|
|
publishHostServerStatus(targetId, {
|
|
...(status ?? { kind: 'managed', environmentId }),
|
|
launchSessionEnded: true
|
|
})
|
|
}
|
|
}
|
|
|
|
/** After a verified restart or this client's relaunch: the cached verdict no longer describes the host. */
|
|
export function forgetLaunchSessionVerdict(targetId: string): void {
|
|
const connection = connectionManager?.getConnection(targetId)
|
|
if (connection) {
|
|
checked.delete(connection)
|
|
}
|
|
}
|