mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 11:37:25 +00:00
* wip(layout): applyDefaultTabs command in the layout model (dark) * wip(layout): applyDefaultTabs takes the window's proposed ids; main refuses a taken one * wip(layout): main closes or keeps an exited split pane by the window's rule (dark) * wip(layout): a create or split sent again after its close is refused (dark) * wip: typed test doubles * wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e) * wip(layout): main writes a worktree's default tabs in one command and starts them, window or none (dark; twin e) * wip(layout): main provisions setup and default tabs even when the agent fails to start; no window takes over (dark) * wip(layout): pane start status in frames; a failed start keeps its pane; a terminal close stands after a failed write (dark) * wip: lint fixes (max-lines, test throws) * test(layout): a terminal close stands after a real failed profile write; switch off still refuses * fix(layout): checked types in the close-failure rule and its tests * fix(layout): a failed start keeps its pane whatever failed; default tabs start once; a tab's last pane keeps its card (dark) * test(layout): the agent's kept pane counts as the workspace's terminal; main starts it whatever failed * fix(layout): the switch-off create path gains no await * fix(layout): a failed start never writes back a pane the user closed (dark) * fix(layout): a failed start's pane goes through main's create rules, so a closed tab stays closed (dark) * test(layout): D1a's switch-on close tests assert the intent main receives (both builds) * test(layout): the close fixture answers main's command without naming the method * fix(layout): a layout command that changes a pane's terminal raises its binding generation (dark, layout-owner build only) * fix(layout): main shows unshown live terminals again when a client attaches or the terminal service is replaced (dark, layout-owner build only) Headless servers had no retry: a terminal the startup pass declined stayed tabless until the next restart. The pass now also runs when a client authenticates and after the local terminal provider is installed or replaced, one pass at a time. A terminal whose pane record fails to save after main bound it is no longer reported as left without a pane. * feat(layout): main stores each pane's last grid on its tab layout (dark, layout-owner build only) A new main-only setPaneGrid command records the grid a pane's terminal was last sized to, on its leaf. It is saved as the layout's optional lastGridsByLeafId, which older builds drop on read, so a downgrade loses only the hint. It moves with its pane. * feat(layout): main remembers a resized terminal's grid on its pane and starts the pane at it (dark, layout-owner build only) Each accepted resize, from a window or a client, is written once per burst as the pane's last grid. A pane main holds starts at that grid, after a size the caller measured and before its group's size, then 120x40. * fix(layout): a re-created pane binds as designed; the id-only ratchet reads a shared import (dark) * fix(layout): a pane's grid resized just before quit is saved, and a grid change publishes nothing (dark, layout-owner build only) The desktop and serve quit and orcad's shutdown write any grid still inside its one-second burst before the final store flush; a quit with none waiting adds no await. A grid is main's own record: it is not in a published pane, so a resize burst sends no layout frame, and its write patches only the pane's tab. * feat(layout): a pane's status names sleeping, resuming and held, and what a status on a bound pane means (dark, layout-owner build only) * fix(layout): a pane's start status may sit on a pane still bound to its old terminal (dark) * fix(layout): main starts fresh a restored pane bound to another workspace's terminal (dark, layout-owner build only) The window's restore rule, now in main for every host: a pane whose stored terminal id was minted for another workspace is unbound, so it starts fresh, and that terminal, if live, is placed in its own workspace. The id check moves to shared, which the window now imports. * fix(layout): a start refused because a newer build's terminal service may still run the pane shows it held, not failed (dark, layout-owner build only) * fix(layout): a pane's start status ends when its pane binds again by any path (dark) * feat(layout): main's one start of a restored pane with no live terminal, shared by every non-window trigger (dark, layout-owner build only) Restores from the pane's own stored terminal and resumes a local agent record; concurrent triggers share one start, and a start that lost to another starter resolves to the winner's terminal. Not wired yet. * feat(layout): a client's first view and a CLI start of a restored pane go through main's one starter (dark, layout-owner build only) Twin j: the phone's first-view start of a pending row moves into its own runtime layer and, in the layout-owner build, runs through RestoredPaneStarter, which A1's send, wait, read and wake call as ensureRestoredPaneStarted. It restores from the pane's own stored terminal, resumes the agent the host recorded there, and resolves once the terminal is registered. With the switch off, the phone's start is unchanged. * test(layout): same-id restore ends a status; a replayed split brings back a closed pane by design * test(layout): a window restore, a client first view and a CLI send of one restored pane start one terminal (layout-owner build) * fix(layout): the restore pass releases a pane from another workspace's terminal through main's layout command, and two files stay under the line limit (dark, layout-owner build only) A new main-only releasePaneTerminal command unbinds a pane from the terminal it names and stops nothing, so the pass writes no layout outside a command. The stored terminal layout schema moves to its own file, and a pane's own size (measured, else its last grid) is one helper. * test(layout): drop a needless spread in the pane release test * feat(layout): main shows a restored pane whose terminal is gone as sleeping, and a window treats a status-only frame as a binding change (dark, layout-owner build only) After a census that answered, each local pane bound to a terminal the host no longer runs gets status 'sleeping' and keeps the id it restores from; any bind ends the status. Relay and paired server panes are their own host's to judge. A window rebuilds nothing for a frame that only changes a pane's status. * test(layout): restored history reaches each start across two host restarts with no client acking the cold restore A guard, not a fail-first test: the cold-restore ack only drops the daemon's in-memory copy (daemon-pty-session-control.ts) and the re-anchored checkpoint carries the history (daemon-pty-spawn-result.ts), so a host with no client keeps it. C1c moves the ack into main. * test(layout): the activation gate and worktree-reopen tests assert the same behaviour through main's command and frame (layout-owner build) In a layout-owner build main places live terminals and draws them through its frames, so the gate's adoption tests now check the window makes no tab or binding of its own, adopts only what main's frames hold, and resumes no agent over a live terminal main has not placed yet. The worktree-reopen tests feed main's frame for the restored pane and answer layout.apply, checking one tab bound to the live terminal and one resumed tab across reopens. * fix(layout): a restored SSH pane reattaches over its relay, and the restore starter returns the terminal it started (layout-owner build) An SSH terminal id names its connection, not a workspace, so the rule that drops another workspace's terminal no longer releases it or starts the pane fresh. The starter now resolves to the id its start registered instead of re-reading the stored layout, whose bind write can still be in flight, and a tab saved with no layout counts as one held pane. * fix(terminal): keep shipped exit and default-tab behaviour with the layout switch on An exited pane closes through the legacy retirement unless the keep rule holds; the default tab's title and color still reach the window and the phone's live tabs. * fix(layout): the restore starter's start result types a missing terminal id as none * fix(layout): a workspace's first tab waits for main's placement of its live terminals, so activation opens no extra shell beside one (layout-owner build) A window activating a workspace before main's placement pass had read the host census found no tab and asked main for the first one; main admitted it into the still-empty workspace, then the pass added the live terminal's own tab beside it. An initial create now waits out a pass in flight. * fix(layout): the restore starter never resolves to another workspace's live terminal, and a first view of a pane main dropped is refused (layout-owner build) * test(layout): the first-tab placement test overrides the worktree map with its declared type * fix(layout): the first-surface wait lives with main's placement pass and keeps the runtime file under the line limit * fix(terminal): one id guard for every pane write; keep panes on an unvouched -1 Main's taken and closed-id guard now runs where every pane write goes, so a runtime create replayed after its tab closed is refused before it spawns. An exit code nothing vouches for keeps the pane, as the window does; a kept pane sends the phone no retirement. Converts the switch-on tests that asserted the deleted window hand-off and title bridge. * fix(layout): a workspace's first surface waits only for the placement pass running when it arrives, and the switch-off build applies commands as before * fix(terminal): keep the closed-tab guard on every pane write, held-id checks on client commands A paired host starts each pane of a client's tab as a create under the tab's id, so a held tab id is refused only for a client's own layout command. * fix(layout): a quit's pane-grid writes never hold the stop past one second, and orcad awaits nothing for them when none wait The grid flush before the final save (desktop and serve quit, orcad stop) is now raced against a 1 s budget; a grid it misses only means that pane restarts at the default size. With the switch off no grid is ever recorded, so the flush is null and orcad no longer awaits even that. * fix(terminal): a provider-reported -1 keeps the pane, as the window does Only a requested stop or a host-confirmed death closes a pane on a negative exit code. Pins that the closed-tab guard never fires in the shipped build. * fix(layout): a quit's grid flush swallows its own failures, keeps its budget timer live, and the quit's no-await note names it * chore(terminal): satisfy the exhaustive-switch and test typing checks * test(terminal): give the replay test's live process a cwd * fix(layout): a quit writes pending pane grids before the SSH shutdown, and a workspace's first tab waits at most 0.5 s for main's placement pass The quit's grid flush (still capped at 1 s) now runs before beginSshShutdown, so nothing is awaited between it and the final save; with no grid waiting, both start synchronously as before. A first tab waits for the running placement pass at most 500 ms, then is admitted. The quit test's SSH mock now names the module the quit imports. * test(layout): the quit tests prove the final save starts in the same turn as the SSH shutdown, in order after the PTY kill * chore: keep main's bytes for files the merge's format hook rewrote (native-chat formatting is #31184's) --------- Co-authored-by: E2E Test <e2e@test.local>
316 lines
15 KiB
TypeScript
316 lines
15 KiB
TypeScript
import { app, type BrowserWindow } from 'electron'
|
|
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch'
|
|
import { placeUnshownLiveTerminalsBestEffort } from '../runtime/terminal/adoption/unshown-live-terminal-placement'
|
|
import { is } from '@electron-toolkit/utils'
|
|
import {
|
|
getCanonicalUserDataPath,
|
|
migrateMobilePairingDataToCanonicalUserDataPath
|
|
} from '../persistence'
|
|
import { OrcaRuntimeRpcServer } from '../runtime/runtime-rpc'
|
|
import { registerMobileHandlers } from '../ipc/mobile'
|
|
import { createRuntimeEnvironmentMobileRelayHosts } from '../ipc/runtime-environments/runtime-environment-mobile-relay-hosts'
|
|
import { getLocalPtyProvider } from '../ipc/pty/provider/registry'
|
|
import { registerHeadlessPtyRuntime } from '../ipc/pty/register-headless-runtime'
|
|
import { LocalPtyProvider } from '../providers/local-pty-provider'
|
|
import { publishHeadlessRuntimeGraph } from '../runtime/headless-runtime-graph'
|
|
import { OffscreenBrowserBackend } from '../browser/offscreen-browser-backend'
|
|
import { browserManager } from '../browser/browser-manager'
|
|
import { getDesktopRelayStatus } from './main-process-relay-status'
|
|
import { startDesktopRelayService } from './main-process-relay-startup'
|
|
import { getServeOptions, getBundledWebClientRoot, printServeReady } from './main-process-serve'
|
|
import {
|
|
bindTerminalRuntimeStartupServices,
|
|
handleCodexHomePtySpawned,
|
|
handlePtyExit,
|
|
startTerminalRuntimeStartupServices
|
|
} from './main-process-pty-startup'
|
|
import { prepareCodexRuntimeHomeForLaunch } from './codex-launch-preparation'
|
|
import { prepareCodexSessionResumeForLaunch } from './codex-session-resume-launch'
|
|
import { startWindowsDesktopBeforeShellPathReady } from './windows-desktop-shell-path-startup'
|
|
import { repairKnownPoisonedInstallDirBeforeWindow } from './windows-install-dir-acl-recovery'
|
|
import { installServeQuitHandling } from './serve-quit-handling'
|
|
import { installCloudSessionShutdownFlush } from './cloud-session-shutdown-flush'
|
|
import { registerHeadlessServeSshHandlers } from './headless-serve-ssh-registration'
|
|
import { settleServeDesktopActivation } from './serve-desktop-activation'
|
|
import {
|
|
recordRuntimeRpcStartFailure,
|
|
showRuntimeRpcStartupFailureDialog
|
|
} from '../runtime/runtime-rpc-startup-failure'
|
|
import { registerCliForHeadlessServe } from './serve-cli-registration'
|
|
import { startAppImageCopyPreExtraction } from '../cli/appimage-copy-pre-extraction'
|
|
import { scheduleAllPendingHistoryTreeRemovals } from '../terminal-history-deletion'
|
|
import { triggerStartupNotificationRegistration } from '../ipc/startup-notification-registration'
|
|
import { startDesktopPushService } from './main-process-push-startup'
|
|
import { mainProcessState as state } from './main-process-state'
|
|
import { logStartupMilestone } from './startup-diagnostics'
|
|
import { setupHeadlessServeAutoUpdater } from '../window/main-window-updater'
|
|
import { preserveStateBeforeUpdateRestart } from '../update-restart-preservation'
|
|
import { resolveUpdateInstallMode } from '../updater'
|
|
import { scheduleAgentLaunchRecordWarmup } from './agent-launch-record-warmup'
|
|
import { emitServeBrowserIdentityActionLine } from '../server/serve-stdout-boundary'
|
|
import { getBrowserIdentityModeStatus } from '../browser/browser-identity-mode-store'
|
|
|
|
type RuntimeService = NonNullable<typeof state.runtime>
|
|
|
|
export type MainProcessRuntimeLaunchOptions = {
|
|
openMainWindow: (options?: { revealOnDidFinishLoad?: boolean }) => BrowserWindow
|
|
handleMacAppActivation: () => void
|
|
}
|
|
|
|
function settleDesktopActivation(): void {
|
|
const gate = state.desktopActivationGate
|
|
if (!gate) {
|
|
return
|
|
}
|
|
settleServeDesktopActivation(gate, {
|
|
hasPersistentPtyProvider: !(getLocalPtyProvider() instanceof LocalPtyProvider)
|
|
})
|
|
}
|
|
|
|
function installRuntimeRpc(
|
|
runtime: RuntimeService,
|
|
serveOptions: ReturnType<typeof getServeOptions> | null
|
|
): OrcaRuntimeRpcServer {
|
|
// Why: existing installs may have pairing creds under the late app.getPath('userData'); copy them forward before switching to the canonical path.
|
|
migrateMobilePairingDataToCanonicalUserDataPath(app.getPath('userData'))
|
|
// Why: parallel E2E Electron instances would race the fixed port (EADDRINUSE); port 0 gives each a random OS-assigned port.
|
|
const isE2E = Boolean(process.env.ORCA_E2E_USER_DATA_DIR)
|
|
const requestedE2EWsPort = process.env.ORCA_E2E_RUNTIME_WS_PORT
|
|
const e2eWsPort = requestedE2EWsPort === undefined ? 0 : Number(requestedE2EWsPort)
|
|
if (isE2E && (!Number.isInteger(e2eWsPort) || e2eWsPort < 0 || e2eWsPort > 65_535)) {
|
|
throw new Error(`Invalid ORCA_E2E_RUNTIME_WS_PORT value: ${requestedE2EWsPort}`)
|
|
}
|
|
// Why: pin dev to 6769 so `pnpm dev` doesn't race packaged Orca on 6768 and fall back to a random port, breaking deterministic mobile pairing/repro (STA-1511).
|
|
const devWsPort = is.dev && !isE2E ? 6769 : undefined
|
|
const runtimeRpc = new OrcaRuntimeRpcServer({
|
|
runtime,
|
|
// Why: mobile pairing needs the stable pre-setName() path (getCanonicalUserDataPath), not a late app.getPath('userData') that drops paired devices across restarts.
|
|
userDataPath: getCanonicalUserDataPath(),
|
|
enableWebSocket: true,
|
|
// Why: STA-2370 — the desktop app binds the WS listener to loopback until the user pairs a device;
|
|
// `orca serve` is an explicit remote opt-in, and E2E keeps the wide bind its harness connects over.
|
|
exposeNetworkByDefault: Boolean(serveOptions) || isE2E,
|
|
...(isE2E ? { wsPort: e2eWsPort } : {}),
|
|
...(devWsPort !== undefined ? { wsPort: devWsPort } : {}),
|
|
...(serveOptions?.wsPort !== undefined
|
|
? {
|
|
wsPort: serveOptions.wsPort,
|
|
// Why: only explicit `orca serve --port` overrides a stale STA-1511 fallback (issue #8535); default/dev stay fallback-first for pairing stability.
|
|
preferPinnedWsPort: true,
|
|
requirePinnedWsPort: true
|
|
}
|
|
: {}),
|
|
webClientRoot: getBundledWebClientRoot()
|
|
})
|
|
state.runtimeRpc = runtimeRpc
|
|
runtimeRpc.setMobileDesktopRelayHosts(createRuntimeEnvironmentMobileRelayHosts())
|
|
registerMobileHandlers(runtimeRpc, {
|
|
getRelayStatus: getDesktopRelayStatus,
|
|
consumePendingUnpairedDeviceAuthFailure: (webContentsId) => {
|
|
if (
|
|
!state.mainWindow ||
|
|
state.mainWindow.isDestroyed() ||
|
|
state.mainWindow.webContents.id !== webContentsId ||
|
|
!state.pendingUnpairedDeviceAuthFailure
|
|
) {
|
|
return false
|
|
}
|
|
state.pendingUnpairedDeviceAuthFailure = false
|
|
return true
|
|
}
|
|
})
|
|
// Why: repeated direct auth failures otherwise look like a client that never connects; point users to re-pairing.
|
|
runtimeRpc.setOnUnpairedDeviceAuthFailure(() => {
|
|
// Why: runtime startup races renderer mount; retain the one-shot until the listener consumes it.
|
|
state.pendingUnpairedDeviceAuthFailure = true
|
|
if (state.mainWindow && !state.mainWindow.isDestroyed()) {
|
|
state.mainWindow.webContents.send('mobile:unpairedDeviceAuthFailure')
|
|
}
|
|
})
|
|
return runtimeRpc
|
|
}
|
|
|
|
async function launchServeMode(
|
|
runtime: RuntimeService,
|
|
runtimeRpc: OrcaRuntimeRpcServer,
|
|
serveOptions: NonNullable<ReturnType<typeof getServeOptions>>
|
|
): Promise<void> {
|
|
// Why here: headless serve has no window to unblock, so keep the persisted proxy strictly
|
|
// ahead of every fetcher this phase can reach (relay, CLI install, RPC clients).
|
|
await state.initialProxyApplicationReady
|
|
// Why: give managed WSL launchers a brief chance to migrate before headless PTYs go live, without slow repairs withholding all RPC readiness.
|
|
logStartupMilestone('wsl-cli-barrier-start')
|
|
await state.managedWslCliStartupBarrierReady
|
|
logStartupMilestone('wsl-cli-barrier-resolved', {
|
|
reconciliation: state.managedWslCliReconciliationStatus
|
|
})
|
|
// Why: headless PTYs must not start on the fallback provider, then get swept when an activated renderer registers desktop lifecycle handlers.
|
|
await state.localPtyStartupReady
|
|
await state.localPtyProviderStartupReady
|
|
await registerHeadlessPtyRuntime(
|
|
runtime,
|
|
prepareCodexRuntimeHomeForLaunch,
|
|
() => state.store!.getSettings(),
|
|
(target) => state.claudeRuntimeAuth!.prepareForClaudeLaunch(target),
|
|
state.store!,
|
|
prepareCodexSessionResumeForLaunch,
|
|
{ onCodexHomePtySpawned: handleCodexHomePtySpawned, onPtyExit: handlePtyExit }
|
|
)
|
|
await runtime.refreshRestoredOrchestrationAuthority()
|
|
await runtime.reconcileLegacyWorkerTerminals()
|
|
// Switch point: main shows live terminals no pane shows, before any client attaches.
|
|
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
|
await placeUnshownLiveTerminalsBestEffort(runtime)
|
|
}
|
|
// Why: headless servers can't mount <webview> panes; use offscreen WebContents, gated on a real display so browser.headless.v1 stays honest.
|
|
if (state.headlessBrowserDisplayAvailable) {
|
|
runtime.setOffscreenBrowserBackend(
|
|
new OffscreenBrowserBackend(browserManager, {
|
|
getAgentBrowserBridge: () => state.agentBrowserBridge,
|
|
onPageNavigated: (page) => runtime.serveBrowserPageNavigated(page)
|
|
})
|
|
)
|
|
}
|
|
publishHeadlessRuntimeGraph(runtime)
|
|
// Why before RPC binds: the first paired client must already see this host's SSH targets.
|
|
registerHeadlessServeSshHandlers(state.store!, runtime)
|
|
await runtimeRpc.start().catch((error) => {
|
|
console.error('[runtime] Failed to start headless RPC transport:', error)
|
|
throw error
|
|
})
|
|
scheduleAgentLaunchRecordWarmup(null)
|
|
// Why: a phone paired to a headless host still registers and unregisters its token;
|
|
// it simply never receives a push, because nothing dispatches notifications here.
|
|
startDesktopPushService(runtimeRpc)
|
|
settleDesktopActivation()
|
|
installServeQuitHandling()
|
|
await registerCliForHeadlessServe()
|
|
// Why: headless serve never opens a renderer, so arm scheduled automation dispatch here.
|
|
state.automations?.start()
|
|
// Why: serve deletes worktrees too, and the history GC that normally drains delete tombstones is
|
|
// armed from the main window — without this, a quit mid-removal leaks the tree until a desktop launch.
|
|
void scheduleAllPendingHistoryTreeRemovals()
|
|
emitServeBrowserIdentityActionLine(getBrowserIdentityModeStatus())
|
|
await printServeReady(serveOptions)
|
|
const store = state.store!
|
|
setupHeadlessServeAutoUpdater(store, {
|
|
onBeforeUpdateQuit: () =>
|
|
preserveStateBeforeUpdateRestart({ codexRuntimeHome: state.codexRuntimeHome, store }),
|
|
onBeforeUpdateQuitFailure: 'abort',
|
|
updateInstallMode: resolveUpdateInstallMode(true)
|
|
})
|
|
}
|
|
|
|
async function launchDesktopMode(
|
|
runtimeRpc: OrcaRuntimeRpcServer,
|
|
shellPathReady: Promise<void>,
|
|
desktopWindow: BrowserWindow | null,
|
|
openMainWindow: MainProcessRuntimeLaunchOptions['openMainWindow']
|
|
): Promise<void> {
|
|
// Preserve the pre-split startup failure contract if composition ever hands
|
|
// this phase an incomplete runtime graph.
|
|
if (!runtimeRpc) {
|
|
throw new Error('runtime_rpc_unavailable')
|
|
}
|
|
// Why: window and RPC startup run in parallel; registerPtyHandlers gates PTY spawns so RPC binds without racing the daemon provider swap.
|
|
const [win, runtimeRpcStartResult] = await Promise.all([
|
|
Promise.resolve(desktopWindow ?? openMainWindow()),
|
|
shellPathReady
|
|
.then(() => runtimeRpc.start())
|
|
.then(
|
|
() => ({ ok: true as const }),
|
|
(error: unknown) => {
|
|
recordRuntimeRpcStartFailure(error)
|
|
return { ok: false as const, error }
|
|
}
|
|
)
|
|
])
|
|
scheduleAgentLaunchRecordWarmup(win)
|
|
if (!runtimeRpcStartResult.ok) {
|
|
// Why gated: this dialog is the only launch-phase text read through translateMain, and i18n
|
|
// now settles alongside this phase — without the wait a non-English user could get the
|
|
// English defaultValue fallback. Still off the renderer's path (it is failure-only).
|
|
void state.mainProcessI18nReady.then(() =>
|
|
showRuntimeRpcStartupFailureDialog(win, runtimeRpcStartResult.error)
|
|
)
|
|
}
|
|
// Why before the proxy await: a mint in that window must wait for the installer, not fail
|
|
// provider_missing; the installer itself holds its first request until the proxy lands.
|
|
startDesktopRelayService(runtimeRpc)
|
|
// Why after the window and not before it: the default-session request guard already holds every
|
|
// fetcher until the persisted proxy lands, so this only has to keep the launch phase itself
|
|
// ordered ahead of the push gateway — it must not gate the renderer.
|
|
await state.initialProxyApplicationReady
|
|
// Why after the proxy await: the push gateway client is an app-owned fetcher, so it must not
|
|
// issue its first request ahead of the persisted proxy.
|
|
startDesktopPushService(runtimeRpc)
|
|
// Why: macOS notification permission dialog must fire after the window is shown, else it's hidden behind the maximized window.
|
|
win.once('show', () => {
|
|
// Why: store can be null if init failed earlier; bail rather than throw inside an Electron event listener.
|
|
const store = state.store
|
|
if (store && store.getOnboarding().closedAt !== null) {
|
|
triggerStartupNotificationRegistration(store)
|
|
}
|
|
})
|
|
}
|
|
|
|
export async function initializeMainProcessRuntimeLaunch(
|
|
options: MainProcessRuntimeLaunchOptions
|
|
): Promise<void> {
|
|
const runtime = state.runtime
|
|
const shellPathHydration = state.windowsShellPathHydration
|
|
if (!runtime || !shellPathHydration) {
|
|
throw new Error('Runtime and shell-path services must be initialized before launch')
|
|
}
|
|
let serveOptions: ReturnType<typeof getServeOptions> | null = null
|
|
try {
|
|
serveOptions = state.isServeMode ? getServeOptions(process.argv) : null
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : String(error))
|
|
app.exit(1)
|
|
return
|
|
}
|
|
state.serveOptions = serveOptions
|
|
installCloudSessionShutdownFlush()
|
|
const runtimeRpc = installRuntimeRpc(runtime, serveOptions)
|
|
const shellPathReady = shellPathHydration.whenReady()
|
|
// Why published: the renderer's git-environment barrier must fence on the same
|
|
// generation the terminal startup services wait for, not a later re-read.
|
|
state.shellPathReady = shellPathReady
|
|
// Why before any window: the poisoned install DACL kills the renderer at init, and
|
|
// the probe that detects it cannot finish before createMainWindow. Bounded, and a
|
|
// no-op (one absent-file read) unless a previous launch already recorded the verdict.
|
|
const aclGate = await repairKnownPoisonedInstallDirBeforeWindow({
|
|
isServeMode: state.isServeMode || serveOptions !== null,
|
|
userDataPath: app.getPath('userData'),
|
|
appVersion: app.getVersion()
|
|
})
|
|
if (aclGate !== 'not-marked' && aclGate !== 'skipped') {
|
|
logStartupMilestone('install-dir-acl-repair-blocking-done', { mode: aclGate })
|
|
}
|
|
let desktopWindow: BrowserWindow | null = null
|
|
if (process.platform === 'win32' && app.isPackaged && !serveOptions) {
|
|
const desktopStartup = startWindowsDesktopBeforeShellPathReady({
|
|
bindServices: bindTerminalRuntimeStartupServices,
|
|
openWindow: () => options.openMainWindow({ revealOnDidFinishLoad: true }),
|
|
shellPathReady,
|
|
startServices: startTerminalRuntimeStartupServices
|
|
})
|
|
desktopWindow = desktopStartup.window
|
|
} else {
|
|
// Why before the daemon starts: its host choice waits briefly for a copy unpacking here.
|
|
// Serve unpacks for itself, with a deadline and progress on its own output.
|
|
if (!serveOptions) {
|
|
void startAppImageCopyPreExtraction()
|
|
}
|
|
await shellPathReady
|
|
bindTerminalRuntimeStartupServices(Promise.resolve(startTerminalRuntimeStartupServices()))
|
|
}
|
|
app.on('activate', options.handleMacAppActivation)
|
|
if (serveOptions) {
|
|
await launchServeMode(runtime, runtimeRpc, serveOptions)
|
|
return
|
|
}
|
|
await launchDesktopMode(runtimeRpc, shellPathReady, desktopWindow, options.openMainWindow)
|
|
}
|