Files
orca/src/main/ipc/runtime-environments.ts
T
OrcaWinandm4air 5f308bfa9c revert: take the 26 Phase 3 (#16741 port) PRs back out of main (#24559)
* Revert "feat(orcad): source-side dormant export of a relay-hosted SSH target (#16741 T6-8) (#24519)"

This reverts commit 783101b304.

* Revert "feat(ssh): update, roll back, recover and stop a managed orcad server (#16741 T6-5 follow-up) (#24463)"

This reverts commit 38c2d1dcb9.

* Revert "feat(ssh): deploy and pair an empty managed orcad server over SSH (#16741 T6-5) (#24453)"

This reverts commit 8b76683b40.

* Revert "fix(ssh): orcad GC honors the activation journal; readiness requires proven daemon coverage (#16741 T6 follow-up) (#24451)"

This reverts commit d3f8c5063b.

* Revert "feat(ssh): remote orcad stop by request file and journaled decommission (#16741 T6-4) (#24449)"

This reverts commit 43d9b43d3f.

* 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 commit b093d3ab20.

* Revert "feat(ssh): crash-safe orcad activation, rollback and recovery (#16741 T6-2) (#24423)"

This reverts commit 1a9ac0e955.

* Revert "feat(runtime): SSH access links for paired servers in a downgrade-safe sidecar (#16741 T5-1+T5-2) (#24420)"

This reverts commit 99db2bfae4.

* Revert "feat(relay): capability-gated owner reset with a durable preparation journal (#16741 T3 R1) (#24418)"

This reverts commit 34a582bd39.

* Revert "feat(ssh): track connection-manager drains, test probes and provider continuations (#16741 T2 P3+P8a) (#24407)"

This reverts commit d53063d2b1.

* Revert "feat(daemon): idle retirement, session census and recovery-only provider (#16741 T2 P4b) (#24409)"

This reverts commit ff212dbbef.

* Revert "feat(ssh): add pty.resumeClient and split SSH PTY process listing (#16741 T2 P5+P6) (#24414)"

This reverts commit 92cb71765e.

* Revert "feat(relay): await owned watcher and agent children on shutdown (#16741 T2 P1) (#24400)"

This reverts commit 6b36e4f85b.

* Revert "feat(session): retry failed renderer session writes and verify local folder PTYs (#16741 T2 P9) (#24406)"

This reverts commit d23ecef301.

* Revert "feat(ssh): remote orcad primitives on the pinned Node runtime (#16741 T6-1) (#24419)"

This reverts commit dd87ae578d.

* Revert "fix(runtime): fence runtime-environment subscriptions and status probes by identity (#16741 T5-3) (#24421)"

This reverts commit ece9e4d2e3.

* Revert "feat(orcad): migration manifest and dormant-state contracts (#16741 T6-7) (#24422)"

This reverts commit 3fbdaba262.

* Revert "feat(ssh): wire SshConnection through the work and transport close ledgers (#16741 T2 P2) (#24401)"

This reverts commit 4e8edc8872.

* Revert "feat(profiles): carry markdown frontmatter visibility in project transfers (#16741 T2 P7) (#24405)"

This reverts commit 60c93263cc.

* Revert "fix(runtime): project the PTY incarnation onto mobile session tabs (#24413)"

This reverts commit 99e0303572.

* Revert "feat(daemon): tag daemon stream data with the PTY incarnation id (#16741 T2 P4a) (#24402)"

This reverts commit 817af768b0.

* Revert "feat(ssh): port the SSH connection work ledger and transport close ledger (#16741 T2) (#24210)"

This reverts commit c9918931c8.

* Revert "feat(relay): fence and drain file and git response streams on shutdown (#24185)"

This reverts commit dc08ffeba9.

* Revert "refactor(runtime-rpc): extract the Node WebSocket lifecycle; opt-in pinned port (#24186)"

This reverts commit a789233bbb.

* Revert "feat(relay): route relay handlers through work admission; producer publication drain (#24181)"

This reverts commit 0b812bd698.

* Revert "feat(relay): land the #16741 T1 seam (work drain, publication drain, release gate) (#24156)"

This reverts commit 3aa2d3af7c.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-02 00:52:32 -07:00

296 lines
12 KiB
TypeScript

import { app, ipcMain } from 'electron'
import { randomUUID } from 'node:crypto'
import { listEnvironments, resolveEnvironment } from '../../shared/runtime-environment-store'
import type { RemoteRuntimeSubscription } from '../../shared/remote-runtime-client'
import type { Store } from '../persistence'
import {
isRuntimeEnvironmentManuallyDisconnected,
registerRuntimeEnvironmentConnectivityHandlers,
registerRuntimeEnvironmentPassiveHandlers
} from './runtime-environment-connectivity-handlers'
import {
closeRemoteRuntimeRequestConnection,
getRuntimeEnvironmentStatusOwner
} from './runtime-environment-request-connections'
import { registerRuntimeEnvironmentRecoveryHandler } from './runtime-environment-recovery-handler'
import {
advanceRuntimeEnvironmentTransportGeneration,
getRuntimeEnvironmentTransportGeneration
} from './runtime-environment-transport-generation'
import {
resetSharedControlSupport,
subscribeRuntimeEnvironment
} from './runtime-environment-transport-routing'
import { RUNTIME_ENVIRONMENT_HANDLER_CHANNELS } from './runtime-environment-handler-channels'
import { retirePairedRuntimeBrowserClientHostEnvironment } from '../browser/paired-runtime-browser-client-host-runtime'
import { registerRuntimeEnvironmentBrowserClientHostHandler } from './runtime-environment-browser-client-host-handler'
import { advanceRuntimeEnvironmentCapabilityIncarnation } from './runtime-environment-capability-evidence'
type RetainedRemoteRuntimeSubscription = RemoteRuntimeSubscription & {
environmentId: string
ownerWebContentsId: number
removeDestroyedListener: () => void
notifyClosed: () => void
}
const remoteRuntimeSubscriptions = new Map<string, RetainedRemoteRuntimeSubscription>()
const getUserDataPath = (): string => app.getPath('userData')
function closeSubscriptionsForEnvironment(environmentId: string): void {
// Why: removed runtimes must not retain terminal/browser WebSockets until renderer teardown.
for (const [subscriptionId, subscription] of remoteRuntimeSubscriptions) {
if (subscription.environmentId !== environmentId) {
continue
}
remoteRuntimeSubscriptions.delete(subscriptionId)
// Why: one failing teardown must not abandon this environment's other
// sockets -- that strands exactly the dead handles this sweep exists to
// retire. Guard the two steps independently so neither can skip the other,
// and so the isolation stays structural rather than resting on a claim that
// nothing inside notifyClosed will ever throw.
try {
subscription.close()
} catch (error) {
console.warn('[runtime-environments] subscription close failed during retirement:', error)
}
try {
// Why: a shared-control logical close never calls back, so notify directly.
subscription.notifyClosed()
} catch (error) {
console.warn('[runtime-environments] subscription close notice failed:', error)
}
}
}
/** Returns once the environment's client-hosted browser pages have been released. */
export function invalidateRuntimeEnvironmentTransport(environmentId: string): Promise<void> {
// Why: a same-id re-pair must retire every transport that still authenticates as the old peer.
advanceRuntimeEnvironmentCapabilityIncarnation(environmentId)
advanceRuntimeEnvironmentTransportGeneration(environmentId)
closeRemoteRuntimeRequestConnection(environmentId)
closeSubscriptionsForEnvironment(environmentId)
return retirePairedRuntimeBrowserClientHostEnvironment(
environmentId,
new Error('Runtime environment transport was invalidated')
).then(
() => undefined,
(error) => {
console.warn('[runtime-environments] browser client host retirement failed:', error)
}
)
}
export function registerRuntimeEnvironmentHandlers(store: Store): void {
// Why: keep direct re-registration safe even though register-core-handlers
// normally guards this path; otherwise the binary send listener can stack.
resetSharedControlSupport()
for (const channel of RUNTIME_ENVIRONMENT_HANDLER_CHANNELS) {
ipcMain.removeHandler(channel)
}
ipcMain.removeAllListeners('runtimeEnvironments:subscriptionBinary')
registerRuntimeEnvironmentConnectivityHandlers({
store,
getUserDataPath,
invalidateTransport: invalidateRuntimeEnvironmentTransport
})
registerRuntimeEnvironmentBrowserClientHostHandler({
getUserDataPath,
getSettings: () => store.getSettings()
})
registerRuntimeEnvironmentRecoveryHandler()
registerRuntimeEnvironmentPassiveHandlers(getUserDataPath)
for (const environment of listEnvironments(getUserDataPath())) {
if (!isRuntimeEnvironmentManuallyDisconnected(environment.id)) {
getRuntimeEnvironmentStatusOwner(getUserDataPath(), environment.id).activate()
}
}
ipcMain.handle(
'runtimeEnvironments:subscribe',
async (
event,
args: {
selector: string
method: string
params?: unknown
timeoutMs?: number
subscriptionId?: string
expectedEnvironmentPairingRevision?: number
expectedEnvironmentRuntimeId?: string
}
): Promise<{ subscriptionId: string; requestId: string }> => {
const subscriptionId =
typeof args.subscriptionId === 'string' && args.subscriptionId.length > 0
? args.subscriptionId
: randomUUID()
if (remoteRuntimeSubscriptions.has(subscriptionId)) {
throw new Error('Runtime environment subscription id already exists')
}
const environment = resolveEnvironment(getUserDataPath(), args.selector)
if (isRuntimeEnvironmentManuallyDisconnected(environment.id)) {
throw new Error('runtime_manually_disconnected')
}
const pairingRevision = environment.pairingRevision ?? environment.createdAt
if (
args.expectedEnvironmentPairingRevision !== undefined &&
pairingRevision !== args.expectedEnvironmentPairingRevision
) {
throw new Error('Runtime environment pairing changed; refresh and try again')
}
if (
args.expectedEnvironmentRuntimeId !== undefined &&
environment.runtimeId !== args.expectedEnvironmentRuntimeId
) {
throw new Error('Runtime environment identity changed; refresh and try again')
}
const transportGeneration = getRuntimeEnvironmentTransportGeneration(environment.id)
const transportIsCurrent = (): boolean =>
getRuntimeEnvironmentTransportGeneration(environment.id) === transportGeneration
const sender = event.sender
const ownerWebContentsId = sender.id
let senderDestroyed = sender.isDestroyed()
let subscription: RemoteRuntimeSubscription | null = null
let destroyedListenerAttached = false
const removeDestroyedListener = (): void => {
if (!destroyedListenerAttached) {
return
}
destroyedListenerAttached = false
sender.removeListener('destroyed', closeSubscription)
}
const closeSubscription = (): void => {
senderDestroyed = true
const retained = remoteRuntimeSubscriptions.get(subscriptionId) ?? null
remoteRuntimeSubscriptions.delete(subscriptionId)
if (retained) {
retained.close()
return
}
removeDestroyedListener()
subscription?.close()
}
// Why: the renderer treats close as terminal and drops its handle, so send it once.
// Latch before sending so a re-entrant call cannot duplicate it, and never
// throw: a dying renderer must not abort its siblings' retirement.
let closeNotified = false
const notifyClosed = (): void => {
if (closeNotified || sender.isDestroyed()) {
return
}
closeNotified = true
try {
sender.send('runtimeEnvironments:subscriptionEvent', { subscriptionId, type: 'close' })
} catch {
// The renderer is gone; there is no one left to tell.
}
}
sender.once('destroyed', closeSubscription)
destroyedListenerAttached = true
try {
subscription = await subscribeRuntimeEnvironment(
getUserDataPath(),
environment.id,
args.method,
args.params,
args.timeoutMs,
{
onEvent: (payload) => {
if (payload.type === 'close') {
// Why: retirement advances the generation before closing, so gating
// close on it stranded the renderer with a dead subscription.
notifyClosed()
return
}
if (transportIsCurrent() && !sender.isDestroyed()) {
sender.send('runtimeEnvironments:subscriptionEvent', {
subscriptionId,
...payload
})
}
},
onClose: () => {
const retained = remoteRuntimeSubscriptions.get(subscriptionId) ?? null
retained?.removeDestroyedListener()
remoteRuntimeSubscriptions.delete(subscriptionId)
}
},
transportIsCurrent
)
} catch (error) {
removeDestroyedListener()
throw error
}
let pairingIsCurrent = false
try {
const currentEnvironment = resolveEnvironment(getUserDataPath(), environment.id)
pairingIsCurrent =
(currentEnvironment.pairingRevision ?? currentEnvironment.createdAt) === pairingRevision
} catch {
pairingIsCurrent = false
}
if (!transportIsCurrent() || !pairingIsCurrent) {
removeDestroyedListener()
subscription.close()
throw new Error('Runtime environment pairing changed; refresh and try again')
}
if (senderDestroyed || sender.isDestroyed()) {
removeDestroyedListener()
subscription.close()
return { subscriptionId, requestId: subscription.requestId }
}
remoteRuntimeSubscriptions.set(subscriptionId, {
requestId: subscription.requestId,
environmentId: environment.id,
ownerWebContentsId,
removeDestroyedListener,
notifyClosed,
sendBinary: (bytes) => subscription?.sendBinary(bytes) ?? false,
close: () => {
removeDestroyedListener()
subscription?.close()
}
})
return { subscriptionId, requestId: subscription.requestId }
}
)
ipcMain.handle(
'runtimeEnvironments:unsubscribe',
(event, args: { subscriptionId: string }): { unsubscribed: boolean } => {
const subscription = remoteRuntimeSubscriptions.get(args.subscriptionId)
if (!subscription || subscription.ownerWebContentsId !== event.sender.id) {
return { unsubscribed: false }
}
remoteRuntimeSubscriptions.delete(args.subscriptionId)
subscription.close()
return { unsubscribed: true }
}
)
ipcMain.on(
'runtimeEnvironments:subscriptionBinary',
(event, args: { subscriptionId?: unknown; bytes?: unknown }) => {
if (typeof args.subscriptionId !== 'string') {
return
}
const bytes = toBinaryPayload(args.bytes)
if (!bytes) {
return
}
const subscription = remoteRuntimeSubscriptions.get(args.subscriptionId)
if (subscription?.ownerWebContentsId === event.sender.id) {
subscription.sendBinary(bytes)
}
}
)
}
function toBinaryPayload(value: unknown): Uint8Array<ArrayBufferLike> | null {
if (value instanceof Uint8Array) {
return value
}
if (value instanceof ArrayBuffer) {
return new Uint8Array(value)
}
if (ArrayBuffer.isView(value)) {
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
}
return null
}