From 034aa70e206e430828bca5bbaf4930556d195715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 12 May 2025 16:53:18 +0200 Subject: [PATCH] [PM-8297] Improve updater behavior --- apps/desktop/src/main.ts | 2 +- apps/desktop/src/main/updater.main.ts | 92 ++++++++++++++++++--------- package-lock.json | 16 ++--- package.json | 2 +- 4 files changed, 73 insertions(+), 39 deletions(-) diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 20c632ec4ac..02309c90078 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -201,7 +201,7 @@ export class Main { (win) => this.trayMain.setupWindowListeners(win), ); this.messagingMain = new MessagingMain(this, this.desktopSettingsService); - this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain); + this.updaterMain = new UpdaterMain(this.i18nService, this.logService, this.windowMain); const messageSubject = new Subject>>(); this.messagingService = MessageSender.combine( diff --git a/apps/desktop/src/main/updater.main.ts b/apps/desktop/src/main/updater.main.ts index 0e2efa66f91..324e7578db5 100644 --- a/apps/desktop/src/main/updater.main.ts +++ b/apps/desktop/src/main/updater.main.ts @@ -1,8 +1,9 @@ import { dialog, shell } from "electron"; import log from "electron-log"; -import { autoUpdater } from "electron-updater"; +import { autoUpdater, UpdateDownloadedEvent, VerifyUpdateSupport } from "electron-updater"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { isAppImage, isDev, isMacAppStore, isWindowsPortable, isWindowsStore } from "../utils"; @@ -15,13 +16,18 @@ export class UpdaterMain { private doingUpdateCheck = false; private doingUpdateCheckWithFeedback = false; private canUpdate = false; + private updateDownloaded: UpdateDownloadedEvent = null; + private originalRolloutFunction: VerifyUpdateSupport; constructor( private i18nService: I18nService, + private logService: LogService, private windowMain: WindowMain, ) { autoUpdater.logger = log; + this.originalRolloutFunction = autoUpdater.isUserWithinRollout; + const linuxCanUpdate = process.platform === "linux" && isAppImage(); const windowsCanUpdate = process.platform === "win32" && !isWindowsStore() && !isWindowsPortable(); @@ -35,10 +41,16 @@ export class UpdaterMain { global.setInterval(async () => await this.checkForUpdate(), UpdaterCheckInterval); autoUpdater.on("checking-for-update", () => { + this.logService.debug("[Updater] Checking for update..."); this.doingUpdateCheck = true; }); - autoUpdater.on("update-available", async () => { + autoUpdater.on("update-available", async (info) => { + this.logService.debug( + `[Updater] Update available (with feedback: ${this.doingUpdateCheckWithFeedback})`, + info, + ); + if (this.doingUpdateCheckWithFeedback) { if (this.windowMain.win == null) { this.reset(); @@ -57,20 +69,18 @@ export class UpdaterMain { }); if (result.response === 0) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - autoUpdater.downloadUpdate(); + await autoUpdater.downloadUpdate(); } else { this.reset(); } } }); - autoUpdater.on("update-not-available", () => { + autoUpdater.on("update-not-available", async (info) => { + this.logService.debug("[Updater] No update available", info); + if (this.doingUpdateCheckWithFeedback && this.windowMain.win != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - dialog.showMessageBox(this.windowMain.win, { + await dialog.showMessageBox(this.windowMain.win, { message: this.i18nService.t("noUpdatesAvailable"), buttons: [this.i18nService.t("ok")], defaultId: 0, @@ -82,29 +92,19 @@ export class UpdaterMain { }); autoUpdater.on("update-downloaded", async (info) => { + this.logService.debug("[Updater] Update downloaded", info); + if (this.windowMain.win == null) { return; } - const result = await dialog.showMessageBox(this.windowMain.win, { - type: "info", - title: this.i18nService.t("bitwarden") + " - " + this.i18nService.t("restartToUpdate"), - message: this.i18nService.t("restartToUpdate"), - detail: this.i18nService.t("restartToUpdateDesc", info.version), - buttons: [this.i18nService.t("restart"), this.i18nService.t("later")], - cancelId: 1, - defaultId: 0, - noLink: true, - }); - - if (result.response === 0) { - // Quit and install have a different window logic, setting `isQuitting` just to be safe. - this.windowMain.isQuitting = true; - autoUpdater.quitAndInstall(true, true); - } + this.updateDownloaded = info; + await this.promptRestartUpdate(info); }); autoUpdater.on("error", (error) => { + this.logService.error("[Updater] Error in auto-updater", error); + if (this.doingUpdateCheckWithFeedback) { dialog.showErrorBox( this.i18nService.t("updateError"), @@ -117,15 +117,23 @@ export class UpdaterMain { } async checkForUpdate(withFeedback = false) { - if (this.doingUpdateCheck || isDev()) { + if (isDev()) { + return; + } + + if (this.updateDownloaded && withFeedback) { + await this.promptRestartUpdate(this.updateDownloaded); + return; + } + + if (this.doingUpdateCheck) { + this.logService.debug("[Updater] Already checking for update"); return; } if (!this.canUpdate) { if (withFeedback) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - shell.openExternal("https://github.com/bitwarden/clients/releases"); + void shell.openExternal("https://github.com/bitwarden/clients/releases"); } return; @@ -134,6 +142,10 @@ export class UpdaterMain { this.doingUpdateCheckWithFeedback = withFeedback; if (withFeedback) { autoUpdater.autoDownload = false; + + // If the user has explicitly checked for updates, we want to bypass + // the current staging rollout percentage + autoUpdater.isUserWithinRollout = (info) => true; } await autoUpdater.checkForUpdates(); @@ -141,7 +153,29 @@ export class UpdaterMain { private reset() { autoUpdater.autoDownload = true; + // Reset the rollout check to the default behavior + autoUpdater.isUserWithinRollout = this.originalRolloutFunction; this.doingUpdateCheck = false; + this.updateDownloaded = null; + } + + private async promptRestartUpdate(info: UpdateDownloadedEvent) { + const result = await dialog.showMessageBox(this.windowMain.win, { + type: "info", + title: this.i18nService.t("bitwarden") + " - " + this.i18nService.t("restartToUpdate"), + message: this.i18nService.t("restartToUpdate"), + detail: this.i18nService.t("restartToUpdateDesc", info.version), + buttons: [this.i18nService.t("restart"), this.i18nService.t("later")], + cancelId: 1, + defaultId: 0, + noLink: true, + }); + + if (result.response === 0) { + // Quit and install have a different window logic, setting `isQuitting` just to be safe. + this.windowMain.isQuitting = true; + autoUpdater.quitAndInstall(true, true); + } } private userDisabledUpdates(): boolean { diff --git a/package-lock.json b/package-lock.json index 81c9384fbbb..f3855b9bb2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -138,7 +138,7 @@ "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1", "electron-store": "8.2.0", - "electron-updater": "6.3.9", + "electron-updater": "6.6.4", "eslint": "8.57.1", "eslint-config-prettier": "10.1.2", "eslint-import-resolver-typescript": "4.3.4", @@ -18485,13 +18485,13 @@ "license": "ISC" }, "node_modules/electron-updater": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.9.tgz", - "integrity": "sha512-2PJNONi+iBidkoC5D1nzT9XqsE8Q1X28Fn6xRQhO3YX8qRRyJ3mkV4F1aQsuRnYPqq6Hw+E51y27W75WgDoofw==", + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.6.4.tgz", + "integrity": "sha512-RcuWXbXTQkI5X3vir/eJ9S0VZQJ73eaQdlrA2J0mVrb0sbkHtnkjBMGo9umFtX8axn6r/pShN3CORxdmiF4OOw==", "dev": true, "license": "MIT", "dependencies": { - "builder-util-runtime": "9.2.10", + "builder-util-runtime": "9.3.2", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", @@ -18502,9 +18502,9 @@ } }, "node_modules/electron-updater/node_modules/builder-util-runtime": { - "version": "9.2.10", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", - "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.3.2.tgz", + "integrity": "sha512-7QDXJ1FwT6d9ZhG4kuObUUPY8/ENBS/Ky26O4hR5vbeoRGavgekS2Jxv+8sCn/v23aPGU2DXRWEeJuijN2ooYA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8ba1c95df6a..1f17de13ec2 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1", "electron-store": "8.2.0", - "electron-updater": "6.3.9", + "electron-updater": "6.6.4", "eslint": "8.57.1", "eslint-config-prettier": "10.1.2", "eslint-import-resolver-typescript": "4.3.4",