Always install the newest eligible desktop update (#2149)

* Reapply "Always revalidate desktop updates before install"

This reverts commit 623c05aa4d.

* fix(desktop): make update revalidation safe on quit

Updater-triggered quits now bypass normal quit handling, and manifest revalidation gives up after five seconds without allowing a late install.

* fix(desktop): preserve fail-closed update installs

Keep cached updates deferred when quit-time validation is offline, and report validation timeouts separately from superseded releases.

* fix(desktop): preserve updater quit behavior

Automatic installs remain silent without relaunching, updater handoff is bounded, and background preparation failures remain visible without pinning manifest checks.

* fix(desktop): keep AppImage updates manual

Preserve the AppImage safety exemption by skipping ordinary quit-time installation while retaining the explicit Update now path.

* fix(desktop): serialize update preparation

* fix(desktop): recognize macOS updater quit handoff
This commit is contained in:
Mohamed Boudra
2026-07-17 10:06:32 +02:00
committed by GitHub
parent a8ebd390fa
commit 70472dc945
8 changed files with 909 additions and 328 deletions
+29 -13
View File
@@ -76,18 +76,12 @@ export function getStagingUserId(): Promise<string> {
return cachedStagingUserIdPromise;
}
// AppImages have no install step. electron-updater "installs" by unlinking the
// running file and mv-ing the downloaded one into place; on app quit it does this
// via a *blocking* execFileSync(newAppImage, { APPIMAGE_EXIT_AFTER_INSTALL: "true" }).
// That env var is only honored by AppImageLauncher, so without it the freshly
// launched process boots the full app and never exits — the quit hangs forever,
// with the old binary already deleted. We therefore install AppImages only on
// explicit quitAndInstall (the "Update now" button), which takes the non-blocking
// spawn path. Every other target keeps auto-install-on-quit, which works there.
export function shouldAutoInstallOnQuit(input: {
export function shouldInstallAppUpdateOnQuit(input: {
platform: NodeJS.Platform;
isAppImage: boolean;
}): boolean {
// AppImage's no-relaunch install path blocks while launching the replacement
// binary, which can hang after the running file has already been replaced.
return !(input.platform === "linux" && input.isAppImage);
}
@@ -97,10 +91,10 @@ class ElectronAppUpdateRuntime implements AppUpdateRuntime {
configure(input: AppUpdateRuntimeConfiguration): void {
autoUpdater.autoDownload = true;
autoUpdater.autoRunAppAfterInstall = true;
autoUpdater.autoInstallOnAppQuit = shouldAutoInstallOnQuit({
platform: process.platform,
isAppImage: Boolean(process.env.APPIMAGE),
});
// Paseo revalidates the current manifest before explicitly installing on quit.
// Electron's built-in handler would install an older download without checking
// whether a newer release has superseded it.
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater.allowPrerelease = input.releaseChannel === "beta";
autoUpdater.channel = input.releaseChannel === "beta" ? "beta" : "latest";
autoUpdater.allowDowngrade = false;
@@ -143,6 +137,7 @@ class ElectronAppUpdateRuntime implements AppUpdateRuntime {
}
quitAndInstall(isSilent: boolean, isForceRunAfter: boolean): void {
autoUpdater.autoRunAppAfterInstall = isForceRunAfter;
autoUpdater.quitAndInstall(isSilent, isForceRunAfter);
}
}
@@ -194,3 +189,24 @@ export async function downloadAndInstallUpdate(
onBeforeQuit,
);
}
export async function installAppUpdateOnQuit({
currentVersion,
releaseChannel,
signal,
}: {
currentVersion: string;
releaseChannel: AppReleaseChannel;
signal: AbortSignal;
}): Promise<boolean> {
if (
!shouldInstallAppUpdateOnQuit({
platform: process.platform,
isAppImage: Boolean(process.env.APPIMAGE),
})
) {
return false;
}
return appUpdateService.installUpdateOnQuit({ currentVersion, releaseChannel, signal });
}