2024-08-17 13:19:05 +02:00
|
|
|
import { BrowserWindow } from 'electron';
|
2024-07-28 13:09:30 +02:00
|
|
|
import { autoUpdater, UpdateInfo } from 'electron-updater';
|
|
|
|
import path = require('path');
|
2024-08-17 13:19:05 +02:00
|
|
|
import Logger, { LoggerWrapper } from '@joplin/utils/Logger';
|
|
|
|
import type ShimType from '@joplin/lib/shim';
|
2024-08-27 19:04:18 +02:00
|
|
|
const shim: typeof ShimType = require('@joplin/lib/shim').default;
|
|
|
|
import { GitHubRelease, GitHubReleaseAsset } from '../../utils/checkForUpdatesUtils';
|
|
|
|
import * as semver from 'semver';
|
2024-07-28 13:09:30 +02:00
|
|
|
|
|
|
|
export enum AutoUpdaterEvents {
|
|
|
|
CheckingForUpdate = 'checking-for-update',
|
|
|
|
UpdateAvailable = 'update-available',
|
|
|
|
UpdateNotAvailable = 'update-not-available',
|
|
|
|
Error = 'error',
|
|
|
|
DownloadProgress = 'download-progress',
|
|
|
|
UpdateDownloaded = 'update-downloaded',
|
|
|
|
}
|
|
|
|
|
2024-08-27 19:04:18 +02:00
|
|
|
export const defaultUpdateInterval = 12 * 60 * 60 * 1000;
|
|
|
|
export const initialUpdateStartup = 5 * 1000;
|
|
|
|
const releasesLink = 'https://objects.joplinusercontent.com/r/releases';
|
2024-09-13 12:03:14 +02:00
|
|
|
export type Architecture = typeof process.arch;
|
|
|
|
interface PlatformAssets {
|
|
|
|
[platform: string]: {
|
|
|
|
[arch in Architecture]?: string;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
const supportedPlatformAssets: PlatformAssets = {
|
|
|
|
'darwin': {
|
|
|
|
'x64': 'latest-mac.yml',
|
|
|
|
'arm64': 'latest-mac-arm64.yml',
|
|
|
|
},
|
|
|
|
'win32': {
|
|
|
|
'x64': 'latest.yml',
|
|
|
|
'ia32': 'latest.yml',
|
|
|
|
},
|
2024-09-02 13:24:19 +02:00
|
|
|
};
|
2024-07-28 13:09:30 +02:00
|
|
|
|
|
|
|
export interface AutoUpdaterServiceInterface {
|
2024-09-21 14:02:22 +02:00
|
|
|
checkForUpdates(isManualCheck: boolean): void;
|
2024-08-27 19:04:18 +02:00
|
|
|
updateApp(): void;
|
2024-09-02 13:24:19 +02:00
|
|
|
fetchLatestRelease(includePreReleases: boolean): Promise<GitHubRelease>;
|
2024-09-13 12:03:14 +02:00
|
|
|
getDownloadUrlForPlatform(release: GitHubRelease, platform: string, arch: string): string;
|
2024-07-28 13:09:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
|
2024-08-17 13:19:05 +02:00
|
|
|
private window_: BrowserWindow;
|
|
|
|
private logger_: LoggerWrapper;
|
|
|
|
private devMode_: boolean;
|
|
|
|
private enableDevMode = true; // force the updater to work in "dev" mode
|
2024-09-21 14:02:22 +02:00
|
|
|
private enableAutoDownload = true; // automatically download an update when it is found
|
2024-08-17 13:19:05 +02:00
|
|
|
private autoInstallOnAppQuit = false; // automatically install the downloaded update once the user closes the application
|
|
|
|
private includePreReleases_ = false;
|
|
|
|
private allowDowngrade = false;
|
2024-09-21 14:02:22 +02:00
|
|
|
private isManualCheckInProgress = false;
|
2024-08-17 13:19:05 +02:00
|
|
|
|
2024-08-27 19:04:18 +02:00
|
|
|
public constructor(mainWindow: BrowserWindow, logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) {
|
2024-08-17 13:19:05 +02:00
|
|
|
this.window_ = mainWindow;
|
|
|
|
this.logger_ = logger;
|
|
|
|
this.devMode_ = devMode;
|
|
|
|
this.includePreReleases_ = includePreReleases;
|
2024-07-28 13:09:30 +02:00
|
|
|
this.configureAutoUpdater();
|
|
|
|
}
|
|
|
|
|
2024-09-21 14:02:22 +02:00
|
|
|
public checkForUpdates = async (isManualCheck = false): Promise<void> => {
|
2024-08-27 19:04:18 +02:00
|
|
|
try {
|
2024-09-21 14:02:22 +02:00
|
|
|
this.isManualCheckInProgress = isManualCheck;
|
2024-09-02 13:24:19 +02:00
|
|
|
await this.checkForLatestRelease();
|
2024-08-27 19:04:18 +02:00
|
|
|
} 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.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
public updateApp = (): void => {
|
|
|
|
autoUpdater.quitAndInstall(false, true);
|
2024-07-28 13:09:30 +02:00
|
|
|
};
|
|
|
|
|
2024-09-02 13:24:19 +02:00
|
|
|
public fetchLatestRelease = async (includePreReleases: boolean): Promise<GitHubRelease> => {
|
|
|
|
const releases = await this.fetchReleases(includePreReleases);
|
|
|
|
const release = releases[0];
|
|
|
|
|
|
|
|
if (!release) {
|
|
|
|
throw new Error('No suitable release found');
|
|
|
|
}
|
|
|
|
|
|
|
|
return release;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2024-09-13 12:03:14 +02:00
|
|
|
public getDownloadUrlForPlatform(release: GitHubRelease, platform: string, arch: string): string {
|
|
|
|
if (!supportedPlatformAssets[platform]) {
|
2024-09-02 13:24:19 +02:00
|
|
|
throw new Error(`The AutoUpdaterService does not support the following platform: ${platform}`);
|
|
|
|
}
|
|
|
|
|
2024-09-13 12:03:14 +02:00
|
|
|
const platformAssets = supportedPlatformAssets[platform];
|
|
|
|
const assetName: string | undefined = platformAssets ? platformAssets[arch as Architecture] : undefined;
|
|
|
|
if (!assetName) {
|
|
|
|
throw new Error(`The AutoUpdaterService does not support the architecture: ${arch} for platform: ${platform}`);
|
|
|
|
}
|
|
|
|
|
2024-09-02 13:24:19 +02:00
|
|
|
const asset: GitHubReleaseAsset = release.assets.find(a => a.name === assetName);
|
|
|
|
if (!asset) {
|
2024-09-13 12:03:14 +02:00
|
|
|
throw new Error(`Yml file: ${assetName} not found for version: ${release.tag_name} platform: ${platform} and architecture: ${arch}`);
|
2024-09-02 13:24:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return asset.browser_download_url;
|
|
|
|
}
|
|
|
|
|
|
|
|
private fetchReleases = async (includePreReleases: boolean): Promise<GitHubRelease[]> => {
|
2024-08-27 19:04:18 +02:00
|
|
|
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)}`);
|
2024-07-28 13:09:30 +02:00
|
|
|
}
|
2024-08-27 19:04:18 +02:00
|
|
|
|
2024-09-02 13:24:19 +02:00
|
|
|
const releases: GitHubRelease[] = await response.json();
|
|
|
|
const sortedReleasesByVersion = releases.sort((a, b) => semver.rcompare(a.tag_name, b.tag_name));
|
|
|
|
const filteredReleases = sortedReleasesByVersion.filter(release => includePreReleases || !release.prerelease);
|
|
|
|
|
|
|
|
return filteredReleases;
|
2024-07-28 13:09:30 +02:00
|
|
|
};
|
|
|
|
|
2024-09-02 13:24:19 +02:00
|
|
|
private checkForLatestRelease = async (): Promise<void> => {
|
2024-07-28 13:09:30 +02:00
|
|
|
try {
|
2024-09-02 13:24:19 +02:00
|
|
|
const release: GitHubRelease = await this.fetchLatestRelease(this.includePreReleases_);
|
|
|
|
|
|
|
|
try {
|
2024-09-13 12:03:14 +02:00
|
|
|
let assetUrl = this.getDownloadUrlForPlatform(release, shim.platformName(), process.arch);
|
2024-09-02 13:24:19 +02:00
|
|
|
// electron's autoUpdater appends automatically the platform's yml file to the link so we should remove it
|
|
|
|
assetUrl = assetUrl.substring(0, assetUrl.lastIndexOf('/'));
|
|
|
|
autoUpdater.setFeedURL({ provider: 'generic', url: assetUrl });
|
|
|
|
await autoUpdater.checkForUpdates();
|
2024-09-21 14:02:22 +02:00
|
|
|
this.isManualCheckInProgress = false;
|
2024-09-02 13:24:19 +02:00
|
|
|
} catch (error) {
|
|
|
|
this.logger_.error(`Update download url failed: ${error.message}`);
|
2024-08-17 13:19:05 +02:00
|
|
|
}
|
2024-09-02 13:24:19 +02:00
|
|
|
|
2024-07-28 13:09:30 +02:00
|
|
|
} catch (error) {
|
2024-09-02 13:24:19 +02:00
|
|
|
this.logger_.error(`Fetching releases failed: ${error.message}`);
|
2024-07-28 13:09:30 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private configureAutoUpdater = (): void => {
|
2024-08-17 13:19:05 +02:00
|
|
|
autoUpdater.logger = (this.logger_) as Logger;
|
|
|
|
if (this.devMode_) {
|
|
|
|
this.logger_.info('Development mode: using dev-app-update.yml');
|
2024-07-28 13:09:30 +02:00
|
|
|
autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml');
|
2024-08-17 13:19:05 +02:00
|
|
|
autoUpdater.forceDevUpdateConfig = this.enableDevMode;
|
2024-07-28 13:09:30 +02:00
|
|
|
}
|
|
|
|
|
2024-08-17 13:19:05 +02:00
|
|
|
autoUpdater.autoDownload = this.enableAutoDownload;
|
|
|
|
autoUpdater.autoInstallOnAppQuit = this.autoInstallOnAppQuit;
|
|
|
|
autoUpdater.allowPrerelease = this.includePreReleases_;
|
|
|
|
autoUpdater.allowDowngrade = this.allowDowngrade;
|
2024-07-28 13:09:30 +02:00
|
|
|
|
|
|
|
autoUpdater.on(AutoUpdaterEvents.CheckingForUpdate, this.onCheckingForUpdate);
|
|
|
|
autoUpdater.on(AutoUpdaterEvents.UpdateNotAvailable, this.onUpdateNotAvailable);
|
|
|
|
autoUpdater.on(AutoUpdaterEvents.UpdateAvailable, this.onUpdateAvailable);
|
|
|
|
autoUpdater.on(AutoUpdaterEvents.DownloadProgress, this.onDownloadProgress);
|
|
|
|
autoUpdater.on(AutoUpdaterEvents.UpdateDownloaded, this.onUpdateDownloaded);
|
|
|
|
autoUpdater.on(AutoUpdaterEvents.Error, this.onError);
|
|
|
|
};
|
|
|
|
|
|
|
|
private onCheckingForUpdate = () => {
|
2024-08-17 13:19:05 +02:00
|
|
|
this.logger_.info('Checking for update...');
|
2024-07-28 13:09:30 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
private onUpdateNotAvailable = (_info: UpdateInfo): void => {
|
2024-09-21 14:02:22 +02:00
|
|
|
if (this.isManualCheckInProgress) {
|
|
|
|
this.window_.webContents.send(AutoUpdaterEvents.UpdateNotAvailable);
|
|
|
|
}
|
|
|
|
|
2024-08-17 13:19:05 +02:00
|
|
|
this.logger_.info('Update not available.');
|
2024-07-28 13:09:30 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
private onUpdateAvailable = (info: UpdateInfo): void => {
|
2024-08-17 13:19:05 +02:00
|
|
|
this.logger_.info(`Update available: ${info.version}.`);
|
2024-07-28 13:09:30 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
private onDownloadProgress = (progressObj: { bytesPerSecond: number; percent: number; transferred: number; total: number }): void => {
|
2024-08-17 13:19:05 +02:00
|
|
|
this.logger_.info(`Download progress... ${progressObj.percent}% completed`);
|
2024-07-28 13:09:30 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
private onUpdateDownloaded = (info: UpdateInfo): void => {
|
2024-08-17 13:19:05 +02:00
|
|
|
this.logger_.info('Update downloaded.');
|
2024-07-28 13:09:30 +02:00
|
|
|
void this.promptUserToUpdate(info);
|
|
|
|
};
|
|
|
|
|
|
|
|
private onError = (error: Error): void => {
|
2024-08-17 13:19:05 +02:00
|
|
|
this.logger_.error('Error in auto-updater.', error);
|
2024-07-28 13:09:30 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
private promptUserToUpdate = async (info: UpdateInfo): Promise<void> => {
|
2024-08-17 13:19:05 +02:00
|
|
|
this.window_.webContents.send(AutoUpdaterEvents.UpdateDownloaded, info);
|
|
|
|
};
|
2024-07-28 13:09:30 +02:00
|
|
|
}
|