Add electron updater with custom updater function (#56493)

* Add `electron-updater`

* Implement custom updater

* Provide custom updater implementation to `electron-updater`

* Add a light check for checksum format

* Improve comments

* SHA-256 -> SHA-512

* Code style fix
This commit is contained in:
Grzegorz Zdunek
2025-07-11 15:28:18 +00:00
committed by GitHub
parent 7449994ba7
commit 223e4b58b2
9 changed files with 279 additions and 0 deletions
+46
View File
@@ -496,6 +496,9 @@ importers:
electron-builder:
specifier: ^26.0.12
version: 26.0.12
electron-updater:
specifier: ^6.6.5
version: 6.6.5
electron-vite:
specifier: ^3.1.0
version: 3.1.0(@swc/core@1.12.9)(vite@6.3.5(@types/node@22.15.34)(yaml@2.8.0))
@@ -3564,6 +3567,10 @@ packages:
resolution: {integrity: sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==}
engines: {node: '>=12.0.0'}
builder-util-runtime@9.3.2:
resolution: {integrity: sha512-7QDXJ1FwT6d9ZhG4kuObUUPY8/ENBS/Ky26O4hR5vbeoRGavgekS2Jxv+8sCn/v23aPGU2DXRWEeJuijN2ooYA==}
engines: {node: '>=12.0.0'}
builder-util@26.0.11:
resolution: {integrity: sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==}
@@ -4179,6 +4186,9 @@ packages:
electron-to-chromium@1.5.150:
resolution: {integrity: sha512-rOOkP2ZUMx1yL4fCxXQKDHQ8ZXwisb2OycOQVKHgvB3ZI4CvehOd4y2tfnnLDieJ3Zs1RL1Dlp3cMkyIn7nnXA==}
electron-updater@6.6.5:
resolution: {integrity: sha512-jnk38WfByl2Pb0cje02xls/pJkvkq3AQZI7usDCLriU23adkerLTkRrugbCPuUxUOa79nY1g/rokHPWHZFBKyA==}
electron-vite@3.1.0:
resolution: {integrity: sha512-M7aAzaRvSl5VO+6KN4neJCYLHLpF/iWo5ztchI/+wMxIieDZQqpbCYfaEHHHPH6eupEzfvZdLYdPdmvGqoVe0Q==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -5560,9 +5570,16 @@ packages:
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.escaperegexp@4.1.2:
resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
lodash.flattendeep@4.4.0:
resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==}
lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -6831,6 +6848,9 @@ packages:
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
tiny-typed-emitter@2.1.0:
resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==}
tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
@@ -11062,6 +11082,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
builder-util-runtime@9.3.2:
dependencies:
debug: 4.4.1
sax: 1.4.1
transitivePeerDependencies:
- supports-color
builder-util@26.0.11:
dependencies:
7zip-bin: 5.2.0
@@ -11811,6 +11838,19 @@ snapshots:
electron-to-chromium@1.5.150: {}
electron-updater@6.6.5:
dependencies:
builder-util-runtime: 9.3.2
fs-extra: 10.1.0
js-yaml: 4.1.0
lazy-val: 1.0.5
lodash.escaperegexp: 4.1.2
lodash.isequal: 4.5.0
semver: 7.7.2
tiny-typed-emitter: 2.1.0
transitivePeerDependencies:
- supports-color
electron-vite@3.1.0(@swc/core@1.12.9)(vite@6.3.5(@types/node@22.15.34)(yaml@2.8.0)):
dependencies:
'@babel/core': 7.27.7
@@ -13776,8 +13816,12 @@ snapshots:
lodash.debounce@4.0.8: {}
lodash.escaperegexp@4.1.2: {}
lodash.flattendeep@4.4.0: {}
lodash.isequal@4.5.0: {}
lodash.merge@4.6.2: {}
lodash@4.17.21: {}
@@ -15234,6 +15278,8 @@ snapshots:
tiny-invariant@1.3.3: {}
tiny-typed-emitter@2.1.0: {}
tiny-warning@1.0.3: {}
tinyglobby@0.2.14:
@@ -0,0 +1,7 @@
/**
* A publisher class is required by `electron-updater` even when using a custom provider.
* Although we don't use `electron-updater` to publish updates,
* this config allows generating `app-update.yml` file at build time (kept in
* app resources), which is required at runtime by `electron-updater` to work.
*/
export default class Noop {}
+4
View File
@@ -0,0 +1,4 @@
provider: custom
# Downloads updates to <system cache dir>/teleport-connect-dev-updater directory.
# E.g. ~/Library/Caches/teleport-connect-dev-updater on macOS.
updaterCacheDirName: teleport-connect-dev-updater
@@ -59,6 +59,7 @@ if (process.env.TEAMID) {
module.exports = {
appId,
asar: true,
publish: [{ provider: 'custom' }],
asarUnpack: '**\\*.{node,dll}',
afterPack: packed => {
// @electron-universal adds the `ElectronAsarIntegrity` key to every .plist
+1
View File
@@ -47,6 +47,7 @@
"@xterm/xterm": "^5.5.0",
"electron": "37.1.0",
"electron-builder": "^26.0.12",
"electron-updater": "^6.6.5",
"electron-vite": "^3.1.0",
"events": "3.3.0",
"jest-canvas-mock": "^2.5.2",
+2
View File
@@ -31,6 +31,7 @@ import { enableWebHandlersProtection } from 'teleterm/mainProcess/protocolHandle
import { manageRootClusterProxyHostAllowList } from 'teleterm/mainProcess/rootClusterProxyHostAllowList';
import { getRuntimeSettings } from 'teleterm/mainProcess/runtimeSettings';
import { WindowsManager } from 'teleterm/mainProcess/windowsManager';
import { AppUpdater } from 'teleterm/services/appUpdater';
import { createConfigService } from 'teleterm/services/config';
import { createFileStorage } from 'teleterm/services/fileStorage';
import { createFileLoggerService, LoggerColor } from 'teleterm/services/logger';
@@ -81,6 +82,7 @@ async function initializeApp(): Promise<void> {
nativeTheme.themeSource = configService.get('theme').value;
const windowsManager = new WindowsManager(appStateFileStorage, settings);
new AppUpdater();
process.on('uncaughtException', (error, origin) => {
logger.error(origin, error);
@@ -0,0 +1,57 @@
/**
* Teleport
* Copyright (C) 2025 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
autoUpdater,
AppUpdater as ElectronAppUpdater,
} from 'electron-updater';
import { ProviderRuntimeOptions } from 'electron-updater/out/providers/Provider';
import Logger from 'teleterm/logger';
import { ClientToolsUpdateProvider } from './clientToolsUpdateProvider';
export class AppUpdater {
private readonly logger = new Logger('AppUpdater');
constructor() {
autoUpdater.setFeedURL({
provider: 'custom',
// Wraps ClientToolsUpdateProvider to allow passing getClientToolsVersion.
updateProvider: class extends ClientToolsUpdateProvider {
constructor(
options: unknown,
updater: ElectronAppUpdater,
runtimeOptions: ProviderRuntimeOptions
) {
super(() => undefined, updater, runtimeOptions);
}
},
});
autoUpdater.logger = this.logger;
autoUpdater.autoDownload = true;
autoUpdater.allowDowngrade = true;
autoUpdater.autoInstallOnAppQuit = true;
// Enables checking for updates and downloading them in dev mode.
// It makes testing this feature easier.
// Only installing updates requires the packaged app.
// Downloads are saved to the path specified in dev-app-update.yml.
autoUpdater.forceDevUpdateConfig = true;
}
}
@@ -0,0 +1,142 @@
/**
* Teleport
* Copyright (C) 2025 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { app } from 'electron';
import {
AppUpdater,
DebUpdater,
MacUpdater,
NsisUpdater,
Provider,
ResolvedUpdateFileInfo,
RpmUpdater,
UpdateInfo,
} from 'electron-updater';
import { ProviderRuntimeOptions } from 'electron-updater/out/providers/Provider';
const CHECKSUM_FETCH_TIMEOUT = 5_000;
// Example: 99a2fe26681073de56de4229dd9cd6655fef22759579b7b9bc359e018ea1007099a2fe26681073de56de4229dd9cd6655fef22759579b7b9bc359e018ea10070 Teleport Connect-17.5.4-mac.zip
const CHECKSUM_FORMAT = /^.+\s+.+$/;
/**
* Implements electron-updater's `Provider` with client tools updates.
* The official docs does not provide examples for creating custom providers.
* This implementation is inspired by existing built-in providers, such as `GenericProvider`.
* https://github.com/electron-userland/electron-builder/blob/065c6a456e34e7f8c13cba483d433502b9325168/packages/electron-updater/src/providers/GenericProvider.ts
* */
export class ClientToolsUpdateProvider extends Provider<UpdateInfo> {
constructor(
private getClientToolsVersion: ClientToolsVersionGetter,
private nativeUpdater: AppUpdater,
runtimeOptions: ProviderRuntimeOptions
) {
super(runtimeOptions);
}
/**
* Fetches metadata about the latest available update.
* This method is called during the check for updates.
*/
override async getLatestVersion(): Promise<UpdateInfo> {
const clientTools = await this.getClientToolsVersion();
// If no client tools version is specified, return the current version
// to simulate an up-to-date state.
if (!clientTools) {
return {
version: app.getVersion(),
releaseDate: '',
path: '',
sha512: '',
files: [],
};
}
const { baseUrl, version } = clientTools;
const fileUrl = `https://${baseUrl}/${makeDownloadFilename(this.nativeUpdater, version)}`;
const sha512 = await fetchChecksum(fileUrl);
return {
version,
releaseDate: '',
path: '',
sha512: '',
files: [
{
// Effective only on Windows.
isAdminRightsRequired: true,
url: fileUrl,
sha512,
},
],
};
}
/**
* Resolves file information before downloading.
* Since full URLs are already constructed in `getLatestVersion`,
* the files are returned without modification.
*/
override resolveFiles(updateInfo: UpdateInfo): ResolvedUpdateFileInfo[] {
return updateInfo.files.map(fileInfo => ({
url: new URL(fileInfo.url),
info: fileInfo,
}));
}
}
/** Should return undefined when client tools version is not available. */
export type ClientToolsVersionGetter = () => Promise<
| {
/** Base URL for downloading Teleport packages. e.g. cdn.teleport.dev. */
baseUrl: string;
/** Version to download. */
version: string;
}
| undefined
>;
function makeDownloadFilename(updater: AppUpdater, version: string): string {
if (updater instanceof MacUpdater) {
return `Teleport Connect-${version}-mac.zip`;
}
if (updater instanceof NsisUpdater) {
return `Teleport Connect Setup-${version}.exe`;
}
if (updater instanceof RpmUpdater) {
return `teleport-connect-${version}.x86_64.rpm`;
}
if (updater instanceof DebUpdater) {
return `teleport-connect_${version}_amd64.deb`;
}
throw new Error(`Unsupported app updater: ${updater?.constructor?.name}`);
}
async function fetchChecksum(fileUrl: string): Promise<string> {
const checksumUrl = `${fileUrl}.sha512`;
const checksum = await fetch(checksumUrl, {
signal: AbortSignal.timeout(CHECKSUM_FETCH_TIMEOUT),
});
const checksumText = await checksum.text();
if (!CHECKSUM_FORMAT.test(checksumText)) {
throw new Error(`Invalid checksum format ${checksumText}`);
}
return checksumText.split(' ').at(0);
}
@@ -0,0 +1,19 @@
/**
* Teleport
* Copyright (C) 2025 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export * from './appUpdater';