mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 16:02:11 +00:00
merge: PR #20098 into tier1-rc
This commit is contained in:
@@ -2,7 +2,7 @@ import { mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { createServer, type Socket } from 'node:net'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getRuntimeMetadataPath } from '../../shared/runtime-bootstrap'
|
||||
import type { RuntimeStatus } from '../../shared/runtime-types'
|
||||
import { RuntimeClient } from './client'
|
||||
@@ -86,6 +86,57 @@ describe.skipIf(process.platform === 'win32')('CLI runtime status', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Why: `kill(pid, 0)` answers EPERM when the pid exists under another uid — an Orca the
|
||||
// CLI was pointed at with ORCA_USER_DATA_PATH, or one started with sudo. Reading that
|
||||
// refusal as absence reports a live app as a dead one
|
||||
// (docs/reference/ssh-execution-boundary.md).
|
||||
describe.skipIf(process.platform === 'win32')('CLI status pid fallback', () => {
|
||||
async function statusWithUnreachableRuntime(
|
||||
killError: NodeJS.ErrnoException
|
||||
): Promise<Awaited<ReturnType<RuntimeClient['getCliStatus']>>> {
|
||||
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-status-probe-'))
|
||||
writeFileSync(
|
||||
getRuntimeMetadataPath(userDataPath),
|
||||
JSON.stringify({
|
||||
runtimeId: 'runtime-unreachable',
|
||||
pid: 424242,
|
||||
// Nothing is listening here, so `status.get` fails and the pid probe decides.
|
||||
transport: { kind: 'unix', endpoint: join(userDataPath, 'absent.sock') },
|
||||
authToken: 'token',
|
||||
startedAt: Date.now()
|
||||
})
|
||||
)
|
||||
const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => {
|
||||
throw killError
|
||||
})
|
||||
try {
|
||||
return await new RuntimeClient(userDataPath).getCliStatus()
|
||||
} finally {
|
||||
killSpy.mockRestore()
|
||||
}
|
||||
}
|
||||
|
||||
it('keeps an unsignalable app running rather than calling the bootstrap stale', async () => {
|
||||
const status = await statusWithUnreachableRuntime(
|
||||
Object.assign(new Error('kill EPERM'), { code: 'EPERM' })
|
||||
)
|
||||
|
||||
expect(status.result.app).toMatchObject({ running: true, pid: 424242 })
|
||||
expect(status.result.runtime.state).toBe('starting')
|
||||
expect(status.result.graph.state).toBe('starting')
|
||||
})
|
||||
|
||||
it('still reports a stale bootstrap when the host proves the pid is gone', async () => {
|
||||
const status = await statusWithUnreachableRuntime(
|
||||
Object.assign(new Error('kill ESRCH'), { code: 'ESRCH' })
|
||||
)
|
||||
|
||||
expect(status.result.app).toMatchObject({ running: false, pid: null })
|
||||
expect(status.result.runtime.state).toBe('stale_bootstrap')
|
||||
expect(status.result.graph.state).toBe('not_running')
|
||||
})
|
||||
})
|
||||
|
||||
describe('projectRemoteAppStatus', () => {
|
||||
function remoteStatus(overrides: Partial<RuntimeStatus> = {}): RuntimeStatus {
|
||||
return {
|
||||
|
||||
@@ -106,7 +106,9 @@ function isProcessRunning(pid: number | null | undefined): boolean {
|
||||
try {
|
||||
process.kill(pid, 0)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
} catch (error) {
|
||||
// Why: only ESRCH proves the pid is gone. EPERM means it exists under another uid, and
|
||||
// reporting that as `stale_bootstrap` calls a live Orca dead.
|
||||
return (error as NodeJS.ErrnoException)?.code !== 'ESRCH'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,6 +518,37 @@ describe('PtyHandler', () => {
|
||||
expect(JSON.parse(live).map((entry: { id: string }) => entry.id)).toEqual(['pty-21'])
|
||||
})
|
||||
|
||||
// Why: the pid gate is the one place revive turns an observation into "this pane is
|
||||
// finished". `kill(pid, 0)` answers EPERM when the process exists under another uid, and
|
||||
// the same ESRCH-only rule `reapPtyProvenExited` applies has to hold here
|
||||
// (docs/reference/ssh-execution-boundary.md).
|
||||
it('keeps a pane whose pid refuses the probe and drops only a proven-gone one', async () => {
|
||||
const state = JSON.stringify([
|
||||
{ id: 'pty-30', pid: 424242, cols: 80, rows: 24, cwd: LIVE_CWD },
|
||||
{ id: 'pty-31', pid: 434343, cols: 80, rows: 24, cwd: LIVE_CWD }
|
||||
])
|
||||
const killSpy = vi.spyOn(process, 'kill').mockImplementation((pid) => {
|
||||
if (pid === 424242) {
|
||||
throw Object.assign(new Error('kill EPERM'), { code: 'EPERM' })
|
||||
}
|
||||
if (pid === 434343) {
|
||||
throw Object.assign(new Error('kill ESRCH'), { code: 'ESRCH' })
|
||||
}
|
||||
return true
|
||||
})
|
||||
try {
|
||||
await dispatcher.callRequest('pty.revive', { state })
|
||||
} finally {
|
||||
killSpy.mockRestore()
|
||||
}
|
||||
|
||||
expect(mockPtySpawn).toHaveBeenCalledTimes(1)
|
||||
const live = (await dispatcher.callRequest('pty.serialize', {
|
||||
ids: ['pty-30', 'pty-31']
|
||||
})) as string
|
||||
expect(JSON.parse(live).map((entry: { id: string }) => entry.id)).toEqual(['pty-30'])
|
||||
})
|
||||
|
||||
describe('a Windows relay reviving a WSL pane', () => {
|
||||
const worktreeId = 'r::/remote/wsl-worktree'
|
||||
const historyFile = join(
|
||||
|
||||
@@ -2907,10 +2907,10 @@ export class PtyHandler {
|
||||
if (this.ptys.has(entry.id) || this.pendingReviveIds.has(entry.id)) {
|
||||
continue
|
||||
}
|
||||
// Only re-attach if the original process is still alive
|
||||
try {
|
||||
process.kill(entry.pid, 0)
|
||||
} catch {
|
||||
// Only re-attach if the host proves the original process is still there. `isProcessAlive`
|
||||
// is ESRCH-only for the same reason `reapPtyProvenExited` is: a refusal this host cannot
|
||||
// resolve is unverifiable, not absence (docs/reference/ssh-execution-boundary.md).
|
||||
if (!Number.isInteger(entry.pid) || entry.pid <= 0 || !isProcessAlive(entry.pid)) {
|
||||
continue
|
||||
}
|
||||
const ownedPath = entry.worktreeId
|
||||
|
||||
Reference in New Issue
Block a user