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/gui/UpdateNotification/UpdateNotification.tsx b/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx index 05ea6e5025f..c7b1f6d3b8e 100644 --- a/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx +++ b/packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx @@ -4,8 +4,8 @@ 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 { NotyfEvent, NotyfNotification } from 'notyf'; +import { AutoUpdaterEvents, UpdateNotificationMessage } from '../../services/autoUpdater/AutoUpdaterService'; +import { NotyfNotification } from 'notyf'; import { _ } from '@joplin/lib/locale'; import { htmlentities } from '@joplin/utils/html'; import shim from '@joplin/lib/shim'; @@ -16,11 +16,11 @@ interface UpdateNotificationProps { export enum UpdateNotificationEvents { ApplyUpdate = 'apply-update', - UpdateNotAvailable = 'update-not-available', Dismiss = 'dismiss-update-notification', } const changelogLink = 'https://github.com/laurent22/joplin/releases'; +const notificationDuration = 5000; // 5 seconds window.openChangelogLink = () => { shim.openUrl(changelogLink); @@ -87,10 +87,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 = `
@@ -105,28 +105,29 @@ 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(() => { 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 93105a08f9c..15cf06331d0 100644 --- a/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts +++ b/packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts @@ -14,6 +14,14 @@ 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; @@ -53,6 +61,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; @@ -62,15 +71,28 @@ 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.'); + if (isManualCheck) { + this.sendNotification('Update check already in progress.'); + } + return; + } + + this.lockUpdateProcess(); + this.isManualCheckInProgress = isManualCheck; + try { - this.isManualCheckInProgress = isManualCheck; + autoUpdater.allowPrerelease = includePreReleases; 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 +156,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}`); } @@ -145,6 +166,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'); @@ -171,9 +193,10 @@ 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(); this.logger_.info('Update not available.'); }; @@ -187,6 +210,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 +221,21 @@ 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; + }; + + private sendNotification = (message: string): void => { + const notificationMessage: UpdateNotificationMessage = { + message: message, + }; + this.window_.webContents.send(AutoUpdaterEvents.NotificationMessage, notificationMessage); + }; } 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,