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:
@@ -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)}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user