Files
orca/src/main/ipc/runtime.ts
T

158 lines
5.6 KiB
TypeScript

import { BrowserWindow, ipcMain } from 'electron'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import type {
RuntimeBrowserDriverState,
RuntimeStatus,
RuntimeSyncWindowGraphResult,
RuntimeSyncWindowGraph,
RuntimeTerminalDriverState
} from '../../shared/runtime-types'
import type { RuntimeRpcResponse } from '../../shared/runtime-rpc-envelope'
import { TERMINAL_FIT_RESTORE_DEADLINE_MS } from '../../shared/terminal-fit-restore-deadline'
import { RpcDispatcher } from '../runtime/rpc/dispatcher'
import { ALL_RPC_METHODS } from '../runtime/rpc/methods'
function boundTerminalFitRestore(pending: Promise<boolean>): Promise<boolean> {
let timer: ReturnType<typeof setTimeout> | undefined
const deadline = new Promise<boolean>((resolve) => {
timer = setTimeout(() => resolve(false), TERMINAL_FIT_RESTORE_DEADLINE_MS)
timer.unref?.()
})
return Promise.race([pending, deadline]).finally(() => clearTimeout(timer))
}
export function registerRuntimeHandlers(runtime: OrcaRuntimeService): void {
const pendingTerminalFitRestores = new Map<string, Promise<boolean>>()
ipcMain.removeHandler('runtime:syncWindowGraph')
ipcMain.removeHandler('runtime:getStatus')
ipcMain.removeHandler('runtime:call')
ipcMain.handle(
'runtime:syncWindowGraph',
(event, graph: RuntimeSyncWindowGraph): RuntimeSyncWindowGraphResult => {
const window = BrowserWindow.fromWebContents(event.sender)
if (!window) {
throw new Error('Runtime graph sync must originate from a BrowserWindow')
}
return runtime.syncWindowGraph(window.id, graph)
}
)
ipcMain.handle('runtime:getStatus', (): RuntimeStatus => {
return runtime.getStatus()
})
ipcMain.handle(
'runtime:call',
async (
_event,
args: { method: string; params?: unknown }
): Promise<RuntimeRpcResponse<unknown>> => {
return (await new RpcDispatcher({ runtime, methods: ALL_RPC_METHODS }).dispatch({
id: 'desktop-ipc',
authToken: 'desktop-ipc',
method: args.method,
params: args.params
})) as RuntimeRpcResponse<unknown>
}
)
ipcMain.removeHandler('runtime:getTerminalFitOverrides')
ipcMain.handle(
'runtime:getTerminalFitOverrides',
(): {
ptyId: string
mode: 'mobile-fit' | 'remote-desktop-fit'
cols: number
rows: number
}[] => {
const overrides = runtime.getAllTerminalFitOverrides()
return Array.from(overrides.entries()).map(([ptyId, override]) => ({
ptyId,
...override
}))
}
)
ipcMain.removeHandler('runtime:getTerminalDrivers')
ipcMain.handle(
'runtime:getTerminalDrivers',
(): { ptyId: string; driver: RuntimeTerminalDriverState }[] => {
const drivers = runtime.getAllTerminalDrivers()
return Array.from(drivers.entries()).map(([ptyId, driver]) => ({ ptyId, driver }))
}
)
ipcMain.removeHandler('runtime:getBrowserDrivers')
ipcMain.handle(
'runtime:getBrowserDrivers',
(): { browserPageId: string; driver: RuntimeBrowserDriverState }[] => {
const drivers = runtime.getAllBrowserDrivers()
return Array.from(drivers.entries()).map(([browserPageId, driver]) => ({
browserPageId,
driver
}))
}
)
// Why: the desktop "Restore" button sets the display mode to 'desktop' and
// applies it, which restores the PTY to its original dimensions and emits
// a 'resized' event to any active mobile subscriber. This uses the same
// code path as the mobile toggle button (terminal.setDisplayMode RPC).
ipcMain.removeHandler('runtime:restoreTerminalFit')
ipcMain.handle('runtime:restoreTerminalFit', async (_event, args: { ptyId: string }) => {
// Why: this IPC powers the desktop "Take back" button. Beyond restoring
// PTY dims (the original semantic), it now also reclaims the input
// floor for the desktop via the driver state machine. The lock banner
// unmounts and desktop input/resize are unblocked until the next
// mobile interaction takes the floor again. See
// docs/mobile-presence-lock.md.
//
// Why async: reclaimTerminalForDesktop awaits applyMobileDisplayMode's
// PTY-resize chain. Returning the unresolved Promise to ipcMain made
// Electron try to structured-clone a Promise — "An object could not
// be cloned" error — and the renderer's restoreTerminalFit() rejected
// with no useful info.
// Why: keep one underlying reclaim per PTY even after callers time out;
// layout serialization means a retry cannot bypass the wedged operation.
let pending = pendingTerminalFitRestores.get(args.ptyId)
if (!pending) {
try {
let tracked!: Promise<boolean>
const clearTrackedRestore = (): void => {
if (pendingTerminalFitRestores.get(args.ptyId) === tracked) {
pendingTerminalFitRestores.delete(args.ptyId)
}
}
tracked = runtime.reclaimTerminalForDesktop(args.ptyId).then(
(restored) => {
clearTrackedRestore()
return restored
},
() => {
clearTrackedRestore()
return false
}
)
pending = tracked
pendingTerminalFitRestores.set(args.ptyId, pending)
} catch {
return { restored: false }
}
}
return { restored: await boundTerminalFitRestore(pending) }
})
ipcMain.removeHandler('runtime:reclaimBrowserForDesktop')
ipcMain.handle(
'runtime:reclaimBrowserForDesktop',
(_event, args: { browserPageId: string }): { reclaimed: boolean } => {
try {
return { reclaimed: runtime.reclaimBrowserForDesktop(args.browserPageId) }
} catch {
return { reclaimed: false }
}
}
)
}