Files
orca/src/cli/runtime/launch.ts
T
Neil 0ed5f4bb21 fix(orcad): materialize the app's agent-browser driver into the local slot (#34150)
* fix(orcad): materialize the app's agent-browser driver into the local slot

Replaces #34111's ORCA_AGENT_BROWSER_DRIVER hand-off, Windows profile copy and
claimed snapshot store. Packaging now references the app's own
resources/agent-browser-<target> from the orcad template manifest
(packagedBrowser, like packagedRipgrep), so the materialized slot carries it and
every local orcad launch finds it beside orcad-server.js. macOS signs the driver
before hashing; Windows reseals it after SignPath. The resolver now also tries
the musl driver name on Linux.

Refs #33846

* build(orcad): let a checkout keep template drivers with ORCAD_OMIT_AGENT_BROWSER=0
2026-10-27 01:28:20 -07:00

302 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { spawn, type SpawnOptions } from 'node:child_process'
import { spawnProcess } from '@orca/process-host'
import type { ProcessSpec } from '@orca/process-host/process-spec'
import {
SERVE_UPDATE_HANDOFF_PATH_ENV,
getServeUpdateHandoffPath
} from '../../shared/serve-update-handoff'
import {
pinLaunchUserDataPath,
pinServeUserDataPath,
resolveLaunchUserDataPath
} from './launch-user-data-path'
import { foregroundServeIsolatedProfileSwitches } from '../../shared/electron-isolated-profile-switches'
import { headlessServeOzonePlatformSwitches } from '../../shared/headless-serve-ozone-platform'
import { getMacAppBundlePath } from './mac-app-update-bundle'
import { getPlatformUserDataPath } from './metadata'
import { isSameUserDataPath } from '../../shared/serve-user-data-path'
import {
readServeUpdateHandoffSync,
resumeInterruptedServeUpdate,
superviseForegroundServe
} from './serve-update-supervisor'
import { RuntimeClientError } from './types'
import {
getExecutableAppArgs,
resolveAppRoot,
resolveForegroundOrcaExecutable,
stripElectronRunAsNode
} from './orca-app-executable'
import { SERVE_RUNTIME_ELECTRON, SERVE_RUNTIME_ENV } from '../../shared/orcad-local-serve-selection'
import { findBundledWebClientRoot } from '../../shared/bundled-web-client-root'
import { resolveLocalServeRuntime, serveWithOrcad } from './serve-orcad-launch'
import { findAppSpeechRecognizerDir } from './serve-speech-recognizer'
import { serveOptionArgs, type ServeOrcaAppArgs } from '../../shared/orcad-serve-argv'
import { waitForRecipeJson } from './serve-recipe-json'
import {
preflightElectronServe,
recordElectronServeStartupAbort,
refuseIfProfileServed
} from './serve-electron-preflight'
import { MacServeStartupAbortError } from './serve-signal-exit-diagnostic'
import { superviseServeService } from './serve-supervised-service'
import { reexecThisServeUnderInitReaper } from './serve-pid1-init-reaper'
import { supervisedAppImageUpdate } from './serve-appimage-update'
import { relocateThisServeSupervisor } from './serve-supervisor-relocation'
import {
noteServeSupervisorStart,
restartThisServeSupervisorIfReplaced
} from './serve-replaced-app-restart'
import { isSupervisedSpawnHeld } from './serve-supervised-stop'
import { openServeBundlePark } from '../../shared/serve-bundle-park'
import {
redactingSpawn,
shouldRedactSupervisedStdout,
type ServeSpawnChild
} from './serve-pairing-redaction'
const STARTUP_ABORT_MS = 30_000
export function launchOrcaApp(): void {
const overrideCommand = process.env.ORCA_OPEN_COMMAND
if (typeof overrideCommand === 'string' && overrideCommand.trim().length > 0) {
spawnDetached(overrideCommand, [], { shell: true })
return
}
// Launch on the same profile whose metadata openOrca awaits.
const userDataPath = resolveLaunchUserDataPath()
const overrideExecutable = process.env.ORCA_APP_EXECUTABLE
if (typeof overrideExecutable === 'string' && overrideExecutable.trim().length > 0) {
const pinned = pinLaunchUserDataPath(
getExecutableAppArgs(overrideExecutable),
stripElectronRunAsNode(process.env),
userDataPath
)
spawnDetached(overrideExecutable, pinned.args, {
...getExecutableSpawnOptions(overrideExecutable),
env: pinned.env
})
return
}
if (process.env.ELECTRON_RUN_AS_NODE === '1') {
if (process.platform === 'darwin') {
const appBundlePath = getMacAppBundlePath(process.execPath)
if (appBundlePath) {
// Launch the macOS bundle through Launch Services to preserve its app lifecycle.
spawnDetached('open', getMacOpenArgs(appBundlePath, userDataPath), {
env: stripElectronRunAsNode(process.env)
})
return
}
}
const pinned = pinLaunchUserDataPath(
getExecutableAppArgs(process.execPath),
stripElectronRunAsNode(process.env),
userDataPath
)
spawnDetached(process.execPath, pinned.args, { env: pinned.env })
return
}
throw new RuntimeClientError(
'runtime_open_failed',
'Could not determine how to launch Orca. Start Orca manually and try again.'
)
}
/** Nondefault profiles need a new instance: open drops env and only new instances receive --args. */
export function getMacOpenArgs(appBundlePath: string, userDataPath: string): string[] {
if (isSameUserDataPath(userDataPath, getPlatformUserDataPath())) {
return [appBundlePath]
}
return ['-n', appBundlePath, '--args', `--user-data-dir=${userDataPath}`]
}
function spawnDetached(command: string, args: string[], options: SpawnOptions): void {
const child = spawn(command, args, {
detached: true,
stdio: 'ignore',
...options
})
// Handle asynchronous spawn errors; openOrca reports startup failure via its timeout.
child.once('error', () => {})
child.unref()
}
export function serveOrcaApp(args: ServeOrcaAppArgs = {}): Promise<number> {
// Install the PID 1 reaper before spawning so orphaned descendants are collected.
reexecThisServeUnderInitReaper()
noteServeSupervisorStart()
const executable = resolveForegroundOrcaExecutable()
// Runtime selection, orcad, and Electron must use the same profile.
const userDataPath = resolveLaunchUserDataPath()
if (args.supervise) {
// Successful relocation replaces this process and never resolves.
return relocateThisServeSupervisor(resolveAppRoot(), userDataPath).then(() => {
let attempt = 0
return superviseServeService({
userDataPath,
executable,
appRoot: resolveAppRoot(),
force: args.force === true,
park: openServeBundlePark({ executable }),
appImageUpdate: supervisedAppImageUpdate(executable, userDataPath, args.force === true),
// --force overrides the brake once; later attempts use its updated state.
runOnce: () =>
serveOnce(
executable,
{ ...args, force: args.force === true && attempt++ === 0 },
userDataPath
)
})
})
}
return serveOnce(executable, args, userDataPath)
}
function serveOnce(
executable: string,
args: ServeOrcaAppArgs,
userDataPath: string
): Promise<number> {
if (process.env[SERVE_RUNTIME_ENV] === SERVE_RUNTIME_ELECTRON) {
return serveWithElectron(executable, args, userDataPath)
}
return serveWithSelectedRuntime(executable, args, userDataPath)
}
async function serveWithSelectedRuntime(
executable: string,
args: ServeOrcaAppArgs,
userDataPath: string
): Promise<number> {
const selection = await resolveLocalServeRuntime({
executable,
appRoot: resolveAppRoot(),
userDataPath,
usesMacUpdateHandoff: args.recipeJson !== true && getMacAppBundlePath(executable) !== null
})
if (selection.kind === 'orcad') {
// Exit 3 waits for the profile owner; orcad’s exit 78 would count against the crash brake.
const refusal = args.supervise ? await refuseIfProfileServed(args, userDataPath) : null
if (refusal !== null) {
return refusal
}
process.stderr.write(`[serve] running on orcad ${selection.version}\n`)
return serveWithOrcad(
selection,
args,
userDataPath,
stripElectronRunAsNode(process.env),
findBundledWebClientRoot(resolveAppRoot()),
findAppSpeechRecognizerDir(resolveAppRoot())
)
}
if (selection.reason) {
process.stderr.write(`[serve] using Electron serve: ${selection.reason}\n`)
}
return serveWithElectron(executable, args, userDataPath)
}
function serveWithElectron(
executable: string,
args: ServeOrcaAppArgs,
userDataPath: string
): Promise<number> {
const preflight = preflightElectronServe(args, userDataPath)
if (preflight === null) {
return spawnElectronServe(executable, args, userDataPath)
}
return Promise.resolve(preflight).then(
(refusal) => refusal ?? spawnElectronServe(executable, args, userDataPath)
)
}
function spawnElectronServe(
executable: string,
args: ServeOrcaAppArgs,
userDataPath: string
): Promise<number> {
const pinned = pinServeUserDataPath(
getExecutableAppArgs(executable),
stripElectronRunAsNode(process.env),
userDataPath
)
const childArgs = pinned.args
childArgs.push('--serve', ...serveOptionArgs(args, '--serve-'))
// Isolated profiles need keychain bypass to avoid blocking readiness on authorization (#25453).
childArgs.push(...foregroundServeIsolatedProfileSwitches())
childArgs.push(...headlessServeOzonePlatformSwitches(process.platform))
const handoffPath =
args.recipeJson !== true && getMacAppBundlePath(executable)
? getServeUpdateHandoffPath(userDataPath)
: null
const childEnv = pinned.env
if (handoffPath) {
childEnv[SERVE_UPDATE_HANDOFF_PATH_ENV] = handoffPath
}
const spec: ProcessSpec = {
program: executable,
args: childArgs,
detached: args.recipeJson === true,
cwd: resolveAppRoot(),
stdio:
args.recipeJson === true
? ['ignore', 'pipe', 'inherit']
: handoffPath
? ['inherit', 'inherit', 'inherit', 'ipc']
: 'inherit',
env: childEnv
}
const spawnServer: ServeSpawnChild = shouldRedactSupervisedStdout(args.supervise === true)
? redactingSpawn(spawnProcess)
: spawnProcess
// Check before every spawn after stop/park gates; overrides are not replaced by app updates.
const spawnServeChild: ServeSpawnChild =
handoffPath && executable === process.execPath
? (childSpec) => {
restartThisServeSupervisorIfReplaced(resolveAppRoot())
return spawnServer(childSpec)
}
: spawnServer
const interruptedHandoff = handoffPath ? readServeUpdateHandoffSync(handoffPath) : null
if (interruptedHandoff?.phase === 'install-requested') {
// Node mode avoids NSRunningApplication registration, retaining launchd ownership during ShipIt swaps.
return resumeInterruptedServeUpdate({
spec,
spawnChild: spawnServeChild,
handoffPath: handoffPath!,
handoff: interruptedHandoff
})
}
if (isSupervisedSpawnHeld()) {
return Promise.resolve(0)
}
const child = spawnServeChild(spec)
const spawnedAt = Date.now()
if (args.recipeJson) {
return waitForRecipeJson(child)
}
return superviseForegroundServe({
spec,
spawnChild: spawnServeChild,
child,
handoffPath,
expectedHandoff: null
}).catch((error: unknown) => {
// Registration aborts happen before child JS runs; record only immediate startup failures.
if (error instanceof MacServeStartupAbortError && Date.now() - spawnedAt < STARTUP_ABORT_MS) {
recordElectronServeStartupAbort(userDataPath, error.message)
}
throw error
})
}
function getExecutableSpawnOptions(executable: string): Pick<SpawnOptions, 'shell'> {
return process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(executable) ? { shell: true } : {}
}