Skip to content

Commit 7216301

Browse files
Desktop: Seamless-Updates - added flow for automatic updates for releases (laurent22#10857)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
1 parent 0cbf8d8 commit 7216301

File tree

5 files changed

+108
-48
lines changed

5 files changed

+108
-48
lines changed

packages/app-desktop/ElectronAppWrapper.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import Logger, { LoggerWrapper } from '@joplin/utils/Logger';
22
import { PluginMessage } from './services/plugins/PluginRunner';
3-
// import AutoUpdaterService from './services/autoUpdater/AutoUpdaterService';
4-
import shim from '@joplin/lib/shim';
3+
import AutoUpdaterService from './services/autoUpdater/AutoUpdaterService';
4+
import type ShimType from '@joplin/lib/shim';
5+
const shim: typeof ShimType = require('@joplin/lib/shim').default;
56
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
67

78
import { BrowserWindow, Tray, screen } from 'electron';
@@ -42,7 +43,7 @@ export default class ElectronAppWrapper {
4243
private rendererProcessQuitReply_: RendererProcessQuitReply = null;
4344
private pluginWindows_: PluginWindows = {};
4445
private initialCallbackUrl_: string = null;
45-
// private updaterService_: AutoUpdaterService = null;
46+
private updaterService_: AutoUpdaterService = null;
4647
private customProtocolHandler_: CustomProtocolHandler = null;
4748

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

330+
ipcMain.on('apply-update-now', () => {
331+
this.updaterService_.updateApp();
332+
});
333+
329334
// Let us register listeners on the window, so we can update the state
330335
// automatically (the listeners will be removed when the window is closed)
331336
// and restore the maximized or full screen state
@@ -358,6 +363,7 @@ export default class ElectronAppWrapper {
358363
}
359364

360365
public quit() {
366+
this.stopLookingForUpdates();
361367
this.electronApp_.quit();
362368
}
363369

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

471+
public initializeAutoUpdaterService(logger: LoggerWrapper, initializedShim: typeof ShimType, devMode: boolean, includePreReleases: boolean) {
472+
if (shim.isWindows() || shim.isMac()) {
473+
this.updaterService_ = new AutoUpdaterService(this.win_, logger, initializedShim, devMode, includePreReleases);
474+
this.updaterService_.startPeriodicUpdateCheck();
475+
}
476+
}
477+
478+
public stopLookingForUpdates() {
479+
if (this.updaterService_ !== null) {
480+
this.updaterService_.stopPeriodicUpdateCheck();
481+
}
482+
}
483+
465484
public getCustomProtocolHandler() {
466485
return this.customProtocolHandler_;
467486
}
@@ -476,13 +495,6 @@ export default class ElectronAppWrapper {
476495

477496
this.createWindow();
478497

479-
// TODO: Disabled for now - needs to be behind a feature flag
480-
481-
// if (!shim.isLinux()) {
482-
// this.updaterService_ = new AutoUpdaterService();
483-
// this.updaterService_.startPeriodicUpdateCheck();
484-
// }
485-
486498
this.electronApp_.on('before-quit', () => {
487499
this.willQuitApp_ = true;
488500
});

packages/app-desktop/app.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/Plug
55
import resourceEditWatcherReducer, { defaultState as resourceEditWatcherDefaultState } from '@joplin/lib/services/ResourceEditWatcher/reducer';
66
import PluginRunner from './services/plugins/PluginRunner';
77
import PlatformImplementation from './services/plugins/PlatformImplementation';
8-
import shim from '@joplin/lib/shim';
8+
import type ShimType from '@joplin/lib/shim';
9+
const shim: typeof ShimType = require('@joplin/lib/shim').default;
910
import AlarmService from '@joplin/lib/services/AlarmService';
1011
import AlarmServiceDriverNode from '@joplin/lib/services/AlarmServiceDriverNode';
1112
import Logger, { TargetType } from '@joplin/utils/Logger';
@@ -569,17 +570,19 @@ class Application extends BaseApplication {
569570
// Note: Auto-update is a misnomer in the code.
570571
// The code below only checks, if a new version is available.
571572
// We only allow Windows and macOS users to automatically check for updates
572-
if (shim.isWindows() || shim.isMac()) {
573-
const runAutoUpdateCheck = () => {
574-
if (Setting.value('autoUpdateEnabled')) {
575-
void checkForUpdates(true, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
576-
}
577-
};
573+
if (!Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
574+
if (shim.isWindows() || shim.isMac()) {
575+
const runAutoUpdateCheck = () => {
576+
if (Setting.value('autoUpdateEnabled')) {
577+
void checkForUpdates(true, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
578+
}
579+
};
578580

579-
// Initial check on startup
580-
shim.setTimeout(() => { runAutoUpdateCheck(); }, 5000);
581-
// Then every x hours
582-
shim.setInterval(() => { runAutoUpdateCheck(); }, 12 * 60 * 60 * 1000);
581+
// Initial check on startup
582+
shim.setTimeout(() => { runAutoUpdateCheck(); }, 5000);
583+
// Then every x hours
584+
shim.setInterval(() => { runAutoUpdateCheck(); }, 12 * 60 * 60 * 1000);
585+
}
583586
}
584587

585588
initializeUserFetcher();
@@ -685,6 +688,15 @@ class Application extends BaseApplication {
685688
SearchEngine.instance().scheduleSyncTables();
686689
});
687690

691+
if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
692+
bridge().electronApp().initializeAutoUpdaterService(
693+
Logger.create('AutoUpdaterService'),
694+
shim,
695+
Setting.value('env') === 'dev',
696+
Setting.value('autoUpdate.includePreReleases'),
697+
);
698+
}
699+
688700
// setTimeout(() => {
689701
// void populateDatabase(reg.db(), {
690702
// clearDatabase: true,

packages/app-desktop/gui/UpdateNotification/UpdateNotification.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { AutoUpdaterEvents } from '../../services/autoUpdater/AutoUpdaterService
88
import { NotyfNotification } from 'notyf';
99
import { _ } from '@joplin/lib/locale';
1010
import { htmlentities } from '@joplin/utils/html';
11+
import shim from '@joplin/lib/shim';
1112

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

2324
window.openChangelogLink = () => {
24-
ipcRenderer.send('open-link', changelogLink);
25+
shim.openUrl(changelogLink);
2526
};
2627

2728
const UpdateNotification = ({ themeId }: UpdateNotificationProps) => {
Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { app } from 'electron';
1+
import { BrowserWindow } from 'electron';
22
import { autoUpdater, UpdateInfo } from 'electron-updater';
3-
import log from 'electron-log';
43
import path = require('path');
54
import { setInterval } from 'timers';
6-
5+
import Logger, { LoggerWrapper } from '@joplin/utils/Logger';
6+
import type ShimType from '@joplin/lib/shim';
77

88
export enum AutoUpdaterEvents {
99
CheckingForUpdate = 'checking-for-update',
@@ -24,48 +24,69 @@ export interface AutoUpdaterServiceInterface {
2424
}
2525

2626
export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
27+
private window_: BrowserWindow;
28+
private logger_: LoggerWrapper;
29+
private initializedShim_: typeof ShimType;
30+
private devMode_: boolean;
2731
private updatePollInterval_: ReturnType<typeof setInterval>|null = null;
28-
29-
public constructor() {
32+
private enableDevMode = true; // force the updater to work in "dev" mode
33+
private enableAutoDownload = false; // automatically download an update when it is found
34+
private autoInstallOnAppQuit = false; // automatically install the downloaded update once the user closes the application
35+
private includePreReleases_ = false;
36+
private allowDowngrade = false;
37+
38+
public constructor(mainWindow: BrowserWindow, logger: LoggerWrapper, initializedShim: typeof ShimType, devMode: boolean, includePreReleases: boolean) {
39+
this.window_ = mainWindow;
40+
this.logger_ = logger;
41+
this.initializedShim_ = initializedShim;
42+
this.devMode_ = devMode;
43+
this.includePreReleases_ = includePreReleases;
3044
this.configureAutoUpdater();
3145
}
3246

3347
public startPeriodicUpdateCheck = (interval: number = defaultUpdateInterval): void => {
3448
this.stopPeriodicUpdateCheck();
35-
this.updatePollInterval_ = setInterval(() => {
49+
this.updatePollInterval_ = this.initializedShim_.setInterval(() => {
3650
void this.checkForUpdates();
3751
}, interval);
38-
setTimeout(this.checkForUpdates, initialUpdateStartup);
52+
this.initializedShim_.setTimeout(this.checkForUpdates, initialUpdateStartup);
3953
};
4054

4155
public stopPeriodicUpdateCheck = (): void => {
4256
if (this.updatePollInterval_) {
43-
clearInterval(this.updatePollInterval_);
57+
this.initializedShim_.clearInterval(this.updatePollInterval_);
4458
this.updatePollInterval_ = null;
4559
}
4660
};
4761

4862
public checkForUpdates = async (): Promise<void> => {
4963
try {
50-
await autoUpdater.checkForUpdates(); // Use async/await
64+
if (this.includePreReleases_) {
65+
// 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
66+
this.logger_.info('To be implemented...');
67+
} else {
68+
await autoUpdater.checkForUpdates();
69+
}
5170
} catch (error) {
52-
log.error('Failed to check for updates:', error);
71+
this.logger_.error('Failed to check for updates:', error);
5372
if (error.message.includes('ERR_CONNECTION_REFUSED')) {
54-
log.info('Server is not reachable. Will try again later.');
73+
this.logger_.info('Server is not reachable. Will try again later.');
5574
}
5675
}
5776
};
5877

5978
private configureAutoUpdater = (): void => {
60-
autoUpdater.logger = log;
61-
log.transports.file.level = 'info';
62-
if (this.electronIsDev()) {
63-
log.info('Development mode: using dev-app-update.yml');
79+
autoUpdater.logger = (this.logger_) as Logger;
80+
if (this.devMode_) {
81+
this.logger_.info('Development mode: using dev-app-update.yml');
6482
autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml');
65-
autoUpdater.forceDevUpdateConfig = true;
83+
autoUpdater.forceDevUpdateConfig = this.enableDevMode;
6684
}
6785

68-
autoUpdater.autoDownload = false;
86+
autoUpdater.autoDownload = this.enableAutoDownload;
87+
autoUpdater.autoInstallOnAppQuit = this.autoInstallOnAppQuit;
88+
autoUpdater.allowPrerelease = this.includePreReleases_;
89+
autoUpdater.allowDowngrade = this.allowDowngrade;
6990

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

78-
private electronIsDev = (): boolean => !app.isPackaged;
79-
8099
private onCheckingForUpdate = () => {
81-
log.info('Checking for update...');
100+
this.logger_.info('Checking for update...');
82101
};
83102

84103
private onUpdateNotAvailable = (_info: UpdateInfo): void => {
85-
log.info('Update not available.');
104+
this.logger_.info('Update not available.');
86105
};
87106

88107
private onUpdateAvailable = (info: UpdateInfo): void => {
89-
log.info(`Update available: ${info.version}.`);
108+
this.logger_.info(`Update available: ${info.version}.`);
90109
};
91110

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

96115
private onUpdateDownloaded = (info: UpdateInfo): void => {
97-
log.info('Update downloaded. It will be installed on restart.');
116+
this.logger_.info('Update downloaded.');
98117
void this.promptUserToUpdate(info);
99118
};
100119

101120
private onError = (error: Error): void => {
102-
log.error('Error in auto-updater.', error);
121+
this.logger_.error('Error in auto-updater.', error);
103122
};
104123

105124
private promptUserToUpdate = async (info: UpdateInfo): Promise<void> => {
106-
log.info(`Update is available: ${info.version}.`);
125+
this.window_.webContents.send(AutoUpdaterEvents.UpdateDownloaded, info);
126+
};
127+
128+
public updateApp = (): void => {
129+
autoUpdater.quitAndInstall(false, true);
107130
};
108131
}

packages/lib/models/settings/builtInMetadata.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,18 @@ const builtInMetadata = (Setting: typeof SettingType) => {
15521552
appTypes: [AppType.Mobile],
15531553
},
15541554

1555+
'featureFlag.autoUpdaterServiceEnabled': {
1556+
value: true,
1557+
type: SettingItemType.Bool,
1558+
public: true,
1559+
storage: SettingStorage.File,
1560+
label: () => 'Enable auto-updates',
1561+
description: () => 'Enable this feature to receive notifications about updates and install them instead of manually downloading them. Restart app to start receiving auto-updates.',
1562+
section: 'application',
1563+
isGlobal: true,
1564+
},
1565+
1566+
15551567
// 'featureFlag.syncAccurateTimestamps': {
15561568
// value: false,
15571569
// type: SettingItemType.Bool,

0 commit comments

Comments
 (0)