Skip to content

Commit

Permalink
Desktop: Seamless-Updates - added flow for automatic updates for rele…
Browse files Browse the repository at this point in the history
…ases (#10857)

Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
  • Loading branch information
AliceHincu and laurent22 authored Aug 17, 2024
1 parent 0cbf8d8 commit 7216301
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 48 deletions.
32 changes: 22 additions & 10 deletions packages/app-desktop/ElectronAppWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Logger, { LoggerWrapper } from '@joplin/utils/Logger';
import { PluginMessage } from './services/plugins/PluginRunner';
// import AutoUpdaterService from './services/autoUpdater/AutoUpdaterService';
import shim from '@joplin/lib/shim';
import AutoUpdaterService 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';
Expand Down Expand Up @@ -42,7 +43,7 @@ export default class ElectronAppWrapper {
private rendererProcessQuitReply_: RendererProcessQuitReply = null;
private pluginWindows_: PluginWindows = {};
private initialCallbackUrl_: string = null;
// private updaterService_: AutoUpdaterService = null;
private updaterService_: AutoUpdaterService = null;
private customProtocolHandler_: CustomProtocolHandler = null;

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
Expand Down Expand Up @@ -326,6 +327,10 @@ export default class ElectronAppWrapper {
}
});

ipcMain.on('apply-update-now', () => {
this.updaterService_.updateApp();
});

// Let us register listeners on the window, so we can update the state
// automatically (the listeners will be removed when the window is closed)
// and restore the maximized or full screen state
Expand Down Expand Up @@ -358,6 +363,7 @@ export default class ElectronAppWrapper {
}

public quit() {
this.stopLookingForUpdates();
this.electronApp_.quit();
}

Expand Down Expand Up @@ -462,6 +468,19 @@ export default class ElectronAppWrapper {
this.customProtocolHandler_ ??= handleCustomProtocols(logger);
}

public initializeAutoUpdaterService(logger: LoggerWrapper, initializedShim: typeof ShimType, devMode: boolean, includePreReleases: boolean) {
if (shim.isWindows() || shim.isMac()) {
this.updaterService_ = new AutoUpdaterService(this.win_, logger, initializedShim, devMode, includePreReleases);
this.updaterService_.startPeriodicUpdateCheck();
}
}

public stopLookingForUpdates() {
if (this.updaterService_ !== null) {
this.updaterService_.stopPeriodicUpdateCheck();
}
}

