From 242ed3a93414d61584fee4ad8449cb392a11ccec Mon Sep 17 00:00:00 2001 From: Hampus Date: Mon, 21 Sep 2026 20:03:33 +0200 Subject: [PATCH] fix(desktop): drop orphaned Squirrel uninstall entry (#2885) --- fluxer_desktop/src/common/DesktopIdentity.ts | 3 +- .../src/main/WindowsLegacyUninstallEntry.ts | 87 +++++++++++++++++++ fluxer_desktop/src/main/index.ts | 6 ++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 fluxer_desktop/src/main/WindowsLegacyUninstallEntry.ts diff --git a/fluxer_desktop/src/common/DesktopIdentity.ts b/fluxer_desktop/src/common/DesktopIdentity.ts index 0ededef77..d3b5694ca 100644 --- a/fluxer_desktop/src/common/DesktopIdentity.ts +++ b/fluxer_desktop/src/common/DesktopIdentity.ts @@ -6,7 +6,8 @@ export const DESKTOP_APP_NAME = BUILD_CHANNEL === 'canary' ? 'Fluxer Canary' : ' export const MACOS_BUNDLE_ID = BUILD_CHANNEL === 'canary' ? 'app.fluxer.canary' : 'app.fluxer'; export const LINUX_DESKTOP_ENTRY_ID = BUILD_CHANNEL === 'canary' ? 'fluxer-canary' : 'fluxer'; export const WINDOWS_SHORTCUT_AUTHOR = 'Fluxer Platform AB'; -const WINDOWS_VELOPACK_ID = BUILD_CHANNEL === 'canary' ? 'fluxer_desktop_canary' : 'fluxer_desktop'; +export const WINDOWS_VELOPACK_ID = BUILD_CHANNEL === 'canary' ? 'fluxer_desktop_canary' : 'fluxer_desktop'; +export const WINDOWS_LEGACY_SQUIRREL_ID = 'fluxer_app'; export const WINDOWS_APP_USER_MODEL_ID = BUILD_CHANNEL === 'canary' ? 'Fluxer.Fluxer.Canary' : 'Fluxer.Fluxer'; export const WINDOWS_LEGACY_APP_USER_MODEL_IDS = [`velopack.${WINDOWS_VELOPACK_ID}`]; export const WINDOWS_TOAST_ACTIVATOR_CLSID = diff --git a/fluxer_desktop/src/main/WindowsLegacyUninstallEntry.ts b/fluxer_desktop/src/main/WindowsLegacyUninstallEntry.ts new file mode 100644 index 000000000..4498e350d --- /dev/null +++ b/fluxer_desktop/src/main/WindowsLegacyUninstallEntry.ts @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {execFile} from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import {WINDOWS_LEGACY_SQUIRREL_ID, WINDOWS_VELOPACK_ID} from '@electron/common/DesktopIdentity'; +import log from 'electron-log'; + +const UNINSTALL_REGISTRY_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall'; + +interface RegResult { + status: number; + stdout: string; +} + +function runReg(args: Array): Promise { + return new Promise((resolve, reject) => { + execFile('reg.exe', args, {encoding: 'utf8', windowsHide: true}, (error, stdout) => { + if (!error) { + resolve({status: 0, stdout}); + return; + } + if (typeof error.code === 'number') { + resolve({status: error.code, stdout}); + return; + } + reject(error); + }); + }); +} + +function normalizeWindowsPath(target: string): string { + return path.resolve(target).replace(/\\+$/, '').toLowerCase(); +} + +function isRunningFromMigratedSquirrelRoot(): boolean { + const localAppData = process.env.LOCALAPPDATA; + if (!localAppData) return false; + const currentDir = path.dirname(process.execPath); + if (path.basename(currentDir).toLowerCase() !== 'current') return false; + const rootAppDir = path.dirname(currentDir); + if (normalizeWindowsPath(rootAppDir) !== normalizeWindowsPath(path.join(localAppData, WINDOWS_LEGACY_SQUIRREL_ID))) { + return false; + } + return fs.existsSync(path.join(rootAppDir, 'Update.exe')); +} + +function uninstallKey(appId: string): string { + return `${UNINSTALL_REGISTRY_KEY}\\${appId}`; +} + +function parseInstallLocation(stdout: string): string | null { + const match = stdout.match(/^\s*InstallLocation\s+REG_(?:EXPAND_)?SZ\s+(.*?)\s*$/im); + return match ? match[1] : null; +} + +async function velopackEntryDescribesLegacyRoot(): Promise { + const {status, stdout} = await runReg(['query', uninstallKey(WINDOWS_VELOPACK_ID), '/v', 'InstallLocation']); + if (status !== 0) return false; + const installLocation = parseInstallLocation(stdout); + if (!installLocation) return false; + return path.basename(installLocation.replace(/\\+$/, '')).toLowerCase() === WINDOWS_LEGACY_SQUIRREL_ID; +} + +async function removeLegacySquirrelUninstallEntryAsync(): Promise { + const legacyKey = uninstallKey(WINDOWS_LEGACY_SQUIRREL_ID); + const legacyEntry = await runReg(['query', legacyKey]); + if (legacyEntry.status !== 0) return; + if (!(await velopackEntryDescribesLegacyRoot())) { + log.info('[LegacyUninstallEntry] Keeping the Squirrel uninstall entry, no Velopack entry describes this install'); + return; + } + const removal = await runReg(['delete', legacyKey, '/f']); + if (removal.status !== 0) { + log.warn('[LegacyUninstallEntry] Failed to remove the Squirrel uninstall entry', {status: removal.status}); + return; + } + log.info('[LegacyUninstallEntry] Removed the Squirrel uninstall entry', {key: legacyKey}); +} + +export function removeLegacySquirrelUninstallEntry(): void { + if (process.platform !== 'win32') return; + if (!isRunningFromMigratedSquirrelRoot()) return; + removeLegacySquirrelUninstallEntryAsync().catch((error: unknown) => { + log.warn('[LegacyUninstallEntry] Failed to clean up the Squirrel uninstall entry', error); + }); +} diff --git a/fluxer_desktop/src/main/index.ts b/fluxer_desktop/src/main/index.ts index 2d9831a4e..5c536cbee 100644 --- a/fluxer_desktop/src/main/index.ts +++ b/fluxer_desktop/src/main/index.ts @@ -75,6 +75,7 @@ import { setQuitting, showWindow, } from '@electron/main/Window'; +import {removeLegacySquirrelUninstallEntry} from '@electron/main/WindowsLegacyUninstallEntry'; import {removeFluxerVulkanLayerRegistrations} from '@electron/main/WindowsVulkanLayerCleanup'; import {app, dialog, netLog} from 'electron'; import log from 'electron-log'; @@ -361,6 +362,11 @@ if (launchConfigurationError) { } catch (error: unknown) { log.error('[Init] Failed to remove stale Vulkan layer registrations:', error); } + try { + runStartupPhase('legacy-uninstall-entry-cleanup', removeLegacySquirrelUninstallEntry); + } catch (error: unknown) { + log.error('[Init] Failed to remove the legacy Squirrel uninstall entry:', error); + } try { runStartupPhase('native-screen-capture-handlers', registerNativeScreenCaptureHandlers); } catch (error: unknown) {