From 5faee24ffa0fffa617553832b54216bd7d6c93fc Mon Sep 17 00:00:00 2001 From: AliceHincu Date: Tue, 24 Sep 2024 22:39:14 +0300 Subject: [PATCH 1/6] Seamless-Updates - implemented locking mechanism for checking updates --- .../autoUpdater/AutoUpdaterService.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts index 93105a08f9c..9136bd4b8a1 100644 --- a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts +++ b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts @@ -53,6 +53,7 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { private includePreReleases_ = false; private allowDowngrade = false; private isManualCheckInProgress = false; + private isUpdateInProgress = false; public constructor(mainWindow: BrowserWindow, logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) { this.window_ = mainWindow; @@ -63,14 +64,23 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { } public checkForUpdates = async (isManualCheck = false): Promise => { + if (this.isUpdateInProgress) { + this.logger_.info('Update check already in progress. Waiting for the current check to finish.'); + return; + } + + this.lockUpdateProcess(); + this.isManualCheckInProgress = isManualCheck; + try { - this.isManualCheckInProgress = isManualCheck; await this.checkForLatestRelease(); } catch (error) { this.logger_.error('Failed to check for updates:', error); if (error.message.includes('ERR_CONNECTION_REFUSED')) { this.logger_.info('Server is not reachable. Will try again later.'); } + } finally { + this.isManualCheckInProgress = false; } }; @@ -134,7 +144,6 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { assetUrl = assetUrl.substring(0, assetUrl.lastIndexOf('/')); autoUpdater.setFeedURL({ provider: 'generic', url: assetUrl }); await autoUpdater.checkForUpdates(); - this.isManualCheckInProgress = false; } catch (error) { this.logger_.error(`Update download url failed: ${error.message}`); } @@ -187,6 +196,7 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { private onUpdateDownloaded = (info: UpdateInfo): void => { this.logger_.info('Update downloaded.'); + this.unlockUpdateProcess(); void this.promptUserToUpdate(info); }; @@ -197,4 +207,14 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { private promptUserToUpdate = async (info: UpdateInfo): Promise => { this.window_.webContents.send(AutoUpdaterEvents.UpdateDownloaded, info); }; + + private lockUpdateProcess = (): void => { + this.logger_.info('Locking update process'); + this.isUpdateInProgress = true; + }; + + private unlockUpdateProcess = (): void => { + this.logger_.info('Unlocking update process'); + this.isUpdateInProgress = false; + }; } From 4f44d0e400b052864673b9a6d9e706f0bd0da24b Mon Sep 17 00:00:00 2001 From: AliceHincu Date: Wed, 25 Sep 2024 00:03:13 +0300 Subject: [PATCH 2/6] Seamless-Updates - implemented locking mechanism for checking updates --- packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts index 9136bd4b8a1..93d26fcc342 100644 --- a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts +++ b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts @@ -154,6 +154,7 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { }; private configureAutoUpdater = (): void => { + this.logger_.info('Initiating ...'); autoUpdater.logger = (this.logger_) as Logger; if (this.devMode_) { this.logger_.info('Development mode: using dev-app-update.yml'); @@ -183,6 +184,7 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { this.window_.webContents.send(AutoUpdaterEvents.UpdateNotAvailable); } + this.unlockUpdateProcess(); this.logger_.info('Update not available.'); }; From 8251159587c1d06fe0652c26d8946264eb609950 Mon Sep 17 00:00:00 2001 From: AliceHincu Date: Wed, 25 Sep 2024 01:10:56 +0300 Subject: [PATCH 3/6] Seamless-Updates - managing remote process in update service --- packages/app-desktop/ElectronAppWrapper.ts | 11 +++++------ packages/app-desktop/app.ts | 15 ++++++++++----- packages/app-desktop/gui/MenuBar.tsx | 2 +- .../services/autoUpdater/AutoUpdaterService.ts | 7 ++++++- packages/lib/models/settings/builtInMetadata.ts | 6 +++--- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/packages/app-desktop/ElectronAppWrapper.ts b/packages/app-desktop/ElectronAppWrapper.ts index ba271a401b1..6379ee0dfad 100644 --- a/packages/app-desktop/ElectronAppWrapper.ts +++ b/packages/app-desktop/ElectronAppWrapper.ts @@ -1,11 +1,11 @@ import Logger, { LoggerWrapper } from '@joplin/utils/Logger'; import { PluginMessage } from './services/plugins/PluginRunner'; -import AutoUpdaterService, { defaultUpdateInterval, initialUpdateStartup } from './services/autoUpdater/AutoUpdaterService'; +import AutoUpdaterService, { CheckForUpdatesArgs, 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'; -import { BrowserWindow, Tray, screen } from 'electron'; +import { BrowserWindow, IpcMainEvent, Tray, screen } from 'electron'; import bridge from './bridge'; const url = require('url'); const path = require('path'); @@ -332,8 +332,8 @@ export default class ElectronAppWrapper { this.updaterService_.updateApp(); }); - ipcMain.on('check-for-updates', () => { - void this.updaterService_.checkForUpdates(true); + ipcMain.on('check-for-updates', (_event: IpcMainEvent, _message: string, args: CheckForUpdatesArgs) => { + void this.updaterService_.checkForUpdates(true, args.includePreReleases); }); // Let us register listeners on the window, so we can update the state @@ -478,12 +478,11 @@ export default class ElectronAppWrapper { if (shim.isWindows() || shim.isMac()) { if (!this.updaterService_) { this.updaterService_ = new AutoUpdaterService(this.win_, logger, devMode, includePreReleases); - this.startPeriodicUpdateCheck(); } } } - private startPeriodicUpdateCheck = (updateInterval: number = defaultUpdateInterval): void => { + public startPeriodicUpdateCheck = (updateInterval: number = defaultUpdateInterval): void => { this.stopPeriodicUpdateCheck(); this.updatePollInterval_ = setInterval(() => { void this.updaterService_.checkForUpdates(false); diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 31109526969..ccfce8862b1 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -405,12 +405,17 @@ class Application extends BaseApplication { } private setupAutoUpdaterService() { + // since the remote process doesn't stop running after app is closed, we need to initialize the service, even if its flag is set to false, or else it will throw an error. + bridge().electronApp().initializeAutoUpdaterService( + Logger.create('AutoUpdaterService'), + Setting.value('env') === 'dev', + Setting.value('autoUpdate.includePreReleases'), + ); + + // since the remote process doesn't stop running after app is closed, the period check starts only if the flag is set to true and the app is quit from the system tray. + // if the user sets the flag to true and closes the app but does not quit the app from the system tray, the periodic check won't start. The manual check will work. if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) { - bridge().electronApp().initializeAutoUpdaterService( - Logger.create('AutoUpdaterService'), - Setting.value('env') === 'dev', - Setting.value('autoUpdate.includePreReleases'), - ); + bridge().electronApp().startPeriodicUpdateCheck(); } } diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 5f39fc90430..939f8c76547 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -577,7 +577,7 @@ function useMenu(props: Props) { function _checkForUpdates() { if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) { - ipcRenderer.send('check-for-updates'); + ipcRenderer.send('check-for-updates', '', { includePreReleases: Setting.value('autoUpdate.includePreReleases') }); } else { void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') }); } diff --git a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts index 93d26fcc342..7c5c0dede8a 100644 --- a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts +++ b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts @@ -16,6 +16,10 @@ export enum AutoUpdaterEvents { UpdateDownloaded = 'update-downloaded', } +export interface CheckForUpdatesArgs { + includePreReleases: boolean; +} + export const defaultUpdateInterval = 12 * 60 * 60 * 1000; export const initialUpdateStartup = 5 * 1000; const releasesLink = 'https://objects.joplinusercontent.com/r/releases'; @@ -63,7 +67,7 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { this.configureAutoUpdater(); } - public checkForUpdates = async (isManualCheck = false): Promise => { + public checkForUpdates = async (isManualCheck = false, includePreReleases = this.includePreReleases_): Promise => { if (this.isUpdateInProgress) { this.logger_.info('Update check already in progress. Waiting for the current check to finish.'); return; @@ -73,6 +77,7 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { this.isManualCheckInProgress = isManualCheck; try { + autoUpdater.allowPrerelease = includePreReleases; await this.checkForLatestRelease(); } catch (error) { this.logger_.error('Failed to check for updates:', error); diff --git a/packages/lib/models/settings/builtInMetadata.ts b/packages/lib/models/settings/builtInMetadata.ts index d9ce7057776..9de07c0a1c7 100644 --- a/packages/lib/models/settings/builtInMetadata.ts +++ b/packages/lib/models/settings/builtInMetadata.ts @@ -1127,8 +1127,8 @@ const builtInMetadata = (Setting: typeof SettingType) => { }, - autoUpdateEnabled: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: false, appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') }, - 'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/help/about/prereleases') }, + autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: false, appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') }, + 'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s. Restart app (quit app from system tray) to start getting them', 'https://joplinapp.org/help/about/prereleases') }, 'autoUploadCrashDumps': { value: false, @@ -1559,7 +1559,7 @@ const builtInMetadata = (Setting: typeof SettingType) => { storage: SettingStorage.File, appTypes: [AppType.Desktop], label: () => 'Enable auto-updates', - description: () => 'Enable this feature to receive notifications about updates and install them instead of manually downloading them. Restart app to start receiving auto-updates.', + description: () => 'Enable this feature to receive notifications about updates and install them instead of manually downloading them. Restart app (quit app from system tray) to start receiving auto-updates.', show: () => shim.isWindows() || shim.isMac(), section: 'application', isGlobal: true, From a2940b75f31a991548a990ebf497fb57ce7f2905 Mon Sep 17 00:00:00 2001 From: AliceHincu Date: Wed, 25 Sep 2024 18:01:49 +0300 Subject: [PATCH 4/6] Seamless-Updates - notify if update check is already in progress --- .../UpdateNotification/UpdateNotification.tsx | 13 ++++++------- .../services/autoUpdater/AutoUpdaterService.ts | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx b/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx index 05ea6e5025f..92b8b775710 100644 --- a/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx +++ b/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx @@ -4,7 +4,7 @@ import { themeStyle } from '@joplin/lib/theme'; import NotyfContext from '../NotyfContext'; import { UpdateInfo } from 'electron-updater'; import { ipcRenderer, IpcRendererEvent } from 'electron'; -import { AutoUpdaterEvents } from '../../services/autoUpdater/AutoUpdaterService'; +import { AutoUpdaterEvents, UpdateNotificationMessage } from '../../services/autoUpdater/AutoUpdaterService'; import { NotyfEvent, NotyfNotification } from 'notyf'; import { _ } from '@joplin/lib/locale'; import { htmlentities } from '@joplin/utils/html'; @@ -16,7 +16,6 @@ interface UpdateNotificationProps { export enum UpdateNotificationEvents { ApplyUpdate = 'apply-update', - UpdateNotAvailable = 'update-not-available', Dismiss = 'dismiss-update-notification', } @@ -87,10 +86,10 @@ const UpdateNotification = ({ themeId }: UpdateNotificationProps) => { notificationRef.current = notification; }, [notyf, theme]); - const handleUpdateNotAvailable = useCallback(() => { + const handleNotificationMessage = useCallback((_event: IpcRendererEvent, args: UpdateNotificationMessage) => { if (notificationRef.current) return; - const noUpdateMessageHtml = htmlentities(_('No updates available')); + const noUpdateMessageHtml = htmlentities(_('%s', args.message)); const messageHtml = `
@@ -117,16 +116,16 @@ const UpdateNotification = ({ themeId }: UpdateNotificationProps) => { useEffect(() => { ipcRenderer.on(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded); - ipcRenderer.on(AutoUpdaterEvents.UpdateNotAvailable, handleUpdateNotAvailable); + ipcRenderer.on(AutoUpdaterEvents.NotificationMessage, handleNotificationMessage); document.addEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate); document.addEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification); return () => { ipcRenderer.removeListener(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded); - ipcRenderer.removeListener(AutoUpdaterEvents.UpdateNotAvailable, handleUpdateNotAvailable); + ipcRenderer.removeListener(AutoUpdaterEvents.NotificationMessage, handleNotificationMessage); document.removeEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate); }; - }, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded, handleUpdateNotAvailable]); + }, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded, handleNotificationMessage]); return ( diff --git a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts index 7c5c0dede8a..fabec687100 100644 --- a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts +++ b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts @@ -14,11 +14,15 @@ export enum AutoUpdaterEvents { Error = 'error', DownloadProgress = 'download-progress', UpdateDownloaded = 'update-downloaded', + NotificationMessage = 'notify-with-message', } export interface CheckForUpdatesArgs { includePreReleases: boolean; } +export interface UpdateNotificationMessage { + message: string; +} export const defaultUpdateInterval = 12 * 60 * 60 * 1000; export const initialUpdateStartup = 5 * 1000; @@ -70,6 +74,9 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { public checkForUpdates = async (isManualCheck = false, includePreReleases = this.includePreReleases_): Promise => { if (this.isUpdateInProgress) { this.logger_.info('Update check already in progress. Waiting for the current check to finish.'); + if (this.isManualCheckInProgress) { + this.sendNotification('Update check already in progress.'); + } return; } @@ -186,7 +193,7 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { private onUpdateNotAvailable = (_info: UpdateInfo): void => { if (this.isManualCheckInProgress) { - this.window_.webContents.send(AutoUpdaterEvents.UpdateNotAvailable); + this.sendNotification('Update is not available.'); } this.unlockUpdateProcess(); @@ -224,4 +231,11 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { this.logger_.info('Unlocking update process'); this.isUpdateInProgress = false; }; + + private sendNotification = (message: string): void => { + const notificationMessage: UpdateNotificationMessage = { + message: message, + }; + this.window_.webContents.send(AutoUpdaterEvents.NotificationMessage, notificationMessage); + }; } From d03c27cbbfe1f0506e53de942d0ff7b4bffe7856 Mon Sep 17 00:00:00 2001 From: AliceHincu Date: Wed, 25 Sep 2024 18:11:59 +0300 Subject: [PATCH 5/6] Seamless-Updates - notification message is now shown everytime instead of once --- .../UpdateNotification/UpdateNotification.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx b/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx index 92b8b775710..c7b1f6d3b8e 100644 --- a/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx +++ b/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx @@ -5,7 +5,7 @@ import NotyfContext from '../NotyfContext'; import { UpdateInfo } from 'electron-updater'; import { ipcRenderer, IpcRendererEvent } from 'electron'; import { AutoUpdaterEvents, UpdateNotificationMessage } from '../../services/autoUpdater/AutoUpdaterService'; -import { NotyfEvent, NotyfNotification } from 'notyf'; +import { NotyfNotification } from 'notyf'; import { _ } from '@joplin/lib/locale'; import { htmlentities } from '@joplin/utils/html'; import shim from '@joplin/lib/shim'; @@ -20,6 +20,7 @@ export enum UpdateNotificationEvents { } const changelogLink = 'https://github.com/laurent22/joplin/releases'; +const notificationDuration = 5000; // 5 seconds window.openChangelogLink = () => { shim.openUrl(changelogLink); @@ -104,14 +105,15 @@ const UpdateNotification = ({ themeId }: UpdateNotificationProps) => { x: 'right', y: 'bottom', }, - duration: 5000, + duration: notificationDuration, }); - - notification.on(NotyfEvent.Dismiss, () => { - notificationRef.current = null; - }); - notificationRef.current = notification; + + setTimeout(() => { + if (notificationRef.current === notification) { + notificationRef.current = null; + } + }, notificationDuration); }, [notyf, theme]); useEffect(() => { From b9f018d3ca9d2286ef0ffccb0afd8db42ead3a08 Mon Sep 17 00:00:00 2001 From: AliceHincu Date: Wed, 25 Sep 2024 18:51:24 +0300 Subject: [PATCH 6/6] Seamless-Updates - notify if update check is already in progress --- packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts index fabec687100..15cf06331d0 100644 --- a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts +++ b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts @@ -74,7 +74,7 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface { public checkForUpdates = async (isManualCheck = false, includePreReleases = this.includePreReleases_): Promise => { if (this.isUpdateInProgress) { this.logger_.info('Update check already in progress. Waiting for the current check to finish.'); - if (this.isManualCheckInProgress) { + if (isManualCheck) { this.sendNotification('Update check already in progress.'); } return;