From fc18a6be45d4473bfee58ea95f052da2df3abf27 Mon Sep 17 00:00:00 2001 From: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com> Date: Sat, 7 Nov 2026 09:32:52 +0000 Subject: [PATCH] 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 --- .../process-gone-classification.ts | 33 ++++++++++++++++-- .../process-gone-recorder.test.ts | 34 +++++++++++++++++++ .../crash-reporting/process-gone-recorder.ts | 8 ++--- src/main/speech/stt-worker-process.ts | 3 ++ src/main/speech/stt-worker-utility-process.ts | 3 +- 5 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/main/crash-reporting/process-gone-classification.ts b/src/main/crash-reporting/process-gone-classification.ts index eaea3df1d42..60b3d5f8406 100644 --- a/src/main/crash-reporting/process-gone-classification.ts +++ b/src/main/crash-reporting/process-gone-classification.ts @@ -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): { + 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. diff --git a/src/main/crash-reporting/process-gone-recorder.test.ts b/src/main/crash-reporting/process-gone-recorder.test.ts index 8b5bc07c54a..483af25c0a0 100644 --- a/src/main/crash-reporting/process-gone-recorder.test.ts +++ b/src/main/crash-reporting/process-gone-recorder.test.ts @@ -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(() => { diff --git a/src/main/crash-reporting/process-gone-recorder.ts b/src/main/crash-reporting/process-gone-recorder.ts index 9907f4c2c1b..6445ecf0419 100644 --- a/src/main/crash-reporting/process-gone-recorder.ts +++ b/src/main/crash-reporting/process-gone-recorder.ts @@ -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 diff --git a/src/main/speech/stt-worker-process.ts b/src/main/speech/stt-worker-process.ts index 5b4c31ee6a9..bfa17341bef 100644 --- a/src/main/speech/stt-worker-process.ts +++ b/src/main/speech/stt-worker-process.ts @@ -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.' diff --git a/src/main/speech/stt-worker-utility-process.ts b/src/main/speech/stt-worker-utility-process.ts index 403066663e2..871fad4f841 100644 --- a/src/main/speech/stt-worker-utility-process.ts +++ b/src/main/speech/stt-worker-utility-process.ts @@ -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({