fix(cli): explain SIGABRT serve exits instead of naming the signal (#10464)

* fix(cli): explain SIGABRT serve exits instead of naming the signal (#10461)

`orca serve` reported only "Orca serve exited via SIGABRT", which sent a P0
investigation down a code-signature path while a diagnostic crash report sat
unread on disk. On darwin + SIGABRT the signal-exit path now names the macOS
application-startup abort, its usual sandbox/SSH/CI causes, and points at
~/Library/Logs/DiagnosticReports/Orca-*.ips via the existing nextSteps channel.
Other platforms and signals get a clear message with no invented cause.

* fix(cli): stop asserting the SIGABRT exit happened at startup

* fix(cli): stop steering macOS SIGABRT users away from SSH serve
This commit is contained in:
Brennan Benson 2026-07-24 23:28:26 -07:00 committed by GitHub
parent 12fa5ff79e
commit 9ae8f340ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 132 additions and 2 deletions

View File

@ -0,0 +1,99 @@
import { EventEmitter } from 'node:events'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { serveSignalExitError } from './serve-signal-exit-diagnostic'
import { superviseForegroundServe } from './serve-update-supervisor'
import { RuntimeClientError } from './types'
class FakeChildProcess extends EventEmitter {
kill = vi.fn()
pid = 5150
}
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')!
function setPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', { configurable: true, value: platform })
}
function superviseUntilExit(code: number | null, signal: NodeJS.Signals | null): Promise<number> {
const child = new FakeChildProcess()
const supervised = superviseForegroundServe({
executable: '/Applications/Orca.app/Contents/MacOS/Orca',
childArgs: ['--serve'],
spawnOptions: {},
spawnChild: vi.fn() as never,
handoffPath: null,
child: child as never,
expectedHandoff: null
})
child.emit('exit', code, signal)
return supervised
}
afterEach(() => {
Object.defineProperty(process, 'platform', originalPlatform)
})
describe('serveSignalExitError', () => {
it('explains the macOS window-server abort on darwin SIGABRT', () => {
const error = serveSignalExitError('SIGABRT', 'darwin')
expect(error).toBeInstanceOf(RuntimeClientError)
expect(error.code).toBe('runtime_serve_failed')
expect(error.message).toContain('aborted with SIGABRT on macOS')
expect(error.message).toContain('macOS window server')
expect(error.data).toMatchObject({
nextSteps: [
expect.stringContaining('macOS desktop login'),
expect.stringContaining('~/Library/Logs/DiagnosticReports/Orca-*.ips')
]
})
})
it('does not claim the macOS cause off darwin', () => {
for (const platform of ['linux', 'win32'] as const) {
const error = serveSignalExitError('SIGABRT', platform)
expect(error.message).toBe('Orca serve exited via SIGABRT.')
expect(error.data).toBeUndefined()
}
})
it('does not claim the macOS cause for other darwin signals', () => {
const error = serveSignalExitError('SIGKILL', 'darwin')
expect(error.message).toBe('Orca serve exited via SIGKILL.')
expect(error.data).toBeUndefined()
})
it('stays clear when neither a code nor a signal is reported', () => {
expect(serveSignalExitError(null, 'darwin').message).toBe(
'Orca serve exited without reporting an exit code or signal.'
)
})
})
describe('superviseForegroundServe signal exits', () => {
it('throws the macOS diagnostic when the child aborts on darwin', async () => {
setPlatform('darwin')
await expect(superviseUntilExit(null, 'SIGABRT')).rejects.toThrow(
/aborted with SIGABRT on macOS/
)
})
it('reports the plain signal on linux', async () => {
setPlatform('linux')
await expect(superviseUntilExit(null, 'SIGABRT')).rejects.toThrow(
'Orca serve exited via SIGABRT.'
)
})
it('returns numeric exit codes unchanged', async () => {
setPlatform('darwin')
await expect(superviseUntilExit(0, null)).resolves.toBe(0)
await expect(superviseUntilExit(7, null)).resolves.toBe(7)
})
})

View File

@ -0,0 +1,31 @@
import { RuntimeClientError } from './types'
export const MAC_CRASH_REPORT_GLOB = '~/Library/Logs/DiagnosticReports/Orca-*.ips'
export function serveSignalExitError(
signal: NodeJS.Signals | null,
platform: NodeJS.Platform = process.platform
): RuntimeClientError {
if (!signal) {
return new RuntimeClientError(
'runtime_serve_failed',
'Orca serve exited without reporting an exit code or signal.'
)
}
if (platform !== 'darwin' || signal !== 'SIGABRT') {
return new RuntimeClientError('runtime_serve_failed', `Orca serve exited via ${signal}.`)
}
// Why: the startup abort happens inside +[NSApplication sharedApplication], before any of our JS
// runs, so the parent CLI is the only place it can be explained. We only see the signal, never the
// phase, so the cause is offered as the likely one rather than asserted.
return new RuntimeClientError(
'runtime_serve_failed',
'Orca serve aborted with SIGABRT on macOS. This most often happens at application startup, when the process cannot register with the macOS window server, which is common in restricted or sandboxed environments, SSH sessions without a GUI login, and CI.',
{
nextSteps: [
'Re-run `orca serve` outside a sandboxed or restricted environment, with a macOS desktop login active.',
`Look for a crash report at ${MAC_CRASH_REPORT_GLOB}.`
]
}
)
}

View File

@ -6,7 +6,7 @@ import {
parseServeUpdateHandoffState,
type ServeUpdateHandoffState
} from '../../shared/serve-update-handoff'
import { RuntimeClientError } from './types'
import { serveSignalExitError } from './serve-signal-exit-diagnostic'
import { waitForMacBundleVersion } from './mac-app-update-bundle'
export const SERVE_REPLACEMENT_READY_TIMEOUT_MS = 60_000
@ -80,7 +80,7 @@ export async function superviseForegroundServe(
if (typeof result.code === 'number') {
return result.code
}
throw new RuntimeClientError('runtime_serve_failed', `Orca serve exited via ${result.signal}`)
throw serveSignalExitError(result.signal)
}
const installed = await waitForMacBundleVersion(args.executable, handoff.targetVersion)