mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 16:02:55 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user