feat(desktop): prefer the bundled renderer, own update prompts (#3341)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-09 03:01:15 +02:00
1 parent 7a5b9a0ded
commit 5072488f1e
33 files changed
+2197 -113

No files matched your search

+82 -5
View File
@@ -44,11 +44,12 @@ import {
ModuleUnreachableLaunchDecision,
ModuleUpdatePlanner,
ModuleUpdaterBlockReason,
type ServedModuleSelection,
} from '@electron/main/ModuleUpdatePlanner';
import {compareModuleVersions, type ModuleVersion, parseModuleVersion} from '@electron/main/ModuleVersion';
import {resolveDesktopPackageOrigin} from '@electron/main/ShellDownloadFormats';
import type {DesktopModuleEnsureResult} from '@fluxer/desktop_ipc/src/ModuleContract';
import {DesktopModuleEnsureStatus} from '@fluxer/desktop_ipc/src/ModuleContract';
import {DESKTOP_RENDERER_MODULE_NAME, DesktopModuleEnsureStatus} from '@fluxer/desktop_ipc/src/ModuleContract';
const MODULE_UPDATER_BACKOFF_BASE_MS = 1000;
const MODULE_UPDATER_BACKOFF_CAP_MS = 30000;
@@ -82,6 +83,7 @@ const ModuleUpdaterReportType = Object.freeze({
STORAGE_ERROR: 'storage-error',
ROLLBACK: 'rollback',
ROLLBACK_FLOOR_BYPASS: 'rollback-floor-bypass',
BUNDLED_RENDERER_PREFERRED: 'bundled-renderer-preferred',
BLOCKED: 'blocked',
} as const);
@@ -97,6 +99,7 @@ const MODULE_UPDATER_REPORT_SAMPLE_RATES: Readonly<Record<ModuleUpdaterReportTyp
[ModuleUpdaterReportType.STORAGE_ERROR]: 1,
[ModuleUpdaterReportType.ROLLBACK]: 1,
[ModuleUpdaterReportType.ROLLBACK_FLOOR_BYPASS]: 1,
[ModuleUpdaterReportType.BUNDLED_RENDERER_PREFERRED]: 1,
[ModuleUpdaterReportType.BLOCKED]: 1,
});
@@ -198,6 +201,17 @@ function isUpdateServerUnreachable(error: unknown): boolean {
return false;
}
function parseBundledRendererVersion(source: string | null): ModuleVersion | null {
if (source == null) {
return null;
}
try {
return parseModuleVersion(source, 'bundled renderer version');
} catch {
return null;
}
}
function defaultSleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
@@ -263,6 +277,8 @@ interface ModuleUpdaterOptions {
readonly arch?: NodeJS.Architecture;
readonly packageOrigin?: string;
readonly hasOfflineRenderer?: boolean;
readonly bundledRendererVersion?: string | null;
readonly preferUnversionedBundle?: boolean;
readonly forceStartupUpdate?: boolean;
readonly selfUpdateShellFirst?: (latestVersion: string, requiredSecurityUpdate: boolean) => Promise<void>;
readonly onState?: (state: ModuleUpdaterSplashState) => void;
@@ -319,7 +335,13 @@ export class ModuleUpdater {
this.manifestFeed = {releaseChannel, platform: this.platform, arch};
const packageOrigin = (options.packageOrigin ?? resolveDesktopPackageOrigin()).replace(/\/+$/u, '');
this.packageOrigin = packageOrigin;
this.planner = new ModuleUpdatePlanner(this.store, this.shellVersion, this.hasOfflineRenderer);
this.planner = new ModuleUpdatePlanner(
this.store,
this.shellVersion,
this.hasOfflineRenderer,
parseBundledRendererVersion(options.bundledRendererVersion ?? null),
options.preferUnversionedBundle ?? false,
);
this.onState = options.onState ?? (() => {});
this.reporter = options.report ?? (() => {});
this.sleep = options.sleep ?? defaultSleep;
@@ -395,7 +417,12 @@ export class ModuleUpdater {
}
public async run(): Promise<ModuleUpdaterOutcome> {
const attempt = await this.store.beginBootAttempt();
let attempt = await this.store.beginBootAttempt({bundled: this.planner.bundledModules()});
const retiredRenderer = await this.retireRendererCoveredByBundle();
const retiredMissing = await this.retireUninstalledModules();
if (retiredRenderer || retiredMissing) {
attempt = {...attempt, committed: this.store.getCommitted()};
}
await this.restoreCachedUpdatePolicy();
if (attempt.rolledBack) {
this.report({
@@ -562,6 +589,37 @@ export class ModuleUpdater {
return {module: moduleName, status: DesktopModuleEnsureStatus.INSTALLED};
}
private async retireRendererCoveredByBundle(): Promise<boolean> {
const stale = await this.planner.staleRendererModule();
if (stale == null) {
return false;
}
if (!(await this.store.retireModule(DESKTOP_RENDERER_MODULE_NAME))) {
return false;
}
this.report({
type: ModuleUpdaterReportType.BUNDLED_RENDERER_PREFERRED,
module: DESKTOP_RENDERER_MODULE_NAME,
message: `retired installed renderer ${stale} because the renderer bundled with this shell is at least as new`,
});
return true;
}
private async retireUninstalledModules(): Promise<boolean> {
let retired = false;
for (const moduleName of await this.planner.uninstalledCommittedModules()) {
if (await this.store.retireModule(moduleName)) {
retired = true;
this.report({
type: ModuleUpdaterReportType.PACKAGE_MISSING,
module: moduleName,
message: 'dropped a committed module whose files are gone, the bundled renderer does not need it to launch',
});
}
}
return retired;
}
private async resolveManifestForEnsure(): Promise<DesktopModuleUpdateManifest> {
return this.observeManifest(await this.manifestRepository.resolveMemoizedDocument());
}
@@ -571,7 +629,8 @@ export class ModuleUpdater {
if (
this.selfUpdateShellFirst == null ||
compareModuleVersions(manifest.shell.latestVersion, this.shellVersion) <= 0 ||
this.shellFirstAttempted.has(latestVersion)
this.shellFirstAttempted.has(latestVersion) ||
(this.platform === 'linux' && !this.isSecurityUpdateRequired())
) {
return false;
}
@@ -623,7 +682,7 @@ export class ModuleUpdater {
const wanted: Array<ModulePlanItem> = [];
for (const moduleName of Array.from(names).sort()) {
const entry = manifest.modules[moduleName];
if (entry == null || !planner.isShellCompatible(entry)) {
if (entry == null || !planner.isShellCompatible(entry) || planner.bundleCoversModule(moduleName, manifest)) {
continue;
}
if (committed[moduleName] === entry.sha256 && (await planner.isInstalled(moduleName, entry.sha256))) {
@@ -668,6 +727,21 @@ export class ModuleUpdater {
return {installed, unresolved};
}
public async selectServedModules(modules: Readonly<Record<string, string>>): Promise<ServedModuleSelection> {
return await this.planner.selectServedModules(modules);
}
public async revertLaunch(attempt: ModuleLaunchAttempt): Promise<Readonly<Record<string, string>> | null> {
const reverted = await this.store.revertLaunchAttempt(attempt, this.planner.bundledModules());
if (reverted != null) {
this.report({
type: ModuleUpdaterReportType.ROLLBACK,
message: 'reverted an in place module update whose renderer never confirmed it started',
});
}
return reverted;
}
public async markLaunchSucceeded(attempt: ModuleLaunchAttempt): Promise<void> {
if (!(await this.store.markLaunchAttemptSucceeded(attempt))) {
return;
@@ -946,6 +1020,9 @@ export class ModuleUpdater {
}
} else {
for (const moduleName of blockingModuleNames(manifest)) {
if (this.planner.bundleCoversModule(moduleName, manifest)) {
continue;
}
floor[moduleName] = manifest.modules[moduleName].sha256;
}
}