Suppress macOS renderer teardown crash reports (#4655)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong 2026-06-04 16:02:51 -04:00 committed by GitHub
parent 13f062d093
commit 20c4ee7895
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 249 additions and 2 deletions

View File

@ -1087,6 +1087,64 @@ describe('createMainWindow', () => {
consoleError.mockRestore()
})
it('does not notify the crash recorder when renderer teardown follows a confirmed window close', () => {
const windowHandlers: Record<string, (...args: any[]) => void> = {}
const ipcHandlers: Record<string, (...args: any[]) => void> = {}
const webContents = {
on: vi.fn((event, handler) => {
windowHandlers[event] = handler
}),
setZoomLevel: vi.fn(),
setBackgroundThrottling: vi.fn(),
invalidate: vi.fn(),
setWindowOpenHandler: vi.fn(),
send: vi.fn(),
isCrashed: vi.fn(() => false)
}
const browserWindowInstance = {
webContents,
on: vi.fn((event, handler) => {
windowHandlers[event] = handler
}),
isDestroyed: vi.fn(() => false),
isMaximized: vi.fn(() => true),
isFullScreen: vi.fn(() => false),
getSize: vi.fn(() => [1200, 800]),
setSize: vi.fn(),
maximize: vi.fn(),
show: vi.fn(),
loadFile: vi.fn(),
loadURL: vi.fn(),
close: vi.fn(() => {
windowHandlers.close({} as never)
})
}
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
vi.mocked(ipcMain.on).mockImplementation((channel, handler) => {
ipcHandlers[channel] = handler as (...args: any[]) => void
return ipcMain
})
browserWindowMock.mockImplementation(function () {
return browserWindowInstance
})
const onRendererProcessGone = vi.fn()
createMainWindow(null, { onRendererProcessGone })
ipcHandlers['window:confirm-close']?.()
windowHandlers['render-process-gone']?.(
{} as never,
{
reason: 'killed',
exitCode: 9
} as never
)
expect(onRendererProcessGone).not.toHaveBeenCalled()
consoleError.mockRestore()
})
it('does not persist pending bounds after bypassing close for a gone renderer', () => {
vi.useFakeTimers()

View File

@ -623,10 +623,17 @@ export function createMainWindow(
resetTerminalInputFocus()
resetFloatingTerminalInputFocus()
resetShortcutRecorderFocus()
if (opts?.shouldRecordRendererCrash?.(details, rendererWebContentsId) !== false) {
// Why: macOS can report BrowserWindow teardown as renderer `killed`/SIGKILL
// after a confirmed close; that is window lifecycle noise, not a crash.
if (
!windowClosing &&
opts?.shouldRecordRendererCrash?.(details, rendererWebContentsId) !== false
) {
opts?.onRendererProcessGone?.(details, rendererWebContentsId)
}
console.error('[window] Renderer process gone; close confirmation will be bypassed', details)
if (!windowClosing) {
console.error('[window] Renderer process gone; close confirmation will be bypassed', details)
}
scheduleRendererRecovery(details)
})
mainWindow.webContents.on('destroyed', () => {

View File

@ -0,0 +1,182 @@
#!/usr/bin/env node
const { app, BrowserWindow, ipcMain } = require('electron')
const modeArg = process.argv.find((arg) => arg.startsWith('--mode='))
const mode = modeArg ? modeArg.slice('--mode='.length) : 'natural-close'
const timeoutArg = process.argv.find((arg) => arg.startsWith('--timeout-ms='))
const timeoutMs = timeoutArg ? Number(timeoutArg.slice('--timeout-ms='.length)) : 5000
const validModes = new Set([
'natural-close',
'sigkill-after-confirmed-close',
'sigkill-during-native-close',
'sigkill-before-close'
])
if (!validModes.has(mode) || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
console.error(
JSON.stringify({
event: 'invalid-args',
mode,
validModes: Array.from(validModes),
timeoutMs
})
)
process.exit(2)
}
let win = null
let closeConfirmed = false
let windowClosing = false
let rendererPid = 0
let renderProcessGoneDetails = null
let crashRecorderWouldRunBeforeFix = false
let crashRecorderWouldRunAfterFix = false
let timeout = null
function log(event, data = {}) {
console.log(
JSON.stringify({
at: new Date().toISOString(),
event,
mode,
closeConfirmed,
windowClosing,
rendererPid,
...data
})
)
}
function finish(exitCode = 0) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
log('summary', {
renderProcessGoneDetails,
crashRecorderWouldRunBeforeFix,
crashRecorderWouldRunAfterFix
})
setTimeout(() => {
if (win && !win.isDestroyed()) {
win.destroy()
}
app.exit(exitCode)
}, 50)
}
function killRenderer(reason) {
if (!rendererPid) {
log('kill-renderer-skipped', { reason, skipped: 'missing-renderer-pid' })
return
}
try {
process.kill(rendererPid, 'SIGKILL')
log('sent-sigkill', { reason })
} catch (error) {
log('sent-sigkill-failed', {
reason,
errorName: error instanceof Error ? error.name : typeof error,
errorMessage: error instanceof Error ? error.message : String(error)
})
}
}
app.on('child-process-gone', (_event, details) => {
log('child-process-gone', { details })
})
app.whenReady().then(async () => {
win = new BrowserWindow({
width: 420,
height: 220,
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
win.webContents.on('render-process-gone', (_event, details) => {
renderProcessGoneDetails = details
crashRecorderWouldRunBeforeFix = true
crashRecorderWouldRunAfterFix = !windowClosing
log('render-process-gone', {
details,
crashRecorderWouldRunBeforeFix,
crashRecorderWouldRunAfterFix
})
finish()
})
win.webContents.on('destroyed', () => {
log('webcontents-destroyed')
})
win.on('close', (event) => {
log('window-close')
if (!closeConfirmed) {
event.preventDefault()
log('window-close-prevented-pending-confirm')
win.webContents.send('harness:close-requested')
return
}
windowClosing = true
log('window-close-confirmed')
if (mode === 'sigkill-after-confirmed-close') {
event.preventDefault()
killRenderer('after-confirmed-close')
} else if (mode === 'sigkill-during-native-close') {
killRenderer('during-native-close')
}
})
win.on('closed', () => {
log('window-closed')
if (!renderProcessGoneDetails) {
finish()
}
})
ipcMain.on('harness:confirm-close', () => {
closeConfirmed = true
log('renderer-confirmed-close')
if (mode === 'sigkill-before-close') {
killRenderer('before-close')
} else {
win.close()
}
})
await win.loadURL(
`data:text/html;charset=utf-8,${encodeURIComponent(`
<!doctype html>
<html>
<body>
<main>renderer close teardown repro</main>
<script>
const { ipcRenderer } = require('electron')
ipcRenderer.on('harness:close-requested', () => {
ipcRenderer.send('harness:confirm-close')
})
</script>
</body>
</html>
`)}`
)
rendererPid = win.webContents.getOSProcessId()
log('loaded')
timeout = setTimeout(() => {
log('timeout')
finish(1)
}, timeoutMs)
setTimeout(() => {
log('requesting-close')
win.close()
}, 100)
})