fix: handle detached launch spawn errors (#3957)

This commit is contained in:
Trevin Chow 2026-06-03 15:46:00 -07:00 committed by GitHub
parent 964dbbc4e6
commit 2c2ac2edbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 50 additions and 18 deletions

View File

@ -1,3 +1,4 @@
import { EventEmitter } from 'events'
import { resolve } from 'path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@ -9,7 +10,12 @@ vi.mock('child_process', () => ({
spawn: spawnMock
}))
import { serveOrcaApp } from './launch'
import { launchOrcaApp, serveOrcaApp } from './launch'
class FakeChildProcess extends EventEmitter {
kill = vi.fn()
unref = vi.fn()
}
describe('serveOrcaApp', () => {
beforeEach(() => {
@ -147,3 +153,27 @@ describe('serveOrcaApp', () => {
}
})
})
describe('launchOrcaApp', () => {
beforeEach(() => {
spawnMock.mockReset()
})
afterEach(() => {
delete process.env.ORCA_OPEN_COMMAND
delete process.env.ORCA_APP_EXECUTABLE
delete process.env.ORCA_APP_EXECUTABLE_NEEDS_APP_ROOT
})
it('handles asynchronous detached spawn errors without throwing', async () => {
process.env.ORCA_APP_EXECUTABLE = '/missing/Orca'
const child = new FakeChildProcess()
spawnMock.mockReturnValue(child)
launchOrcaApp()
child.emit('error', new Error('ENOENT'))
await Promise.resolve()
expect(child.unref).toHaveBeenCalled()
})
})

View File

@ -5,22 +5,16 @@ import { RuntimeClientError } from './types'
export function launchOrcaApp(): void {
const overrideCommand = process.env.ORCA_OPEN_COMMAND
if (typeof overrideCommand === 'string' && overrideCommand.trim().length > 0) {
spawnProcess(overrideCommand, {
detached: true,
stdio: 'ignore',
shell: true
}).unref()
spawnDetached(overrideCommand, [], { shell: true })
return
}
const overrideExecutable = process.env.ORCA_APP_EXECUTABLE
if (typeof overrideExecutable === 'string' && overrideExecutable.trim().length > 0) {
spawnProcess(overrideExecutable, getExecutableAppArgs(), {
detached: true,
stdio: 'ignore',
spawnDetached(overrideExecutable, getExecutableAppArgs(), {
...getExecutableSpawnOptions(overrideExecutable),
env: stripElectronRunAsNode(process.env)
}).unref()
})
return
}
@ -31,20 +25,16 @@ export function launchOrcaApp(): void {
// Why: launching the inner MacOS binary directly can trigger macOS app
// launch failures and bypass normal bundle lifecycle. The public
// packaged CLI should re-open the .app the same way Finder does.
spawnProcess('open', [appBundlePath], {
detached: true,
stdio: 'ignore',
spawnDetached('open', [appBundlePath], {
env: stripElectronRunAsNode(process.env)
}).unref()
})
return
}
}
spawnProcess(process.execPath, [], {
detached: true,
stdio: 'ignore',
spawnDetached(process.execPath, [], {
env: stripElectronRunAsNode(process.env)
}).unref()
})
return
}
@ -54,6 +44,18 @@ export function launchOrcaApp(): void {
)
}
function spawnDetached(command: string, args: string[], options: SpawnOptions): void {
const child = spawnProcess(command, args, {
detached: true,
stdio: 'ignore',
...options
})
// Why: detached launch errors are reported asynchronously after this function
// returns; openOrca already reports the user-facing timeout if startup fails.
child.once('error', () => {})
child.unref()
}
export function serveOrcaApp(
args: {
json?: boolean