fix(desktop): reveal renderer startup failures (#1032)

This commit is contained in:
程序员阿江(Relakkes) 2026-07-17 22:54:43 +08:00
parent e72020247b
commit 26e863902f
3 changed files with 109 additions and 6 deletions

View File

@ -1,10 +1,10 @@
import { app, BrowserWindow, clipboard, ipcMain, Notification, screen, session, WebContentsView } from 'electron'
import { app, BrowserWindow, clipboard, dialog, ipcMain, Notification, screen, session, WebContentsView } from 'electron'
import { autoUpdater } from 'electron-updater'
import path from 'node:path'
import { ELECTRON_EVENT_CHANNELS, ELECTRON_INTERNAL_CHANNELS, ELECTRON_IPC_CHANNELS, type ElectronIpcChannel } from './ipc/channels'
import { isElectronIpcChannel, validateElectronIpcPayload } from './ipc/capabilities'
import { ElectronServerRuntime } from './services/serverRuntime'
import { electronHostDiagnosticsFile } from './services/sidecarManager'
import { electronHostDiagnosticsFile, sanitizeHostDiagnostic } from './services/sidecarManager'
import { openDialog, saveDialog } from './services/dialogs'
import { openExternalUrl, openSystemPath, openSystemSettingsUrl } from './services/shell'
import {
@ -38,6 +38,7 @@ import { logNotificationSmokeRendererAck, scheduleNotificationSmoke } from './se
import { normalizeZoomFactor } from './services/zoom'
import { resolveRendererEntry } from './services/rendererEntry'
import { writeWindowSmokeSnapshot } from './services/windowSmoke'
import { loadAndRevealMainWindow } from './services/windowStartup'
import {
installWindowLifecycle,
readWindowState,
@ -410,10 +411,20 @@ async function createMainWindow() {
writeWindowSmokeSnapshot(mainWindow, 'after-create')
await loadRendererEntry(mainWindow)
restoreWindowMaximized(mainWindow, restoredState)
showMainWindow(mainWindow, app)
await loadAndRevealMainWindow({
load: () => loadRendererEntry(mainWindow!),
beforeReveal: () => restoreWindowMaximized(mainWindow!, restoredState),
reveal: () => showMainWindow(mainWindow, app),
onLoadFailure: (error) => {
const detail = sanitizeHostDiagnostic(error instanceof Error ? error.message : String(error))
console.error(`[desktop] failed to load Electron renderer: ${detail}`)
writeWindowSmokeSnapshot(mainWindow, 'renderer-load-failed')
dialog.showErrorBox(
'启动错误 / Startup Error',
`桌面界面加载失败,请重启应用。如果问题持续存在,请附上诊断日志反馈。\n\nThe desktop interface could not be loaded. Restart the app and include diagnostics when reporting the problem.\n\n${detail}`,
)
},
})
refreshWindowsDragHitTest(mainWindow, process.platform)
writeWindowSmokeSnapshot(mainWindow, 'after-final-show')
}

View File

@ -0,0 +1,57 @@
import { describe, expect, it, vi } from 'vitest'
import { loadAndRevealMainWindow } from './windowStartup'
describe('Electron main window startup', () => {
it('reveals the main window after the renderer loads', async () => {
const order: string[] = []
const onLoadFailure = vi.fn()
const result = await loadAndRevealMainWindow({
load: async () => { order.push('load') },
beforeReveal: () => { order.push('before-reveal') },
reveal: () => { order.push('reveal') },
onLoadFailure,
})
expect(result).toEqual({ loaded: true })
expect(order).toEqual(['load', 'before-reveal', 'reveal'])
expect(onLoadFailure).not.toHaveBeenCalled()
})
it('still reveals the main window and reports the error when renderer loading fails', async () => {
const order: string[] = []
const failure = new Error('renderer entry unavailable')
const result = await loadAndRevealMainWindow({
load: async () => {
order.push('load')
throw failure
},
beforeReveal: () => { order.push('before-reveal') },
reveal: () => { order.push('reveal') },
onLoadFailure: (error) => {
expect(error).toBe(failure)
order.push('failure')
},
})
expect(result).toEqual({ loaded: false, error: failure })
expect(order).toEqual(['load', 'before-reveal', 'reveal', 'failure'])
})
it('treats an undefined rejection as a load failure', async () => {
const reveal = vi.fn()
const onLoadFailure = vi.fn()
const result = await loadAndRevealMainWindow({
load: () => Promise.reject(undefined),
beforeReveal: vi.fn(),
reveal,
onLoadFailure,
})
expect(result).toEqual({ loaded: false, error: undefined })
expect(reveal).toHaveBeenCalledTimes(1)
expect(onLoadFailure).toHaveBeenCalledWith(undefined)
})
})

View File

@ -0,0 +1,35 @@
export type MainWindowStartupOptions = {
load: () => Promise<void>
beforeReveal: () => void
reveal: () => void
onLoadFailure: (error: unknown) => void
}
export type MainWindowStartupResult =
| { loaded: true }
| { loaded: false, error: unknown }
export async function loadAndRevealMainWindow({
load,
beforeReveal,
reveal,
onLoadFailure,
}: MainWindowStartupOptions): Promise<MainWindowStartupResult> {
let loaded = true
let loadError: unknown
try {
await load()
} catch (error) {
loaded = false
loadError = error
}
beforeReveal()
reveal()
if (!loaded) {
onLoadFailure(loadError)
return { loaded: false, error: loadError }
}
return { loaded: true }
}