From 11abbcc6301b621828f37384a693523dd2de1240 Mon Sep 17 00:00:00 2001 From: xpufx Date: Wed, 17 Jun 2026 10:09:35 +0300 Subject: [PATCH] fix(desktop): stop AppImage updates hanging on quit and deleting the app (#1485) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(desktop): disable auto-install on quit for AppImage only electron-updater's AppImageUpdater.doInstall() uses execFileSync with APPIMAGE_EXIT_AFTER_INSTALL when autoInstallOnAppQuit is true. For AppImages, this blocks the old process indefinitely because the new process has no installer step to exit from — the mv already completed the install. Scoped to AppImage only (process.platform === 'linux' && process.env.APPIMAGE). .deb/.rpm/Windows/macOS keep auto-install-on-quit working as before. * fix(desktop): keep AppImage filename stable across updates electron-updater renames a versioned AppImage to a new path on update and unlinks the old one. That orphaned the previous binary, broke desktop shortcuts, and dangled the ~/.local/bin/paseo CLI symlink, which points at $APPIMAGE. Dropping ${version} from the AppImage artifactName makes the updater overwrite the file in place instead; deb/rpm keep versioned names. Also documents why AppImage must not auto-install on quit (the blocking execFileSync gated on APPIMAGE_EXIT_AFTER_INSTALL, honored only by AppImageLauncher) and extracts that gate into a unit-tested helper. --------- Co-authored-by: Mohamed Boudra --- packages/desktop/electron-builder.yml | 7 +++++++ .../desktop/src/features/auto-updater.test.ts | 10 ++++++++++ packages/desktop/src/features/auto-updater.ts | 20 ++++++++++++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/desktop/electron-builder.yml b/packages/desktop/electron-builder.yml index 78774033c..000909477 100644 --- a/packages/desktop/electron-builder.yml +++ b/packages/desktop/electron-builder.yml @@ -52,6 +52,13 @@ linux: - deb - rpm - tar.gz +appImage: + # Keep the AppImage filename stable across versions (no ${version}) so + # electron-updater overwrites the file in place instead of renaming it to a new + # versioned path. A rename orphans the old binary, breaks desktop shortcuts, and + # dangles the ~/.local/bin/paseo CLI symlink, which points at $APPIMAGE. The + # version stays discoverable via the release tag and the in-app version. + artifactName: "Paseo-${arch}.${ext}" win: artifactName: "Paseo-Setup-${version}-${arch}.${ext}" icon: assets/icon.ico diff --git a/packages/desktop/src/features/auto-updater.test.ts b/packages/desktop/src/features/auto-updater.test.ts index f810b8da3..4de12d87d 100644 --- a/packages/desktop/src/features/auto-updater.test.ts +++ b/packages/desktop/src/features/auto-updater.test.ts @@ -19,8 +19,18 @@ import { resolveStagingUserId, rolloutManifestSchema, shouldAdmitToRollout, + shouldAutoInstallOnQuit, } from "./auto-updater"; +describe("shouldAutoInstallOnQuit", () => { + it("auto-installs on quit everywhere except Linux AppImage", () => { + expect(shouldAutoInstallOnQuit({ platform: "linux", isAppImage: true })).toBe(false); + expect(shouldAutoInstallOnQuit({ platform: "linux", isAppImage: false })).toBe(true); + expect(shouldAutoInstallOnQuit({ platform: "darwin", isAppImage: false })).toBe(true); + expect(shouldAutoInstallOnQuit({ platform: "win32", isAppImage: false })).toBe(true); + }); +}); + describe("shouldAdmitToRollout", () => { it("admits beta, missing rollout hours, zero-hour rollout, and missing release date", () => { expect( diff --git a/packages/desktop/src/features/auto-updater.ts b/packages/desktop/src/features/auto-updater.ts index 92ad1b213..876114585 100644 --- a/packages/desktop/src/features/auto-updater.ts +++ b/packages/desktop/src/features/auto-updater.ts @@ -76,13 +76,31 @@ export function getStagingUserId(): Promise { 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: { + platform: NodeJS.Platform; + isAppImage: boolean; +}): boolean { + return !(input.platform === "linux" && input.isAppImage); +} + class ElectronAppUpdateRuntime implements AppUpdateRuntime { private configured = false; configure(input: AppUpdateRuntimeConfiguration): void { autoUpdater.autoDownload = true; - autoUpdater.autoInstallOnAppQuit = true; autoUpdater.autoRunAppAfterInstall = true; + autoUpdater.autoInstallOnAppQuit = shouldAutoInstallOnQuit({ + platform: process.platform, + isAppImage: Boolean(process.env.APPIMAGE), + }); autoUpdater.allowPrerelease = input.releaseChannel === "beta"; autoUpdater.channel = input.releaseChannel === "beta" ? "beta" : "latest"; autoUpdater.allowDowngrade = false;