From e403fb6cce65241456ab9884f8a8efb6fec52d8a Mon Sep 17 00:00:00 2001 From: Hampus Date: Wed, 14 Oct 2026 16:30:34 +0200 Subject: [PATCH] feat(desktop): add FLUXER_DISABLE_MODULE_UPDATES opt-out (#3466) --- fluxer_desktop/electron-builder.config.cjs | 13 ++- fluxer_desktop/scripts/build.mjs | 35 ++++-- .../src/common/BuilderShellVersion.test.mjs | 68 +++++++++++ fluxer_desktop/src/main/Bootstrap.ts | 106 ++++++++++-------- .../src/main/BootstrapEntry.test.mjs | 23 ++++ fluxer_desktop/src/main/DesktopTray.ts | 17 +-- fluxer_desktop/src/main/LaunchOptions.ts | 5 + fluxer_desktop/src/main/Menu.ts | 5 +- .../src/main/ModuleUpdater.test.mjs | 70 ++++++++++++ fluxer_desktop/src/main/ModuleUpdater.ts | 33 ++++-- fluxer_desktop/src/main/index.ts | 4 +- 11 files changed, 302 insertions(+), 77 deletions(-) create mode 100644 fluxer_desktop/src/common/BuilderShellVersion.test.mjs diff --git a/fluxer_desktop/electron-builder.config.cjs b/fluxer_desktop/electron-builder.config.cjs index d5ca80c6e..7a598126d 100644 --- a/fluxer_desktop/electron-builder.config.cjs +++ b/fluxer_desktop/electron-builder.config.cjs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later const {execFile} = require('node:child_process'); +const {readFileSync} = require('node:fs'); const fs = require('node:fs/promises'); const os = require('node:os'); const path = require('node:path'); @@ -102,6 +103,7 @@ const isMacBuild = process.argv.includes('--mac'); const isWindowsBuild = process.argv.includes('--win'); const targetPlatform = isLinuxBuild ? 'linux' : isMacBuild ? 'darwin' : isWindowsBuild ? 'win32' : process.platform; const metadataName = isLinuxBuild ? linuxPackageName : packageName; +const shellVersion = process.env.VERSION || readBundledRendererVersion(); const supportedTargetArchs = ['x64', 'arm64']; const supportedMacTargetArchs = [...supportedTargetArchs, 'universal']; const electronArch = process.env.ELECTRON_ARCH; @@ -644,6 +646,15 @@ function packagePathParts(packageName) { return [packageName]; } +function readBundledRendererVersion() { + try { + const {version} = JSON.parse(readFileSync(path.join(__dirname, 'dist', 'renderer', 'version.json'), 'utf8')); + return typeof version === 'string' && /^\d+\.\d+\.\d+$/u.test(version) ? version : null; + } catch { + return null; + } +} + function isNonEmptyString(value) { return typeof value === 'string' && value.length > 0; } @@ -1623,7 +1634,7 @@ module.exports = { extraMetadata: { main: 'dist/main/index.js', name: metadataName, - ...(process.env.VERSION ? {version: process.env.VERSION} : {}), + ...(shellVersion ? {version: shellVersion} : {}), ...(targetPlatform === 'linux' ? {desktopName: `${linuxDesktopId}.desktop`} : {}), }, extraResources: [ diff --git a/fluxer_desktop/scripts/build.mjs b/fluxer_desktop/scripts/build.mjs index a6f5b246b..43ac1d9ae 100644 --- a/fluxer_desktop/scripts/build.mjs +++ b/fluxer_desktop/scripts/build.mjs @@ -39,18 +39,20 @@ const isProduction = process.env.FLUXER_DESKTOP_PRODUCTION === 'true' || process.env.GITHUB_ACTIONS === 'true'; const skipNative = process.env.FLUXER_SKIP_NATIVE === 'true'; -const embeddedBuildVersion = process.env.PUBLIC_BUILD_VERSION || process.env.BUILD_VERSION || ''; +let embeddedBuildVersion = process.env.PUBLIC_BUILD_VERSION || process.env.BUILD_VERSION || ''; const embeddedReleaseChannel = process.env.PUBLIC_RELEASE_CHANNEL || process.env.RELEASE_CHANNEL || ''; const modulesEnabled = process.env.FLUXER_MODULES === '1' || process.env.FLUXER_MODULES === 'true'; const offlineBuild = process.env.FLUXER_OFFLINE === '1' || process.env.FLUXER_OFFLINE === 'true'; -const publicBuildDefines = { - 'process.env.PUBLIC_BUILD_VERSION': JSON.stringify(embeddedBuildVersion), - 'process.env.BUILD_VERSION': JSON.stringify(embeddedBuildVersion), - 'process.env.PUBLIC_RELEASE_CHANNEL': JSON.stringify(embeddedReleaseChannel), - 'process.env.RELEASE_CHANNEL': JSON.stringify(embeddedReleaseChannel), - 'process.env.FLUXER_MODULES': JSON.stringify(modulesEnabled ? '1' : ''), - 'process.env.FLUXER_OFFLINE': JSON.stringify(offlineBuild ? '1' : ''), -}; +function publicBuildDefines() { + return { + 'process.env.PUBLIC_BUILD_VERSION': JSON.stringify(embeddedBuildVersion), + 'process.env.BUILD_VERSION': JSON.stringify(embeddedBuildVersion), + 'process.env.PUBLIC_RELEASE_CHANNEL': JSON.stringify(embeddedReleaseChannel), + 'process.env.RELEASE_CHANNEL': JSON.stringify(embeddedReleaseChannel), + 'process.env.FLUXER_MODULES': JSON.stringify(modulesEnabled ? '1' : ''), + 'process.env.FLUXER_OFFLINE': JSON.stringify(offlineBuild ? '1' : ''), + }; +} const electronExternals = [ 'electron', 'electron-log', @@ -116,6 +118,16 @@ function readBundledRendererVersion() { } } +function fallBackToBundledRendererVersion() { + if (embeddedBuildVersion !== '') { + return; + } + const bundledVersion = readBundledRendererVersion(); + if (BUNDLED_RENDERER_VERSION_PATTERN.test(bundledVersion)) { + embeddedBuildVersion = bundledVersion; + } +} + class RendererServiceWorkerPresentError extends Error { constructor(present) { super( @@ -674,7 +686,7 @@ async function buildMain() { define: { 'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development'), [MAIN_APP_OUTPUT_FILE_DEFINE]: JSON.stringify(MAIN_APP_OUTPUT_FILE_NAME), - ...publicBuildDefines, + ...publicBuildDefines(), }, banner: { js: `import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);`, @@ -806,7 +818,7 @@ function preloadBuildOptions(entryFileName, outputFileName) { tsconfig: DESKTOP_TSCONFIG, define: { 'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development'), - ...publicBuildDefines, + ...publicBuildDefines(), }, }; } @@ -887,6 +899,7 @@ async function build() { if (!options.useSharedRenderer) { copyRendererIntoDist(); } + fallBackToBundledRendererVersion(); if (modulesEnabled) { prunePackedRendererModules(); } diff --git a/fluxer_desktop/src/common/BuilderShellVersion.test.mjs b/fluxer_desktop/src/common/BuilderShellVersion.test.mjs new file mode 100644 index 000000000..fcf813d13 --- /dev/null +++ b/fluxer_desktop/src/common/BuilderShellVersion.test.mjs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import {createRequire} from 'node:module'; +import os from 'node:os'; +import path from 'node:path'; +import {after, describe, test} from 'node:test'; + +const require = createRequire(import.meta.url); +const DESKTOP_DIR = path.resolve(import.meta.dirname, '..', '..'); +const PREFLIGHT_MODULES = path.join('src', 'main', 'NativeModulePreflightModules.json'); +const temporaryRoots = []; + +after(() => { + for (const root of temporaryRoots) { + fs.rmSync(root, {recursive: true, force: true}); + } +}); + +function loadMetadataVersion({version, rendererVersionJson}) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'builder-shell-version-')); + temporaryRoots.push(root); + fs.copyFileSync( + path.join(DESKTOP_DIR, 'electron-builder.config.cjs'), + path.join(root, 'electron-builder.config.cjs'), + ); + fs.mkdirSync(path.join(root, 'src', 'main'), {recursive: true}); + fs.copyFileSync(path.join(DESKTOP_DIR, PREFLIGHT_MODULES), path.join(root, PREFLIGHT_MODULES)); + if (rendererVersionJson != null) { + fs.mkdirSync(path.join(root, 'dist', 'renderer'), {recursive: true}); + fs.writeFileSync(path.join(root, 'dist', 'renderer', 'version.json'), rendererVersionJson); + } + const previous = process.env.VERSION; + if (version == null) { + delete process.env.VERSION; + } else { + process.env.VERSION = version; + } + try { + return require(path.join(root, 'electron-builder.config.cjs')).extraMetadata.version; + } finally { + if (previous === undefined) { + delete process.env.VERSION; + } else { + process.env.VERSION = previous; + } + } +} + +describe('the packaged shell version', () => { + test('comes from VERSION whenever it is set', () => { + assert.equal( + loadMetadataVersion({version: '2026.1014.2', rendererVersionJson: '{"version":"2026.1014.9"}'}), + '2026.1014.2', + ); + }); + + test('falls back to the bundled renderer version when VERSION is unset', () => { + assert.equal(loadMetadataVersion({rendererVersionJson: '{"version":"2026.1014.9"}'}), '2026.1014.9'); + }); + + test('keeps the package.json version when the renderer has no numeric version', () => { + assert.equal(loadMetadataVersion({rendererVersionJson: '{"version":"dev"}'}), undefined); + assert.equal(loadMetadataVersion({rendererVersionJson: 'null'}), undefined); + assert.equal(loadMetadataVersion({}), undefined); + }); +}); diff --git a/fluxer_desktop/src/main/Bootstrap.ts b/fluxer_desktop/src/main/Bootstrap.ts index cc5d3a480..9db6868a7 100644 --- a/fluxer_desktop/src/main/Bootstrap.ts +++ b/fluxer_desktop/src/main/Bootstrap.ts @@ -159,7 +159,7 @@ async function runModuleBootstrap(): Promise { const {configureUserDataPath} = await import('@electron/common/UserDataPath'); const {armNativeProbeCache} = await import('@electron/main/NativeProbeCache'); const {isStartMinimizedLaunch} = await import('@electron/main/AutostartLaunch'); - const {isDesktopUpdateRequested} = await import('@electron/main/LaunchOptions'); + const {areModuleUpdatesDisabled, isDesktopUpdateRequested} = await import('@electron/main/LaunchOptions'); const {relaunchStableLaunchPath} = await import('@electron/main/LinuxLaunchPath'); const {getDesktopLocalAppProtocol} = await import('@electron/main/LocalAppProtocol'); const { @@ -410,6 +410,10 @@ async function runModuleBootstrap(): Promise { logger.warn('Failed to record whether an update is waiting', error); } }; + const moduleUpdatesDisabled = areModuleUpdatesDisabled(); + if (moduleUpdatesDisabled) { + logger.info('Module updates are disabled by FLUXER_DISABLE_MODULE_UPDATES'); + } const updater = new ModuleUpdater({ store, shellVersion: app.getVersion(), @@ -417,6 +421,7 @@ async function runModuleBootstrap(): Promise { bundledRendererVersion: BUNDLED_RENDERER_VERSION, preferUnversionedBundle: app.isPackaged, forceStartupUpdate: store.shellVersionChanged || updateWasPending || isDesktopUpdateRequested(), + moduleUpdatesDisabled, selfUpdateShellFirst: shellUpdatePlan.capability === ShellUpdateCapability.SELF_UPDATE ? async (latestVersion) => { @@ -705,35 +710,37 @@ async function runModuleBootstrap(): Promise { }, }; - const desktopUpdate = new DesktopUpdateRun({ - probe: () => updater.checkForUpdate(), - canSelfUpdateShell: shellUpdatePlan.capability === ShellUpdateCapability.SELF_UPDATE, - runShellSelfUpdate: () => runShellSelfUpdateOnSplash(), - installModules: async () => { - const launchAttempt = await updater.installPending(); - if (launchAttempt != null) { - await refreshModuleRoots(launchAttempt.committed); - } - return launchAttempt; - }, - takeover: updateTakeover, - publish: (check) => { - recordPendingUpdate(check); - publishDesktopUpdateCheck(check); - }, - reportFailure: (failure) => { - void import('@electron/main/DesktopUpdatePrompt') - .then(({reportDesktopUpdateFailure}) => reportDesktopUpdateFailure(failure)) - .catch((error: unknown) => { - logger.error('Failed to report the desktop update failure', error); - }); - }, - openDownloadsPage: async () => { - const {DOWNLOAD_PAGE_URL} = await import('@electron/main/UpdaterDownloads'); - await shell.openExternal(DOWNLOAD_PAGE_URL); - }, - logger, - }); + const desktopUpdate = moduleUpdatesDisabled + ? null + : new DesktopUpdateRun({ + probe: () => updater.checkForUpdate(), + canSelfUpdateShell: shellUpdatePlan.capability === ShellUpdateCapability.SELF_UPDATE, + runShellSelfUpdate: () => runShellSelfUpdateOnSplash(), + installModules: async () => { + const launchAttempt = await updater.installPending(); + if (launchAttempt != null) { + await refreshModuleRoots(launchAttempt.committed); + } + return launchAttempt; + }, + takeover: updateTakeover, + publish: (check) => { + recordPendingUpdate(check); + publishDesktopUpdateCheck(check); + }, + reportFailure: (failure) => { + void import('@electron/main/DesktopUpdatePrompt') + .then(({reportDesktopUpdateFailure}) => reportDesktopUpdateFailure(failure)) + .catch((error: unknown) => { + logger.error('Failed to report the desktop update failure', error); + }); + }, + openDownloadsPage: async () => { + const {DOWNLOAD_PAGE_URL} = await import('@electron/main/UpdaterDownloads'); + await shell.openExternal(DOWNLOAD_PAGE_URL); + }, + logger, + }); const permit = await runModuleUpdateLoop(); localNetworkHint.disarm(); @@ -741,25 +748,30 @@ async function runModuleBootstrap(): Promise { recordPendingUpdate({shellNewer: false, modulesChanged: false}); } await refreshModuleRoots(permit.launchAttempt.committed); - setOnDemandModuleInstaller( - createOnDemandModuleInstaller({ - ensure: (moduleName) => updater.ensureModule(moduleName), - refresh: serveOnDemandModules, - onFailure: (moduleName, error) => { - logger.error('Failed to install a module on demand', {module: moduleName, error}); - }, - }), - ); markSplashLaunching(); - armDesktopUpdate({ - check: async (manual) => { - const check = await desktopUpdate.check({manual}); - recordPendingUpdate(check); - return check; - }, - start: () => desktopUpdate.start(), - }); + if (desktopUpdate != null) { + setOnDemandModuleInstaller( + createOnDemandModuleInstaller({ + ensure: (moduleName) => updater.ensureModule(moduleName), + refresh: serveOnDemandModules, + onFailure: (moduleName, error) => { + logger.error('Failed to install a module on demand', {module: moduleName, error}); + }, + }), + ); + armDesktopUpdate({ + check: async (manual) => { + const check = await desktopUpdate.check({manual}); + recordPendingUpdate(check); + return check; + }, + start: () => desktopUpdate.start(), + }); + } armMainWindowHandoff(updater, permit.launchAttempt, () => { + if (desktopUpdate == null) { + return; + } desktopUpdate.markLaunchSettled(); preloadUpdateSplash(); armDesktopUpdatePoll( diff --git a/fluxer_desktop/src/main/BootstrapEntry.test.mjs b/fluxer_desktop/src/main/BootstrapEntry.test.mjs index 93d121401..8f3d3c0f7 100644 --- a/fluxer_desktop/src/main/BootstrapEntry.test.mjs +++ b/fluxer_desktop/src/main/BootstrapEntry.test.mjs @@ -275,3 +275,26 @@ describe('Bootstrap entry point', () => { ); }); }); + +describe('FLUXER_DISABLE_MODULE_UPDATES', () => { + test('reads the environment variable like the other FLUXER_DISABLE switches', async () => { + const {areModuleUpdatesDisabled} = await import('./LaunchOptions.ts'); + const previous = process.env.FLUXER_DISABLE_MODULE_UPDATES; + try { + delete process.env.FLUXER_DISABLE_MODULE_UPDATES; + assert.equal(areModuleUpdatesDisabled(), false); + for (const value of ['1', 'true', 'YES', 'on']) { + process.env.FLUXER_DISABLE_MODULE_UPDATES = value; + assert.equal(areModuleUpdatesDisabled(), true); + } + process.env.FLUXER_DISABLE_MODULE_UPDATES = '0'; + assert.equal(areModuleUpdatesDisabled(), false); + } finally { + if (previous === undefined) { + delete process.env.FLUXER_DISABLE_MODULE_UPDATES; + } else { + process.env.FLUXER_DISABLE_MODULE_UPDATES = previous; + } + } + }); +}); diff --git a/fluxer_desktop/src/main/DesktopTray.ts b/fluxer_desktop/src/main/DesktopTray.ts index 8a4af75d8..2ff0ac686 100644 --- a/fluxer_desktop/src/main/DesktopTray.ts +++ b/fluxer_desktop/src/main/DesktopTray.ts @@ -13,6 +13,7 @@ import type { } from '@electron/common/Types'; import {getDesktopUpdateState, observeDesktopUpdateState} from '@electron/main/DesktopUpdateGate'; import {checkForUpdatesFromShell, startDesktopUpdateFromShell} from '@electron/main/DesktopUpdatePrompt'; +import {areModuleUpdatesDisabled} from '@electron/main/LaunchOptions'; import {relaunchStableLaunchPath} from '@electron/main/LinuxLaunchPath'; import {onLocaleChange, t} from '@electron/main/MainI18n'; import {app, type BrowserWindow, clipboard, Menu, nativeImage, Tray} from 'electron'; @@ -399,13 +400,15 @@ function buildTrayMenu(): Menu { click: () => runTrayMenuAction(startDesktopUpdateFromShell), }); } - menuTemplate.push({ - label: t('desktop.tray.checkForUpdates'), - click: () => - runTrayMenuAction(() => { - void checkForUpdatesFromShell(); - }), - }); + if (!areModuleUpdatesDisabled()) { + menuTemplate.push({ + label: t('desktop.tray.checkForUpdates'), + click: () => + runTrayMenuAction(() => { + void checkForUpdatesFromShell(); + }), + }); + } if (trayState.buildInfo) { menuTemplate.push({ label: t('desktop.tray.copyBuildInfo'), diff --git a/fluxer_desktop/src/main/LaunchOptions.ts b/fluxer_desktop/src/main/LaunchOptions.ts index e8db79566..ff2508bb5 100644 --- a/fluxer_desktop/src/main/LaunchOptions.ts +++ b/fluxer_desktop/src/main/LaunchOptions.ts @@ -159,9 +159,14 @@ export function describeLaunchDiagnosticOptions(argv: ReadonlyArray = pr spellcheck: getSpellcheckLaunchMode(argv), v8CodeCacheDisabled: shouldDisableV8CodeCache(argv), nativeGatewayDisableReason: getNativeGatewayDisableReason(argv), + moduleUpdatesDisabled: areModuleUpdatesDisabled(), }; } +export function areModuleUpdatesDisabled(): boolean { + return truthyEnv('FLUXER_DISABLE_MODULE_UPDATES'); +} + export function isDesktopUpdateRequested(argv: ReadonlyArray = process.argv): boolean { return hasFlag(argv, DESKTOP_UPDATE_ARGS); } diff --git a/fluxer_desktop/src/main/Menu.ts b/fluxer_desktop/src/main/Menu.ts index 8f03307f6..0b9e3d4f1 100644 --- a/fluxer_desktop/src/main/Menu.ts +++ b/fluxer_desktop/src/main/Menu.ts @@ -2,6 +2,7 @@ import {DESKTOP_APP_NAME} from '@electron/common/DesktopIdentity'; import {checkForUpdatesFromShell} from '@electron/main/DesktopUpdatePrompt'; +import {areModuleUpdatesDisabled} from '@electron/main/LaunchOptions'; import {onLocaleChange, t} from '@electron/main/MainI18n'; import {openExternalDeduped} from '@electron/main/OpenExternal'; import {buildTroubleshootingMenuItems} from '@electron/main/Troubleshooting'; @@ -31,7 +32,7 @@ function buildTemplate(): Array { role: 'about', label: t('desktop.appMenu.about', {appName}), }, - checkForUpdatesItem(), + ...(areModuleUpdatesDisabled() ? [] : [checkForUpdatesItem()]), {type: 'separator'}, { label: t('desktop.appMenu.preferences'), @@ -209,7 +210,7 @@ function buildTemplate(): Array { ? `${t('desktop.appMenu.help')}${MACOS_HELP_MENU_TITLE_AUTODETECT_OPT_OUT}` : t('desktop.appMenu.help'), submenu: [ - ...(isMac ? [] : [checkForUpdatesItem(), {type: 'separator' as const}]), + ...(isMac || areModuleUpdatesDisabled() ? [] : [checkForUpdatesItem(), {type: 'separator' as const}]), { label: t('desktop.appMenu.website'), click: async () => { diff --git a/fluxer_desktop/src/main/ModuleUpdater.test.mjs b/fluxer_desktop/src/main/ModuleUpdater.test.mjs index b17b7a5f4..8ca276043 100644 --- a/fluxer_desktop/src/main/ModuleUpdater.test.mjs +++ b/fluxer_desktop/src/main/ModuleUpdater.test.mjs @@ -127,6 +127,7 @@ function createUpdater(store, modules, options = {}) { hasOfflineRenderer: options.bundledRendererVersion != null, bundledRendererVersion: options.bundledRendererVersion ?? null, forceStartupUpdate: options.forceStartupUpdate ?? false, + moduleUpdatesDisabled: options.moduleUpdatesDisabled ?? false, selfUpdateShellFirst: options.selfUpdateShellFirst, onState: options.onState, fetch: async (url, init) => { @@ -506,6 +507,75 @@ describe('ModuleUpdater.run', () => { }); }); +describe('ModuleUpdater with module updates disabled', () => { + test('launches the verified set without fetching even when the manifest demands a shell update', async () => { + const userDataPath = createUserData(); + const store = await openStore(userDataPath); + installModuleOnDisk(getModuleStoreRoot(userDataPath), 'fluxer_renderer', RENDERER_SHA, [['index.js', 'renderer']]); + await store.commit({fluxer_renderer: RENDERER_SHA}); + assert.equal((await createUpdater(store, {fluxer_renderer: RENDERER_SHA}).updater.run()).status, 'launching'); + const statuses = []; + let shellUpdates = 0; + const {updater, requests, sleeps} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA}, + { + shellLatest: '2026.824.0', + shellMinimum: '2026.824.0', + forceStartupUpdate: true, + moduleUpdatesDisabled: true, + onState: (state) => statuses.push(state.status), + selfUpdateShellFirst: async () => { + shellUpdates += 1; + return {reason: 'no-update'}; + }, + }, + ); + + const outcome = await updater.run(); + + assert.equal(outcome.status, 'unreachable-launch'); + assert.deepEqual(outcome.committed, {fluxer_renderer: RENDERER_SHA}); + assert.deepEqual(requests, []); + assert.deepEqual(sleeps, []); + assert.equal(shellUpdates, 0); + assert.deepEqual(statuses, ['unreachable-launch']); + assert.equal(outcome.launchAttempt.committed.fluxer_renderer, RENDERER_SHA); + }); + + test('launches the bundled renderer on a fresh store without fetching', async () => { + const store = await openStore(createUserData()); + const {updater, requests} = createUpdater( + store, + {fluxer_renderer: NEXT_RENDERER_SHA}, + {bundledRendererVersion: SHELL_VERSION, moduleUpdatesDisabled: true}, + ); + + const outcome = await updater.run(); + + assert.equal(outcome.status, 'unreachable-launch'); + assert.deepEqual(outcome.committed, {}); + assert.deepEqual(requests, []); + }); + + test('blocks at once on the unreachable splash without fetching when nothing is installed', async () => { + const store = await openStore(createUserData()); + const {updater, requests, sleeps} = createUpdater( + store, + {fluxer_renderer: RENDERER_SHA}, + {moduleUpdatesDisabled: true}, + ); + + const outcome = await updater.run(); + + assert.equal(outcome.status, 'blocked-update-required'); + assert.equal(outcome.reason, 'nothing-installed'); + assert.equal(outcome.updateServerUnreachable, true); + assert.deepEqual(requests, []); + assert.deepEqual(sleeps, []); + }); +}); + describe('ModuleUpdater checks without downloading and installs only when asked', () => { async function storeOnRenderer() { const userDataPath = createUserData(); diff --git a/fluxer_desktop/src/main/ModuleUpdater.ts b/fluxer_desktop/src/main/ModuleUpdater.ts index 40b614da5..3b2b2a1ed 100644 --- a/fluxer_desktop/src/main/ModuleUpdater.ts +++ b/fluxer_desktop/src/main/ModuleUpdater.ts @@ -267,6 +267,7 @@ interface ModuleUpdaterOptions { readonly bundledRendererVersion?: string | null; readonly preferUnversionedBundle?: boolean; readonly forceStartupUpdate?: boolean; + readonly moduleUpdatesDisabled?: boolean; readonly selfUpdateShellFirst?: (latestVersion: string) => Promise<{readonly reason: string}>; readonly onState?: (state: ModuleUpdaterSplashState) => void; readonly report?: (report: ModuleUpdaterReport) => void; @@ -286,6 +287,7 @@ export class ModuleUpdater { private readonly shellVersion: ModuleVersion; private readonly hasOfflineRenderer: boolean; private readonly forceStartupUpdate: boolean; + private readonly moduleUpdatesDisabled: boolean; private readonly selfUpdateShellFirst: ((latestVersion: string) => Promise<{readonly reason: string}>) | null; private readonly shellFirstAttempts = new Map(); private readonly manifestFeed: ModuleManifestFeedIdentity; @@ -314,6 +316,7 @@ export class ModuleUpdater { this.shellVersion = parseModuleVersion(options.shellVersion, 'desktop shell version'); this.hasOfflineRenderer = options.hasOfflineRenderer ?? false; this.forceStartupUpdate = options.forceStartupUpdate ?? false; + this.moduleUpdatesDisabled = options.moduleUpdatesDisabled ?? false; this.selfUpdateShellFirst = options.selfUpdateShellFirst ?? null; const releaseChannel = options.releaseChannel ?? BUILD_CHANNEL; this.platform = options.platform ?? process.platform; @@ -418,6 +421,18 @@ export class ModuleUpdater { return reverted; } } + if (this.moduleUpdatesDisabled) { + const decision = await this.planner.evaluateUnreachableLaunch(this.startupUpdateEnforced); + if (decision.kind === ModuleUnreachableLaunchDecision.LAUNCH) { + return await this.launchUnreachable(); + } + return this.blockUpdateRequired( + decision.reason, + [], + 'module updates are disabled by FLUXER_DISABLE_MODULE_UPDATES', + {updateServerUnreachable: true}, + ); + } if (this.platform === 'linux' && !(await this.startupUpdateRequired())) { return await this.launchIfAtFloor(); } @@ -486,13 +501,7 @@ export class ModuleUpdater { if (exhausted || !fetched) { const decision = await this.planner.evaluateUnreachableLaunch(this.startupUpdateEnforced); if (decision.kind === ModuleUnreachableLaunchDecision.LAUNCH) { - const launchAttempt = await this.store.recordLaunchAttempt(); - this.emit(ModuleUpdaterStatus.UNREACHABLE_LAUNCH); - return { - status: ModuleUpdaterStatus.UNREACHABLE_LAUNCH, - committed: this.store.getCommitted(), - launchAttempt, - }; + return await this.launchUnreachable(); } if (exhausted) { return this.blockUpdateRequired(decision.reason, [], describeErrorChain(error), { @@ -802,6 +811,16 @@ export class ModuleUpdater { }; } + private async launchUnreachable(): Promise { + const launchAttempt = await this.store.recordLaunchAttempt(); + this.emit(ModuleUpdaterStatus.UNREACHABLE_LAUNCH); + return { + status: ModuleUpdaterStatus.UNREACHABLE_LAUNCH, + committed: this.store.getCommitted(), + launchAttempt, + }; + } + private async launchRevertedSetBypassingFloor( committed: Readonly>, ): Promise { diff --git a/fluxer_desktop/src/main/index.ts b/fluxer_desktop/src/main/index.ts index 86b6067df..db15d3423 100644 --- a/fluxer_desktop/src/main/index.ts +++ b/fluxer_desktop/src/main/index.ts @@ -73,7 +73,7 @@ import { registerIpcHandlers, } from '@electron/main/IpcHandlers'; import {initializeJumpList} from '@electron/main/JumpList'; -import {describeLaunchDiagnosticOptions} from '@electron/main/LaunchOptions'; +import {areModuleUpdatesDisabled, describeLaunchDiagnosticOptions} from '@electron/main/LaunchOptions'; import {cleanupLegacyHarvestHandlers, registerLegacyHarvestHandlers} from '@electron/main/LegacyHarvestIpc'; import {initializeLegacyOriginHarvest} from '@electron/main/LegacyOriginHarvest'; import {cleanupVirtmic, registerVirtmicHandlers} from '@electron/main/LinuxAudioCapture'; @@ -600,7 +600,7 @@ if (launchConfigurationError) { showWindow, }); }); - if (process.env.FLUXER_OFFLINE !== '1') { + if (process.env.FLUXER_OFFLINE !== '1' && !areModuleUpdatesDisabled()) { const {registerUpdater} = await import('@electron/main/Updater'); registerUpdater(getMainWindow); }