mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 16:02:37 +00:00
* fix(windows): refuse tree-kills of Orca's own Chromium pids and record the rest G2 is 20 field reports that share only a symptom. It is at least four fingerprints: ~15 Windows `reason=killed exitCode=1`, 3 POSIX SIGKILL under memory pressure (G4-oom), 2 duplicate reports of one macOS V8 Proxy Resolver SIGKILL, and 1 `0x80000003` install-dir ACL crash (G1; #17740 ships in v1.4.196 only, not 1.4.195). Nothing here claims to fix all of them. Two changes: 1. Behaviour. `classifyWindowsTreeKillTarget` returns `own` for any direct child of the main process — which our renderer, GPU and network-service utility all are — so PTY teardown could `taskkill /T /F` Orca's own UI (#10680). Both that classifier and `terminateWindowsProcessTree` now refuse any pid Electron is currently accounting for in `getAppMetrics()`. 2. Diagnosis. An Orca-issued kill and an external one are byte-identical in every field the crash report records today, so the cluster is undecidable. Every main-process force-kill choke point now records a durable `self_tree_kill` breadcrumb, and `process_gone` reports carry `selfInitiatedTreeKills` naming the pid and its offset from the death. A refused kill records `self_tree_kill_refused_own_chromium`, which is falsifiable: if it ever shows up in the field, we were the killer. * fix(crash-reporting): coalesce self-kill breadcrumbs and scope the discriminator Round-1 review remediation. Three blocking findings, all accepted. 1. Breadcrumb flood (accepted). recordSelfInitiatedTreeKill wrote an uncoalesced durable crumb from two routine teardown paths, and the reviewer reproduced 12 terminal closes x 3 process groups completely evicting the 30-slot ring — including this PR's own refusal crumb — plus a forced writeSync per killed group. It now uses the existing recordCoalescedDurableCrashBreadcrumb (5s window for pid-addressed taskkills, 60s for routine group/job teardown), so a burst costs one ring slot and one flush. The refusal crumb is coalesced per victim pid, so a retry loop cannot flood while a distinct pid always gets its own crumb. Regression test replays the reviewer's exact 12x3 reproduction and asserts the refusal crumb and a pre-existing gpu_process_crashed both survive. 2. Undifferentiated count (accepted). posix-process-group and win-pty-job are structurally incapable of reaching a Chromium process, and scope was absent from the persisted string. Scope is now in every entry (`<scope>/<site>/pid<N> +Nms`), and the count is split: selfInitiatedTreeKillCount now counts only pid-addressed taskkills — the kills that can land on a recycled pid that is now our renderer — with pty-scoped sweeps in selfInitiatedGroupKillCount. The list is renamed selfInitiatedKills because it carries both, and sorts pid-addressed kills first so truncation never drops the discriminating ones for teardown noise. The reviewer's repro (routine macOS terminal close + unrelated exit-133 crash) now yields selfInitiatedTreeKillCount undefined. 3. Recording gaps and a false comment (accepted). New admitSelfInitiatedTreeKill gate: it refuses own-Chromium pids and records the rest, and all three main-process taskkill families now go through it — terminateWindowsProcessTree plus codex-accounts/service.ts and claude-accounts (which keep their own spawn lifetimes). The false "single taskkill choke point" comment is gone. The runProcess choke point the investigation asked for is instrumented via a setProcessTreeKillObserver seam in src/shared/child-process — shared code runs in the CLI and relay so it cannot import the main breadcrumb store — registered in main preflight. The codex app-server POSIX group teardowns and the claude POSIX branch record too. The module doc no longer claims absence is discriminating: it enumerates what is instrumented and names the direct process.kill(-pid) sites that are not. Non-blocking, also fixed: - Breadcrumb calls moved out of the try blocks whose catch is the ESRCH contract (posix-pty-process-groups, codex teardown, claude POSIX), so a throw from the diagnostic path can never be reported as a failed kill. - Detail truncation now bounds the first entry too, matching its comment. - own-chromium-tree-kill-refusal.test.ts renamed to own-chromium-tree-kill-guard.test.ts, colocated with the module it tests. Not changed, with reasons: - Date.now() vs performance.now(): kept. Offsets are computed against goneAt = Date.now() in process-gone-recorder; a monotonic clock here would make the offsets meaningless. The reviewer verified this and agreed it is not a defect. - app.getAppMetrics() per force-kill remains unbenchmarked. It reads in-process browser state rather than enumerating the OS process table, and a TTL cache would let a recycled pid slip past the refusal, so it stays uncached. - The ~15 remaining direct process.kill(-pid) sites (browser routes, notebooks, automation prechecks, ephemeral VM recipes) are not instrumented. Rather than claim coverage this PR does not have, the module doc names them. claude-command-process.ts crossed the 300-line cap, so terminateClaudeProcess moved to claude-login-process-termination.ts. No max-lines suppression added. * fix(crash-reporting): scope the self-kill guard to its real host topology Round-2 review findings on the own-Chromium tree-kill guard. BLOCKING 1 — "the own-Chromium refusal is a no-op in the process that issues the pty-descendant-sweep taskkill". Correct on the mechanism, wrong on the consequence; REBUTTED in part and documented in full. Confirmed: the only non-test `setAppEnvironment` installs are main-process-preflight.ts:177 (Electron) and orcad-entry.ts:84 (Node, whose `getAppMetrics()` is `[]`); daemon-init-fresh-import.ts is a test harness. So in the standalone daemon `readOrcaChromiumProcessPids()` is empty and `admitSelfInitiatedTreeKill` always admits. But that is not a live hazard. `killWithDescendantSweep` reaches `terminateWindowsProcessTree` only when `verifyWindowsTreeKillTarget` returns `own`, and that walks ancestry back to `deps.ownerPid ?? process.pid` — the KILLING process's pid. In the daemon that is the daemon's pid. Orca's Chromium processes are children of Electron main, a sibling of the daemon, so their chain never reaches it: hop 0 lands on main, and within MAX_ANCESTOR_HOPS the walk dead-ends and returns `foreign`. The reviewer's probe passes `ownerPid: 1000` with the renderer as a direct child of 1000 — that is the Electron-main topology, where the AppEnvironment IS installed and the guard DOES fire, not the daemon's. On an orcad/SSH host there is no Chromium on the box at all, so `[]` is accurate rather than degraded. Locked in as tests rather than prose (own-chromium-tree-kill-guard.test.ts): a renderer classifies `foreign` from a daemon ownerPid with an empty pid set, and `own` from main's ownerPid with an empty set — the falsifiable pair showing the pid set is load-bearing in main and nowhere else. Documented the host coverage in orca-chromium-process-pids.ts and own-chromium-tree-kill-guard.ts. One genuine hole the finding exposes: `signalProcessTree`'s `taskkillTree` is a fourth pid-addressed taskkill family (non-blocking item 2), it runs in the daemon/relay/CLI where the guard cannot run, and it guarded only on `!child.pid`. Reusing the predicate the codex login teardown already uses, the win32 branch now refuses a reaped child and falls back to `killRoot` — the same shape as the existing `!child.pid` branch. That closes the reaped-then-recycled pid path in every host. BLOCKING 2 — module doc overstates coverage. Rewritten: the ring is per-process and its only reader lives in Electron main, so a count on a `render-process-gone` covers main-issued kills only. Sites are now split into main-only, main-and- other-hosts (runProcess choke point, POSIX PTY group sweep, Windows Job Object — which record into a ring nothing reads when they run in the daemon or relay), and never-instrumented, with the note that a daemon/relay omission is a diagnostics gap, not a missed suspect, per the topology argument above. BLOCKING 3 — the three out-of-main instrumentation sites were untested. Added regression coverage: the runProcess seam on both branches plus the reaped-child refusal (process-tree-termination.test.ts), the group sweep recording only groups it actually signalled and skipping an ESRCH group (posix-pty-process-groups.test.ts), and the Job Object recording the shell pid only on `terminated` (windows-pty-job.test.ts). Verified red: reverting the three production files to origin/main fails 7 of the new tests. BLOCKING 4 — the Windows evidence validates a single-process model. Accepted. The main2.js arms exercise `pty-descendant-sweep` inside one Electron process; that models the in-process/degraded daemon and the local PTY provider, not the standalone daemon. Arm C's "the 449351d6 shape is not producible with the guard" holds for main-issued kills only. In the daemon the shape is blocked one layer earlier, by the ancestry check, which the arms do not exercise. NON-BLOCKING taken: `recordSelfInitiatedTreeKill` moved outside the native `terminateJob` try in windows-pty-job.ts, so a diagnostics throw can no longer downgrade a real termination to `unavailable` and escalate callers to a broader kill; covered by a test. The "all three families" parenthetical is gone with the doc rewrite. `pnpm build:relay` run: exit 0, all seven targets built. NON-BLOCKING declined: codex-accounts/service.ts records before the spawn because a refusal must prevent the spawn — the crumb means "we were about to kill this pid", which is the artifact worth having; the existing comment already says so. `app.getAppMetrics()` perf is unbenchmarked and unchanged by this round. Verification: pnpm tc clean; oxlint clean on touched paths; check:code-quality:changed 0 new findings; oxfmt applied. 730 tests pass across shared/child-process, main/crash-reporting, main/pty, main/windows and the guard and descendant-sweep suites. The 4 failures in providers/git/codex-integration reproduce on HEAD without these changes. * fix(crash-reporting): keep the reaped-pid skip from flipping the termination barrier The win32 hasExited short-circuit correctly avoids taskkill on a pid Windows may have reissued, but it resolved `true` — verified tree termination. A taskkill against a reaped pid already resolved `false`, and run-process turns `true` into barrierTerminationVerified + terminationReporter.report(), which releases the git admission grant on root exit instead of on `close`. That admits the next git command while a descendant holding the inherited pipes is still writing the repo. Resolve `false` so the skip changes only which process we refuse to signal, not what the barrier claims.
331 lines
18 KiB
TypeScript
331 lines
18 KiB
TypeScript
import { app, ipcMain, powerMonitor, session } from 'electron'
|
|
import { is } from '@electron-toolkit/utils'
|
|
import os from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { maybeRedirectCliLaunch } from './cli-launch-redirect'
|
|
import { argvRequestsServeMode, normalizeServeModeArgv } from './serve-mode-argv'
|
|
import {
|
|
configureDevUserDataPath,
|
|
configureElectronNetworkCompatibility,
|
|
configureOrcaUserDataPathEnv,
|
|
disableUnsupportedChromiumFeatures,
|
|
enableMainProcessGpuFeatures,
|
|
installDevParentDisconnectQuit,
|
|
installDevParentSignalQuit,
|
|
installDevParentWatchdog,
|
|
patchPackagedProcessPath,
|
|
optOutOfHiddenPageWakeUpThrottling
|
|
} from './configure-process'
|
|
import { installServeSupervisorDisconnectQuit } from '../serve-update-handoff'
|
|
import {
|
|
installUncaughtPipeErrorGuard,
|
|
installUnhandledRejectionLogging
|
|
} from './main-process-error-guards'
|
|
import { hydrateShellPath, mergePathSegments } from './hydrate-shell-path'
|
|
import { configureRemoteServerUpdater } from '../runtime/remote-server-updater'
|
|
import {
|
|
getRemoteServerUpdaterSnapshot,
|
|
checkForRemoteServerUpdate,
|
|
downloadRemoteServerUpdate,
|
|
installRemoteServerUpdate,
|
|
isQuittingForUpdate
|
|
} from '../updater'
|
|
import { getDevInstanceIdentity, shouldApplyPreReadyAppName } from './dev-instance-identity'
|
|
import { enableRendererHeapHeadroom } from './renderer-heap-headroom'
|
|
import { isStartupDiagnosticsEnabled, logStartupDiagnostic } from './startup-diagnostics'
|
|
import { startEventLoopStallProbe } from './event-loop-stall-probe'
|
|
import { startMainThreadChurnProbe } from '../diagnostics/main-thread-churn-probe'
|
|
import { settledDiffCache } from '../git/source-control/git-read-cache-invalidation'
|
|
import { reserveServeStdoutForReadiness } from '../server/serve-stdout-boundary'
|
|
import { createServeDesktopActivationGate } from './serve-desktop-activation'
|
|
import {
|
|
shouldBypassSingleInstanceLock,
|
|
shouldSkipSingleInstanceLock,
|
|
acquireSingleInstanceLock,
|
|
logSingleInstanceLockBypass,
|
|
logSingleInstanceLockFailure,
|
|
SINGLE_INSTANCE_ALREADY_RUNNING_EXIT_CODE
|
|
} from './single-instance-lock'
|
|
import { setAppEnvironment } from '../../shared/app-environment'
|
|
import { ElectronAppEnvironment } from '../host/electron-app-environment'
|
|
import { installProcessTreeKillBreadcrumbObserver } from '../crash-reporting/self-initiated-tree-kill-log'
|
|
import { setSecretStore } from '../../shared/secret-store'
|
|
import { ElectronSecretStore } from '../host/electron-secret-store'
|
|
import { setPtyHostBindings } from '../ipc/pty-host-bindings'
|
|
import { electronRuntimeDesktopSurface } from '../host/electron-runtime-desktop-surface'
|
|
import { setRuntimeDesktopSurface } from '../runtime/runtime-desktop-surface'
|
|
import { electronRuntimeBrowserCommandsFactory } from '../host/electron-browser-commands'
|
|
import { setRuntimeBrowserCommandsFactory } from '../runtime/runtime-browser-commands-factory'
|
|
import { electronHttpClient } from '../host/electron-http-client'
|
|
import { setMainHttpClient } from '../network/http-client'
|
|
import { electronSpeechServiceFactories } from '../host/electron-speech-services'
|
|
import { setSpeechServiceFactories } from '../speech/speech-runtime-service'
|
|
import { setWorktreeWatcherRemoval } from '../ipc/worktree-watcher-removal'
|
|
import { desktopWorktreeWatcherRemoval } from '../ipc/filesystem-watcher'
|
|
import { setDefaultProxySessionResolver } from '../network/proxy-settings'
|
|
import { initDataPath, getCanonicalUserDataPath } from '../persistence'
|
|
import { applyMacPressAndHoldDefaultAtStartup } from '../macos-press-and-hold-default'
|
|
import { initSessionParseCachePersistence } from '../ai-vault/session-parse-cache-persistence'
|
|
import { initOrcaProfilePaths } from '../orca-profiles/profile-index-store'
|
|
import { initStatsPath } from '../stats/collector'
|
|
import { initClaudeUsagePath } from '../claude-usage/store'
|
|
import { initCodexUsagePath } from '../codex-usage/store'
|
|
import { initOpenCodeUsagePath } from '../opencode-usage/store'
|
|
import { registerDocPreviewSchemePrivileges } from '../browser/doc-preview-protocol'
|
|
import { startCrashpadCapture } from '../crash-reporting/crashpad-capture'
|
|
import { CrashReportStore } from '../crash-reporting/crash-report-store'
|
|
import { recordCrashBreadcrumb } from '../crash-reporting/crash-breadcrumb-store'
|
|
import { recordDurableCrashBreadcrumb } from '../crash-reporting/durable-crash-breadcrumb'
|
|
import { GpuCrashDiagnosticsRecorder } from '../crash-reporting/gpu-crash-diagnostics'
|
|
import { getMainProcessLifecycleIdentity } from '../crash-reporting/main-process-lifecycle-identity'
|
|
import {
|
|
ensureVirtualDisplayForHeadlessServe,
|
|
hasUsableLinuxDisplay,
|
|
MISSING_LINUX_DISPLAY_MESSAGE
|
|
} from './ensure-virtual-display'
|
|
import { maybeApplyGpuFallbackForThisLaunch, registerGpuLifecycleHandlers } from './gpu-lifecycle'
|
|
import { mainProcessState as state } from './main-process-state'
|
|
import { initializeSyntheticTitleRuntime } from './synthetic-title-runtime'
|
|
|
|
export type MainProcessPreflightOptions = {
|
|
focusExistingWindow: () => void
|
|
requestDesktopActivation: (argv?: readonly string[]) => void
|
|
}
|
|
|
|
/** Performs all module-scope work that must happen before Electron's ready event. */
|
|
export function runMainProcessPreflight(options: MainProcessPreflightOptions): boolean {
|
|
// Why: on Windows a CLI launch that lost ELECTRON_RUN_AS_NODE would boot the GUI and exit silently; redirect to node mode before the lock gate below.
|
|
// The redirect runs before the serve-argv rewrite so it still matches on the launch argv verbatim.
|
|
// Direct serve stays in-process so its signal handlers own all children.
|
|
const cliLaunchRedirect = maybeRedirectCliLaunch({
|
|
isPackaged: app.isPackaged,
|
|
resourcesPath: process.resourcesPath,
|
|
execPath: process.execPath
|
|
})
|
|
if (cliLaunchRedirect.redirected) {
|
|
app.exit(cliLaunchRedirect.status)
|
|
}
|
|
// Why: extracted AppRun / binary launches can land CLI-form `serve` args on the
|
|
// Electron process without the CLI rewrite that injects `--serve` (#12677).
|
|
// Guarded so a normal GUI launch keeps its original argv array identity.
|
|
if (argvRequestsServeMode(process.argv)) {
|
|
process.argv = normalizeServeModeArgv(process.argv)
|
|
}
|
|
state.isServeMode = process.argv.includes('--serve')
|
|
// Fail before Chromium's missing-display teardown can segfault (#13719).
|
|
if (app.isPackaged && !state.isServeMode && !hasUsableLinuxDisplay()) {
|
|
process.stderr.write(`${MISSING_LINUX_DISPLAY_MESSAGE}\n`)
|
|
app.exit(1)
|
|
}
|
|
if (state.isServeMode) {
|
|
reserveServeStdoutForReadiness()
|
|
}
|
|
state.devInstanceIdentity = getDevInstanceIdentity(is.dev)
|
|
state.devAgentHookEndpointNamespace = state.devInstanceIdentity.isDev
|
|
? state.devInstanceIdentity.appUserModelId
|
|
: undefined
|
|
state.desktopActivationGate = createServeDesktopActivationGate({
|
|
initialState: state.isServeMode ? 'initializing' : 'ready',
|
|
activateWindow: () => {
|
|
// Why: an updater replacement must not resurrect the old app bundle.
|
|
if (!isQuittingForUpdate()) {
|
|
options.focusExistingWindow()
|
|
}
|
|
},
|
|
onBlocked: (reason) => console.error(`[serve] Desktop activation blocked: ${reason}`)
|
|
})
|
|
installUncaughtPipeErrorGuard()
|
|
// Why (issue #9441): without this, one rejected background promise during startup restore kills main silently (exit 1, no crash report).
|
|
installUnhandledRejectionLogging()
|
|
// Why: expose the app version via process.env so main and the forked daemon can set TERM_PROGRAM_VERSION without importing electron.
|
|
process.env.ORCA_APP_VERSION = app.getVersion()
|
|
configureRemoteServerUpdater({
|
|
getSnapshot: getRemoteServerUpdaterSnapshot,
|
|
check: checkForRemoteServerUpdate,
|
|
download: downloadRemoteServerUpdate,
|
|
install: installRemoteServerUpdate
|
|
})
|
|
patchPackagedProcessPath()
|
|
// Why: the sync seed above covers early IPC (homebrew/nix); the async login-shell probe below (packaged only) then adds the user's rc PATH.
|
|
if (app.isPackaged && process.platform !== 'win32') {
|
|
void hydrateShellPath().then((result) => {
|
|
if (result.ok) {
|
|
mergePathSegments(result.segments)
|
|
} else {
|
|
// Why: on failure the seeded fallbacks stay in front. For an nvm user that is
|
|
// now their `default` version rather than the newest install, so it is usually
|
|
// survivable — but it is still not what their shell would have resolved. Name
|
|
// the reason so it shows up in a log bundle instead of as a missing CLI.
|
|
console.warn(
|
|
`[shell-path] login-shell probe failed (${result.failureReason}); using seeded PATH`
|
|
)
|
|
}
|
|
})
|
|
}
|
|
// Why before any spawn: `signalProcessTree` is shared with the CLI and relay, so
|
|
// it can only reach the main-process breadcrumb store through a registered observer.
|
|
installProcessTreeKillBreadcrumbObserver()
|
|
const isDev = is.dev
|
|
configureDevUserDataPath(isDev)
|
|
configureOrcaUserDataPathEnv()
|
|
// Why these four lines are one step (#16761): the two above decide where userData lives, and
|
|
// everything below may resolve a path. Installing the accessor any later leaves a window where an
|
|
// early resolve either throws — which is what killed `orca serve` — or, worse, memoizes the
|
|
// pre-override directory and silently writes user state to the wrong place for the whole session.
|
|
// Safe this early: ElectronAppEnvironment holds no state and calls `app` lazily per accessor, so it
|
|
// changes no timing, and initDataPath only joins strings.
|
|
setAppEnvironment(new ElectronAppEnvironment())
|
|
// Why captured now: after the dev/E2E override above, and before app.setName('Orca') (whenReady)
|
|
// changes how userData resolves on a case-sensitive filesystem. See persistence.ts:20-28.
|
|
initDataPath()
|
|
state.startupDiagnosticsEnabled = isStartupDiagnosticsEnabled()
|
|
if (state.startupDiagnosticsEnabled) {
|
|
logStartupDiagnostic('before-single-instance-lock', {
|
|
version: app.getVersion(),
|
|
packaged: app.isPackaged,
|
|
platform: process.platform,
|
|
osRelease: os.release(),
|
|
userData: app.getPath('userData'),
|
|
e2eUserData: Boolean(process.env.ORCA_E2E_USER_DATA_DIR)
|
|
})
|
|
startEventLoopStallProbe()
|
|
}
|
|
// Self-gated on ORCA_MAIN_THREAD_DIAGNOSTICS; runs the whole session to catch steady-state churn (issue #7576).
|
|
// Why the diff-cache counters ride along: a stamp the filesystem reports unstably makes the cache
|
|
// look exactly like a cold start, and only the hit/miss/unprovable split tells the two apart.
|
|
startMainThreadChurnProbe({ extraStats: () => ({ diffCache: settledDiffCache.stats() }) })
|
|
// Why: acquire AFTER configureDevUserDataPath — Electron derives lock identity from `userData`, so dev/packaged lock in separate namespaces.
|
|
// Why skip in dev: parallel `pnpm dev` from multiple worktrees would make the second exit silently; packaged keeps the lock (corruption PR #1326 / #1312).
|
|
const bypass = shouldBypassSingleInstanceLock({ isDev, isServeMode: state.isServeMode })
|
|
const skip = shouldSkipSingleInstanceLock({ isDev, isServeMode: state.isServeMode })
|
|
if (bypass) {
|
|
// Why: diagnostic escape hatch for macOS builds where Electron reports a false lock loss before any app logs exist.
|
|
logSingleInstanceLockBypass()
|
|
}
|
|
const hasLock = skip || bypass || acquireSingleInstanceLock(app, options.requestDesktopActivation)
|
|
if (state.startupDiagnosticsEnabled) {
|
|
logStartupDiagnostic('single-instance-lock-result', {
|
|
acquired: hasLock,
|
|
bypassed: bypass,
|
|
skippedForDev: skip
|
|
})
|
|
}
|
|
if (!hasLock) {
|
|
// Why: a false-negative lock loss otherwise looks like a silent crash on packaged macOS; `open --stderr` can capture this line.
|
|
logSingleInstanceLockFailure()
|
|
// Why: a graceful quit is deferred pre-ready, so this launch would still walk into Linux display init and SIGSEGV (#11935).
|
|
app.exit(SINGLE_INSTANCE_ALREADY_RUNNING_EXIT_CODE)
|
|
return false
|
|
}
|
|
// Why first in this block: the accessor throws until installed and everything below may read a
|
|
// credential. The constructor does not touch `safeStorage` — it resolves lazily per call — so
|
|
// installing here changes no timing, in particular not the pre-ready Keychain service-name
|
|
// resolution. The app-environment port and the userData capture install earlier still, next to
|
|
// the path decision they depend on.
|
|
setSecretStore(new ElectronSecretStore())
|
|
// Why at process level, not per-window: pty.ts registers against injected surfaces so
|
|
// it can load without electron, and an Electron main process always has ipcMain —
|
|
// whether a window exists is irrelevant. Installing this in attachMainWindowServices
|
|
// meant `orca serve` registered its PTY handlers against no-ops before any window
|
|
// attached, so a paired desktop owner never received them.
|
|
setPtyHostBindings({ ipc: ipcMain, power: powerMonitor })
|
|
// Why also at process level: the runtime's notification, window-lookup and
|
|
// tab-create-reply channel are desktop-only. A Node host installs none and the
|
|
// runtime routes notifications to paired clients instead.
|
|
setRuntimeDesktopSurface(electronRuntimeDesktopSurface)
|
|
// Why here: constructing RuntimeBrowserCommands is what pulls the Chromium browser
|
|
// cluster into the graph. The desktop installs it; a Node host installs none and every
|
|
// browser RPC rejects, which capability filtering already tells clients about.
|
|
setRuntimeBrowserCommandsFactory(electronRuntimeBrowserCommandsFactory)
|
|
// Why here: proxy-settings only needed electron for `session.defaultSession`. The
|
|
// desktop supplies it; a Node host has no Chromium proxy config to consult, so the
|
|
// environment variables are the whole answer there.
|
|
setDefaultProxySessionResolver(() => session.defaultSession)
|
|
// Why here: integrations use Chromium's network stack on the desktop. A Node host
|
|
// falls back to the platform default, which is a real behavioural difference (proxy
|
|
// read from the environment, Node's user agent) rather than a transparent swap.
|
|
setMainHttpClient(electronHttpClient)
|
|
// Why here: constructing the speech services is what pulls Electron's streaming net
|
|
// request in. A host without them rejects speech calls rather than pretending.
|
|
setSpeechServiceFactories(electronSpeechServiceFactories)
|
|
setWorktreeWatcherRemoval(desktopWorktreeWatcherRemoval)
|
|
// Why: couple to dev-parent only for electron-vite desktop runs; `orca serve`'s parent (CLI shim/background shell) isn't the intended server lifetime.
|
|
const shouldCoupleToDevParent = isDev && !state.isServeMode
|
|
installDevParentDisconnectQuit(shouldCoupleToDevParent)
|
|
installDevParentWatchdog(shouldCoupleToDevParent)
|
|
installDevParentSignalQuit(shouldCoupleToDevParent)
|
|
// Why not at module scope with the other lifetime couplings (#16761): this resolves the handoff
|
|
// path, so it throws until setAppEnvironment() above installs the accessor — which killed every
|
|
// `orca serve` process before it could listen. After initDataPath() specifically, so the
|
|
// path-equality check against the CLI's env var uses the dir captured before app.setName().
|
|
// Safe to defer, and must stay synchronous: no 'disconnect' can be delivered until this module
|
|
// finishes evaluating, so moving this behind an await would open a real orphan window.
|
|
installServeSupervisorDisconnectQuit(state.isServeMode)
|
|
// Why here: initDataPath above gives the canonical userData path for the record file; the write
|
|
// itself lands for the next launch (see macos-press-and-hold-default.ts).
|
|
applyMacPressAndHoldDefaultAtStartup(getCanonicalUserDataPath())
|
|
// Why: use the canonical userData path — late app.getPath('userData') can resolve differently across restarts, defeating persistence.
|
|
initSessionParseCachePersistence({
|
|
filePath: join(getCanonicalUserDataPath(), 'ai-vault', 'session-parse-cache.json'),
|
|
appVersion: app.getVersion()
|
|
})
|
|
initOrcaProfilePaths()
|
|
// Why: same timing as initDataPath — capture userData before app.setName changes it. See persistence.ts:20-28.
|
|
initStatsPath()
|
|
initClaudeUsagePath()
|
|
initCodexUsagePath()
|
|
initOpenCodeUsagePath()
|
|
// Why: Electron resolves the macOS safeStorage Keychain service name
|
|
// ("<app name> Safe Storage") before `ready`, so the setName in whenReady is
|
|
// too late to move it — dev otherwise lands on the package.json name. Dev-only
|
|
// so a packaged build keeps deriving the key from its own CFBundleName.
|
|
// Safe here: dev always pins userData via app.setPath (configure-process.ts),
|
|
// so setName cannot shift the paths captured just above.
|
|
if (state.devInstanceIdentity && shouldApplyPreReadyAppName(state.devInstanceIdentity)) {
|
|
app.setName(state.devInstanceIdentity.appName)
|
|
}
|
|
// Why: Electron freezes the privileged scheme table at ready, so the doc-preview
|
|
// scheme must be declared here or its webview loses fetch/secure-origin privileges.
|
|
registerDocPreviewSchemePrivileges()
|
|
// Why: must precede app.whenReady() so Crashpad is installed before the
|
|
// first renderer spawns; a CHECK before this point is still exit-code-only.
|
|
startCrashpadCapture()
|
|
state.crashReports = CrashReportStore.fromUserData()
|
|
state.gpuCrashDiagnostics =
|
|
process.platform === 'win32'
|
|
? new GpuCrashDiagnosticsRecorder({
|
|
provider: {
|
|
getGPUInfo: (infoType) => app.getGPUInfo(infoType),
|
|
getGPUFeatureStatus: () => app.getGPUFeatureStatus()
|
|
},
|
|
recordBreadcrumb: (data) => recordDurableCrashBreadcrumb('gpu_crash_hardware', data)
|
|
})
|
|
: null
|
|
recordCrashBreadcrumb('app_started', {
|
|
packaged: app.isPackaged,
|
|
platform: process.platform,
|
|
...getMainProcessLifecycleIdentity()
|
|
})
|
|
disableUnsupportedChromiumFeatures()
|
|
// Why: unconditional — a GPU-fallback launch skips enableMainProcessGpuFeatures() below.
|
|
optOutOfHiddenPageWakeUpThrottling()
|
|
configureElectronNetworkCompatibility()
|
|
enableRendererHeapHeadroom()
|
|
maybeApplyGpuFallbackForThisLaunch()
|
|
if (!state.gpuFallbackActiveThisLaunch) {
|
|
enableMainProcessGpuFeatures()
|
|
}
|
|
// Why: headless serve's offscreen BrowserWindows need an X display (Xvfb) on Linux; the result gates whether the offscreen backend is installed.
|
|
state.headlessBrowserDisplayAvailable = ensureVirtualDisplayForHeadlessServe({
|
|
isServeMode: state.isServeMode
|
|
})
|
|
// Why: continuing without Xvfb lets Ozone initialize without a display and SIGSEGV (#17615).
|
|
if (state.isServeMode && !state.headlessBrowserDisplayAvailable) {
|
|
process.stderr.write(`${MISSING_LINUX_DISPLAY_MESSAGE}\n`)
|
|
app.exit(1)
|
|
}
|
|
initializeSyntheticTitleRuntime()
|
|
registerGpuLifecycleHandlers()
|
|
return true
|
|
}
|