Refactor force-update updater mode

This commit is contained in:
automated-signal 2024-11-12 15:55:54 -06:00 committed by GitHub
parent 51a9f62778
commit 1fd0144579
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 63 additions and 33 deletions

View file

@ -12,7 +12,7 @@ import { throttle } from 'lodash';
import type { ParserConfiguration } from 'dashdash'; import type { ParserConfiguration } from 'dashdash';
import { createParser } from 'dashdash'; import { createParser } from 'dashdash';
import { FAILSAFE_SCHEMA, safeLoad } from 'js-yaml'; import { FAILSAFE_SCHEMA, safeLoad } from 'js-yaml';
import { gt, lt } from 'semver'; import { gt, gte, lt } from 'semver';
import config from 'config'; import config from 'config';
import got from 'got'; import got from 'got';
import { v4 as getGuid } from 'uuid'; import { v4 as getGuid } from 'uuid';
@ -20,6 +20,7 @@ import type { BrowserWindow } from 'electron';
import { app, ipcMain } from 'electron'; import { app, ipcMain } from 'electron';
import * as durations from '../util/durations'; import * as durations from '../util/durations';
import { missingCaseError } from '../util/missingCaseError';
import { getTempPath, getUpdateCachePath } from '../../app/attachments'; import { getTempPath, getUpdateCachePath } from '../../app/attachments';
import { markShouldNotQuit, markShouldQuit } from '../../app/window_state'; import { markShouldNotQuit, markShouldQuit } from '../../app/window_state';
import { DialogType } from '../types/Dialogs'; import { DialogType } from '../types/Dialogs';
@ -83,6 +84,7 @@ enum DownloadMode {
DifferentialOnly = 'DifferentialOnly', DifferentialOnly = 'DifferentialOnly',
FullOnly = 'FullOnly', FullOnly = 'FullOnly',
Automatic = 'Automatic', Automatic = 'Automatic',
ForceUpdate = 'ForceUpdate',
} }
type DownloadUpdateResultType = Readonly<{ type DownloadUpdateResultType = Readonly<{
@ -97,6 +99,12 @@ export type UpdaterOptionsType = Readonly<{
canRunSilently: () => boolean; canRunSilently: () => boolean;
}>; }>;
enum CheckType {
Normal = 'Normal',
AllowSameVersion = 'AllowSameVersion',
ForceDownload = 'ForceDownload',
}
export abstract class Updater { export abstract class Updater {
protected fileName: string | undefined; protected fileName: string | undefined;
@ -148,7 +156,7 @@ export abstract class Updater {
// //
public async force(): Promise<void> { public async force(): Promise<void> {
return this.checkForUpdatesMaybeInstall(true); return this.checkForUpdatesMaybeInstall(CheckType.ForceDownload);
} }
// If the updater was about to restart the app but the user cancelled it, show dialog // If the updater was about to restart the app but the user cancelled it, show dialog
@ -163,7 +171,7 @@ export abstract class Updater {
); );
this.restarting = false; this.restarting = false;
markShouldNotQuit(); markShouldNotQuit();
drop(this.force()); drop(this.checkForUpdatesMaybeInstall(CheckType.AllowSameVersion));
} }
public async start(): Promise<void> { public async start(): Promise<void> {
@ -172,7 +180,7 @@ export abstract class Updater {
this.schedulePoll(); this.schedulePoll();
await this.deletePreviousInstallers(); await this.deletePreviousInstallers();
await this.checkForUpdatesMaybeInstall(); await this.checkForUpdatesMaybeInstall(CheckType.Normal);
} }
// //
@ -184,7 +192,7 @@ export abstract class Updater {
protected abstract installUpdate( protected abstract installUpdate(
updateFilePath: string, updateFilePath: string,
isSilent: boolean isSilent: boolean
): Promise<void>; ): Promise<() => Promise<void>>;
// //
// Protected methods // Protected methods
@ -223,7 +231,7 @@ export abstract class Updater {
this.logger.info('updater/markCannotUpdate: retrying after user action'); this.logger.info('updater/markCannotUpdate: retrying after user action');
this.markedCannotUpdate = false; this.markedCannotUpdate = false;
await this.checkForUpdatesMaybeInstall(); await this.checkForUpdatesMaybeInstall(CheckType.Normal);
}); });
} }
@ -255,7 +263,7 @@ export abstract class Updater {
private async safePoll(): Promise<void> { private async safePoll(): Promise<void> {
try { try {
this.logger.info('updater/start: polling now'); this.logger.info('updater/start: polling now');
await this.checkForUpdatesMaybeInstall(); await this.checkForUpdatesMaybeInstall(CheckType.Normal);
} catch (error) { } catch (error) {
this.logger.error(`updater/start: ${Errors.toLogFormat(error)}`); this.logger.error(`updater/start: ${Errors.toLogFormat(error)}`);
} finally { } finally {
@ -306,8 +314,11 @@ export abstract class Updater {
if (!downloadResult) { if (!downloadResult) {
logger.warn('downloadAndInstall: no update was downloaded'); logger.warn('downloadAndInstall: no update was downloaded');
strictAssert( strictAssert(
mode !== DownloadMode.Automatic && mode !== DownloadMode.FullOnly, mode !== DownloadMode.ForceUpdate &&
'Automatic and full mode downloads are guaranteed to happen or error' mode !== DownloadMode.Automatic &&
mode !== DownloadMode.FullOnly,
'Automatic/full/force update mode downloads are ' +
'guaranteed to happen or error'
); );
return false; return false;
} }
@ -330,14 +341,21 @@ export abstract class Updater {
); );
} }
await this.installUpdate( const isSilent =
updateFilePath,
updateInfo.vendor?.requireUserConfirmation !== 'true' && updateInfo.vendor?.requireUserConfirmation !== 'true' &&
this.canRunSilently() this.canRunSilently();
);
const handler = await this.installUpdate(updateFilePath, isSilent);
if (isSilent || mode === DownloadMode.ForceUpdate) {
await handler();
} else {
this.setUpdateListener(handler);
}
const mainWindow = this.getMainWindow(); const mainWindow = this.getMainWindow();
if (mainWindow) { if (mode === DownloadMode.ForceUpdate) {
logger.info('downloadAndInstall: force update, no dialog...');
} else if (mainWindow) {
logger.info('downloadAndInstall: showing update dialog...'); logger.info('downloadAndInstall: showing update dialog...');
mainWindow.webContents.send( mainWindow.webContents.send(
'show-update-dialog', 'show-update-dialog',
@ -362,21 +380,38 @@ export abstract class Updater {
} }
} }
private async checkForUpdatesMaybeInstall(force = false): Promise<void> { private async checkForUpdatesMaybeInstall(
checkType: CheckType
): Promise<void> {
const { logger } = this; const { logger } = this;
logger.info('checkForUpdatesMaybeInstall: checking for update...'); logger.info('checkForUpdatesMaybeInstall: checking for update...');
const updateInfo = await this.checkForUpdates(force); const updateInfo = await this.checkForUpdates(checkType);
if (!updateInfo) { if (!updateInfo) {
return; return;
} }
const { version: newVersion } = updateInfo; const { version: newVersion } = updateInfo;
if (!force && this.version && !gt(newVersion, this.version)) { if (checkType === CheckType.ForceDownload) {
await this.downloadAndInstall(updateInfo, DownloadMode.ForceUpdate);
return; return;
} }
if (checkType === CheckType.Normal) {
// Verify that the downloaded version is greater than downloaded
if (this.version && !gt(newVersion, this.version)) {
return;
}
} else if (checkType === CheckType.AllowSameVersion) {
// Verify that the downloaded version is greater or the same as downloaded
if (this.version && !gte(newVersion, this.version)) {
return;
}
} else {
throw missingCaseError(checkType);
}
const autoDownloadUpdates = await this.getAutoDownloadUpdateSetting(); const autoDownloadUpdates = await this.getAutoDownloadUpdateSetting();
if (autoDownloadUpdates) { if (autoDownloadUpdates) {
await this.downloadAndInstall(updateInfo, DownloadMode.Automatic); await this.downloadAndInstall(updateInfo, DownloadMode.Automatic);
@ -443,7 +478,7 @@ export abstract class Updater {
} }
private async checkForUpdates( private async checkForUpdates(
forceUpdate = false checkType: CheckType
): Promise<UpdateInformationType | undefined> { ): Promise<UpdateInformationType | undefined> {
const yaml = await getUpdateYaml(); const yaml = await getUpdateYaml();
const parsedYaml = parseYaml(yaml); const parsedYaml = parseYaml(yaml);
@ -482,7 +517,7 @@ export abstract class Updater {
return; return;
} }
if (!forceUpdate && !isVersionNewer(version)) { if (checkType === CheckType.Normal && !isVersionNewer(version)) {
this.logger.info( this.logger.info(
`checkForUpdates: ${version} is not newer than ${packageJson.version}; ` + `checkForUpdates: ${version} is not newer than ${packageJson.version}; ` +
'no new update available' 'no new update available'
@ -493,7 +528,7 @@ export abstract class Updater {
this.logger.info( this.logger.info(
`checkForUpdates: found newer version ${version} ` + `checkForUpdates: found newer version ${version} ` +
`forceUpdate=${forceUpdate}` `checkType=${checkType}`
); );
const fileName = getUpdateFileName( const fileName = getUpdateFileName(
@ -576,6 +611,7 @@ export abstract class Updater {
const baseUrl = getUpdatesBase(); const baseUrl = getUpdatesBase();
const updateFileUrl = `${baseUrl}/${fileName}`; const updateFileUrl = `${baseUrl}/${fileName}`;
// Show progress on DifferentialOnly, FullOnly, and ForceUpdate
const updateOnProgress = mode !== DownloadMode.Automatic; const updateOnProgress = mode !== DownloadMode.Automatic;
const signatureFileName = getSignatureFileName(fileName); const signatureFileName = getSignatureFileName(fileName);

View file

@ -16,7 +16,9 @@ export class MacOSUpdater extends Updater {
// No installers are cache on macOS // No installers are cache on macOS
} }
protected async installUpdate(updateFilePath: string): Promise<void> { protected async installUpdate(
updateFilePath: string
): Promise<() => Promise<void>> {
const { logger } = this; const { logger } = this;
logger.info('downloadAndInstall: handing download to electron...'); logger.info('downloadAndInstall: handing download to electron...');
@ -38,11 +40,11 @@ export class MacOSUpdater extends Updater {
// At this point, closing the app will cause the update to be installed automatically // At this point, closing the app will cause the update to be installed automatically
// because Squirrel has cached the update file and will do the right thing. // because Squirrel has cached the update file and will do the right thing.
this.setUpdateListener(async () => { return async () => {
logger.info('downloadAndInstall: restarting...'); logger.info('downloadAndInstall: restarting...');
this.markRestarting(); this.markRestarting();
autoUpdater.quitAndInstall(); autoUpdater.quitAndInstall();
}); };
} }
private async handToAutoUpdate(filePath: string): Promise<void> { private async handToAutoUpdate(filePath: string): Promise<void> {

View file

@ -46,10 +46,10 @@ export class WindowsUpdater extends Updater {
protected async installUpdate( protected async installUpdate(
updateFilePath: string, updateFilePath: string,
isSilent: boolean isSilent: boolean
): Promise<void> { ): Promise<() => Promise<void>> {
const { logger } = this; const { logger } = this;
const doInstall = async () => { return async () => {
logger.info('downloadAndInstall: installing...'); logger.info('downloadAndInstall: installing...');
try { try {
await this.install(updateFilePath, isSilent); await this.install(updateFilePath, isSilent);
@ -64,14 +64,6 @@ export class WindowsUpdater extends Updater {
this.setUpdateListener(this.restart); this.setUpdateListener(this.restart);
this.restart(); this.restart();
}; };
if (isSilent) {
logger.info('downloadAndInstall: running immediately...');
await doInstall();
return;
}
this.setUpdateListener(doInstall);
} }
protected restart(): void { protected restart(): void {