feat(electron): download updates silently

Start stable desktop update downloads in the background during automatic checks, then prompt only after the update is ready to install.

Keep manual update checks interactive so users still confirm the download after explicitly checking.
This commit is contained in:
digua committed 2026-06-26 14:03:45 +08:00
1 parent f74aae1bb4
commit ef71543237
2 files changed
+158 -9

No files matched your search

+131
View File
@@ -0,0 +1,131 @@
import assert from 'node:assert/strict'
import { EventEmitter } from 'node:events'
import { mock, test } from 'node:test'
class FakeAutoUpdater extends EventEmitter {
autoDownload = false
autoInstallOnAppQuit = true
downloadCalls = 0
checkCalls = 0
quitInstallCalls = 0
feedUrls: unknown[] = []
setFeedURL(url: unknown): void {
this.feedUrls.push(url)
}
async checkForUpdates(): Promise<void> {
this.checkCalls++
}
async downloadUpdate(): Promise<void> {
this.downloadCalls++
}
quitAndInstall(): void {
this.quitInstallCalls++
}
}
type DialogCall = {
title?: string
message?: string
detail?: string
buttons?: string[]
}
async function loadUpdaterModule() {
const autoUpdater = new FakeAutoUpdater()
const dialogCalls: DialogCall[] = []
const logMessages: string[] = []
await mock.module('electron', {
namedExports: {
app: {
isPackaged: true,
},
dialog: {
async showMessageBox(options: DialogCall) {
dialogCalls.push(options)
return { response: 1 }
},
},
},
})
await mock.module('electron-updater', {
namedExports: { autoUpdater },
})
await mock.module('@electron-toolkit/utils', {
namedExports: { platform: { isWindows: false } },
})
await mock.module('./logger', {
namedExports: {
logger: {
info(message: string) {
logMessages.push(message)
},
error(message: string) {
logMessages.push(message)
},
},
},
})
await mock.module('./network/proxy', {
namedExports: {
getActiveProxyUrl: () => undefined,
},
})
await mock.module('./worker/workerManager', {
namedExports: {
closeWorkerAsync: async () => undefined,
},
})
await mock.module('./i18n', {
namedExports: {
t: (key: string, params?: Record<string, string>) => `${key}${params?.version ? `:${params.version}` : ''}`,
},
})
const mod = await import('./update.js')
return {
autoUpdater,
dialogCalls,
checkUpdate: mod.checkUpdate as (win: { webContents: { send: (...args: unknown[]) => void } }) => void,
manualCheckForUpdates: mod.manualCheckForUpdates as () => void,
}
}
test('automatic stable updates download silently before showing install prompt', async () => {
const { autoUpdater, dialogCalls, checkUpdate, manualCheckForUpdates } = await loadUpdaterModule()
const sent: unknown[][] = []
checkUpdate({
webContents: {
send: (...args: unknown[]) => sent.push(args),
},
})
autoUpdater.emit('update-available', { version: '0.28.2' })
await Promise.resolve()
assert.equal(autoUpdater.downloadCalls, 1)
assert.equal(dialogCalls.length, 0)
assert.equal(autoUpdater.autoDownload, false)
assert.equal(autoUpdater.autoInstallOnAppQuit, false)
autoUpdater.emit('update-downloaded', { version: '0.28.2' })
await Promise.resolve()
assert.equal(dialogCalls.length, 1)
assert.equal(dialogCalls[0]?.title, 'update.downloadComplete')
assert.equal(dialogCalls[0]?.message, 'update.readyToInstall')
manualCheckForUpdates()
autoUpdater.emit('update-available', { version: '0.28.3' })
await Promise.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(autoUpdater.downloadCalls, 1)
})
+27 -9
View File
@@ -129,6 +129,26 @@ const checkUpdate = (win) => {
// }
let showUpdateMessageBox = false
let isDownloadingUpdate = false
// 自动检查走静默下载;手动检查仍由用户确认后再下载。
const startDownloadUpdate = (): void => {
if (isDownloadingUpdate) return
isDownloadingUpdate = true
autoUpdater
.downloadUpdate()
.then(() => {
console.log('wait for post download operation')
})
.catch((downloadError) => {
// 下载失败记录到日志,不显示给用户
logger.error(`[Update] Download update failed: ${downloadError}`)
})
.finally(() => {
isDownloadingUpdate = false
})
}
autoUpdater.on('update-available', (info) => {
// win.webContents.send('show-message', 'electron:发现新版本')
if (showUpdateMessageBox) return
@@ -145,6 +165,12 @@ const checkUpdate = (win) => {
return
}
if (!isManualCheck) {
logger.info(`[Update] New version ${info.version} found, downloading silently`)
startDownloadUpdate()
return
}
showUpdateMessageBox = true
dialog
@@ -161,15 +187,7 @@ const checkUpdate = (win) => {
.then((result) => {
showUpdateMessageBox = false
if (result.response === 0) {
autoUpdater
.downloadUpdate()
.then(() => {
console.log('wait for post download operation')
})
.catch((downloadError) => {
// 下载失败记录到日志,不显示给用户
logger.error(`[Update] Download update failed: ${downloadError}`)
})
startDownloadUpdate()
}
})
})