From 2afc2ca369445dbb43a667589a23f68f02628dad Mon Sep 17 00:00:00 2001 From: Alice <53339016+AliceHincu@users.noreply.github.com> Date: Tue, 27 Aug 2024 20:04:18 +0300 Subject: [PATCH] Desktop: Seamless-Updates: implemented flow for prereleases (#10892) --- packages/app-desktop/ElectronAppWrapper.ts | 31 ++++-- packages/app-desktop/app.ts | 19 ++-- .../autoUpdater/AutoUpdaterService.ts | 100 ++++++++++++------ .../app-desktop/utils/checkForUpdatesUtils.ts | 2 +- packages/tools/cspell/dictionary4.txt | 1 + 5 files changed, 101 insertions(+), 52 deletions(-) diff --git a/packages/app-desktop/ElectronAppWrapper.ts b/packages/app-desktop/ElectronAppWrapper.ts index 7f2026844..25e421bcc 100644 --- a/packages/app-desktop/ElectronAppWrapper.ts +++ b/packages/app-desktop/ElectronAppWrapper.ts @@ -1,6 +1,6 @@ import Logger, { LoggerWrapper } from '@joplin/utils/Logger'; import { PluginMessage } from './services/plugins/PluginRunner'; -import AutoUpdaterService from './services/autoUpdater/AutoUpdaterService'; +import AutoUpdaterService, { defaultUpdateInterval, initialUpdateStartup } from './services/autoUpdater/AutoUpdaterService'; import type ShimType from '@joplin/lib/shim'; const shim: typeof ShimType = require('@joplin/lib/shim').default; import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils'; @@ -45,6 +45,7 @@ export default class ElectronAppWrapper { private initialCallbackUrl_: string = null; private updaterService_: AutoUpdaterService = null; private customProtocolHandler_: CustomProtocolHandler = null; + private updatePollInterval_: ReturnType|null = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public constructor(electronApp: any, env: string, profilePath: string|null, isDebugMode: boolean, initialCallbackUrl: string) { @@ -363,7 +364,7 @@ export default class ElectronAppWrapper { } public quit() { - this.stopLookingForUpdates(); + this.stopPeriodicUpdateCheck(); this.electronApp_.quit(); } @@ -468,18 +469,30 @@ export default class ElectronAppWrapper { this.customProtocolHandler_ ??= handleCustomProtocols(logger); } - public initializeAutoUpdaterService(logger: LoggerWrapper, initializedShim: typeof ShimType, devMode: boolean, includePreReleases: boolean) { + // Electron's autoUpdater has to be init from the main process + public async initializeAutoUpdaterService(logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) { if (shim.isWindows() || shim.isMac()) { - this.updaterService_ = new AutoUpdaterService(this.win_, logger, initializedShim, devMode, includePreReleases); - this.updaterService_.startPeriodicUpdateCheck(); + if (!this.updaterService_) { + this.updaterService_ = new AutoUpdaterService(this.win_, logger, devMode, includePreReleases); + this.startPeriodicUpdateCheck(); + } } } - public stopLookingForUpdates() { - if (this.updaterService_ !== null) { - this.updaterService_.stopPeriodicUpdateCheck(); + private startPeriodicUpdateCheck = (updateInterval: number = defaultUpdateInterval): void => { + this.stopPeriodicUpdateCheck(); + this.updatePollInterval_ = setInterval(() => { + void this.updaterService_.checkForUpdates(); + }, updateInterval); + setTimeout(this.updaterService_.checkForUpdates, initialUpdateStartup); + }; + + private stopPeriodicUpdateCheck = (): void => { + if (this.updatePollInterval_) { + clearInterval(this.updatePollInterval_); + this.updatePollInterval_ = null; } - } + }; public getCustomProtocolHandler() { return this.customProtocolHandler_; diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index be2b98f62..9a56280a1 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -404,6 +404,16 @@ class Application extends BaseApplication { eventManager.on(EventName.ResourceChange, handleResourceChange); } + private async setupAutoUpdaterService() { + if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) { + await bridge().electronApp().initializeAutoUpdaterService( + Logger.create('AutoUpdaterService'), + Setting.value('env') === 'dev', + Setting.value('autoUpdate.includePreReleases'), + ); + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public async start(argv: string[], startOptions: StartOptions = null): Promise { // If running inside a package, the command line, instead of being "node.exe " is "joplin.exe " so @@ -688,14 +698,7 @@ class Application extends BaseApplication { SearchEngine.instance().scheduleSyncTables(); }); - if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) { - bridge().electronApp().initializeAutoUpdaterService( - Logger.create('AutoUpdaterService'), - shim, - Setting.value('env') === 'dev', - Setting.value('autoUpdate.includePreReleases'), - ); - } + await this.setupAutoUpdaterService(); // setTimeout(() => { // void populateDatabase(reg.db(), { diff --git a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts index 56bdcc537..f47879b44 100644 --- a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts +++ b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts @@ -1,9 +1,11 @@ import { BrowserWindow } from 'electron'; import { autoUpdater, UpdateInfo } from 'electron-updater'; import path = require('path'); -import { setInterval } from 'timers'; import Logger, { LoggerWrapper } from '@joplin/utils/Logger'; import type ShimType from '@joplin/lib/shim'; +const shim: typeof ShimType = require('@joplin/lib/shim').default; +import { GitHubRelease, GitHubReleaseAsset } from '../../utils/checkForUpdatesUtils'; +import * as semver from 'semver'; export enum AutoUpdaterEvents { CheckingForUpdate = 'checking-for-update', @@ -14,59 +16,36 @@ export enum AutoUpdaterEvents { UpdateDownloaded = 'update-downloaded', } -const defaultUpdateInterval = 12 * 60 * 60 * 1000; -const initialUpdateStartup = 5 * 1000; +export const defaultUpdateInterval = 12 * 60 * 60 * 1000; +export const initialUpdateStartup = 5 * 1000; +const releasesLink = 'https://objects.joplinusercontent.com/r/releases'; export interface AutoUpdaterServiceInterface { - startPeriodicUpdateCheck(interval?: number): void; - stopPeriodicUpdateCheck(): void; checkForUpdates(): void; + updateApp(): void; } export default class AutoUpdaterService implements AutoUpdaterServiceInterface { private window_: BrowserWindow; private logger_: LoggerWrapper; - private initializedShim_: typeof ShimType; private devMode_: boolean; - private updatePollInterval_: ReturnType|null = null; private enableDevMode = true; // force the updater to work in "dev" mode private enableAutoDownload = false; // automatically download an update when it is found private autoInstallOnAppQuit = false; // automatically install the downloaded update once the user closes the application private includePreReleases_ = false; private allowDowngrade = false; - public constructor(mainWindow: BrowserWindow, logger: LoggerWrapper, initializedShim: typeof ShimType, devMode: boolean, includePreReleases: boolean) { + public constructor(mainWindow: BrowserWindow, logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) { this.window_ = mainWindow; this.logger_ = logger; - this.initializedShim_ = initializedShim; this.devMode_ = devMode; this.includePreReleases_ = includePreReleases; this.configureAutoUpdater(); } - public startPeriodicUpdateCheck = (interval: number = defaultUpdateInterval): void => { - this.stopPeriodicUpdateCheck(); - this.updatePollInterval_ = this.initializedShim_.setInterval(() => { - void this.checkForUpdates(); - }, interval); - this.initializedShim_.setTimeout(this.checkForUpdates, initialUpdateStartup); - }; - - public stopPeriodicUpdateCheck = (): void => { - if (this.updatePollInterval_) { - this.initializedShim_.clearInterval(this.updatePollInterval_); - this.updatePollInterval_ = null; - } - }; - public checkForUpdates = async (): Promise => { try { - if (this.includePreReleases_) { - // If this is set to true, then it will compare the versions semantically and it will also look at tags, so we need to manually get the latest pre-release - this.logger_.info('To be implemented...'); - } else { - await autoUpdater.checkForUpdates(); - } + await this.fetchLatestRelease(); } catch (error) { this.logger_.error('Failed to check for updates:', error); if (error.message.includes('ERR_CONNECTION_REFUSED')) { @@ -75,6 +54,63 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { } }; + public updateApp = (): void => { + autoUpdater.quitAndInstall(false, true); + }; + + private fetchLatestReleases = async (): Promise => { + const response = await fetch(releasesLink); + + if (!response.ok) { + const responseText = await response.text(); + throw new Error(`Cannot get latest release info: ${responseText.substr(0, 500)}`); + } + + return (await response.json()) as GitHubRelease[]; + }; + + private fetchLatestRelease = async (): Promise => { + try { + const releases = await this.fetchLatestReleases(); + + const sortedReleasesByVersion = releases.sort((a, b) => { + return semver.rcompare(a.tag_name, b.tag_name); + }); + const filteredReleases = sortedReleasesByVersion.filter(release => { + return this.includePreReleases_ || !release.prerelease; + }); + const release = filteredReleases[0]; + + if (release) { + let assetUrl = null; + + if (shim.isWindows()) { + const asset = release.assets.find((asset: GitHubReleaseAsset) => asset.name === 'latest.yml'); + if (asset) { + assetUrl = asset.browser_download_url.replace('/latest.yml', ''); + } + } else if (shim.isMac()) { + const asset = release.assets.find((asset: GitHubReleaseAsset) => asset.name === 'latest-mac.yml'); + if (asset) { + assetUrl = asset.browser_download_url.replace('/latest-mac.yml', ''); + } + } + + if (assetUrl) { + autoUpdater.setFeedURL({ + provider: 'generic', + url: assetUrl, + }); + await autoUpdater.checkForUpdates(); + } else { + this.logger_.error('No suitable update asset found for this platform.'); + } + } + } catch (error) { + this.logger_.error(error); + } + }; + private configureAutoUpdater = (): void => { autoUpdater.logger = (this.logger_) as Logger; if (this.devMode_) { @@ -124,8 +160,4 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { private promptUserToUpdate = async (info: UpdateInfo): Promise => { this.window_.webContents.send(AutoUpdaterEvents.UpdateDownloaded, info); }; - - public updateApp = (): void => { - autoUpdater.quitAndInstall(false, true); - }; } diff --git a/packages/app-desktop/utils/checkForUpdatesUtils.ts b/packages/app-desktop/utils/checkForUpdatesUtils.ts index 50a3c8a5d..901c764ac 100644 --- a/packages/app-desktop/utils/checkForUpdatesUtils.ts +++ b/packages/app-desktop/utils/checkForUpdatesUtils.ts @@ -4,7 +4,7 @@ export interface CheckForUpdateOptions { includePreReleases?: boolean; } -interface GitHubReleaseAsset { +export interface GitHubReleaseAsset { name: string; browser_download_url: string; } diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt index b4799bc27..5cd3a4bab 100644 --- a/packages/tools/cspell/dictionary4.txt +++ b/packages/tools/cspell/dictionary4.txt @@ -129,3 +129,4 @@ entypo Zocial agplv Famegear +rcompare \ No newline at end of file