feat(desktop): show native error dialogs on startup failures

Replace silent console.error + app.quit() with dialog.showErrorBox()
so users actually see why the app failed to start instead of it just
disappearing. Covers server spawn errors, timeout, port allocation
failure, mid-session server crashes, and window creation failures.
This commit is contained in:
2026-02-09 03:35:24 +08:00
parent 1f5940438a
commit 2efc57d9ee

View File

@@ -1,7 +1,7 @@
import { spawn } from 'node:child_process' import { spawn } from 'node:child_process'
import { createServer } from 'node:net' import { createServer } from 'node:net'
import { join } from 'node:path' import { join } from 'node:path'
import { app, BrowserWindow, shell } from 'electron' import { app, BrowserWindow, dialog, shell } from 'electron'
import killProcessTree from 'tree-kill' import killProcessTree from 'tree-kill'
const DEV_SERVER_URL = 'http://localhost:3000' const DEV_SERVER_URL = 'http://localhost:3000'
@@ -9,10 +9,17 @@ const DEV_SERVER_URL = 'http://localhost:3000'
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
let serverProcess: ReturnType<typeof spawn> | null = null let serverProcess: ReturnType<typeof spawn> | null = null
let isQuitting = false let isQuitting = false
let serverReady = false
const shouldAbortWindowLoad = (): boolean => const shouldAbortWindowLoad = (): boolean =>
isQuitting || !mainWindow || mainWindow.isDestroyed() isQuitting || !mainWindow || mainWindow.isDestroyed()
const showErrorAndQuit = (title: string, detail: string) => {
if (isQuitting) return
dialog.showErrorBox(title, detail)
app.quit()
}
const getAvailablePort = (): Promise<number> => const getAvailablePort = (): Promise<number> =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const server = createServer() const server = createServer()
@@ -84,10 +91,22 @@ const spawnServer = (port: number): string => {
serverProcess.on('error', (err) => { serverProcess.on('error', (err) => {
console.error('Failed to start server:', err) console.error('Failed to start server:', err)
showErrorAndQuit(
'Startup Failed',
'A required component failed to start. Please reinstall the app.',
)
}) })
serverProcess.on('exit', () => { serverProcess.on('exit', (code) => {
serverProcess = null serverProcess = null
if (!isQuitting && serverReady) {
showErrorAndQuit(
'Service Stopped',
app.isPackaged
? 'The background service stopped unexpectedly. Please restart the app.'
: `Server process exited unexpectedly (code ${code}). Check the server logs for details.`,
)
}
}) })
return `http://127.0.0.1:${port}` return `http://127.0.0.1:${port}`
@@ -98,7 +117,16 @@ const resolveServerUrl = async (): Promise<string | null> => {
return DEV_SERVER_URL return DEV_SERVER_URL
} }
const port = await getAvailablePort() let port: number
try {
port = await getAvailablePort()
} catch {
showErrorAndQuit(
'Startup Failed',
"The app couldn't allocate a local port. Please close other apps and try again.",
)
return null
}
if (isQuitting) { if (isQuitting) {
return null return null
@@ -148,15 +176,16 @@ const createWindow = async () => {
} }
if (!ready) { if (!ready) {
console.error( showErrorAndQuit(
'Startup Failed',
app.isPackaged app.isPackaged
? 'Server binary did not start in time.' ? 'The app is taking too long to start. Please try again.'
: 'Dev server not responding. Run `bun dev` in apps/server first.', : 'Dev server not responding. Run `bun dev` in apps/server first.',
) )
app.quit()
return return
} }
serverReady = true
mainWindow.loadURL(serverUrl) mainWindow.loadURL(serverUrl)
} }
@@ -170,7 +199,12 @@ app
.then(createWindow) .then(createWindow)
.catch((e) => { .catch((e) => {
console.error('Failed to create window:', e) console.error('Failed to create window:', e)
app.quit() showErrorAndQuit(
"App Couldn't Start",
app.isPackaged
? "We couldn't open the application window. Please restart your computer and try again."
: `Failed to create window: ${e instanceof Error ? e.message : String(e)}`,
)
}) })
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
@@ -182,7 +216,15 @@ app.on('window-all-closed', () => {
app.on('activate', () => { app.on('activate', () => {
if (!isQuitting && BrowserWindow.getAllWindows().length === 0) { if (!isQuitting && BrowserWindow.getAllWindows().length === 0) {
createWindow() createWindow().catch((e) => {
console.error('Failed to re-create window:', e)
showErrorAndQuit(
"App Couldn't Start",
app.isPackaged
? "We couldn't open the application window. Please restart the app."
: `Failed to re-create window: ${e instanceof Error ? e.message : String(e)}`,
)
})
} }
}) })