mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
feat(desktop): add FLUXER_DISABLE_MODULE_UPDATES opt-out (#3466)
This commit is contained in:
1 parent
e8dff6a8a1
commit
e403fb6cce
11 files changed
+245
-20
No files matched your search
@@ -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: [
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
return {
|
||||||
'process.env.PUBLIC_BUILD_VERSION': JSON.stringify(embeddedBuildVersion),
|
'process.env.PUBLIC_BUILD_VERSION': JSON.stringify(embeddedBuildVersion),
|
||||||
'process.env.BUILD_VERSION': JSON.stringify(embeddedBuildVersion),
|
'process.env.BUILD_VERSION': JSON.stringify(embeddedBuildVersion),
|
||||||
'process.env.PUBLIC_RELEASE_CHANNEL': JSON.stringify(embeddedReleaseChannel),
|
'process.env.PUBLIC_RELEASE_CHANNEL': JSON.stringify(embeddedReleaseChannel),
|
||||||
'process.env.RELEASE_CHANNEL': JSON.stringify(embeddedReleaseChannel),
|
'process.env.RELEASE_CHANNEL': JSON.stringify(embeddedReleaseChannel),
|
||||||
'process.env.FLUXER_MODULES': JSON.stringify(modulesEnabled ? '1' : ''),
|
'process.env.FLUXER_MODULES': JSON.stringify(modulesEnabled ? '1' : ''),
|
||||||
'process.env.FLUXER_OFFLINE': JSON.stringify(offlineBuild ? '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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,7 +710,9 @@ async function runModuleBootstrap(): Promise<void> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const desktopUpdate = new DesktopUpdateRun({
|
const desktopUpdate = moduleUpdatesDisabled
|
||||||
|
? null
|
||||||
|
: new DesktopUpdateRun({
|
||||||
probe: () => updater.checkForUpdate(),
|
probe: () => updater.checkForUpdate(),
|
||||||
canSelfUpdateShell: shellUpdatePlan.capability === ShellUpdateCapability.SELF_UPDATE,
|
canSelfUpdateShell: shellUpdatePlan.capability === ShellUpdateCapability.SELF_UPDATE,
|
||||||
runShellSelfUpdate: () => runShellSelfUpdateOnSplash(),
|
runShellSelfUpdate: () => runShellSelfUpdateOnSplash(),
|
||||||
@@ -741,6 +748,8 @@ 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);
|
||||||
|
markSplashLaunching();
|
||||||
|
if (desktopUpdate != null) {
|
||||||
setOnDemandModuleInstaller(
|
setOnDemandModuleInstaller(
|
||||||
createOnDemandModuleInstaller({
|
createOnDemandModuleInstaller({
|
||||||
ensure: (moduleName) => updater.ensureModule(moduleName),
|
ensure: (moduleName) => updater.ensureModule(moduleName),
|
||||||
@@ -750,7 +759,6 @@ async function runModuleBootstrap(): Promise<void> {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
markSplashLaunching();
|
|
||||||
armDesktopUpdate({
|
armDesktopUpdate({
|
||||||
check: async (manual) => {
|
check: async (manual) => {
|
||||||
const check = await desktopUpdate.check({manual});
|
const check = await desktopUpdate.check({manual});
|
||||||
@@ -759,7 +767,11 @@ async function runModuleBootstrap(): Promise<void> {
|
|||||||
},
|
},
|
||||||
start: () => desktopUpdate.start(),
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,6 +400,7 @@ function buildTrayMenu(): Menu {
|
|||||||
click: () => runTrayMenuAction(startDesktopUpdateFromShell),
|
click: () => runTrayMenuAction(startDesktopUpdateFromShell),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (!areModuleUpdatesDisabled()) {
|
||||||
menuTemplate.push({
|
menuTemplate.push({
|
||||||
label: t('desktop.tray.checkForUpdates'),
|
label: t('desktop.tray.checkForUpdates'),
|
||||||
click: () =>
|
click: () =>
|
||||||
@@ -406,6 +408,7 @@ function buildTrayMenu(): Menu {
|
|||||||
void checkForUpdatesFromShell();
|
void checkForUpdatesFromShell();
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
if (trayState.buildInfo) {
|
if (trayState.buildInfo) {
|
||||||
menuTemplate.push({
|
menuTemplate.push({
|
||||||
label: t('desktop.tray.copyBuildInfo'),
|
label: t('desktop.tray.copyBuildInfo'),
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user