mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
fix(crash-reporting): trace all app relaunch causes
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { appRelaunchMock, recordDurableCrashBreadcrumbMock } = vi.hoisted(() => ({
|
||||
appRelaunchMock: vi.fn(),
|
||||
recordDurableCrashBreadcrumbMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({ app: { relaunch: appRelaunchMock } }))
|
||||
vi.mock('./crash-reporting/durable-crash-breadcrumb', () => ({
|
||||
recordDurableCrashBreadcrumb: recordDurableCrashBreadcrumbMock
|
||||
}))
|
||||
|
||||
import { relaunchApp } from './app-relaunch'
|
||||
|
||||
beforeEach(() => {
|
||||
appRelaunchMock.mockReset()
|
||||
recordDurableCrashBreadcrumbMock.mockReset()
|
||||
})
|
||||
|
||||
describe('relaunchApp', () => {
|
||||
it('durably records the reason before scheduling the replacement process', () => {
|
||||
relaunchApp('gpu-fallback', { processReason: 'crashed', exitCode: 5 })
|
||||
|
||||
expect(recordDurableCrashBreadcrumbMock).toHaveBeenCalledOnce()
|
||||
expect(recordDurableCrashBreadcrumbMock).toHaveBeenCalledWith('app_relaunch_requested', {
|
||||
processReason: 'crashed',
|
||||
exitCode: 5,
|
||||
reason: 'gpu-fallback'
|
||||
})
|
||||
expect(appRelaunchMock).toHaveBeenCalledOnce()
|
||||
expect(recordDurableCrashBreadcrumbMock.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
appRelaunchMock.mock.invocationCallOrder[0]
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
import { app } from 'electron'
|
||||
import type { CrashReportBreadcrumbData } from '../shared/crash-reporting'
|
||||
import { recordDurableCrashBreadcrumb } from './crash-reporting/durable-crash-breadcrumb'
|
||||
|
||||
export type AppRelaunchReason =
|
||||
| 'admin-restart'
|
||||
| 'gpu-fallback'
|
||||
| 'profile-switch'
|
||||
| 'profile-transfer'
|
||||
| 'renderer-request'
|
||||
|
||||
export function relaunchApp(reason: AppRelaunchReason, data?: CrashReportBreadcrumbData): void {
|
||||
// Why: the current process can exit immediately after app.relaunch(), so
|
||||
// persist the cause before Electron schedules the replacement process.
|
||||
recordDurableCrashBreadcrumb('app_relaunch_requested', { ...data, reason })
|
||||
app.relaunch()
|
||||
}
|
||||
+6
-1
@@ -18,6 +18,7 @@ import { ensureActiveOrcaProfile, initOrcaProfilePaths } from './orca-profiles/p
|
||||
import { getOrcaCloudAuthConfig } from './orca-profiles/profile-cloud-auth-config'
|
||||
import { getProfileUserDataPath } from './orca-profiles/profile-storage-paths'
|
||||
import { applyAppIcon } from './app-icon'
|
||||
import { relaunchApp } from './app-relaunch'
|
||||
import { StatsCollector, initStatsPath } from './stats/collector'
|
||||
import { ClaudeUsageStore, initClaudeUsagePath } from './claude-usage/store'
|
||||
import { CodexUsageStore, initCodexUsagePath } from './codex-usage/store'
|
||||
@@ -1319,7 +1320,11 @@ function handleGpuChildCrash(reason: string, exitCode: number | null): void {
|
||||
return
|
||||
}
|
||||
isQuitting = true
|
||||
app.relaunch()
|
||||
relaunchApp('gpu-fallback', {
|
||||
processReason: reason,
|
||||
exitCode,
|
||||
crashesInWindow: result.crashesInWindow
|
||||
})
|
||||
app.exit(0)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ const {
|
||||
appRelaunchMock,
|
||||
spawnMock,
|
||||
destroySystemTrayMock,
|
||||
relaunchAppMock,
|
||||
showOpenDialogMock,
|
||||
grantFloatingWorkspaceDirectoryMock
|
||||
} = vi.hoisted(() => ({
|
||||
@@ -17,6 +18,7 @@ const {
|
||||
appRelaunchMock: vi.fn(),
|
||||
spawnMock: vi.fn(),
|
||||
destroySystemTrayMock: vi.fn(),
|
||||
relaunchAppMock: vi.fn(),
|
||||
showOpenDialogMock: vi.fn(),
|
||||
grantFloatingWorkspaceDirectoryMock: vi.fn()
|
||||
}))
|
||||
@@ -91,6 +93,10 @@ vi.mock('../tray/system-tray', () => ({
|
||||
destroySystemTray: destroySystemTrayMock
|
||||
}))
|
||||
|
||||
vi.mock('../app-relaunch', () => ({
|
||||
relaunchApp: relaunchAppMock
|
||||
}))
|
||||
|
||||
vi.mock('./floating-workspace-directory', () => ({
|
||||
ensureDefaultFloatingWorkspacePath: vi.fn(),
|
||||
grantFloatingWorkspaceDirectory: grantFloatingWorkspaceDirectoryMock,
|
||||
@@ -113,6 +119,8 @@ describe('registerAppHandlers', () => {
|
||||
appRelaunchMock.mockReset()
|
||||
spawnMock.mockReset()
|
||||
destroySystemTrayMock.mockReset()
|
||||
relaunchAppMock.mockReset()
|
||||
relaunchAppMock.mockImplementation(() => appRelaunchMock())
|
||||
showOpenDialogMock.mockReset()
|
||||
grantFloatingWorkspaceDirectoryMock.mockReset()
|
||||
processKillSpy = vi.spyOn(process, 'kill').mockReturnValue(true)
|
||||
@@ -139,6 +147,7 @@ describe('registerAppHandlers', () => {
|
||||
await vi.advanceTimersByTimeAsync(150)
|
||||
|
||||
expect(destroySystemTrayMock).toHaveBeenCalledTimes(1)
|
||||
expect(relaunchAppMock).toHaveBeenCalledWith('renderer-request')
|
||||
expect(appRelaunchMock).toHaveBeenCalledTimes(1)
|
||||
expect(appExitMock).toHaveBeenCalledWith(0)
|
||||
expect(destroySystemTrayMock.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
@@ -187,6 +196,7 @@ describe('registerAppHandlers', () => {
|
||||
await vi.advanceTimersByTimeAsync(150)
|
||||
|
||||
expect(appRelaunchMock).toHaveBeenCalledTimes(1)
|
||||
expect(relaunchAppMock).toHaveBeenCalledWith('admin-restart')
|
||||
expect(appQuitMock).toHaveBeenCalledTimes(1)
|
||||
expect(appExitMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
+3
-2
@@ -6,6 +6,7 @@ import { app, BrowserWindow, dialog, ipcMain, type IpcMainInvokeEvent } from 'el
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import type { AppIdentity } from '../../shared/app-identity'
|
||||
import type { FloatingTerminalCwdRequest, MarkdownDocument } from '../../shared/types'
|
||||
import { relaunchApp } from '../app-relaunch'
|
||||
import type { Store } from '../persistence'
|
||||
import { getDevInstanceIdentity } from '../startup/dev-instance-identity'
|
||||
import { isPwshAvailable } from '../pwsh'
|
||||
@@ -314,7 +315,7 @@ export function registerAppHandlers(store: Store, options: RegisterAppHandlersOp
|
||||
// Why: app.exit(0) skips before-quit/will-quit, so clean the Windows tray
|
||||
// explicitly before relaunching to avoid a stale notification-area icon.
|
||||
destroySystemTray()
|
||||
app.relaunch()
|
||||
relaunchApp('renderer-request')
|
||||
app.exit(0)
|
||||
}, 150)
|
||||
})
|
||||
@@ -325,7 +326,7 @@ export function registerAppHandlers(store: Store, options: RegisterAppHandlersOp
|
||||
// checkpoints, runtime metadata, and telemetry flush before exit.
|
||||
await runBeforeRelaunchCleanup(options.onBeforeRelaunch)
|
||||
setTimeout(() => {
|
||||
app.relaunch()
|
||||
relaunchApp('admin-restart')
|
||||
app.quit()
|
||||
}, 150)
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ const {
|
||||
appExitMock,
|
||||
appQuitMock,
|
||||
appRelaunchMock,
|
||||
relaunchAppMock,
|
||||
destroySystemTrayMock,
|
||||
createLocalOrcaProfileMock,
|
||||
getOrcaProfileListStateMock,
|
||||
@@ -16,6 +17,7 @@ const {
|
||||
appExitMock: vi.fn(),
|
||||
appQuitMock: vi.fn(),
|
||||
appRelaunchMock: vi.fn(),
|
||||
relaunchAppMock: vi.fn(),
|
||||
destroySystemTrayMock: vi.fn(),
|
||||
createLocalOrcaProfileMock: vi.fn(),
|
||||
getOrcaProfileListStateMock: vi.fn(),
|
||||
@@ -42,6 +44,10 @@ vi.mock('../tray/system-tray', () => ({
|
||||
destroySystemTray: destroySystemTrayMock
|
||||
}))
|
||||
|
||||
vi.mock('../app-relaunch', () => ({
|
||||
relaunchApp: relaunchAppMock
|
||||
}))
|
||||
|
||||
vi.mock('../orca-profiles/profile-index-store', () => ({
|
||||
createLocalOrcaProfile: createLocalOrcaProfileMock,
|
||||
getOrcaProfileListState: getOrcaProfileListStateMock,
|
||||
@@ -70,6 +76,8 @@ describe('registerOrcaProfileHandlers', () => {
|
||||
appExitMock.mockReset()
|
||||
appQuitMock.mockReset()
|
||||
appRelaunchMock.mockReset()
|
||||
relaunchAppMock.mockReset()
|
||||
relaunchAppMock.mockImplementation(() => appRelaunchMock())
|
||||
destroySystemTrayMock.mockReset()
|
||||
createLocalOrcaProfileMock.mockReset()
|
||||
getOrcaProfileListStateMock.mockReset()
|
||||
@@ -159,6 +167,7 @@ describe('registerOrcaProfileHandlers', () => {
|
||||
await vi.advanceTimersByTimeAsync(150)
|
||||
|
||||
expect(appRelaunchMock).toHaveBeenCalledOnce()
|
||||
expect(relaunchAppMock).toHaveBeenCalledWith('profile-switch')
|
||||
// Why quit, not exit: before-quit/will-quit teardown (scrollback capture,
|
||||
// PTY kill, daemon checkpoints) must run on a profile switch.
|
||||
expect(appQuitMock).toHaveBeenCalledOnce()
|
||||
@@ -294,6 +303,7 @@ describe('registerOrcaProfileHandlers', () => {
|
||||
await vi.advanceTimersByTimeAsync(150)
|
||||
|
||||
expect(appRelaunchMock).toHaveBeenCalledOnce()
|
||||
expect(relaunchAppMock).toHaveBeenCalledWith('profile-transfer')
|
||||
expect(appQuitMock).toHaveBeenCalledOnce()
|
||||
expect(appExitMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { app, ipcMain } from 'electron'
|
||||
import type { Store } from '../persistence'
|
||||
import { relaunchApp, type AppRelaunchReason } from '../app-relaunch'
|
||||
import type {
|
||||
CreateLocalOrcaProfileArgs,
|
||||
CreateLocalOrcaProfileResult,
|
||||
@@ -153,9 +154,9 @@ async function runBeforeProfileRelaunch(
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleProfileRelaunch(): void {
|
||||
function scheduleProfileRelaunch(reason: Extract<AppRelaunchReason, `profile-${string}`>): void {
|
||||
setTimeout(() => {
|
||||
app.relaunch()
|
||||
relaunchApp(reason)
|
||||
// Why: app.quit() (not app.exit) so before-quit/will-quit still run —
|
||||
// renderer scrollback capture, PTY kill, stats flush, and daemon final
|
||||
// checkpoints must not be skipped on a profile switch.
|
||||
@@ -215,7 +216,7 @@ export function registerOrcaProfileHandlers(
|
||||
store.flush()
|
||||
setActiveOrcaProfile(profileId)
|
||||
|
||||
scheduleProfileRelaunch()
|
||||
scheduleProfileRelaunch('profile-switch')
|
||||
|
||||
return { status: 'relaunching' }
|
||||
}
|
||||
@@ -244,7 +245,7 @@ export function registerOrcaProfileHandlers(
|
||||
store.freezeWrites()
|
||||
await runBeforeProfileRelaunch(options.onBeforeRelaunch)
|
||||
setActiveOrcaProfile(args.targetProfileId)
|
||||
scheduleProfileRelaunch()
|
||||
scheduleProfileRelaunch('profile-transfer')
|
||||
return { ...result, willRelaunch: true }
|
||||
}
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user