Keep Orca runtime metadata owned by the active process (#288)
This commit is contained in:
parent
90d0fae8f5
commit
2f6d96b346
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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<void>((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<void> {
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue