feat(desktop): add FLUXER_DISABLE_MODULE_UPDATES opt-out (#3466)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-14 16:30:34 +02:00
1 parent e8dff6a8a1
commit e403fb6cce
11 files changed
+302 -77

No files matched your search

+12 -1
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
const {execFile} = require('node:child_process'); const {execFile} = require('node:child_process');
const {readFileSync} = require('node:fs');
const fs = require('node:fs/promises'); const fs = require('node:fs/promises');
const os = require('node:os'); const os = require('node:os');
const path = require('node:path'); const path = require('node:path');
@@ -102,6 +103,7 @@ const isMacBuild = process.argv.includes('--mac');
const isWindowsBuild = process.argv.includes('--win'); const isWindowsBuild = process.argv.includes('--win');
const targetPlatform = isLinuxBuild ? 'linux' : isMacBuild ? 'darwin' : isWindowsBuild ? 'win32' : process.platform; const targetPlatform = isLinuxBuild ? 'linux' : isMacBuild ? 'darwin' : isWindowsBuild ? 'win32' : process.platform;
const metadataName = isLinuxBuild ? linuxPackageName : packageName; const metadataName = isLinuxBuild ? linuxPackageName : packageName;
const shellVersion = process.env.VERSION || readBundledRendererVersion();
const supportedTargetArchs = ['x64', 'arm64']; const supportedTargetArchs = ['x64', 'arm64'];
const supportedMacTargetArchs = [...supportedTargetArchs, 'universal']; const supportedMacTargetArchs = [...supportedTargetArchs, 'universal'];
const electronArch = process.env.ELECTRON_ARCH; const electronArch = process.env.ELECTRON_ARCH;
@@ -644,6 +646,15 @@ function packagePathParts(packageName) {
return [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) { function isNonEmptyString(value) {
return typeof value === 'string' && value.length > 0; return typeof value === 'string' && value.length > 0;
} }
@@ -1623,7 +1634,7 @@ module.exports = {
extraMetadata: { extraMetadata: {
main: 'dist/main/index.js', main: 'dist/main/index.js',
name: metadataName, name: metadataName,
...(process.env.VERSION ? {version: process.env.VERSION} : {}), ...(shellVersion ? {version: shellVersion} : {}),
...(targetPlatform === 'linux' ? {desktopName: `${linuxDesktopId}.desktop`} : {}), ...(targetPlatform === 'linux' ? {desktopName: `${linuxDesktopId}.desktop`} : {}),
}, },
extraResources: [ extraResources: [
+24 -11
View File
@@ -39,18 +39,20 @@ const isProduction =
process.env.FLUXER_DESKTOP_PRODUCTION === 'true' || process.env.FLUXER_DESKTOP_PRODUCTION === 'true' ||
process.env.GITHUB_ACTIONS === 'true'; process.env.GITHUB_ACTIONS === 'true';
const skipNative = process.env.FLUXER_SKIP_NATIVE === '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 embeddedReleaseChannel = process.env.PUBLIC_RELEASE_CHANNEL || process.env.RELEASE_CHANNEL || '';
const modulesEnabled = process.env.FLUXER_MODULES === '1' || process.env.FLUXER_MODULES === 'true'; 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 offlineBuild = process.env.FLUXER_OFFLINE === '1' || process.env.FLUXER_OFFLINE === 'true';
const publicBuildDefines = { function publicBuildDefines() {
'process.env.PUBLIC_BUILD_VERSION': JSON.stringify(embeddedBuildVersion), return {
'process.env.BUILD_VERSION': JSON.stringify(embeddedBuildVersion), 'process.env.PUBLIC_BUILD_VERSION': JSON.stringify(embeddedBuildVersion),
'process.env.PUBLIC_RELEASE_CHANNEL': JSON.stringify(embeddedReleaseChannel), 'process.env.BUILD_VERSION': JSON.stringify(embeddedBuildVersion),
'process.env.RELEASE_CHANNEL': JSON.stringify(embeddedReleaseChannel), 'process.env.PUBLIC_RELEASE_CHANNEL': JSON.stringify(embeddedReleaseChannel),
'process.env.FLUXER_MODULES': JSON.stringify(modulesEnabled ? '1' : ''), 'process.env.RELEASE_CHANNEL': JSON.stringify(embeddedReleaseChannel),
'process.env.FLUXER_OFFLINE': JSON.stringify(offlineBuild ? '1' : ''), 'process.env.FLUXER_MODULES': JSON.stringify(modulesEnabled ? '1' : ''),
}; 'process.env.FLUXER_OFFLINE': JSON.stringify(offlineBuild ? '1' : ''),
};
}
const electronExternals = [ const electronExternals = [
'electron', 'electron',
'electron-log', '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 { class RendererServiceWorkerPresentError extends Error {
constructor(present) { constructor(present) {
super( super(
@@ -674,7 +686,7 @@ async function buildMain() {
define: { define: {
'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development'), 'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development'),
[MAIN_APP_OUTPUT_FILE_DEFINE]: JSON.stringify(MAIN_APP_OUTPUT_FILE_NAME), [MAIN_APP_OUTPUT_FILE_DEFINE]: JSON.stringify(MAIN_APP_OUTPUT_FILE_NAME),
...publicBuildDefines, ...publicBuildDefines(),
}, },
banner: { banner: {
js: `import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);`, 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, tsconfig: DESKTOP_TSCONFIG,
define: { define: {
'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development'), 'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development'),
...publicBuildDefines, ...publicBuildDefines(),
}, },
}; };
} }
@@ -887,6 +899,7 @@ async function build() {
if (!options.useSharedRenderer) { if (!options.useSharedRenderer) {
copyRendererIntoDist(); copyRendererIntoDist();
} }
fallBackToBundledRendererVersion();
if (modulesEnabled) { if (modulesEnabled) {
prunePackedRendererModules(); prunePackedRendererModules();
} }
@@ -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);
});
});
+59 -47
View File
@@ -159,7 +159,7 @@ async function runModuleBootstrap(): Promise<void> {
const {configureUserDataPath} = await import('@electron/common/UserDataPath'); const {configureUserDataPath} = await import('@electron/common/UserDataPath');
const {armNativeProbeCache} = await import('@electron/main/NativeProbeCache'); const {armNativeProbeCache} = await import('@electron/main/NativeProbeCache');
const {isStartMinimizedLaunch} = await import('@electron/main/AutostartLaunch'); 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 {relaunchStableLaunchPath} = await import('@electron/main/LinuxLaunchPath');
const {getDesktopLocalAppProtocol} = await import('@electron/main/LocalAppProtocol'); const {getDesktopLocalAppProtocol} = await import('@electron/main/LocalAppProtocol');
const { const {
@@ -410,6 +410,10 @@ async function runModuleBootstrap(): Promise<void> {
logger.warn('Failed to record whether an update is waiting', error); 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({ const updater = new ModuleUpdater({
store, store,
shellVersion: app.getVersion(), shellVersion: app.getVersion(),
@@ -417,6 +421,7 @@ async function runModuleBootstrap(): Promise<void> {
bundledRendererVersion: BUNDLED_RENDERER_VERSION, bundledRendererVersion: BUNDLED_RENDERER_VERSION,
preferUnversionedBundle: app.isPackaged, preferUnversionedBundle: app.isPackaged,
forceStartupUpdate: store.shellVersionChanged || updateWasPending || isDesktopUpdateRequested(), forceStartupUpdate: store.shellVersionChanged || updateWasPending || isDesktopUpdateRequested(),
moduleUpdatesDisabled,
selfUpdateShellFirst: selfUpdateShellFirst:
shellUpdatePlan.capability === ShellUpdateCapability.SELF_UPDATE shellUpdatePlan.capability === ShellUpdateCapability.SELF_UPDATE
? async (latestVersion) => { ? async (latestVersion) => {
@@ -705,35 +710,37 @@ async function runModuleBootstrap(): Promise<void> {
}, },
}; };
const desktopUpdate = new DesktopUpdateRun({ const desktopUpdate = moduleUpdatesDisabled
probe: () => updater.checkForUpdate(), ? null
canSelfUpdateShell: shellUpdatePlan.capability === ShellUpdateCapability.SELF_UPDATE, : new DesktopUpdateRun({
runShellSelfUpdate: () => runShellSelfUpdateOnSplash(), probe: () => updater.checkForUpdate(),
installModules: async () => { canSelfUpdateShell: shellUpdatePlan.capability === ShellUpdateCapability.SELF_UPDATE,
const launchAttempt = await updater.installPending(); runShellSelfUpdate: () => runShellSelfUpdateOnSplash(),
if (launchAttempt != null) { installModules: async () => {
await refreshModuleRoots(launchAttempt.committed); const launchAttempt = await updater.installPending();
} if (launchAttempt != null) {
return launchAttempt; await refreshModuleRoots(launchAttempt.committed);
}, }
takeover: updateTakeover, return launchAttempt;
publish: (check) => { },
recordPendingUpdate(check); takeover: updateTakeover,
publishDesktopUpdateCheck(check); publish: (check) => {
}, recordPendingUpdate(check);
reportFailure: (failure) => { publishDesktopUpdateCheck(check);
void import('@electron/main/DesktopUpdatePrompt') },
.then(({reportDesktopUpdateFailure}) => reportDesktopUpdateFailure(failure)) reportFailure: (failure) => {
.catch((error: unknown) => { void import('@electron/main/DesktopUpdatePrompt')
logger.error('Failed to report the desktop update failure', error); .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); openDownloadsPage: async () => {
}, const {DOWNLOAD_PAGE_URL} = await import('@electron/main/UpdaterDownloads');
logger, await shell.openExternal(DOWNLOAD_PAGE_URL);
}); },
logger,
});
const permit = await runModuleUpdateLoop(); const permit = await runModuleUpdateLoop();
localNetworkHint.disarm(); localNetworkHint.disarm();
@@ -741,25 +748,30 @@ async function runModuleBootstrap(): Promise<void> {
recordPendingUpdate({shellNewer: false, modulesChanged: false}); recordPendingUpdate({shellNewer: false, modulesChanged: false});
} }
await refreshModuleRoots(permit.launchAttempt.committed); 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(); markSplashLaunching();
armDesktopUpdate({ if (desktopUpdate != null) {
check: async (manual) => { setOnDemandModuleInstaller(
const check = await desktopUpdate.check({manual}); createOnDemandModuleInstaller({
recordPendingUpdate(check); ensure: (moduleName) => updater.ensureModule(moduleName),
return check; refresh: serveOnDemandModules,
}, onFailure: (moduleName, error) => {
start: () => desktopUpdate.start(), 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, () => { armMainWindowHandoff(updater, permit.launchAttempt, () => {
if (desktopUpdate == null) {
return;
}
desktopUpdate.markLaunchSettled(); desktopUpdate.markLaunchSettled();
preloadUpdateSplash(); preloadUpdateSplash();
armDesktopUpdatePoll( armDesktopUpdatePoll(
@@ -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;
}
}
});
});
+10 -7
View File
@@ -13,6 +13,7 @@ import type {
} from '@electron/common/Types'; } from '@electron/common/Types';
import {getDesktopUpdateState, observeDesktopUpdateState} from '@electron/main/DesktopUpdateGate'; import {getDesktopUpdateState, observeDesktopUpdateState} from '@electron/main/DesktopUpdateGate';
import {checkForUpdatesFromShell, startDesktopUpdateFromShell} from '@electron/main/DesktopUpdatePrompt'; import {checkForUpdatesFromShell, startDesktopUpdateFromShell} from '@electron/main/DesktopUpdatePrompt';
import {areModuleUpdatesDisabled} from '@electron/main/LaunchOptions';
import {relaunchStableLaunchPath} from '@electron/main/LinuxLaunchPath'; import {relaunchStableLaunchPath} from '@electron/main/LinuxLaunchPath';
import {onLocaleChange, t} from '@electron/main/MainI18n'; import {onLocaleChange, t} from '@electron/main/MainI18n';
import {app, type BrowserWindow, clipboard, Menu, nativeImage, Tray} from 'electron'; import {app, type BrowserWindow, clipboard, Menu, nativeImage, Tray} from 'electron';
@@ -399,13 +400,15 @@ function buildTrayMenu(): Menu {
click: () => runTrayMenuAction(startDesktopUpdateFromShell), click: () => runTrayMenuAction(startDesktopUpdateFromShell),
}); });
} }
menuTemplate.push({ if (!areModuleUpdatesDisabled()) {
label: t('desktop.tray.checkForUpdates'), menuTemplate.push({
click: () => label: t('desktop.tray.checkForUpdates'),
runTrayMenuAction(() => { click: () =>
void checkForUpdatesFromShell(); runTrayMenuAction(() => {
}), void checkForUpdatesFromShell();
}); }),
});
}
if (trayState.buildInfo) { if (trayState.buildInfo) {
menuTemplate.push({ menuTemplate.push({
label: t('desktop.tray.copyBuildInfo'), label: t('desktop.tray.copyBuildInfo'),
+5
View File
@@ -159,9 +159,14 @@ export function describeLaunchDiagnosticOptions(argv: ReadonlyArray<string> = pr
spellcheck: getSpellcheckLaunchMode(argv), spellcheck: getSpellcheckLaunchMode(argv),
v8CodeCacheDisabled: shouldDisableV8CodeCache(argv), v8CodeCacheDisabled: shouldDisableV8CodeCache(argv),
nativeGatewayDisableReason: getNativeGatewayDisableReason(argv), nativeGatewayDisableReason: getNativeGatewayDisableReason(argv),
moduleUpdatesDisabled: areModuleUpdatesDisabled(),
}; };
} }
export function areModuleUpdatesDisabled(): boolean {
return truthyEnv('FLUXER_DISABLE_MODULE_UPDATES');
}
export function isDesktopUpdateRequested(argv: ReadonlyArray<string> = process.argv): boolean { export function isDesktopUpdateRequested(argv: ReadonlyArray<string> = process.argv): boolean {
return hasFlag(argv, DESKTOP_UPDATE_ARGS); return hasFlag(argv, DESKTOP_UPDATE_ARGS);
} }
+3 -2
View File
@@ -2,6 +2,7 @@
import {DESKTOP_APP_NAME} from '@electron/common/DesktopIdentity'; import {DESKTOP_APP_NAME} from '@electron/common/DesktopIdentity';
import {checkForUpdatesFromShell} from '@electron/main/DesktopUpdatePrompt'; import {checkForUpdatesFromShell} from '@electron/main/DesktopUpdatePrompt';
import {areModuleUpdatesDisabled} from '@electron/main/LaunchOptions';
import {onLocaleChange, t} from '@electron/main/MainI18n'; import {onLocaleChange, t} from '@electron/main/MainI18n';
import {openExternalDeduped} from '@electron/main/OpenExternal'; import {openExternalDeduped} from '@electron/main/OpenExternal';
import {buildTroubleshootingMenuItems} from '@electron/main/Troubleshooting'; import {buildTroubleshootingMenuItems} from '@electron/main/Troubleshooting';
@@ -31,7 +32,7 @@ function buildTemplate(): Array<MenuItemConstructorOptions> {
role: 'about', role: 'about',
label: t('desktop.appMenu.about', {appName}), label: t('desktop.appMenu.about', {appName}),
}, },
checkForUpdatesItem(), ...(areModuleUpdatesDisabled() ? [] : [checkForUpdatesItem()]),
{type: 'separator'}, {type: 'separator'},
{ {
label: t('desktop.appMenu.preferences'), label: t('desktop.appMenu.preferences'),
@@ -209,7 +210,7 @@ function buildTemplate(): Array<MenuItemConstructorOptions> {
? `${t('desktop.appMenu.help')}${MACOS_HELP_MENU_TITLE_AUTODETECT_OPT_OUT}` ? `${t('desktop.appMenu.help')}${MACOS_HELP_MENU_TITLE_AUTODETECT_OPT_OUT}`
: t('desktop.appMenu.help'), : t('desktop.appMenu.help'),
submenu: [ submenu: [
...(isMac ? [] : [checkForUpdatesItem(), {type: 'separator' as const}]), ...(isMac || areModuleUpdatesDisabled() ? [] : [checkForUpdatesItem(), {type: 'separator' as const}]),
{ {
label: t('desktop.appMenu.website'), label: t('desktop.appMenu.website'),
click: async () => { click: async () => {
@@ -127,6 +127,7 @@ function createUpdater(store, modules, options = {}) {
hasOfflineRenderer: options.bundledRendererVersion != null, hasOfflineRenderer: options.bundledRendererVersion != null,
bundledRendererVersion: options.bundledRendererVersion ?? null, bundledRendererVersion: options.bundledRendererVersion ?? null,
forceStartupUpdate: options.forceStartupUpdate ?? false, forceStartupUpdate: options.forceStartupUpdate ?? false,
moduleUpdatesDisabled: options.moduleUpdatesDisabled ?? false,
selfUpdateShellFirst: options.selfUpdateShellFirst, selfUpdateShellFirst: options.selfUpdateShellFirst,
onState: options.onState, onState: options.onState,
fetch: async (url, init) => { 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', () => { describe('ModuleUpdater checks without downloading and installs only when asked', () => {
async function storeOnRenderer() { async function storeOnRenderer() {
const userDataPath = createUserData(); const userDataPath = createUserData();
+26 -7
View File
@@ -267,6 +267,7 @@ interface ModuleUpdaterOptions {
readonly bundledRendererVersion?: string | null; readonly bundledRendererVersion?: string | null;
readonly preferUnversionedBundle?: boolean; readonly preferUnversionedBundle?: boolean;
readonly forceStartupUpdate?: boolean; readonly forceStartupUpdate?: boolean;
readonly moduleUpdatesDisabled?: boolean;
readonly selfUpdateShellFirst?: (latestVersion: string) => Promise<{readonly reason: string}>; readonly selfUpdateShellFirst?: (latestVersion: string) => Promise<{readonly reason: string}>;
readonly onState?: (state: ModuleUpdaterSplashState) => void; readonly onState?: (state: ModuleUpdaterSplashState) => void;
readonly report?: (report: ModuleUpdaterReport) => void; readonly report?: (report: ModuleUpdaterReport) => void;
@@ -286,6 +287,7 @@ export class ModuleUpdater {
private readonly shellVersion: ModuleVersion; private readonly shellVersion: ModuleVersion;
private readonly hasOfflineRenderer: boolean; private readonly hasOfflineRenderer: boolean;
private readonly forceStartupUpdate: boolean; private readonly forceStartupUpdate: boolean;
private readonly moduleUpdatesDisabled: boolean;
private readonly selfUpdateShellFirst: ((latestVersion: string) => Promise<{readonly reason: string}>) | null; private readonly selfUpdateShellFirst: ((latestVersion: string) => Promise<{readonly reason: string}>) | null;
private readonly shellFirstAttempts = new Map<string, {readonly reason: string} | null>(); private readonly shellFirstAttempts = new Map<string, {readonly reason: string} | null>();
private readonly manifestFeed: ModuleManifestFeedIdentity; private readonly manifestFeed: ModuleManifestFeedIdentity;
@@ -314,6 +316,7 @@ export class ModuleUpdater {
this.shellVersion = parseModuleVersion(options.shellVersion, 'desktop shell version'); this.shellVersion = parseModuleVersion(options.shellVersion, 'desktop shell version');
this.hasOfflineRenderer = options.hasOfflineRenderer ?? false; this.hasOfflineRenderer = options.hasOfflineRenderer ?? false;
this.forceStartupUpdate = options.forceStartupUpdate ?? false; this.forceStartupUpdate = options.forceStartupUpdate ?? false;
this.moduleUpdatesDisabled = options.moduleUpdatesDisabled ?? false;
this.selfUpdateShellFirst = options.selfUpdateShellFirst ?? null; this.selfUpdateShellFirst = options.selfUpdateShellFirst ?? null;
const releaseChannel = options.releaseChannel ?? BUILD_CHANNEL; const releaseChannel = options.releaseChannel ?? BUILD_CHANNEL;
this.platform = options.platform ?? process.platform; this.platform = options.platform ?? process.platform;
@@ -418,6 +421,18 @@ export class ModuleUpdater {
return reverted; 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())) { if (this.platform === 'linux' && !(await this.startupUpdateRequired())) {
return await this.launchIfAtFloor(); return await this.launchIfAtFloor();
} }
@@ -486,13 +501,7 @@ export class ModuleUpdater {
if (exhausted || !fetched) { if (exhausted || !fetched) {
const decision = await this.planner.evaluateUnreachableLaunch(this.startupUpdateEnforced); const decision = await this.planner.evaluateUnreachableLaunch(this.startupUpdateEnforced);
if (decision.kind === ModuleUnreachableLaunchDecision.LAUNCH) { if (decision.kind === ModuleUnreachableLaunchDecision.LAUNCH) {
const launchAttempt = await this.store.recordLaunchAttempt(); return await this.launchUnreachable();
this.emit(ModuleUpdaterStatus.UNREACHABLE_LAUNCH);
return {
status: ModuleUpdaterStatus.UNREACHABLE_LAUNCH,
committed: this.store.getCommitted(),
launchAttempt,
};
} }
if (exhausted) { if (exhausted) {
return this.blockUpdateRequired(decision.reason, [], describeErrorChain(error), { return this.blockUpdateRequired(decision.reason, [], describeErrorChain(error), {
@@ -802,6 +811,16 @@ export class ModuleUpdater {
}; };
} }
private async launchUnreachable(): Promise<ModuleUpdaterOutcome> {
const launchAttempt = await this.store.recordLaunchAttempt();
this.emit(ModuleUpdaterStatus.UNREACHABLE_LAUNCH);
return {
status: ModuleUpdaterStatus.UNREACHABLE_LAUNCH,
committed: this.store.getCommitted(),
launchAttempt,
};
}
private async launchRevertedSetBypassingFloor( private async launchRevertedSetBypassingFloor(
committed: Readonly<Record<string, string>>, committed: Readonly<Record<string, string>>,
): Promise<ModuleUpdaterOutcome | null> { ): Promise<ModuleUpdaterOutcome | null> {
+2 -2
View File
@@ -73,7 +73,7 @@ import {
registerIpcHandlers, registerIpcHandlers,
} from '@electron/main/IpcHandlers'; } from '@electron/main/IpcHandlers';
import {initializeJumpList} from '@electron/main/JumpList'; 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 {cleanupLegacyHarvestHandlers, registerLegacyHarvestHandlers} from '@electron/main/LegacyHarvestIpc';
import {initializeLegacyOriginHarvest} from '@electron/main/LegacyOriginHarvest'; import {initializeLegacyOriginHarvest} from '@electron/main/LegacyOriginHarvest';
import {cleanupVirtmic, registerVirtmicHandlers} from '@electron/main/LinuxAudioCapture'; import {cleanupVirtmic, registerVirtmicHandlers} from '@electron/main/LinuxAudioCapture';
@@ -600,7 +600,7 @@ if (launchConfigurationError) {
showWindow, showWindow,
}); });
}); });
if (process.env.FLUXER_OFFLINE !== '1') { if (process.env.FLUXER_OFFLINE !== '1' && !areModuleUpdatesDisabled()) {
const {registerUpdater} = await import('@electron/main/Updater'); const {registerUpdater} = await import('@electron/main/Updater');
registerUpdater(getMainWindow); registerUpdater(getMainWindow);
} }