From 9ae8f340ae5aca1cfba6278ca562bc874a48c396 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:28:26 -0700 Subject: [PATCH] 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 --- .../serve-signal-exit-diagnostic.test.ts | 99 +++++++++++++++++++ .../runtime/serve-signal-exit-diagnostic.ts | 31 ++++++ src/cli/runtime/serve-update-supervisor.ts | 4 +- 3 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 src/cli/runtime/serve-signal-exit-diagnostic.test.ts create mode 100644 src/cli/runtime/serve-signal-exit-diagnostic.ts diff --git a/src/cli/runtime/serve-signal-exit-diagnostic.test.ts b/src/cli/runtime/serve-signal-exit-diagnostic.test.ts new file mode 100644 index 000000000..2379bb588 --- /dev/null +++ b/src/cli/runtime/serve-signal-exit-diagnostic.test.ts @@ -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 { + 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) + }) +}) diff --git a/src/cli/runtime/serve-signal-exit-diagnostic.ts b/src/cli/runtime/serve-signal-exit-diagnostic.ts new file mode 100644 index 000000000..082098c04 --- /dev/null +++ b/src/cli/runtime/serve-signal-exit-diagnostic.ts @@ -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}.` + ] + } + ) +} diff --git a/src/cli/runtime/serve-update-supervisor.ts b/src/cli/runtime/serve-update-supervisor.ts index 343b8b563..d179a4354 100644 --- a/src/cli/runtime/serve-update-supervisor.ts +++ b/src/cli/runtime/serve-update-supervisor.ts @@ -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)