From 2f6d96b34698674f10c47c1cd2effbc3a428ebd2 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Sat, 4 Apr 2026 03:01:19 -0400 Subject: [PATCH] Keep Orca runtime metadata owned by the active process (#288) --- src/main/index.ts | 8 ----- src/main/runtime/runtime-rpc.test.ts | 53 ++++++++++++++++++++++++++-- src/main/runtime/runtime-rpc.ts | 34 ++++++++++++++++-- 3 files changed, 82 insertions(+), 13 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index b3ba8517f..a668d3f21 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,7 +5,6 @@ import { Store } from './persistence' import { killAllPty } from './ipc/pty' import { registerCoreHandlers } from './ipc/register-core-handlers' import { OrcaRuntimeService } from './runtime/orca-runtime' -import { writeRuntimeMetadata } from './runtime/runtime-metadata' import { OrcaRuntimeRpcServer } from './runtime/runtime-rpc' import { registerAppMenu } from './menu/register-app-menu' import { checkForUpdatesFromMenu, isQuittingForUpdate } from './updater' @@ -60,13 +59,6 @@ app.whenReady().then(async () => { store = new Store() runtime = new OrcaRuntimeService(store) - writeRuntimeMetadata(app.getPath('userData'), { - runtimeId: runtime.getRuntimeId(), - pid: process.pid, - transport: null, - authToken: null, - startedAt: runtime.getStartedAt() - }) nativeTheme.themeSource = store.getSettings().theme ?? 'system' registerAppMenu({ diff --git a/src/main/runtime/runtime-rpc.test.ts b/src/main/runtime/runtime-rpc.test.ts index e0945c635..78891d123 100644 --- a/src/main/runtime/runtime-rpc.test.ts +++ b/src/main/runtime/runtime-rpc.test.ts @@ -1,12 +1,13 @@ /* eslint-disable max-lines -- Why: this integration-style RPC test keeps the request/response contract together so regressions in the external CLI surface are easier to spot. */ -import { mkdtempSync } from 'fs' +import { existsSync, mkdtempSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' import { createConnection } from 'net' import { describe, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' +import * as runtimeMetadataModule from './runtime-metadata' import { readRuntimeMetadata } from './runtime-metadata' -import { OrcaRuntimeRpcServer } from './runtime-rpc' +import { createRuntimeTransportMetadata, OrcaRuntimeRpcServer } from './runtime-rpc' vi.mock('../git/worktree', () => ({ listWorktrees: vi.fn().mockResolvedValue([ @@ -107,9 +108,57 @@ describe('OrcaRuntimeRpcServer', () => { expect(metadata?.runtimeId).toBe(runtime.getRuntimeId()) expect(metadata?.authToken).toBeTruthy() expect(metadata?.transport?.endpoint).toBeTruthy() + expect(metadata?.transport).toEqual(server['transport']) await server.stop() + expect(readRuntimeMetadata(userDataPath)).toMatchObject({ + runtimeId: runtime.getRuntimeId() + }) + }) + + it('leaves the last published metadata in place when a runtime stops', async () => { + const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) + const runtime = new OrcaRuntimeService() + const server = new OrcaRuntimeRpcServer({ + runtime, + userDataPath, + pid: 1001 + }) + + await server.start() + const metadata = readRuntimeMetadata(userDataPath) + expect(metadata?.pid).toBe(1001) + + await server.stop() + expect(readRuntimeMetadata(userDataPath)).toMatchObject({ + pid: 1001, + runtimeId: runtime.getRuntimeId() + }) + }) + + it('closes the socket if metadata publication fails during startup', async () => { + const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')) + const runtime = new OrcaRuntimeService() + const server = new OrcaRuntimeRpcServer({ runtime, userDataPath }) + const writeMetadataSpy = vi + .spyOn(runtimeMetadataModule, 'writeRuntimeMetadata') + .mockImplementationOnce(() => { + throw new Error('write failed') + }) + const endpoint = createRuntimeTransportMetadata( + userDataPath, + process.pid, + process.platform, + runtime.getRuntimeId() + ).endpoint + + await expect(server.start()).rejects.toThrow('write failed') expect(readRuntimeMetadata(userDataPath)).toBeNull() + expect(existsSync(endpoint)).toBe(false) + expect(server['transport']).toBeNull() + expect(server['server']).toBeNull() + + writeMetadataSpy.mockRestore() }) it('serves status.get for authenticated callers', async () => { diff --git a/src/main/runtime/runtime-rpc.ts b/src/main/runtime/runtime-rpc.ts index db2f893e9..82a81b372 100644 --- a/src/main/runtime/runtime-rpc.ts +++ b/src/main/runtime/runtime-rpc.ts @@ -5,7 +5,6 @@ import { chmodSync, existsSync, rmSync } from 'fs' import { join } from 'path' import type { OrcaRuntimeService } from './orca-runtime' import { - clearRuntimeMetadata, type RuntimeMetadata, type RuntimeTransportMetadata, writeRuntimeMetadata @@ -103,9 +102,34 @@ export class OrcaRuntimeRpcServer { chmodSync(transport.endpoint, 0o600) } + // Why: publish the transport into in-memory state before writing metadata + // so the bootstrap file always contains the real endpoint/token pair. The + // CLI only discovers the runtime through that file. this.server = server this.transport = transport - this.writeMetadata() + + try { + this.writeMetadata() + } catch (error) { + // Why: a runtime that cannot publish bootstrap metadata is invisible to + // the `orca` CLI. Close the socket immediately instead of leaving behind + // a live but undiscoverable control plane. + this.server = null + this.transport = null + await new Promise((resolve, reject) => { + server.close((closeError) => { + if (closeError) { + reject(closeError) + return + } + resolve() + }) + }).catch(() => {}) + if (transport.kind === 'unix' && existsSync(transport.endpoint)) { + rmSync(transport.endpoint, { force: true }) + } + throw error + } } async stop(): Promise { @@ -113,7 +137,6 @@ export class OrcaRuntimeRpcServer { const transport = this.transport this.server = null this.transport = null - clearRuntimeMetadata(this.userDataPath) if (!server) { return } @@ -129,6 +152,11 @@ export class OrcaRuntimeRpcServer { if (transport?.kind === 'unix' && existsSync(transport.endpoint)) { rmSync(transport.endpoint, { force: true }) } + // Why: we intentionally leave the last metadata file behind instead of + // deleting it on shutdown. Shared userData paths can briefly host multiple + // Orca processes during restarts, updates, or development, and stale + // metadata is safer than letting one process erase another live runtime's + // bootstrap file. } private handleConnection(socket: Socket): void {