fix: use autoUpdater.quitAndInstall() on macOS to actually apply updates

The previous approach used app.relaunch() + app.quit() to avoid a crash
dialog, but this created a race condition where the app relaunches the
old version before electron-updater's installer replaces the .app bundle.

Instead, use autoUpdater.quitAndInstall() on all platforms with proper
cleanup beforehand (kill PTY processes, flush store, destroy windows) to
ensure a clean exit without crash dialogs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Neil
2026-03-23 16:38:57 -07:00
co-authored by Claude Opus 4.6
parent 81d8a2c10c
commit 7ed735c10c
2 changed files with 27 additions and 24 deletions
+1 -3
View File
@@ -145,8 +145,6 @@ function createWindow(): BrowserWindow {
}
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
@@ -271,7 +269,7 @@ app.whenReady().then(() => {
registerUIHandlers(store)
registerFilesystemHandlers(store)
warmSystemFontFamilies()
setupAutoUpdater(mainWindow)
setupAutoUpdater(mainWindow, { onBeforeQuit: () => store?.flush() })
// Clipboard: read text via Electron's native clipboard module so the
// renderer can bypass Chromium's clipboard pipeline (which holds
+26 -21
View File
@@ -2,10 +2,12 @@ import { app, BrowserWindow } from 'electron'
import { autoUpdater } from 'electron-updater'
import { is } from '@electron-toolkit/utils'
import type { UpdateStatus } from '../shared/types'
import { killAllPty } from './ipc/pty'
let mainWindowRef: BrowserWindow | null = null
let currentStatus: UpdateStatus = { state: 'idle' }
let userInitiatedCheck = false
let onBeforeQuitCleanup: (() => void) | null = null
function sendStatus(status: UpdateStatus): void {
currentStatus = status
@@ -46,32 +48,35 @@ export function checkForUpdatesFromMenu(): void {
}
export function quitAndInstall(): void {
if (process.platform === 'darwin') {
// On macOS, autoUpdater.quitAndInstall() calls app.exit() which bypasses
// the normal quit lifecycle, causing the "quit unexpectedly" crash dialog.
// Instead, use app.relaunch() + app.quit() which goes through the proper
// lifecycle. autoInstallOnAppQuit (set in setupAutoUpdater) ensures the
// update is applied during the quit process.
app.relaunch()
app.quit()
} else {
// On Windows/Linux, quitAndInstall works correctly with the NSIS installer.
const windows = BrowserWindow.getAllWindows()
for (const win of windows) {
win.removeAllListeners('close')
win.destroy()
}
setImmediate(() => {
autoUpdater.quitAndInstall(false, true)
})
// autoUpdater.quitAndInstall() calls app.exit() which bypasses the normal
// before-quit lifecycle. Run cleanup that would normally happen there.
killAllPty()
onBeforeQuitCleanup?.()
const windows = BrowserWindow.getAllWindows()
for (const win of windows) {
win.removeAllListeners('close')
win.destroy()
}
setImmediate(() => {
autoUpdater.quitAndInstall(false, true)
})
}
export function setupAutoUpdater(mainWindow: BrowserWindow): void {
export function setupAutoUpdater(
mainWindow: BrowserWindow,
opts?: { onBeforeQuit?: () => void }
): void {
mainWindowRef = mainWindow
onBeforeQuitCleanup = opts?.onBeforeQuit ?? null
if (!app.isPackaged && !is.dev) return
if (is.dev) return
if (!app.isPackaged && !is.dev) {
return
}
if (is.dev) {
return
}
autoUpdater.autoDownload = true
autoUpdater.autoInstallOnAppQuit = true