mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 18:47:40 +00:00
fix(serve): restart the macOS serve supervisor from the updated app (#31200)
* refactor(serve): move the update handoff file reads and writes into their own module Move-only: the supervisor module is at its line budget, and the handoff file IO is a separate concern from the child supervision loop. Claude-Session: serve-supervisor-reexec * fix(serve): restart the macOS serve supervisor from the updated app After a remote update on macOS, the node-mode supervisor kept running the old Orca executable, which the installer moved aside and deleted. Under a LaunchAgent the system charges every descendant's privacy permissions (Accessibility, Screen Recording, files) to that supervisor, so once its path no longer resolved, screen capture and computer use failed for the whole served tree while the permission check still reported them granted. Once the new bundle is verified on disk and no stop is pending, the supervisor now re-runs itself in place (same pid) from the installed executable instead of spawning the replacement itself. The handoff stays install-requested, so startup's existing resume path spawns, verifies and completes it. A --supervise supervisor that runs from its copy outside the app restarts through the app's own CLI entry, so it copies the new release aside again rather than mixing old scripts with the new executable. If the re-run fails it logs and starts the replacement as before. Only applies when serving our own executable on macOS; Linux, Windows, orcad and overridden executables are unchanged. Claude-Session: serve-supervisor-reexec * fix(serve): restart a supervisor whose executable the install replaced Three review fixes on top of the update restart: - Decide the restart by file identity instead of which path observed the install. The supervisor records the device and inode of its executable at start; after any install wait (including resuming an interrupted update at startup) it restarts only if the installed executable is a different file. A supervisor that started on the old app before the swap now restarts too, and the process after a restart never restarts again. - A relocated --supervise supervisor releases its marker on the copy it is leaving, so the next relocation can delete that copy. Before, every update kept one more full copy of the app's scripts for the life of the service. - A failed process.execve aborts the process natively rather than throwing, so the fallback never ran. The restart now checks first that the executable can run and the script entry exists, and otherwise logs and starts the replacement directly as before. Claude-Session: serve-supervisor-reexec * fix(serve): restart a parked serve service whose app a desktop install replaced When the desktop app's Update & Restart parks an `orca serve --supervise` service for another profile, it writes no update handoff for that profile. After the install the service's outer loop just starts its next server, so neither update restart point ran and the supervisor kept the deleted executable. Every server start now runs the same identity check right after the park/stop gate and before spawning, so a supervisor whose executable was replaced restarts in place first. The check is a no-op when the executable is unchanged, so first starts and the process after a restart do not loop. The test drives the real park and commit functions, so the CLI typecheck project now includes that updater module (it imports only shared code). Claude-Session: serve-supervisor-reexec * refactor(serve): check for a replaced app in the one place every server start passes The restart check ran at three call sites and was threaded through the update loop as a hook, and each review round found another start path it missed. Every path (first start, resumed update, after an install, after a park or crash) spawns its server through the function spawnElectronServe builds, after its stop and park gates, so the check now lives in that spawn and nothing else carries it. The restart moves to its own module, the hook and the handoff-file move it forced are gone, and the tests drive serveOrcaApp instead of the hook. Claude-Session: serve-supervisor-reexec
This commit is contained in:
@@ -42,6 +42,10 @@ import {
|
||||
relocateThisServeSupervisor,
|
||||
SERVE_SUPERVISOR_APP_ROOT_ENV
|
||||
} from './serve-supervisor-relocation'
|
||||
import {
|
||||
noteServeSupervisorStart,
|
||||
restartThisServeSupervisorIfReplaced
|
||||
} from './serve-replaced-app-restart'
|
||||
import { serverOnlyServeMessage } from './serve-app-launcher'
|
||||
import { isSupervisedSpawnHeld } from './serve-supervised-stop'
|
||||
import { openServeBundlePark } from '../../shared/serve-bundle-park'
|
||||
@@ -131,6 +135,7 @@ function spawnDetached(command: string, args: string[], options: SpawnOptions):
|
||||
export function serveOrcaApp(args: ServeOrcaAppArgs = {}): Promise<number> {
|
||||
// Why first: as a container or PrivatePIDs init, nothing would reap orphaned descendants.
|
||||
reexecThisServeUnderInitReaper()
|
||||
noteServeSupervisorStart()
|
||||
const executable = resolveForegroundOrcaExecutable()
|
||||
// Why one value: the selector, orcad and Electron must all serve the profile the caller chose.
|
||||
const userDataPath = resolveLaunchUserDataPath()
|
||||
@@ -255,9 +260,18 @@ function spawnElectronServe(
|
||||
: 'inherit',
|
||||
env: childEnv
|
||||
}
|
||||
const spawnServeChild: ServeSpawnChild = shouldRedactSupervisedStdout(args.supervise === true)
|
||||
const spawnServer: ServeSpawnChild = shouldRedactSupervisedStdout(args.supervise === true)
|
||||
? redactingSpawn(spawnProcess)
|
||||
: spawnProcess
|
||||
// Why in the spawn: every server start (first, resumed, after an install or a park) passes here,
|
||||
// after its stop and park gates. Only our own executable: an update never deletes an override.
|
||||
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') {
|
||||
// Why: the node-mode CLI is not an NSRunningApplication, so it can retain launchd ownership while ShipIt swaps the app.
|
||||
|
||||
Reference in New Issue
Block a user