mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
fix(crash-reporting): stop showing "Orca closed unexpectedly" after the speech engine crashes (#39430)
* fix(crash-reporting): don't report a speech process exit as an Orca crash * fix(crash-reporting): keep every speech process exit out of crash reports, including OOM
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { STT_UTILITY_PROCESS_NAME } from '../speech/stt-worker-process'
|
||||
|
||||
export type ProcessGoneSource = 'renderer' | 'child'
|
||||
export type ExpectedTeardownScope = 'none' | 'renderer-reload' | 'app-shutdown'
|
||||
|
||||
@@ -11,9 +13,22 @@ const RECOVERABLE_UTILITY_SERVICE_NAMES = new Set([
|
||||
// Screen-capture service exits are recoverable Chromium churn.
|
||||
'video_capture.mojom.VideoCaptureService'
|
||||
])
|
||||
// Orca's own utility processes whose exits the app already handles and reports in place.
|
||||
const RECOVERABLE_ORCA_UTILITY_NAMES = new Set([STT_UTILITY_PROCESS_NAME])
|
||||
const RECOVERABLE_CHILD_PROCESS_REASONS = new Set(['abnormal-exit', 'crashed', 'killed'])
|
||||
const NON_RECOVERABLE_RENDERER_REASONS = new Set(['integrity-failure'])
|
||||
|
||||
/** Electron's child-process-gone names: a utility's mojo service, and its fork serviceName. */
|
||||
export function childProcessNames(details: Record<string, unknown>): {
|
||||
serviceName?: string
|
||||
name?: string
|
||||
} {
|
||||
return {
|
||||
serviceName: typeof details.serviceName === 'string' ? details.serviceName : undefined,
|
||||
name: typeof details.name === 'string' ? details.name : undefined
|
||||
}
|
||||
}
|
||||
|
||||
function isWindowsControlTerminationExitCode(exitCode: number | null): boolean {
|
||||
if (exitCode === null) {
|
||||
return false
|
||||
@@ -21,24 +36,34 @@ function isWindowsControlTerminationExitCode(exitCode: number | null): boolean {
|
||||
return WINDOWS_CONTROL_TERMINATION_EXIT_CODES.has(exitCode >>> 0)
|
||||
}
|
||||
|
||||
function isRecoverableChromiumChildProcess({
|
||||
function isRecoverableChildProcess({
|
||||
source,
|
||||
processType,
|
||||
serviceName,
|
||||
name,
|
||||
reason
|
||||
}: {
|
||||
source: ProcessGoneSource
|
||||
processType?: string
|
||||
serviceName?: string
|
||||
name?: string
|
||||
reason: string
|
||||
}): boolean {
|
||||
if (source !== 'child') {
|
||||
return false
|
||||
}
|
||||
const normalizedProcessType = processType?.toLowerCase()
|
||||
// Orca handles every exit of its own utilities, including OOM and failed launches.
|
||||
if (
|
||||
normalizedProcessType === 'utility' &&
|
||||
name !== undefined &&
|
||||
RECOVERABLE_ORCA_UTILITY_NAMES.has(name)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (!RECOVERABLE_CHILD_PROCESS_REASONS.has(reason)) {
|
||||
return false
|
||||
}
|
||||
const normalizedProcessType = processType?.toLowerCase()
|
||||
if (normalizedProcessType && RECOVERABLE_CHILD_PROCESS_TYPES.has(normalizedProcessType)) {
|
||||
return true
|
||||
}
|
||||
@@ -54,6 +79,7 @@ export function shouldRecordProcessGoneCrash({
|
||||
source,
|
||||
processType,
|
||||
serviceName,
|
||||
name,
|
||||
reason,
|
||||
exitCode,
|
||||
expectedTeardown
|
||||
@@ -62,11 +88,12 @@ export function shouldRecordProcessGoneCrash({
|
||||
source: ProcessGoneSource
|
||||
processType?: string
|
||||
serviceName?: string
|
||||
name?: string
|
||||
reason: string
|
||||
exitCode: number | null
|
||||
expectedTeardown: ExpectedTeardownScope
|
||||
}): boolean {
|
||||
if (isRecoverableChromiumChildProcess({ source, processType, serviceName, reason })) {
|
||||
if (isRecoverableChildProcess({ source, processType, serviceName, name, reason })) {
|
||||
return false
|
||||
}
|
||||
// Intentional teardown uses killed; explicit crash and OOM reasons remain reportable.
|
||||
|
||||
@@ -64,6 +64,40 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('recordProcessGoneCrash', () => {
|
||||
it('reports no crash when the speech process dies, since dictation already shows the error', async () => {
|
||||
const record = vi.fn().mockResolvedValue({ id: 'report-1' })
|
||||
const utility = (name: string, reason: string, exitCode: number) =>
|
||||
event({
|
||||
source: 'child',
|
||||
processType: 'Utility',
|
||||
reason,
|
||||
exitCode,
|
||||
details: { name, serviceName: 'node.mojom.NodeService', type: 'Utility' }
|
||||
})
|
||||
|
||||
for (const [reason, exitCode] of [
|
||||
['killed', 9],
|
||||
['crashed', 11],
|
||||
['oom', 1]
|
||||
] as const) {
|
||||
recordProcessGoneCrash(
|
||||
{ record, attachDetails } as never,
|
||||
utility('Orca Speech', reason, exitCode),
|
||||
new ProcessGoneDedupe(),
|
||||
noMinidump
|
||||
)
|
||||
}
|
||||
recordProcessGoneCrash(
|
||||
{ record, attachDetails } as never,
|
||||
utility('Some Other Service', 'crashed', 11),
|
||||
new ProcessGoneDedupe(),
|
||||
noMinidump
|
||||
)
|
||||
|
||||
await vi.waitFor(() => expect(record).toHaveBeenCalledOnce())
|
||||
expect(record.mock.calls[0][0].details).toMatchObject({ name: 'Some Other Service' })
|
||||
})
|
||||
|
||||
it('still persists the report when the forced trace flush fails', async () => {
|
||||
const record = vi.fn().mockResolvedValue({ id: 'report-1' })
|
||||
sink.flushMock.mockImplementation(() => {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
trackRendererSiblingAttribution
|
||||
} from './process-gone-sibling-attribution'
|
||||
import {
|
||||
childProcessNames,
|
||||
shouldRecordProcessGoneCrash,
|
||||
type ExpectedTeardownScope,
|
||||
type ProcessGoneSource
|
||||
@@ -163,13 +164,12 @@ export function recordProcessGoneCrash(
|
||||
return
|
||||
}
|
||||
const goneAt = Date.now()
|
||||
const serviceName =
|
||||
typeof event.details.serviceName === 'string' ? event.details.serviceName : undefined
|
||||
const names = childProcessNames(event.details)
|
||||
if (event.source === 'child') {
|
||||
correlateChildProcessDeath({
|
||||
at: goneAt,
|
||||
processType: event.processType,
|
||||
...(serviceName ? { serviceName } : {}),
|
||||
...(names.serviceName ? { serviceName: names.serviceName } : {}),
|
||||
reason: event.reason,
|
||||
exitCode: event.exitCode
|
||||
})
|
||||
@@ -181,7 +181,7 @@ export function recordProcessGoneCrash(
|
||||
platform: process.platform,
|
||||
source: event.source,
|
||||
processType: event.processType,
|
||||
serviceName,
|
||||
...names,
|
||||
reason: event.reason,
|
||||
exitCode: event.exitCode,
|
||||
expectedTeardown: event.expectedTeardown
|
||||
|
||||
@@ -3,6 +3,9 @@ import type { MessagePortMain } from 'electron'
|
||||
import type { WorkerRequestTransport } from '../worker-threads/worker-thread-transport'
|
||||
import type { RecognizerConfigInput } from './stt-worker-model-config'
|
||||
|
||||
/** Electron reports it as the utility process's name when it exits. */
|
||||
export const STT_UTILITY_PROCESS_NAME = 'Orca Speech'
|
||||
|
||||
export const STT_WORKER_EXITED_ERROR = 'The speech engine stopped unexpectedly. Try again.'
|
||||
export const STT_WORKER_HUNG_ERROR = 'The speech engine stopped responding. Try again.'
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { utilityProcess } from 'electron'
|
||||
import { getSttWorkerPath } from './stt-worker-paths'
|
||||
import {
|
||||
logSttWorkerStderr,
|
||||
STT_UTILITY_PROCESS_NAME,
|
||||
SttWorkerChildProcess,
|
||||
type SttWorkerProcess
|
||||
} from './stt-worker-process'
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
/** A native crash or hang in the recognizer stays out of the app's main process. */
|
||||
export function spawnSttUtilityProcess(): SttWorkerProcess {
|
||||
const child = utilityProcess.fork(getSttWorkerPath(), [], {
|
||||
serviceName: 'Orca Speech',
|
||||
serviceName: STT_UTILITY_PROCESS_NAME,
|
||||
stdio: ['ignore', 'ignore', 'pipe']
|
||||
})
|
||||
const worker = new SttWorkerChildProcess({
|
||||
|
||||
Reference in New Issue
Block a user