fix(electron): defer update dialogs until app is active

This commit is contained in:
digua committed 2026-08-12 10:00:12 +08:00
1 parent 22938e4922
commit e482a675a0
2 files changed
+123 -63

No files matched your search

+45 -10
View File
@@ -32,6 +32,29 @@ type DialogCall = {
message?: string
detail?: string
buttons?: string[]
parent?: unknown
}
class FakeUpdateWindow extends EventEmitter {
visible = true
focused = true
destroyed = false
constructor(readonly webContents: { send: (...args: unknown[]) => void }) {
super()
}
isVisible(): boolean {
return this.visible
}
isFocused(): boolean {
return this.focused
}
isDestroyed(): boolean {
return this.destroyed
}
}
async function loadUpdaterModule() {
@@ -45,8 +68,12 @@ async function loadUpdaterModule() {
isPackaged: true,
},
dialog: {
async showMessageBox(options: DialogCall) {
dialogCalls.push(options)
async showMessageBox(parentOrOptions: unknown, maybeOptions?: DialogCall) {
const options = maybeOptions ?? (parentOrOptions as DialogCall)
dialogCalls.push({
...options,
parent: maybeOptions ? parentOrOptions : undefined,
})
return { response: 1 }
},
},
@@ -90,20 +117,20 @@ async function loadUpdaterModule() {
return {
autoUpdater,
dialogCalls,
checkUpdate: mod.checkUpdate as (win: { webContents: { send: (...args: unknown[]) => void } }) => void,
checkUpdate: mod.checkUpdate as unknown as (win: FakeUpdateWindow) => void,
manualCheckForUpdates: mod.manualCheckForUpdates as () => void,
}
}
test('automatic stable updates download silently before showing install prompt', async () => {
test('automatic stable updates defer the install prompt until the app window is active', async () => {
const { autoUpdater, dialogCalls, checkUpdate, manualCheckForUpdates } = await loadUpdaterModule()
const sent: unknown[][] = []
checkUpdate({
webContents: {
send: (...args: unknown[]) => sent.push(args),
},
const win = new FakeUpdateWindow({
send: (...args: unknown[]) => sent.push(args),
})
win.focused = false
checkUpdate(win)
autoUpdater.emit('update-available', { version: '0.28.2' })
await Promise.resolve()
@@ -116,16 +143,24 @@ test('automatic stable updates download silently before showing install prompt',
autoUpdater.emit('update-downloaded', { version: '0.28.2' })
await Promise.resolve()
assert.equal(dialogCalls.length, 0)
win.focused = true
win.emit('focus')
await new Promise<void>((resolve) => setImmediate(resolve))
assert.equal(dialogCalls.length, 1)
assert.equal(dialogCalls[0]?.title, 'update.downloadComplete')
assert.equal(dialogCalls[0]?.message, 'update.readyToInstall')
assert.equal(dialogCalls[0]?.parent, win)
manualCheckForUpdates()
autoUpdater.emit('update-available', { version: '0.28.3' })
await Promise.resolve()
await new Promise<void>((resolve) => setImmediate(resolve))
assert.equal(dialogCalls.length, 2)
assert.equal(dialogCalls[1]?.title, 'update.newVersionTitle:0.28.3')
assert.equal(dialogCalls[1]?.message, 'update.newVersionMessage:0.28.3')
assert.equal(dialogCalls[1]?.parent, win)
assert.equal(autoUpdater.downloadCalls, 1)
})
+78 -53
View File
@@ -1,4 +1,4 @@
import { dialog, app } from 'electron'
import { dialog, app, type BrowserWindow } from 'electron'
import { autoUpdater } from 'electron-updater'
import { platform } from '@electron-toolkit/utils'
import { logger } from '../logger'
@@ -8,24 +8,37 @@ import { configureUpdateProxy, handleUpdateError, resetToDefaultSource } from '.
type AppWithQuitFlag = typeof app & { isQuiting?: boolean }
interface UpdateWindow {
webContents: {
send(channel: string, ...args: unknown[]): void
}
}
const appWithQuitFlag = app as AppWithQuitFlag
let isFirstShow = true
let isManualCheck = false
export function checkUpdate(win: UpdateWindow): void {
function runWhenWindowActive(win: BrowserWindow, callback: () => void): void {
const tryRun = (): boolean => {
if (win.isDestroyed()) {
win.removeListener('focus', tryRun)
return true
}
if (!win.isVisible() || !win.isFocused()) return false
win.removeListener('focus', tryRun)
callback()
return true
}
if (!tryRun()) {
win.on('focus', tryRun)
}
}
export function checkUpdate(win: BrowserWindow): void {
configureUpdateProxy()
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = false
let showUpdateMessageBox = false
let showInstallMessageBox = false
let isDownloadingUpdate = false
const startDownloadUpdate = (): void => {
@@ -63,23 +76,27 @@ export function checkUpdate(win: UpdateWindow): void {
}
showUpdateMessageBox = true
dialog
.showMessageBox({
title: t('update.newVersionTitle', { version: info.version }),
message: t('update.newVersionMessage', { version: info.version }),
detail: t('update.newVersionDetail'),
buttons: [t('update.downloadNow'), t('update.cancel')],
defaultId: 0,
cancelId: 1,
type: 'question',
noLink: true,
})
.then((result) => {
showUpdateMessageBox = false
if (result.response === 0) {
startDownloadUpdate()
}
})
runWhenWindowActive(win, () => {
void dialog
.showMessageBox(win, {
title: t('update.newVersionTitle', { version: info.version }),
message: t('update.newVersionMessage', { version: info.version }),
detail: t('update.newVersionDetail'),
buttons: [t('update.downloadNow'), t('update.cancel')],
defaultId: 0,
cancelId: 1,
type: 'question',
noLink: true,
})
.then((result) => {
if (result.response === 0) {
startDownloadUpdate()
}
})
.finally(() => {
showUpdateMessageBox = false
})
})
})
autoUpdater.on('download-progress', (progress) => {
@@ -88,36 +105,44 @@ export function checkUpdate(win: UpdateWindow): void {
})
autoUpdater.on('update-downloaded', () => {
dialog
.showMessageBox({
title: t('update.downloadComplete'),
message: t('update.readyToInstall'),
buttons: [t('update.install'), t('update.remindLater')],
defaultId: 1,
cancelId: 1,
type: 'question',
})
.then(async (result) => {
if (result.response !== 0) return
if (showInstallMessageBox) return
win.webContents.send('begin-install')
appWithQuitFlag.isQuiting = true
showInstallMessageBox = true
runWhenWindowActive(win, () => {
void dialog
.showMessageBox(win, {
title: t('update.downloadComplete'),
message: t('update.readyToInstall'),
buttons: [t('update.install'), t('update.remindLater')],
defaultId: 1,
cancelId: 1,
type: 'question',
})
.then(async (result) => {
if (result.response !== 0) return
if (platform.isWindows) {
logger.info('[Update] Windows: Closing worker before installing...')
try {
await closeWorkerAsync()
} catch (error) {
logger.error(`[Update] Failed to close worker: ${error}`)
win.webContents.send('begin-install')
appWithQuitFlag.isQuiting = true
if (platform.isWindows) {
logger.info('[Update] Windows: Closing worker before installing...')
try {
await closeWorkerAsync()
} catch (error) {
logger.error(`[Update] Failed to close worker: ${error}`)
}
}
}
setTimeout(() => {
setImmediate(() => {
autoUpdater.quitAndInstall(true, true)
})
}, 100)
})
setTimeout(() => {
setImmediate(() => {
autoUpdater.quitAndInstall(true, true)
})
}, 100)
})
.finally(() => {
showInstallMessageBox = false
})
})
})
autoUpdater.on('update-not-available', () => {
@@ -155,8 +180,8 @@ export function manualCheckForUpdates(): void {
})
}
export function simulateUpdateDialog(_win: UpdateWindow): void {
void dialog.showMessageBox({
export function simulateUpdateDialog(win: BrowserWindow): void {
void dialog.showMessageBox(win, {
title: t('update.newVersionTitle', { version: '9.9.9' }),
message: t('update.newVersionMessage', { version: '9.9.9' }),
detail: t('update.newVersionDetail'),