fix(crash-reporting): stop filing Windows OS shutdown as renderer crashes (#12938)
This commit is contained in:
parent
f8786d5224
commit
2a93cae293
|
|
@ -0,0 +1,193 @@
|
|||
import { performance } from 'node:perf_hooks'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
shouldRecordProcessGoneCrash,
|
||||
shouldRecoverRendererAfterProcessGone
|
||||
} from './process-gone-classification'
|
||||
import {
|
||||
markSystemSessionEnding,
|
||||
resetExpectedTeardownStateForTest,
|
||||
resolveExpectedTeardownScope,
|
||||
WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS
|
||||
} from './expected-teardown-state'
|
||||
|
||||
function shouldRecordKilledRenderer(expectedTeardown: 'none' | 'renderer-reload' | 'app-shutdown') {
|
||||
return shouldRecordProcessGoneCrash({
|
||||
source: 'renderer',
|
||||
processType: 'renderer',
|
||||
reason: 'killed',
|
||||
exitCode: 1,
|
||||
expectedTeardown
|
||||
})
|
||||
}
|
||||
|
||||
let now: number
|
||||
|
||||
beforeEach(() => {
|
||||
now = 1_000
|
||||
resetExpectedTeardownStateForTest(() => now)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
resetExpectedTeardownStateForTest()
|
||||
})
|
||||
|
||||
describe('expected teardown state', () => {
|
||||
it('uses a product-chosen five-second harm bound, not a Windows lifetime guarantee', () => {
|
||||
// Restart Manager may wait 30s; tree-kills after this bound remain reportable by design.
|
||||
expect(WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS).toBe(5_000)
|
||||
})
|
||||
|
||||
it('uses the production monotonic clock by default', () => {
|
||||
vi.spyOn(performance, 'now').mockReturnValueOnce(1_000).mockReturnValue(1_001)
|
||||
vi.spyOn(Date, 'now').mockReturnValueOnce(10_000).mockReturnValue(15_000)
|
||||
resetExpectedTeardownStateForTest()
|
||||
|
||||
markSystemSessionEnding()
|
||||
|
||||
expect(
|
||||
resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false
|
||||
})
|
||||
).toBe('app-shutdown')
|
||||
})
|
||||
|
||||
it('does not resurrect session-end after an injected clock rollback and catch-up', () => {
|
||||
now = 3_600_000
|
||||
markSystemSessionEnding()
|
||||
now = 0
|
||||
const rollbackScope = resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false
|
||||
})
|
||||
now = 3_600_001
|
||||
const catchUpScope = resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false
|
||||
})
|
||||
|
||||
expect(rollbackScope).toBe('none')
|
||||
expect(catchUpScope).toBe('none')
|
||||
})
|
||||
|
||||
it('does not resurrect session-end after expiry and an injected backtrack', () => {
|
||||
markSystemSessionEnding()
|
||||
now += WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS
|
||||
const expiredScope = resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false
|
||||
})
|
||||
now -= 1
|
||||
const backtrackScope = resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false
|
||||
})
|
||||
|
||||
expect(expiredScope).toBe('none')
|
||||
expect(backtrackScope).toBe('none')
|
||||
})
|
||||
|
||||
it('re-arms the suppression window on repeated session-end events', () => {
|
||||
markSystemSessionEnding()
|
||||
now += WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS - 1
|
||||
markSystemSessionEnding()
|
||||
now += WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS - 1
|
||||
|
||||
expect(
|
||||
resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false
|
||||
})
|
||||
).toBe('app-shutdown')
|
||||
})
|
||||
|
||||
it('classifies killed/1 just inside the session-end window as app shutdown', () => {
|
||||
markSystemSessionEnding()
|
||||
now += WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS - 1
|
||||
const scope = resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false
|
||||
})
|
||||
|
||||
expect(scope).toBe('app-shutdown')
|
||||
expect(shouldRecordKilledRenderer(scope)).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps killed/1 reportable at and just outside the session-end boundary', () => {
|
||||
markSystemSessionEnding()
|
||||
now += WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS
|
||||
const boundaryScope = resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false
|
||||
})
|
||||
now += 1
|
||||
const outsideScope = resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false
|
||||
})
|
||||
|
||||
expect(boundaryScope).toBe('none')
|
||||
expect(outsideScope).toBe('none')
|
||||
expect(shouldRecordKilledRenderer(outsideScope)).toBe(true)
|
||||
})
|
||||
|
||||
it('excludes session-end from recovery while preserving in-app quit suppression', () => {
|
||||
markSystemSessionEnding()
|
||||
const sessionEndScope = resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false,
|
||||
includeSystemSessionEnd: false
|
||||
})
|
||||
const inAppQuitScope = resolveExpectedTeardownScope({
|
||||
isQuitting: true,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false,
|
||||
includeSystemSessionEnd: false
|
||||
})
|
||||
|
||||
expect(sessionEndScope).toBe('none')
|
||||
expect(
|
||||
shouldRecoverRendererAfterProcessGone({
|
||||
reason: 'killed',
|
||||
expectedTeardown: sessionEndScope
|
||||
})
|
||||
).toBe(true)
|
||||
expect(inAppQuitScope).toBe('app-shutdown')
|
||||
expect(
|
||||
shouldRecoverRendererAfterProcessGone({
|
||||
reason: 'killed',
|
||||
expectedTeardown: inAppQuitScope
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('preserves existing update and renderer-reload scopes', () => {
|
||||
expect(
|
||||
resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: true,
|
||||
isExpectedRendererReload: false
|
||||
})
|
||||
).toBe('app-shutdown')
|
||||
expect(
|
||||
resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: true,
|
||||
includeSystemSessionEnd: false
|
||||
})
|
||||
).toBe('renderer-reload')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import { performance } from 'node:perf_hooks'
|
||||
import type { ExpectedTeardownScope } from './process-gone-classification'
|
||||
|
||||
// Why: 5s bounds harm; Restart Manager may wait 30s, so later kills remain reportable.
|
||||
export const WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS = 5_000
|
||||
|
||||
type Clock = () => number
|
||||
|
||||
// Windows performance.now is monotonic and includes time spent suspended.
|
||||
const monotonicNow = (): number => performance.now()
|
||||
let now: Clock = monotonicNow
|
||||
let systemSessionEndedAt: number | null = null
|
||||
|
||||
export function markSystemSessionEnding(): void {
|
||||
systemSessionEndedAt = now()
|
||||
}
|
||||
|
||||
function isRecentSystemSessionEnd(): boolean {
|
||||
if (systemSessionEndedAt === null) {
|
||||
return false
|
||||
}
|
||||
const elapsed = now() - systemSessionEndedAt
|
||||
if (elapsed < 0) {
|
||||
systemSessionEndedAt = null
|
||||
return false
|
||||
}
|
||||
if (elapsed >= WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS) {
|
||||
systemSessionEndedAt = null
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function resolveExpectedTeardownScope({
|
||||
isQuitting,
|
||||
isQuittingForUpdate,
|
||||
isExpectedRendererReload,
|
||||
includeSystemSessionEnd = true
|
||||
}: {
|
||||
isQuitting: boolean
|
||||
isQuittingForUpdate: boolean
|
||||
isExpectedRendererReload: boolean
|
||||
includeSystemSessionEnd?: boolean
|
||||
}): ExpectedTeardownScope {
|
||||
if (
|
||||
isQuitting ||
|
||||
isQuittingForUpdate ||
|
||||
(includeSystemSessionEnd && isRecentSystemSessionEnd())
|
||||
) {
|
||||
return 'app-shutdown'
|
||||
}
|
||||
return isExpectedRendererReload ? 'renderer-reload' : 'none'
|
||||
}
|
||||
|
||||
export function resetExpectedTeardownStateForTest(clock: Clock = monotonicNow): void {
|
||||
now = clock
|
||||
systemSessionEndedAt = null
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
getVersion: () => '1.2.3-test',
|
||||
getAppMetrics: () => []
|
||||
}
|
||||
}))
|
||||
|
||||
import { clearCrashBreadcrumbsForTest } from './crash-breadcrumb-store'
|
||||
import {
|
||||
markSystemSessionEnding,
|
||||
resetExpectedTeardownStateForTest,
|
||||
resolveExpectedTeardownScope,
|
||||
WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS
|
||||
} from './expected-teardown-state'
|
||||
import { ProcessGoneDedupe } from './process-gone-dedupe'
|
||||
import { recordProcessGoneCrash, type ProcessGoneCrashEvent } from './process-gone-recorder'
|
||||
|
||||
function event(overrides: Partial<ProcessGoneCrashEvent> = {}): ProcessGoneCrashEvent {
|
||||
return {
|
||||
source: 'renderer',
|
||||
processType: 'renderer',
|
||||
reason: 'killed',
|
||||
exitCode: 1,
|
||||
expectedTeardown: 'none',
|
||||
details: { processType: 'renderer' },
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
const gpuKill = event({
|
||||
source: 'child',
|
||||
processType: 'GPU',
|
||||
details: { serviceName: 'GPU', type: 'GPU' }
|
||||
})
|
||||
const networkServiceKill = event({
|
||||
source: 'child',
|
||||
processType: 'Utility',
|
||||
details: {
|
||||
name: 'Network Service',
|
||||
serviceName: 'network.mojom.NetworkService',
|
||||
type: 'Utility'
|
||||
}
|
||||
})
|
||||
const rendererKill = event()
|
||||
|
||||
function currentTeardownScope() {
|
||||
return resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false
|
||||
})
|
||||
}
|
||||
|
||||
let now: number
|
||||
|
||||
beforeEach(() => {
|
||||
now = 1_000
|
||||
resetExpectedTeardownStateForTest(() => now)
|
||||
clearCrashBreadcrumbsForTest()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
resetExpectedTeardownStateForTest()
|
||||
clearCrashBreadcrumbsForTest()
|
||||
})
|
||||
|
||||
describe('recordProcessGoneCrash killed/1 ordering', () => {
|
||||
it('reports a genuine lone renderer killed/1 without teardown intent', () => {
|
||||
const record = vi.fn().mockResolvedValue({ id: 'report-1' })
|
||||
|
||||
recordProcessGoneCrash({ record } as never, rendererKill, new ProcessGoneDedupe())
|
||||
|
||||
expect(record).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('reports R3 after matching recoverable sibling churn', () => {
|
||||
const record = vi.fn().mockResolvedValue({ id: 'report-1' })
|
||||
const dedupe = new ProcessGoneDedupe()
|
||||
|
||||
recordProcessGoneCrash({ record } as never, gpuKill, dedupe)
|
||||
recordProcessGoneCrash({ record } as never, networkServiceKill, dedupe)
|
||||
recordProcessGoneCrash({ record } as never, rendererKill, dedupe)
|
||||
|
||||
expect(record).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('suppresses the fleet sequence only after independent session-end intent', () => {
|
||||
const record = vi.fn().mockResolvedValue({ id: 'report-1' })
|
||||
const dedupe = new ProcessGoneDedupe()
|
||||
markSystemSessionEnding()
|
||||
const expectedTeardown = currentTeardownScope()
|
||||
|
||||
recordProcessGoneCrash({ record } as never, event({ ...gpuKill, expectedTeardown }), dedupe)
|
||||
recordProcessGoneCrash(
|
||||
{ record } as never,
|
||||
event({ ...networkServiceKill, expectedTeardown }),
|
||||
dedupe
|
||||
)
|
||||
recordProcessGoneCrash(
|
||||
{ record } as never,
|
||||
event({ ...rendererKill, expectedTeardown }),
|
||||
dedupe
|
||||
)
|
||||
|
||||
expect(expectedTeardown).toBe('app-shutdown')
|
||||
expect(record).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('durably reports killed/1 after the session-end window expires', () => {
|
||||
const record = vi.fn().mockResolvedValue({ id: 'report-1' })
|
||||
markSystemSessionEnding()
|
||||
now += WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS
|
||||
const expectedTeardown = currentTeardownScope()
|
||||
|
||||
recordProcessGoneCrash(
|
||||
{ record } as never,
|
||||
event({ ...rendererKill, expectedTeardown }),
|
||||
new ProcessGoneDedupe()
|
||||
)
|
||||
|
||||
expect(expectedTeardown).toBe('none')
|
||||
expect(record).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('keeps a renderer report filed when session-end intent arrives later', () => {
|
||||
const record = vi.fn().mockResolvedValue({ id: 'report-1' })
|
||||
const scopeBeforeSessionEnd = currentTeardownScope()
|
||||
|
||||
recordProcessGoneCrash(
|
||||
{ record } as never,
|
||||
event({ ...rendererKill, expectedTeardown: scopeBeforeSessionEnd }),
|
||||
new ProcessGoneDedupe()
|
||||
)
|
||||
markSystemSessionEnding()
|
||||
const scopeAfterSessionEnd = currentTeardownScope()
|
||||
recordProcessGoneCrash(
|
||||
{ record } as never,
|
||||
event({ ...rendererKill, expectedTeardown: scopeAfterSessionEnd }),
|
||||
new ProcessGoneDedupe()
|
||||
)
|
||||
|
||||
expect(scopeBeforeSessionEnd).toBe('none')
|
||||
expect(scopeAfterSessionEnd).toBe('app-shutdown')
|
||||
expect(record).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
|
@ -298,6 +298,7 @@ import {
|
|||
type ExpectedTeardownScope
|
||||
} from './crash-reporting/process-gone-classification'
|
||||
import { recordProcessGoneCrash as recordProcessGoneCrashEvent } from './crash-reporting/process-gone-recorder'
|
||||
import { resolveExpectedTeardownScope } from './crash-reporting/expected-teardown-state'
|
||||
import {
|
||||
advanceSyntheticTitleSpinnerEntries,
|
||||
type SyntheticTitleSpinnerEntry
|
||||
|
|
@ -697,14 +698,17 @@ function clearExpectedRendererReload(webContentsId?: number): void {
|
|||
expectedRendererReload.clear(webContentsId)
|
||||
}
|
||||
|
||||
function getExpectedTeardownScope(webContentsId?: number): ExpectedTeardownScope {
|
||||
if (isQuitting || isQuittingForUpdate()) {
|
||||
return 'app-shutdown'
|
||||
}
|
||||
if (webContentsId === undefined) {
|
||||
return 'none'
|
||||
}
|
||||
return expectedRendererReload.matches(webContentsId) ? 'renderer-reload' : 'none'
|
||||
function getExpectedTeardownScope(
|
||||
webContentsId?: number,
|
||||
includeSystemSessionEnd = true
|
||||
): ExpectedTeardownScope {
|
||||
return resolveExpectedTeardownScope({
|
||||
isQuitting,
|
||||
isQuittingForUpdate: isQuittingForUpdate(),
|
||||
isExpectedRendererReload:
|
||||
webContentsId !== undefined && expectedRendererReload.matches(webContentsId),
|
||||
includeSystemSessionEnd
|
||||
})
|
||||
}
|
||||
|
||||
function markRecoveryReloadInFlight(webContentsId: number, durationMs = 10_000): void {
|
||||
|
|
@ -1263,7 +1267,7 @@ function openMainWindow(): BrowserWindow {
|
|||
shouldRecoverRenderer: (details, webContentsId) =>
|
||||
shouldRecoverRendererAfterProcessGone({
|
||||
reason: details.reason,
|
||||
expectedTeardown: getExpectedTeardownScope(webContentsId)
|
||||
expectedTeardown: getExpectedTeardownScope(webContentsId, false)
|
||||
}),
|
||||
onRendererRecoveryExhausted: ({ details, recentRecoveryCount }) => {
|
||||
recordDurableCrashBreadcrumb('renderer_recovery_circuit_breaker_open', {
|
||||
|
|
|
|||
|
|
@ -156,6 +156,33 @@ describe('startup ordering', () => {
|
|||
expect(startIndex).toBeGreaterThan(attachIndex)
|
||||
})
|
||||
|
||||
it('wires bounded teardown state to reporting but not recovery or close behavior', () => {
|
||||
const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8')
|
||||
const scopeStart = source.indexOf('function getExpectedTeardownScope(')
|
||||
const scopeEnd = source.indexOf('function markRecoveryReloadInFlight(', scopeStart)
|
||||
const scope = source.slice(scopeStart, scopeEnd)
|
||||
const windowStart = source.indexOf('const window = createMainWindow(store, {')
|
||||
const windowEnd = source.indexOf('onRendererRecoveryExhausted:', windowStart)
|
||||
const windowOptions = source.slice(windowStart, windowEnd)
|
||||
const recorderStart = source.indexOf('function recordProcessGoneCrash(')
|
||||
const recorderEnd = source.indexOf('function shutdownWatchersOnce(', recorderStart)
|
||||
const recorder = source.slice(recorderStart, recorderEnd)
|
||||
|
||||
expect(scopeStart).toBeGreaterThanOrEqual(0)
|
||||
expect(scopeEnd).toBeGreaterThan(scopeStart)
|
||||
expect(scope).toContain('resolveExpectedTeardownScope({')
|
||||
expect(scope).toContain('includeSystemSessionEnd')
|
||||
expect(windowStart).toBeGreaterThanOrEqual(0)
|
||||
expect(windowEnd).toBeGreaterThan(windowStart)
|
||||
expect(windowOptions).toContain('getIsQuitting: () => isQuitting')
|
||||
expect(windowOptions).toContain(
|
||||
'expectedTeardown: getExpectedTeardownScope(webContentsId, false)'
|
||||
)
|
||||
expect(recorderStart).toBeGreaterThanOrEqual(0)
|
||||
expect(recorderEnd).toBeGreaterThan(recorderStart)
|
||||
expect(recorder).toContain('expectedTeardown: getExpectedTeardownScope(webContentsId)')
|
||||
})
|
||||
|
||||
it('attaches renderer services before starting the TCC prompt watcher', () => {
|
||||
const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8')
|
||||
const attachIndex = source.indexOf('attachMainWindowServices(')
|
||||
|
|
|
|||
|
|
@ -74,6 +74,11 @@ import {
|
|||
} from './createMainWindow'
|
||||
import { ipcMain } from 'electron'
|
||||
import { shouldRecoverRendererAfterProcessGone } from '../crash-reporting/process-gone-classification'
|
||||
import {
|
||||
resetExpectedTeardownStateForTest,
|
||||
resolveExpectedTeardownScope,
|
||||
WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS
|
||||
} from '../crash-reporting/expected-teardown-state'
|
||||
|
||||
function withPlatform<T>(platform: NodeJS.Platform, run: () => T): T {
|
||||
const original = process.platform
|
||||
|
|
@ -102,6 +107,7 @@ describe('createMainWindow', () => {
|
|||
vi.mocked(ipcMain.removeListener).mockReset()
|
||||
vi.mocked(ipcMain.handle).mockReset()
|
||||
vi.mocked(ipcMain.removeHandler).mockReset()
|
||||
resetExpectedTeardownStateForTest()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
|
|
@ -3143,6 +3149,40 @@ describe('createMainWindow', () => {
|
|||
consoleError.mockRestore()
|
||||
})
|
||||
|
||||
it('still preserves PTYs and reloads after Windows session-end', () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const { browserWindowInstance, windowHandlers } = createRendererRecoveryWindowHarness()
|
||||
const onBeforeRecoveryReload = vi.fn()
|
||||
|
||||
withPlatform('win32', () => {
|
||||
createMainWindow(null, {
|
||||
onBeforeRecoveryReload,
|
||||
shouldRecoverRenderer: (details) =>
|
||||
shouldRecoverRendererAfterProcessGone({
|
||||
reason: details.reason,
|
||||
expectedTeardown: resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false,
|
||||
includeSystemSessionEnd: false
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
windowHandlers['session-end']?.({} as never)
|
||||
windowHandlers['render-process-gone']?.(
|
||||
{} as never,
|
||||
{ reason: 'killed', exitCode: 1 } as Electron.RenderProcessGoneDetails
|
||||
)
|
||||
vi.runAllTimers()
|
||||
|
||||
expect(onBeforeRecoveryReload).toHaveBeenCalledWith(143)
|
||||
expect(browserWindowInstance.loadFile).toHaveBeenCalledTimes(2)
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
|
||||
it('does not reload after renderer loss when recovery is disabled', () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
|
|
@ -3730,6 +3770,60 @@ describe('createMainWindow', () => {
|
|||
setPlatform(originalPlatform)
|
||||
})
|
||||
|
||||
it('marks production teardown state on irrevocable Windows session end', () => {
|
||||
setPlatform('win32')
|
||||
resetExpectedTeardownStateForTest(() => 1_000)
|
||||
const { windowHandlers } = setupCloseWindow()
|
||||
|
||||
createMainWindow(null)
|
||||
windowHandlers['session-end']?.({} as never)
|
||||
|
||||
expect(
|
||||
resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false
|
||||
})
|
||||
).toBe('app-shutdown')
|
||||
})
|
||||
|
||||
it.each(['darwin', 'linux'] as const)(
|
||||
'does not mark session teardown state on %s',
|
||||
(platform) => {
|
||||
setPlatform(platform)
|
||||
const { windowHandlers } = setupCloseWindow()
|
||||
|
||||
createMainWindow(null)
|
||||
|
||||
expect(windowHandlers['session-end']).toBeUndefined()
|
||||
expect(
|
||||
resolveExpectedTeardownScope({
|
||||
isQuitting: false,
|
||||
isQuittingForUpdate: false,
|
||||
isExpectedRendererReload: false
|
||||
})
|
||||
).toBe('none')
|
||||
}
|
||||
)
|
||||
|
||||
it('still minimizes to tray after the session-end reporting window expires', () => {
|
||||
setPlatform('win32')
|
||||
let now = 1_000
|
||||
resetExpectedTeardownStateForTest(() => now)
|
||||
const { windowHandlers, webContents, instance } = setupCloseWindow()
|
||||
const store = makeStore(true, true)
|
||||
|
||||
createMainWindow(store as never)
|
||||
windowHandlers['session-end']?.({} as never)
|
||||
now += WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS
|
||||
const preventDefault = vi.fn()
|
||||
windowHandlers.close({ preventDefault } as never)
|
||||
|
||||
expect(preventDefault).toHaveBeenCalledOnce()
|
||||
expect(instance.hide).toHaveBeenCalledOnce()
|
||||
expect(webContents.send).not.toHaveBeenCalledWith('window:close-requested', expect.anything())
|
||||
})
|
||||
|
||||
it('hides to the tray instead of closing when the setting is on', () => {
|
||||
setPlatform('win32')
|
||||
const { windowHandlers, webContents, instance } = setupCloseWindow()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { translateMain } from '../i18n/main-i18n'
|
|||
import { normalizeBrowserNavigationUrl } from '../../shared/browser-url'
|
||||
import { ORCA_BROWSER_GUEST_WEB_PREFERENCES } from '../../shared/browser-guest-web-preferences'
|
||||
import { isCrashReportReason } from '../../shared/crash-reporting'
|
||||
import { markSystemSessionEnding } from '../crash-reporting/expected-teardown-state'
|
||||
import {
|
||||
DEFAULT_RENDERER_RECOVERY_MAX_RECOVERIES,
|
||||
DEFAULT_RENDERER_RECOVERY_WINDOW_MS,
|
||||
|
|
@ -304,6 +305,11 @@ export function createMainWindow(
|
|||
// Why: native paste fallback is privileged IPC; only the top-level renderer may request it.
|
||||
setTrustedUIRendererWebContentsId(rendererWebContentsId)
|
||||
|
||||
// Unlike query-session-end, session-end cannot be canceled before this signal is recorded.
|
||||
if (process.platform === 'win32') {
|
||||
mainWindow.on('session-end', markSystemSessionEnding)
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
// Why: preserve hidden-window power savings; stable native sizing and frame-only invalidation
|
||||
// make wake recovery independent of the throttled viewport.
|
||||
|
|
|
|||
Loading…
Reference in New Issue