fix(runtime): reclaim orca-runtime.json when it stops describing this runtime (#10840)

* fix(runtime): reclaim orca-runtime.json when it stops describing this runtime

On macOS the Chromium single-instance lock is silently defeated whenever
`SingletonSocket`/`SingletonCookie` go missing from the profile — and the
socket they point at lives under `$TMPDIR` (`/var/folders/.../T`), which
macOS purges after 3 days (`com.apple.bsd.dirhelper`,
CLEAN_FILES_OLDER_THAN_DAYS=3). A launch that slips past the lock runs a
full startup, republishes `orca-runtime.json` with its own pid, and leaves
the CLI on a dead pid once it exits: `orca status` reports
`stale_bootstrap` and every terminal command fails `runtime_unavailable`
while the original app keeps serving.

The owner now watches its own discovery record and republishes once no live
runtime is described. Reclaiming only a dead pid is deliberate: two live
runtimes sharing a profile would otherwise fight over the file.

Reproduced on macOS with two real Orca main processes on one profile: the
second instance took the lock and clobbered the record, and killing it left
`stale_bootstrap` against the still-healthy first instance. With this
change the owner reclaimed the record in ~2s and the CLI returned to
`ready`.

Refs #7848

* test(runtime): assert stop() clears the metadata ownership timer

The republish guard alone kept the shutdown test green, so the watch teardown was unasserted. Also drop the doc claim of startup/activation callers that do not exist.

* test(runtime): stand in a real live pid for the sibling-runtime case

Windows never assigns pid 1, so the hardcoded sibling read as dead there and
the watch would reclaim the record. Own a synthetic pid instead and let
process.pid play the live sibling.
This commit is contained in:
Brennan Benson
2026-07-27 17:32:55 -07:00
committed by GitHub
parent c25a130236
commit c75c04eaae
4 changed files with 446 additions and 2 deletions
+38 -1
View File
@@ -6,6 +6,11 @@ import { join } from 'node:path'
import type { RuntimeMetadata, RuntimeTransportMetadata } from '../../shared/runtime-bootstrap'
import type { OrcaRuntimeService } from './orca-runtime'
import { writeRuntimeMetadata } from './runtime-metadata'
import {
RUNTIME_METADATA_OWNERSHIP_POLL_MS,
watchRuntimeMetadataOwnership,
type RuntimeMetadataOwnershipWatch
} from './runtime-metadata-ownership-watch'
import { RpcDispatcher } from './rpc/dispatcher'
import type { RpcRequest, RpcResponse } from './rpc/core'
import { errorResponse } from './rpc/errors'
@@ -57,6 +62,8 @@ type OrcaRuntimeRpcServerOptions = {
// Why: test-only overrides for the two constants below; production must not pass these (defaults set by §3.1).
keepaliveIntervalMs?: number
longPollCap?: number
// Why: test-only override for the ownership reclaim cadence.
metadataOwnershipPollMs?: number
}
export type PairingOfferUnavailableReason =
@@ -450,6 +457,7 @@ export class OrcaRuntimeRpcServer {
private readonly authToken = randomBytes(24).toString('hex')
private readonly keepaliveIntervalMs: number
private readonly longPollCap: number
private readonly metadataOwnershipPollMs: number
private readonly askLongPollCap: number
private readonly relayRevokeOutbox: RelayRevokeOutbox
private deviceRegistry: DeviceRegistry | null = null
@@ -458,6 +466,7 @@ export class OrcaRuntimeRpcServer {
private tlsFingerprint: string | null = null
private activeTransports: RpcTransport[] = []
private transports: RuntimeTransportMetadata[] = []
private metadataOwnershipWatch: RuntimeMetadataOwnershipWatch | null = null
private mobileSocketWiring: MobileSocketWiring | null = null
private mobileRelayPairingProvider: MobileRelayPairingProvider | null = null
private onUnpairedDeviceAuthFailure: (() => void) | null = null
@@ -485,7 +494,8 @@ export class OrcaRuntimeRpcServer {
preferPinnedWsPort = false,
webClientRoot,
keepaliveIntervalMs = KEEPALIVE_INTERVAL_MS,
longPollCap = LONG_POLL_CAP
longPollCap = LONG_POLL_CAP,
metadataOwnershipPollMs = RUNTIME_METADATA_OWNERSHIP_POLL_MS
}: OrcaRuntimeRpcServerOptions) {
this.runtime = runtime
this.dispatcher = new RpcDispatcher({ runtime })
@@ -498,6 +508,7 @@ export class OrcaRuntimeRpcServer {
this.webClientRoot = webClientRoot
this.keepaliveIntervalMs = keepaliveIntervalMs
this.longPollCap = longPollCap
this.metadataOwnershipPollMs = metadataOwnershipPollMs
// Why: derived, not configurable — the reservation must hold for whatever cap a caller picks.
this.askLongPollCap = Math.max(1, Math.floor(longPollCap * ASK_LONG_POLL_SHARE))
this.relayRevokeOutbox = new RelayRevokeOutbox(userDataPath)
@@ -1003,12 +1014,38 @@ export class OrcaRuntimeRpcServer {
await Promise.all(activeTransports.map((t) => t.stop().catch(() => {}))).catch(() => {})
throw error
}
this.metadataOwnershipWatch = watchRuntimeMetadataOwnership({
userDataPath: this.userDataPath,
ownedPid: this.pid,
ownedRuntimeId: this.runtime.getRuntimeId(),
pollIntervalMs: this.metadataOwnershipPollMs,
republish: () => {
// Why: never advertise endpoints we already tore down.
if (this.activeTransports.length === 0) {
return
}
this.writeMetadata()
},
onReclaim: (previous) => {
console.warn(
`[runtime] Reclaimed orca-runtime.json from a dead runtime (pid ${previous?.pid ?? 'none'}); republished pid ${this.pid}.`
)
}
})
}
/** Why: test-only seam — runs one ownership check instead of waiting out the poll interval. */
checkRuntimeMetadataOwnership(): void {
this.metadataOwnershipWatch?.check()
}
async stop(): Promise<void> {
const transports = this.activeTransports
this.activeTransports = []
this.transports = []
this.metadataOwnershipWatch?.stop()
this.metadataOwnershipWatch = null
this.mobileSocketWiring = null
if (transports.length === 0) {
return