mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-21 05:55:42 +08:00
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:
Generated
+46
@@ -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 {}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user