mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 00:27:39 +00:00
* Revert "feat(orcad): source-side dormant export of a relay-hosted SSH target (#16741 T6-8) (#24519)" This reverts commit783101b304. * Revert "feat(ssh): update, roll back, recover and stop a managed orcad server (#16741 T6-5 follow-up) (#24463)" This reverts commit38c2d1dcb9. * Revert "feat(ssh): deploy and pair an empty managed orcad server over SSH (#16741 T6-5) (#24453)" This reverts commit8b76683b40. * Revert "fix(ssh): orcad GC honors the activation journal; readiness requires proven daemon coverage (#16741 T6 follow-up) (#24451)" This reverts commitd3f8c5063b. * Revert "feat(ssh): remote orcad stop by request file and journaled decommission (#16741 T6-4) (#24449)" This reverts commit43d9b43d3f. * Revert "feat(orcad): supervisable server: stop requests, managed stop receipts and a lifetime that keeps its lock on failed teardown (#16741 T6-3) (#24433)" This reverts commitb093d3ab20. * Revert "feat(ssh): crash-safe orcad activation, rollback and recovery (#16741 T6-2) (#24423)" This reverts commit1a9ac0e955. * Revert "feat(runtime): SSH access links for paired servers in a downgrade-safe sidecar (#16741 T5-1+T5-2) (#24420)" This reverts commit99db2bfae4. * Revert "feat(relay): capability-gated owner reset with a durable preparation journal (#16741 T3 R1) (#24418)" This reverts commit34a582bd39. * Revert "feat(ssh): track connection-manager drains, test probes and provider continuations (#16741 T2 P3+P8a) (#24407)" This reverts commitd53063d2b1. * Revert "feat(daemon): idle retirement, session census and recovery-only provider (#16741 T2 P4b) (#24409)" This reverts commitff212dbbef. * Revert "feat(ssh): add pty.resumeClient and split SSH PTY process listing (#16741 T2 P5+P6) (#24414)" This reverts commit92cb71765e. * Revert "feat(relay): await owned watcher and agent children on shutdown (#16741 T2 P1) (#24400)" This reverts commit6b36e4f85b. * Revert "feat(session): retry failed renderer session writes and verify local folder PTYs (#16741 T2 P9) (#24406)" This reverts commitd23ecef301. * Revert "feat(ssh): remote orcad primitives on the pinned Node runtime (#16741 T6-1) (#24419)" This reverts commitdd87ae578d. * Revert "fix(runtime): fence runtime-environment subscriptions and status probes by identity (#16741 T5-3) (#24421)" This reverts commitece9e4d2e3. * Revert "feat(orcad): migration manifest and dormant-state contracts (#16741 T6-7) (#24422)" This reverts commit3fbdaba262. * Revert "feat(ssh): wire SshConnection through the work and transport close ledgers (#16741 T2 P2) (#24401)" This reverts commit4e8edc8872. * Revert "feat(profiles): carry markdown frontmatter visibility in project transfers (#16741 T2 P7) (#24405)" This reverts commit60c93263cc. * Revert "fix(runtime): project the PTY incarnation onto mobile session tabs (#24413)" This reverts commit99e0303572. * Revert "feat(daemon): tag daemon stream data with the PTY incarnation id (#16741 T2 P4a) (#24402)" This reverts commit817af768b0. * Revert "feat(ssh): port the SSH connection work ledger and transport close ledger (#16741 T2) (#24210)" This reverts commitc9918931c8. * Revert "feat(relay): fence and drain file and git response streams on shutdown (#24185)" This reverts commitdc08ffeba9. * Revert "refactor(runtime-rpc): extract the Node WebSocket lifecycle; opt-in pinned port (#24186)" This reverts commita789233bbb. * Revert "feat(relay): route relay handlers through work admission; producer publication drain (#24181)" This reverts commit0b812bd698. * Revert "feat(relay): land the #16741 T1 seam (work drain, publication drain, release gate) (#24156)" This reverts commit3aa2d3af7c. --------- Co-authored-by: m4air <m4air@Mac.localdomain>
159 lines
6.0 KiB
TypeScript
159 lines
6.0 KiB
TypeScript
import { activeSessions } from './ssh-active-relay-sessions'
|
|
import {
|
|
connectInFlight,
|
|
invalidateConnectAttempt,
|
|
resetRelayInFlight,
|
|
testConnectionProbes
|
|
} from './ssh-connect-attempt-registry'
|
|
import { connectionManager } from './ssh-ipc-context'
|
|
import { teardownActiveSshSession } from './ssh-session-teardown'
|
|
|
|
// Why one budget for the whole sequence rather than one per phase: an invalidated connect only
|
|
// observes its cancellation at the next checkpoint, and one blocked in the transport handshake can
|
|
// sit there for the whole SSH timeout. A per-phase timeout lets any single phase consume the global
|
|
// quit deadline; a shared absolute deadline cannot.
|
|
export const SSH_SHUTDOWN_BUDGET_MS = 6_000
|
|
|
|
export type SshShutdownPhase = 'drain' | 'in-flight-join' | 'final-drain'
|
|
export type SshShutdownUnfinished = { targetId: string; phase: SshShutdownPhase }
|
|
export type SshShutdownResult = {
|
|
unfinished: readonly SshShutdownUnfinished[]
|
|
errors: readonly unknown[]
|
|
}
|
|
|
|
type SshShutdownTask = { targetId: string; promise: Promise<unknown> }
|
|
|
|
let sshShutdownDrain: Promise<SshShutdownResult> | null = null
|
|
|
|
export function resetSshShutdownDrain(): void {
|
|
sshShutdownDrain = null
|
|
}
|
|
|
|
async function settleTasksWithinMs(
|
|
tasks: readonly SshShutdownTask[],
|
|
timeoutMs: number
|
|
): Promise<{ timedOut: SshShutdownTask[]; errors: unknown[] }> {
|
|
const pending = new Set(tasks)
|
|
const errors: unknown[] = []
|
|
if (tasks.length === 0) {
|
|
return { timedOut: [], errors }
|
|
}
|
|
const tracked = tasks.map(async (task) => {
|
|
try {
|
|
await task.promise
|
|
} catch (error) {
|
|
errors.push(error)
|
|
}
|
|
pending.delete(task)
|
|
})
|
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
try {
|
|
await Promise.race([
|
|
Promise.all(tracked),
|
|
new Promise<void>((resolve) => {
|
|
timer = setTimeout(resolve, timeoutMs)
|
|
timer.unref?.()
|
|
})
|
|
])
|
|
} finally {
|
|
clearTimeout(timer)
|
|
}
|
|
return { timedOut: [...pending], errors }
|
|
}
|
|
|
|
function sshShutdownTasks(targetIds: readonly string[]): SshShutdownTask[] {
|
|
return [
|
|
...targetIds
|
|
.filter((targetId) => activeSessions.has(targetId))
|
|
.map((targetId) => ({
|
|
targetId,
|
|
promise: teardownActiveSshSession(targetId, (session) => session.detachAndPersist())
|
|
})),
|
|
{ targetId: '*transports', promise: connectionManager?.disconnectAll() ?? Promise.resolve() }
|
|
]
|
|
}
|
|
|
|
async function drainSshShutdown(
|
|
targetIds: readonly string[],
|
|
inFlight: readonly SshShutdownTask[],
|
|
detachErrors: readonly unknown[] = []
|
|
): Promise<SshShutdownResult> {
|
|
const deadline = Date.now() + SSH_SHUTDOWN_BUDGET_MS
|
|
const unfinished: SshShutdownUnfinished[] = []
|
|
const errors: unknown[] = [...detachErrors]
|
|
const runPhase = async (
|
|
phase: SshShutdownPhase,
|
|
tasks: readonly SshShutdownTask[]
|
|
): Promise<boolean> => {
|
|
const remainingMs = deadline - Date.now()
|
|
if (remainingMs <= 0) {
|
|
unfinished.push(...tasks.map((task) => ({ targetId: task.targetId, phase })))
|
|
return false
|
|
}
|
|
const settled = await settleTasksWithinMs(tasks, remainingMs)
|
|
errors.push(...settled.errors)
|
|
unfinished.push(...settled.timedOut.map((task) => ({ targetId: task.targetId, phase })))
|
|
return settled.timedOut.length === 0
|
|
}
|
|
|
|
await runPhase('drain', sshShutdownTasks(targetIds))
|
|
// Why a second drain after the join: a connect paused in old-session teardown still publishes its
|
|
// replacement session and opens a transport before it reaches the cancellation checkpoint, so the
|
|
// first drain can miss both.
|
|
if (await runPhase('in-flight-join', inFlight)) {
|
|
await runPhase('final-drain', sshShutdownTasks([...activeSessions.keys()]))
|
|
}
|
|
|
|
if (errors.length > 0 || unfinished.length > 0) {
|
|
// Why one aggregate line: per-target logging on the quit path competes with the final flush for
|
|
// the little time that remains.
|
|
console.warn(
|
|
`[ssh] Shutdown drain finished with ${errors.length} error(s); unfinished: ${
|
|
unfinished.map((entry) => `${entry.targetId}/${entry.phase}`).join(', ') || 'none'
|
|
}`
|
|
)
|
|
}
|
|
return { unfinished, errors }
|
|
}
|
|
|
|
// Why one entry point that returns rather than awaits: every in-memory transition the final store
|
|
// flush must snapshot happens synchronously here, before this returns, so the caller can start that
|
|
// flush with no await in between. Idempotent — a later call joins the same drain and repeats no
|
|
// state transition.
|
|
//
|
|
// Why no fence latch here: the committed quit path sets it before calling this, so it is already on
|
|
// for the snapshot below. Called without that gate (tests), this degrades to a plain drain.
|
|
export function beginSshShutdown(): Promise<SshShutdownResult> {
|
|
if (sshShutdownDrain) {
|
|
return sshShutdownDrain
|
|
}
|
|
const inFlight: SshShutdownTask[] = [
|
|
...[...connectInFlight.entries()].map(([targetId, attempt]) => ({
|
|
targetId,
|
|
promise: attempt.promise
|
|
})),
|
|
...[...resetRelayInFlight.entries()].map(([targetId, promise]) => ({ targetId, promise })),
|
|
...[...testConnectionProbes].map((promise) => ({ targetId: '*probe', promise }))
|
|
]
|
|
for (const targetId of Array.from(connectInFlight.keys())) {
|
|
invalidateConnectAttempt(targetId)
|
|
}
|
|
const targetIds = [...activeSessions.keys()]
|
|
// Why before any await: this is the whole point of the split. Each session marks its recovery lease
|
|
// detached in memory now, and the final flush persists it — the remote PTYs keep running.
|
|
const detachErrors: unknown[] = []
|
|
for (const session of activeSessions.values()) {
|
|
// Why per-session: this runs synchronously inside a non-async will-quit listener, so one throw
|
|
// (teardownProviders -> webContents.send on a destroyed renderer, routine on quit) would escape
|
|
// it and skip every later session, the drain assignment, and the store flush that persists all
|
|
// of this. Collect and keep going; the drain reports them.
|
|
try {
|
|
session.beginShutdownDetach()
|
|
} catch (error) {
|
|
detachErrors.push(error)
|
|
}
|
|
}
|
|
sshShutdownDrain = drainSshShutdown(targetIds, inFlight, detachErrors)
|
|
return sshShutdownDrain
|
|
}
|