public getCustomProtocolHandler() {
return this.customProtocolHandler_;
}
Expand All @@ -476,13 +495,6 @@ export default class ElectronAppWrapper {

this.createWindow();

// TODO: Disabled for now - needs to be behind a feature flag

// if (!shim.isLinux()) {
// this.updaterService_ = new AutoUpdaterService();
// this.updaterService_.startPeriodicUpdateCheck();
// }

this.electronApp_.on('before-quit', () => {
this.willQuitApp_ = true;
});
Expand Down
34 changes: 23 additions & 11 deletions packages/app-desktop/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/Plug
import resourceEditWatcherReducer, { defaultState as resourceEditWatcherDefaultState } from '@joplin/lib/services/ResourceEditWatcher/reducer';
import PluginRunner from './services/plugins/PluginRunner';
import PlatformImplementation from './services/plugins/PlatformImplementation';
import shim from '@joplin/lib/shim';
import type ShimType from '@joplin/lib/shim';
const shim: typeof ShimType = require('@joplin/lib/shim').default;
import AlarmService from '@joplin/lib/services/AlarmService';
import AlarmServiceDriverNode from '@joplin/lib/services/AlarmServiceDriverNode';
import Logger, { TargetType } from '@joplin/utils/Logger';
Expand Down Expand Up @@ -569,17 +570,19 @@ class Application extends BaseApplication {
// Note: Auto-update is a misnomer in the code.
// The code below only checks, if a new version is available.
// We only allow Windows and macOS users to automatically check for updates
if (shim.isWindows() || shim.isMac()) {
const runAutoUpdateCheck = () => {
if (Setting.value('autoUpdateEnabled')) {
void checkForUpdates(true, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
}
};
if (!Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
if (shim.isWindows() || shim.isMac()) {
const runAutoUpdateCheck = () => {
if (Setting.value('autoUpdateEnabled')) {
void checkForUpdates(true, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
}
};

// Initial check on startup
shim.setTimeout(() => { runAutoUpdateCheck(); }, 5000);
// Then every x hours
shim.setInterval(() => { runAutoUpdateCheck(); }, 12 * 60 * 60 * 1000);
// Initial check on startup
shim.setTimeout(() => { runAutoUpdateCheck(); }, 5000);
// Then every x hours
shim.setInterval(() => { runAutoUpdateCheck(); }, 12 * 60 * 60 * 1000);
}
}

initializeUserFetcher();
Expand Down Expand Up @@ -685,6 +688,15 @@ 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'),
);
}

// setTimeout(() => {
// void populateDatabase(reg.db(), {
// clearDatabase: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AutoUpdaterEvents } 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';

interface UpdateNotificationProps {
themeId: number;
Expand All @@ -21,7 +22,7 @@ export enum UpdateNotificationEvents {
const changelogLink = 'https://github.com/laurent22/joplin/releases';

window.openChangelogLink = () => {
ipcRenderer.send('open-link', changelogLink);
shim.openUrl(changelogLink);
};

const UpdateNotification = ({ themeId }: UpdateNotificationProps) => {
Expand Down
75 changes: 49 additions & 26 deletions packages/app-desktop/services/autoUpdater/AutoUpdaterService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { app } from 'electron';
import { BrowserWindow } from 'electron';
import { autoUpdater, UpdateInfo } from 'electron-updater';
import log from 'electron-log';
import path = require('path');
import { setInterval } from 'timers';

import Logger, { LoggerWrapper } from '@joplin/utils/Logger';
import type ShimType from '@joplin/lib/shim';

export enum AutoUpdaterEvents {
CheckingForUpdate = 'checking-for-update',
Expand All @@ -24,48 +24,69 @@ export interface AutoUpdaterServiceInterface {
}

export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
private window_: BrowserWindow;
private logger_: LoggerWrapper;
private initializedShim_: typeof ShimType;
private devMode_: boolean;
private updatePollInterval_: ReturnType<typeof setInterval>|null = null;

public constructor() {
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) {
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_ = setInterval(() => {
this.updatePollInterval_ = this.initializedShim_.setInterval(() => {
void this.checkForUpdates();
}, interval);
setTimeout(this.checkForUpdates, initialUpdateStartup);
this.initializedShim_.setTimeout(this.checkForUpdates, initialUpdateStartup);
};

public stopPeriodicUpdateCheck = (): void => {
if (this.updatePollInterval_) {
clearInterval(this.updatePollInterval_);
this.initializedShim_.clearInterval(this.updatePollInterval_);
this.updatePollInterval_ = null;
}
};

public checkForUpdates = async (): Promise<void> => {
try {
await autoUpdater.checkForUpdates(); // Use async/await
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();
}
} catch (error) {
log.error('Failed to check for updates:', error);
this.logger_.error('Failed to check for updates:', error);
if (error.message.includes('ERR_CONNECTION_REFUSED')) {
log.info('Server is not reachable. Will try again later.');
this.logger_.info('Server is not reachable. Will try again later.');
}
}
};

private configureAutoUpdater = (): void => {
autoUpdater.logger = log;
log.transports.file.level = 'info';
if (this.electronIsDev()) {
log.info('Development mode: using dev-app-update.yml');
autoUpdater.logger = (this.logger_) as Logger;
if (this.devMode_) {
this.logger_.info('Development mode: using dev-app-update.yml');
autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml');
autoUpdater.forceDevUpdateConfig = true;
autoUpdater.forceDevUpdateConfig = this.enableDevMode;
}

autoUpdater.autoDownload = false;
autoUpdater.autoDownload = this.enableAutoDownload;
autoUpdater.autoInstallOnAppQuit = this.autoInstallOnAppQuit;
autoUpdater.allowPrerelease = this.includePreReleases_;
autoUpdater.allowDowngrade = this.allowDowngrade;

autoUpdater.on(AutoUpdaterEvents.CheckingForUpdate, this.onCheckingForUpdate);
autoUpdater.on(AutoUpdaterEvents.UpdateNotAvailable, this.onUpdateNotAvailable);
Expand All @@ -75,34 +96,36 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
autoUpdater.on(AutoUpdaterEvents.Error, this.onError);
};

private electronIsDev = (): boolean => !app.isPackaged;

private onCheckingForUpdate = () => {
log.info('Checking for update...');
this.logger_.info('Checking for update...');
};

private onUpdateNotAvailable = (_info: UpdateInfo): void => {
log.info('Update not available.');
this.logger_.info('Update not available.');
};

private onUpdateAvailable = (info: UpdateInfo): void => {
log.info(`Update available: ${info.version}.`);
this.logger_.info(`Update available: ${info.version}.`);
};

private onDownloadProgress = (progressObj: { bytesPerSecond: number; percent: number; transferred: number; total: number }): void => {
log.info(`Download progress... ${progressObj.percent}% completed`);
this.logger_.info(`Download progress... ${progressObj.percent}% completed`);
};

private onUpdateDownloaded = (info: UpdateInfo): void => {
log.info('Update downloaded. It will be installed on restart.');
this.logger_.info('Update downloaded.');
void this.promptUserToUpdate(info);
};

private onError = (error: Error): void => {
log.error('Error in auto-updater.', error);
this.logger_.error('Error in auto-updater.', error);
};

private promptUserToUpdate = async (info: UpdateInfo): Promise<void> => {
log.info(`Update is available: ${info.version}.`);
this.window_.webContents.send(AutoUpdaterEvents.UpdateDownloaded, info);
};

public updateApp = (): void => {
autoUpdater.quitAndInstall(false, true);
};
}
12 changes: 12 additions & 0 deletions packages/lib/models/settings/builtInMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1552,6 +1552,18 @@ const builtInMetadata = (Setting: typeof SettingType) => {
appTypes: [AppType.Mobile],
},

'featureFlag.autoUpdaterServiceEnabled': {
value: true,
type: SettingItemType.Bool,
public: true,
storage: SettingStorage.File,
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.',
section: 'application',
isGlobal: true,
},


// 'featureFlag.syncAccurateTimestamps': {
// value: false,
// type: SettingItemType.Bool,
Expand Down

0 comments on commit 7216301

Please sign in to comment.