From e7550b1b7a0683dab2affda568ab7a07d5bcdc00 Mon Sep 17 00:00:00 2001 From: Tim Mok Date: Thu, 19 Dec 2024 14:36:35 -0500 Subject: [PATCH 1/8] Auto-update notification --- build/secrets/.secrets.baseline | 12 +- product.json | 2 + .../common/update.config.contribution.ts | 22 +++- .../electron-main/abstractUpdateService.ts | 113 +++++++++++++---- .../update/electron-main/positronVersion.ts | 97 +++++++++++++++ .../electron-main/updateService.darwin.ts | 45 ++++--- .../electron-main/updateService.linux.ts | 6 +- .../electron-main/updateService.win32.ts | 115 ++++++++---------- .../electron-main/positronVersions.test.ts | 54 ++++++++ 9 files changed, 353 insertions(+), 113 deletions(-) create mode 100644 src/vs/platform/update/electron-main/positronVersion.ts create mode 100644 src/vs/platform/update/test/electron-main/positronVersions.test.ts diff --git a/build/secrets/.secrets.baseline b/build/secrets/.secrets.baseline index ec7810a5ce5..a541c8595b6 100644 --- a/build/secrets/.secrets.baseline +++ b/build/secrets/.secrets.baseline @@ -94,10 +94,6 @@ "path": "detect_secrets.filters.common.is_baseline_file", "filename": "build/secrets/.secrets.baseline" }, - { - "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", - "min_level": 2 - }, { "path": "detect_secrets.filters.heuristic.is_indirect_reference" }, @@ -599,7 +595,7 @@ "filename": "product.json", "hashed_secret": "829eaa1d03499ac3be472b80e2d7a7b7df709fca", "is_verified": false, - "line_number": 43, + "line_number": 45, "is_secret": false }, { @@ -607,7 +603,7 @@ "filename": "product.json", "hashed_secret": "6dddc5671d5ed146759f817632cbb0c2d278fdd0", "is_verified": false, - "line_number": 59, + "line_number": 61, "is_secret": false }, { @@ -615,7 +611,7 @@ "filename": "product.json", "hashed_secret": "98f6b0e364fc315e344dd24ce8ccd59b9a827ccd", "is_verified": false, - "line_number": 75, + "line_number": 77, "is_secret": false }, { @@ -623,7 +619,7 @@ "filename": "product.json", "hashed_secret": "aa5db2e10c46111cbb98fa5d5bf0f7a51937dbfe", "is_verified": false, - "line_number": 229, + "line_number": 231, "is_secret": false } ], diff --git a/product.json b/product.json index 90c80c2b152..3932e94d295 100644 --- a/product.json +++ b/product.json @@ -36,6 +36,8 @@ "nodejsRepository": "https://nodejs.org", "urlProtocol": "positron", "webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/ef65ac1ba57f57f2a3961bfe94aa20481caca4c6/out/vs/workbench/contrib/webview/browser/pre/", + "updateUrl": "https://cdn.posit.co/positron", + "downloadUrl": "https://github.com/posit-dev/positron/releases", "builtInExtensions": [ { "name": "ms-vscode.js-debug-companion", diff --git a/src/vs/platform/update/common/update.config.contribution.ts b/src/vs/platform/update/common/update.config.contribution.ts index d96926b5578..a5e0be3bcfc 100644 --- a/src/vs/platform/update/common/update.config.contribution.ts +++ b/src/vs/platform/update/common/update.config.contribution.ts @@ -9,6 +9,7 @@ import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurati import { Registry } from '../../registry/common/platform.js'; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); +// --- START POSITRON --- configurationRegistry.registerConfiguration({ id: 'update', order: 15, @@ -20,7 +21,7 @@ configurationRegistry.registerConfiguration({ enum: ['none', 'manual', 'start', 'default'], default: 'default', scope: ConfigurationScope.APPLICATION, - description: localize('updateMode', "Configure whether you receive automatic updates. Requires a restart after change. The updates are fetched from a Microsoft online service."), + description: localize('updateMode', "Configure whether you receive automatic updates. Requires a restart after change to take effect."), tags: ['usesOnlineServices'], enumDescriptions: [ localize('none', "Disable updates."), @@ -30,14 +31,21 @@ configurationRegistry.registerConfiguration({ ], policy: { name: 'UpdateMode', - minimumVersion: '1.67', + minimumVersion: '2025.1.0', } }, + 'update.autoUpdateExperimental': { + type: 'boolean', + default: false, + scope: ConfigurationScope.APPLICATION, + description: localize('experimentalAutoUpdate', "CAUTION: Enable automatic update checking. Requires a restart after change to take effect."), + tags: ['usesOnlineServices'], + }, 'update.channel': { type: 'string', default: 'default', scope: ConfigurationScope.APPLICATION, - description: localize('updateMode', "Configure whether you receive automatic updates. Requires a restart after change. The updates are fetched from a Microsoft online service."), + description: localize('updateMode', "Configure whether you receive automatic updates. Requires a restart after change to take effect."), deprecationMessage: localize('deprecated', "This setting is deprecated, please use '{0}' instead.", 'update.mode') }, 'update.enableWindowsBackgroundUpdates': { @@ -50,10 +58,12 @@ configurationRegistry.registerConfiguration({ }, 'update.showReleaseNotes': { type: 'boolean', - default: true, + default: false, scope: ConfigurationScope.APPLICATION, - description: localize('showReleaseNotes', "Show Release Notes after an update. The Release Notes are fetched from a Microsoft online service."), - tags: ['usesOnlineServices'] + description: localize('showReleaseNotes', "Show Release Notes after an update."), + tags: ['usesOnlineServices'], + included: false } } + // --- END POSITRON --- }); diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 48638aa12f7..bff7a748314 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -14,8 +14,24 @@ import { IProductService } from '../../product/common/productService.js'; import { IRequestService } from '../../request/common/request.js'; import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js'; -export function createUpdateURL(platform: string, quality: string, productService: IProductService): string { - return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`; +//--- START POSITRON +// eslint-disable-next-line no-duplicate-imports +import { asJson } from '../../request/common/request.js'; +// eslint-disable-next-line no-duplicate-imports +import { IUpdate } from '../common/update.js'; +import { hasUpdate } from '../electron-main/positronVersion.js'; +import { INativeHostMainService } from '../../native/electron-main/nativeHostMainService.js'; + +export const enum UpdateChannel { + Releases = 'releases', + Prereleases = 'prereleases', + Dailies = 'dailies', + Staging = 'staging', +} + +export function createUpdateURL(platform: string, channel: string, productService: IProductService): string { + return `${productService.updateUrl}/${channel}/${platform}`; + //--- END POSITRON } export type UpdateNotAvailableClassification = { @@ -36,6 +52,11 @@ export abstract class AbstractUpdateService implements IUpdateService { protected url: string | undefined; + // --- START POSITRON --- + // enable the service to download and apply updates automatically + protected enableAutoUpdate: boolean; + // --- END POSITRON --- + private _state: State = State.Uninitialized; private readonly _onStateChange = new Emitter(); @@ -57,8 +78,11 @@ export abstract class AbstractUpdateService implements IUpdateService { @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IRequestService protected requestService: IRequestService, @ILogService protected logService: ILogService, - @IProductService protected readonly productService: IProductService + // --- START POSITRON --- + @IProductService protected readonly productService: IProductService, + @INativeHostMainService protected readonly nativeHostMainService: INativeHostMainService ) { + this.enableAutoUpdate = process.env.POSITRON_AUTO_UPDATE === '1'; lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen) .finally(() => this.initialize()); } @@ -69,38 +93,43 @@ export abstract class AbstractUpdateService implements IUpdateService { * https://github.com/microsoft/vscode/issues/89784 */ protected async initialize(): Promise { - if (!this.environmentMainService.isBuilt) { + const updateChannel = process.env.POSITRON_UPDATE_CHANNEL ?? UpdateChannel.Prereleases; + const autoUpdateFlag = this.configurationService.getValue('update.autoUpdateExperimental'); + + if (!this.environmentMainService.isBuilt && !autoUpdateFlag) { this.setState(State.Disabled(DisablementReason.NotBuilt)); return; // updates are never enabled when running out of sources + } else if (!this.environmentMainService.isBuilt && updateChannel && autoUpdateFlag) { + this.logService.warn('update#ctor - updates enabled in dev environment; attempted update installs will fail'); } - if (this.environmentMainService.disableUpdates) { + if (this.environmentMainService.disableUpdates || !autoUpdateFlag) { this.setState(State.Disabled(DisablementReason.DisabledByEnvironment)); this.logService.info('update#ctor - updates are disabled by the environment'); return; } - if (!this.productService.updateUrl || !this.productService.commit) { + if ((!this.productService.updateUrl || !this.productService.commit) && !updateChannel) { this.setState(State.Disabled(DisablementReason.MissingConfiguration)); this.logService.info('update#ctor - updates are disabled as there is no update URL'); return; } const updateMode = this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode'); - const quality = this.getProductQuality(updateMode); - if (!quality) { + if (updateMode === 'none') { this.setState(State.Disabled(DisablementReason.ManuallyDisabled)); this.logService.info('update#ctor - updates are disabled by user preference'); return; } - this.url = this.buildUpdateFeedUrl(quality); + this.url = this.buildUpdateFeedUrl(updateChannel); if (!this.url) { this.setState(State.Disabled(DisablementReason.InvalidConfiguration)); this.logService.info('update#ctor - updates are disabled as the update URL is badly formed'); return; } + this.logService.debug('update#ctor - update URL is', this.url); // hidden setting if (this.configurationService.getValue('_update.prss')) { @@ -127,10 +156,6 @@ export abstract class AbstractUpdateService implements IUpdateService { } } - private getProductQuality(updateMode: string): string | undefined { - return updateMode === 'none' ? undefined : this.productService.quality; - } - private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise { return timeout(delay) .then(() => this.checkForUpdates(false)) @@ -147,7 +172,33 @@ export abstract class AbstractUpdateService implements IUpdateService { return; } - this.doCheckForUpdates(explicit); + this.setState(State.CheckingForUpdates(explicit)); + + this.requestService.request({ url: this.url }, CancellationToken.None) + .then(asJson) + .then(update => { + if (!update || !update.url || !update.version) { + this.setState(State.Idle(this.getUpdateType())); + return Promise.resolve(null); + } + + if (hasUpdate(update, this.productService.positronVersion)) { + this.logService.info(`update#checkForUpdates, ${update.version} is available`); + this.updateAvailable(update); + } else { + this.logService.info(`update#checkForUpdates, ${this.productService.positronVersion} is the latest version`); + this.setState(State.Idle(this.getUpdateType())); + } + return Promise.resolve(update); + }) + .then(undefined, err => { + this.logService.error(err); + + // only show message when explicitly checking for updates + const message: string | undefined = !!explicit ? (err.message || err) : undefined; + this.setState(State.Idle(this.getUpdateType(), message)); + }); + } async downloadUpdate(): Promise { @@ -161,8 +212,17 @@ export abstract class AbstractUpdateService implements IUpdateService { } protected async doDownloadUpdate(state: AvailableForDownload): Promise { - // noop + if (this.productService.downloadUrl && this.productService.downloadUrl.length > 0) { + // Use the download URL if available as we don't currently detect the package type that was + // installed and the website download page is more useful than the tarball generally. + this.nativeHostMainService.openExternal(undefined, `${this.productService.downloadUrl}/tag/${state.update.version}`); + } else if (state.update.url) { + this.nativeHostMainService.openExternal(undefined, state.update.url); + } + + this.setState(State.Idle(this.getUpdateType())); } + // --- END POSITRON --- async applyUpdate(): Promise { this.logService.trace('update#applyUpdate, state = ', this.state.type); @@ -212,11 +272,16 @@ export abstract class AbstractUpdateService implements IUpdateService { } try { - const context = await this.requestService.request({ url: this.url }, CancellationToken.None); - // The update server replies with 204 (No Content) when no - // update is available - that's all we want to know. - return context.res.statusCode === 204; - + // --- START POSITRON + return this.requestService.request({ url: this.url }, CancellationToken.None) + .then(asJson) + .then(update => { + if (!update || !update.version) { + return Promise.resolve(false); + } + return Promise.resolve(hasUpdate(update, this.productService.positronVersion)); + }); + // --- END POSITRON } catch (error) { this.logService.error('update#isLatestVersion(): failed to check for updates'); this.logService.error(error); @@ -236,6 +301,10 @@ export abstract class AbstractUpdateService implements IUpdateService { // noop } - protected abstract buildUpdateFeedUrl(quality: string): string | undefined; - protected abstract doCheckForUpdates(context: any): void; + // --- START POSITRON --- + protected abstract buildUpdateFeedUrl(channel: string): string | undefined; + protected updateAvailable(context: IUpdate): void { + this.setState(State.AvailableForDownload(context)); + } + // --- END POSITRON --- } diff --git a/src/vs/platform/update/electron-main/positronVersion.ts b/src/vs/platform/update/electron-main/positronVersion.ts new file mode 100644 index 00000000000..d3f790235c5 --- /dev/null +++ b/src/vs/platform/update/electron-main/positronVersion.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IUpdate } from '../common/update.js'; + + +const POSITRON_VERSION_REGEX = /^\d{4}\.\d{2}\.\d+(-\d+)?$/; + +/** + * A Positron version in the format of YYYY.MM.patch-build + * + * The month is zero-padded and the build number is optional. + */ +export interface IPositronVersion { + year: number; + month: number; + patch: number; + build?: number; +} + +/** + * Parses a calendar version from YYYY.MM.patch-build into IPositronVersion + * + * @param version - The version string to parse. + */ +export function parse(version: string): IPositronVersion { + if (!POSITRON_VERSION_REGEX.test(version)) { + throw new Error('Version format must be YYYY.MM.patch-build'); + } + const [year, month, patchBuild] = version.split('.'); + const [patch, build] = patchBuild.split('-').map(Number); + + return { year: Number(year), month: Number(month), patch, build }; +} + +/** + * Checks if a version is valid. Each part of the version must be a number. + * + * @param version - The version string to check. + */ +export function valid(version: string): boolean { + return POSITRON_VERSION_REGEX.test(version); +} + +/** + * Compares two versions of Positron. If either version does not have a build number, it is equal + * given the other parts are equal. + * + * @param v1 - The first version to compare. + * @param v2 - The second version to compare. + * @returns negative if v1 is less than v2, 0 if they are equal, and positive if v1 is greater than v2. + */ +export function compare(v1: string, v2: string): number { + const p1 = parse(v1); + const p2 = parse(v2); + + if (p1.year !== p2.year) { + return p1.year - p2.year; + } + + if (p1.month !== p2.month) { + return p1.month - p2.month; + } + + if (p1.patch !== p2.patch) { + return p1.patch - p2.patch; + } + + if (p1.build === undefined || p2.build === undefined) { + return 0; + } + + return (p1.build || 0) - (p2.build || 0); +} + +/** + * Checks if an update is newer than the current version. + * + * @param update - The update to check. + * @param currentVersion - The current version to compare against. + * @returns true if the update is newer, false otherwise. + */ +export function hasUpdate(update: IUpdate, currentVersion: string): boolean { + const latestVersion = update.version; + + if (!valid(latestVersion)) { + throw new Error(`Invalid version format ${latestVersion}`); + } + + if (!valid(currentVersion)) { + throw new Error(`Invalid version format ${currentVersion}`); + } + + return compare(latestVersion, currentVersion) >= 1; +} diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index d3f27d3711e..6e5212b8547 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -18,6 +18,10 @@ import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { IUpdate, State, StateType, UpdateType } from '../common/update.js'; import { AbstractUpdateService, createUpdateURL, UpdateErrorClassification, UpdateNotAvailableClassification } from './abstractUpdateService.js'; +// --- Start Positron --- +import { INativeHostMainService } from '../../native/electron-main/nativeHostMainService.js'; +// --- End Positron --- + export class DarwinUpdateService extends AbstractUpdateService implements IRelaunchHandler { private readonly disposables = new DisposableStore(); @@ -27,6 +31,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available'); } @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, timestamp) => ({ version, productVersion: version, timestamp })); } + // --- Start Positron --- constructor( @ILifecycleMainService lifecycleMainService: ILifecycleMainService, @IConfigurationService configurationService: IConfigurationService, @@ -34,9 +39,11 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau @IEnvironmentMainService environmentMainService: IEnvironmentMainService, @IRequestService requestService: IRequestService, @ILogService logService: ILogService, - @IProductService productService: IProductService + @IProductService productService: IProductService, + @INativeHostMainService nativeHostMainService: INativeHostMainService ) { - super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); + super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService, nativeHostMainService); + // --- End Positron --- lifecycleMainService.setRelaunchHandler(this); } @@ -73,16 +80,12 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau this.setState(State.Idle(UpdateType.Archive, message)); } - protected buildUpdateFeedUrl(quality: string): string | undefined { - let assetID: string; - if (!this.productService.darwinUniversalAssetId) { - assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64'; - } else { - assetID = this.productService.darwinUniversalAssetId; - } - const url = createUpdateURL(assetID, quality, this.productService); + //--- START POSITRON + protected buildUpdateFeedUrl(channel: string): string | undefined { + const platform = `mac/universal`; + const url = createUpdateURL(platform, channel, this.productService) + '/releases.json'; try { - electron.autoUpdater.setFeedURL({ url }); + electron.autoUpdater.setFeedURL({ url: url }); } catch (e) { // application is very likely not signed this.logService.error('Failed to set update feed URL', e); @@ -91,10 +94,24 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau return url; } - protected doCheckForUpdates(context: any): void { - this.setState(State.CheckingForUpdates(context)); - electron.autoUpdater.checkForUpdates(); + /** + * Manually check for updates and call Electron to install the update if an update is available. + */ + protected override updateAvailable(update: IUpdate): void { + if (!update.url || !update.version) { + this.setState(State.Idle(UpdateType.Archive)); + return; + } + + if (!this.enableAutoUpdate) { + super.updateAvailable(update); + } else { + // We cannot avoid Electron checking the URL again with this call. Electron can only check against + // the app version, which is VS Code's version. + electron.autoUpdater.checkForUpdates(); + } } + //--- END POSITRON private onUpdateAvailable(): void { if (this.state.type !== StateType.CheckingForUpdates) { diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 6e076c72ed8..4ca29044d09 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -17,6 +17,7 @@ import { AbstractUpdateService, createUpdateURL, UpdateNotAvailableClassificatio export class LinuxUpdateService extends AbstractUpdateService { + // --- Start Positron --- constructor( @ILifecycleMainService lifecycleMainService: ILifecycleMainService, @IConfigurationService configurationService: IConfigurationService, @@ -24,11 +25,12 @@ export class LinuxUpdateService extends AbstractUpdateService { @IEnvironmentMainService environmentMainService: IEnvironmentMainService, @IRequestService requestService: IRequestService, @ILogService logService: ILogService, - @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, + @INativeHostMainService nativeHostMainService: INativeHostMainService, @IProductService productService: IProductService ) { - super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); + super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService, nativeHostMainService); } + // --- End Positron --- protected buildUpdateFeedUrl(quality: string): string { return createUpdateURL(`linux-${process.arch}`, quality, this.productService); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 61109e54741..bf400210dfe 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -9,7 +9,6 @@ import { tmpdir } from 'os'; import { timeout } from '../../../base/common/async.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { memoize } from '../../../base/common/decorators.js'; -import { hash } from '../../../base/common/hash.js'; import * as path from '../../../base/common/path.js'; import { URI } from '../../../base/common/uri.js'; import { checksum } from '../../../base/node/crypto.js'; @@ -21,10 +20,12 @@ import { ILifecycleMainService, IRelaunchHandler, IRelaunchOptions } from '../.. import { ILogService } from '../../log/common/log.js'; import { INativeHostMainService } from '../../native/electron-main/nativeHostMainService.js'; import { IProductService } from '../../product/common/productService.js'; -import { asJson, IRequestService } from '../../request/common/request.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, UpdateType } from '../common/update.js'; -import { AbstractUpdateService, createUpdateURL, UpdateErrorClassification, UpdateNotAvailableClassification } from './abstractUpdateService.js'; + +// --- Start Positron --- +import { IRequestService } from '../../request/common/request.js'; +import { AbstractUpdateService, createUpdateURL } from './abstractUpdateService.js'; +// --- End Positron --- async function pollUntil(fn: () => boolean, millis = 1000): Promise { while (!fn()) { @@ -61,15 +62,14 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun constructor( @ILifecycleMainService lifecycleMainService: ILifecycleMainService, @IConfigurationService configurationService: IConfigurationService, - @ITelemetryService private readonly telemetryService: ITelemetryService, @IEnvironmentMainService environmentMainService: IEnvironmentMainService, @IRequestService requestService: IRequestService, @ILogService logService: ILogService, @IFileService private readonly fileService: IFileService, - @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, - @IProductService productService: IProductService + @IProductService productService: IProductService, + @INativeHostMainService nativeHostMainService: INativeHostMainService ) { - super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); + super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService, nativeHostMainService); lifecycleMainService.setRelaunchHandler(this); } @@ -111,70 +111,63 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun return createUpdateURL(platform, quality, this.productService); } - protected doCheckForUpdates(context: any): void { + // --- START POSITRON --- + protected override updateAvailable(update: IUpdate): void { if (!this.url) { return; } - this.setState(State.CheckingForUpdates(context)); + const updateType = getUpdateType(); - this.requestService.request({ url: this.url }, CancellationToken.None) - .then(asJson) - .then(update => { - const updateType = getUpdateType(); + if (!update || !update.url || !update.version || !update.productVersion) { + this.setState(State.Idle(updateType)); + return; + } - if (!update || !update.url || !update.version || !update.productVersion) { - this.telemetryService.publicLog2<{ explicit: boolean }, UpdateNotAvailableClassification>('update:notAvailable', { explicit: !!context }); + if (updateType === UpdateType.Archive) { + this.setState(State.AvailableForDownload(update)); + return; + } - this.setState(State.Idle(updateType)); - return Promise.resolve(null); - } + // Notify about updates for now. Do not download or install them. + if (!this.enableAutoUpdate) { + this.setState(State.AvailableForDownload(update)); + return; + } - if (updateType === UpdateType.Archive) { - this.setState(State.AvailableForDownload(update)); - return Promise.resolve(null); - } + // TODO: Code for installing updates is disabled due to this.enableAutoUpdate being false + this.setState(State.Downloading); + + this.cleanup(update.version).then(() => { + return this.getUpdatePackagePath(update.version).then(updatePackagePath => { + return pfs.Promises.exists(updatePackagePath).then(exists => { + if (exists) { + return Promise.resolve(updatePackagePath); + } - this.setState(State.Downloading); - - return this.cleanup(update.version).then(() => { - return this.getUpdatePackagePath(update.version).then(updatePackagePath => { - return pfs.Promises.exists(updatePackagePath).then(exists => { - if (exists) { - return Promise.resolve(updatePackagePath); - } - - const downloadPath = `${updatePackagePath}.tmp`; - - return this.requestService.request({ url: update.url }, CancellationToken.None) - .then(context => this.fileService.writeFile(URI.file(downloadPath), context.stream)) - .then(update.sha256hash ? () => checksum(downloadPath, update.sha256hash) : () => undefined) - .then(() => pfs.Promises.rename(downloadPath, updatePackagePath, false /* no retry */)) - .then(() => updatePackagePath); - }); - }).then(packagePath => { - this.availableUpdate = { packagePath }; - this.setState(State.Downloaded(update)); - - const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); - if (fastUpdatesEnabled) { - if (this.productService.target === 'user') { - this.doApplyUpdate(); - } - } else { - this.setState(State.Ready(update)); - } - }); + const downloadPath = `${updatePackagePath}.tmp`; + + return this.requestService.request({ url: update.url }, CancellationToken.None) + .then(context => this.fileService.writeFile(URI.file(downloadPath), context.stream)) + .then(update.sha256hash ? () => checksum(downloadPath, update.sha256hash) : () => undefined) + .then(() => pfs.Promises.rename(downloadPath, updatePackagePath, false /* no retry */)) + .then(() => updatePackagePath); }); - }) - .then(undefined, err => { - this.telemetryService.publicLog2<{ messageHash: string }, UpdateErrorClassification>('update:error', { messageHash: String(hash(String(err))) }); - this.logService.error(err); - - // only show message when explicitly checking for updates - const message: string | undefined = !!context ? (err.message || err) : undefined; - this.setState(State.Idle(getUpdateType(), message)); + }).then(packagePath => { + this.availableUpdate = { packagePath }; + this.setState(State.Downloaded(update)); + + const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); + if (fastUpdatesEnabled) { + if (this.productService.target === 'user') { + this.doApplyUpdate(); + } + } else { + this.setState(State.Ready(update)); + } }); + }); + // --- END POSITRON --- } protected override async doDownloadUpdate(state: AvailableForDownload): Promise { diff --git a/src/vs/platform/update/test/electron-main/positronVersions.test.ts b/src/vs/platform/update/test/electron-main/positronVersions.test.ts new file mode 100644 index 00000000000..d47119d01d1 --- /dev/null +++ b/src/vs/platform/update/test/electron-main/positronVersions.test.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { IUpdate } from '../../common/update.js'; +import * as positronVersion from '../../electron-main/positronVersion.js'; + +suite('Positron Version', function () { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('UpdateService - compare update with build number to version without build number', () => { + const update: IUpdate = { version: '2024.11.0-111' }; + assert.strictEqual(positronVersion.hasUpdate(update, '2024.11.0'), false, 'same version'); + assert.strictEqual(positronVersion.hasUpdate(update, '2024.09.0'), true, 'update is newer'); + assert.strictEqual(positronVersion.hasUpdate(update, '2024.12.0'), false, 'update is older'); + + const updateNoBuild: IUpdate = { version: '2024.11.0' }; + assert.strictEqual(positronVersion.hasUpdate(updateNoBuild, '2024.11.0-111'), false, 'same version'); + assert.strictEqual(positronVersion.hasUpdate(updateNoBuild, '2024.09.0-111'), true, 'update is newer'); + assert.strictEqual(positronVersion.hasUpdate(updateNoBuild, '2024.12.0-111'), false, 'update is older'); + }); + + test('UpdateService - compare year version', () => { + const update: IUpdate = { version: '2024.12.0-111' }; + assert.strictEqual(positronVersion.hasUpdate(update, '2024.12.0-111'), false, 'same version'); + assert.strictEqual(positronVersion.hasUpdate(update, '2023.12.0-111'), true, 'update is newer'); + assert.strictEqual(positronVersion.hasUpdate(update, '2025.12.0-111'), false, 'update is older'); + }); + + test('UpdateService - compare month version', () => { + const update: IUpdate = { version: '2024.05.0-111' }; + assert.strictEqual(positronVersion.hasUpdate(update, '2024.05.0-111'), false, 'same version'); + assert.strictEqual(positronVersion.hasUpdate(update, '2024.02.0-111'), true, 'update is newer'); + assert.strictEqual(positronVersion.hasUpdate(update, '2024.12.0-111'), false, 'update is older'); + }); + + test('UpdateService - compare patch version', () => { + const update: IUpdate = { version: '2024.11.10-18' }; + assert.strictEqual(positronVersion.hasUpdate(update, '2024.11.10-18'), false, 'same version'); + assert.strictEqual(positronVersion.hasUpdate(update, '2024.11.0-18'), true, 'update is newer'); + assert.strictEqual(positronVersion.hasUpdate(update, '2024.11.12-18'), false, 'update is older'); + }); + + test('UpdateService - compare build number', () => { + const update: IUpdate = { version: '2024.11.1-18' }; + assert.strictEqual(positronVersion.hasUpdate(update, '2024.11.1-18'), false, 'same version'); + assert.strictEqual(positronVersion.hasUpdate(update, '2024.11.1-10'), true, 'update is newer'); + assert.strictEqual(positronVersion.hasUpdate(update, '2024.11.1-20'), false, 'update is older'); + }); + +}); From 81faf37182683f35a28a02ca93ca2809eb980fae Mon Sep 17 00:00:00 2001 From: Tim Mok Date: Fri, 20 Dec 2024 09:09:24 -0500 Subject: [PATCH 2/8] Windows notifications --- .../electron-main/abstractUpdateService.ts | 2 +- .../electron-main/updateService.darwin.ts | 2 +- .../electron-main/updateService.win32.ts | 42 ++++--------------- 3 files changed, 10 insertions(+), 36 deletions(-) diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index bff7a748314..ce07b28cfe1 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -215,7 +215,7 @@ export abstract class AbstractUpdateService implements IUpdateService { if (this.productService.downloadUrl && this.productService.downloadUrl.length > 0) { // Use the download URL if available as we don't currently detect the package type that was // installed and the website download page is more useful than the tarball generally. - this.nativeHostMainService.openExternal(undefined, `${this.productService.downloadUrl}/tag/${state.update.version}`); + this.nativeHostMainService.openExternal(undefined, this.productService.downloadUrl); } else if (state.update.url) { this.nativeHostMainService.openExternal(undefined, state.update.url); } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 6e5212b8547..7e5865adce0 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -82,7 +82,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau //--- START POSITRON protected buildUpdateFeedUrl(channel: string): string | undefined { - const platform = `mac/universal`; + const platform = 'mac/universal'; const url = createUpdateURL(platform, channel, this.productService) + '/releases.json'; try { electron.autoUpdater.setFeedURL({ url: url }); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index bf400210dfe..fb94dd61116 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -20,7 +20,7 @@ import { ILifecycleMainService, IRelaunchHandler, IRelaunchOptions } from '../.. import { ILogService } from '../../log/common/log.js'; import { INativeHostMainService } from '../../native/electron-main/nativeHostMainService.js'; import { IProductService } from '../../product/common/productService.js'; -import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, UpdateType } from '../common/update.js'; +import { DisablementReason, IUpdate, State, StateType, UpdateType } from '../common/update.js'; // --- Start Positron --- import { IRequestService } from '../../request/common/request.js'; @@ -99,36 +99,17 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun await super.initialize(); } - protected buildUpdateFeedUrl(quality: string): string | undefined { - let platform = `win32-${process.arch}`; + // --- START POSITRON --- + protected buildUpdateFeedUrl(channel: string): string | undefined { + const platform = `win/${process.arch === 'x64' ? 'x86_64' : 'arm64'}`; + const prefix = getUpdateType() === UpdateType.Setup ? 'system-' : 'user-'; - if (getUpdateType() === UpdateType.Archive) { - platform += '-archive'; - } else if (this.productService.target === 'user') { - platform += '-user'; - } + const url = createUpdateURL(platform, channel, this.productService) + `/${prefix}releases.json`; - return createUpdateURL(platform, quality, this.productService); + return url; } - // --- START POSITRON --- protected override updateAvailable(update: IUpdate): void { - if (!this.url) { - return; - } - - const updateType = getUpdateType(); - - if (!update || !update.url || !update.version || !update.productVersion) { - this.setState(State.Idle(updateType)); - return; - } - - if (updateType === UpdateType.Archive) { - this.setState(State.AvailableForDownload(update)); - return; - } - // Notify about updates for now. Do not download or install them. if (!this.enableAutoUpdate) { this.setState(State.AvailableForDownload(update)); @@ -167,15 +148,8 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun } }); }); - // --- END POSITRON --- - } - - protected override async doDownloadUpdate(state: AvailableForDownload): Promise { - if (state.update.url) { - this.nativeHostMainService.openExternal(undefined, state.update.url); - } - this.setState(State.Idle(getUpdateType())); } + // --- END POSITRON --- private async getUpdatePackagePath(version: string): Promise { const cachePath = await this.cachePath; From 53b1c5ce0490e46f71270c9c3565627dc268d5ab Mon Sep 17 00:00:00 2001 From: Tim Mok Date: Fri, 20 Dec 2024 10:17:37 -0500 Subject: [PATCH 3/8] Linux notifications --- .../update/electron-main/updateService.linux.ts | 11 ++++++++--- .../update/electron-main/updateService.win32.ts | 4 +++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 4ca29044d09..8002cf6e627 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -30,11 +30,16 @@ export class LinuxUpdateService extends AbstractUpdateService { ) { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService, nativeHostMainService); } - // --- End Positron --- - protected buildUpdateFeedUrl(quality: string): string { - return createUpdateURL(`linux-${process.arch}`, quality, this.productService); + protected buildUpdateFeedUrl(channel: string): string { + const arch = process.arch === 'x64' ? 'x86_64' : 'arm64'; + const platform = `deb/${arch}`; + const baseUrl = createUpdateURL(platform, channel, this.productService); + + // TODO: properly determine deb or rpm + return `${baseUrl}/releases.json`; } + // --- End Positron --- protected doCheckForUpdates(context: any): void { if (!this.url) { diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index fb94dd61116..dd73236fe2e 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -103,8 +103,10 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun protected buildUpdateFeedUrl(channel: string): string | undefined { const platform = `win/${process.arch === 'x64' ? 'x86_64' : 'arm64'}`; const prefix = getUpdateType() === UpdateType.Setup ? 'system-' : 'user-'; + const baseUrl = createUpdateURL(platform, channel, this.productService); - const url = createUpdateURL(platform, channel, this.productService) + `/${prefix}releases.json`; + // TODO: properly determine if this is a user or system install + const url = `${baseUrl}/${prefix}releases.json`; return url; } From 0ff523082dde752a89fa363e05fd03d166c21c0c Mon Sep 17 00:00:00 2001 From: Tim Mok Date: Thu, 9 Jan 2025 09:58:50 -0500 Subject: [PATCH 4/8] Copyright year change Co-authored-by: Jonathan Signed-off-by: Tim Mok --- src/vs/platform/update/electron-main/positronVersion.ts | 2 +- .../platform/update/test/electron-main/positronVersions.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/update/electron-main/positronVersion.ts b/src/vs/platform/update/electron-main/positronVersion.ts index d3f790235c5..a3d8af6c303 100644 --- a/src/vs/platform/update/electron-main/positronVersion.ts +++ b/src/vs/platform/update/electron-main/positronVersion.ts @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ diff --git a/src/vs/platform/update/test/electron-main/positronVersions.test.ts b/src/vs/platform/update/test/electron-main/positronVersions.test.ts index d47119d01d1..09319386d9e 100644 --- a/src/vs/platform/update/test/electron-main/positronVersions.test.ts +++ b/src/vs/platform/update/test/electron-main/positronVersions.test.ts @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ From b37a88ff16326c5781665eba4147922325c513f6 Mon Sep 17 00:00:00 2001 From: Tim Mok Date: Thu, 9 Jan 2025 09:57:31 -0500 Subject: [PATCH 5/8] Fix code fence style --- .../common/update.config.contribution.ts | 4 ++-- .../electron-main/abstractUpdateService.ts | 20 +++++++++---------- .../electron-main/updateService.darwin.ts | 4 ++-- .../electron-main/updateService.win32.ts | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/vs/platform/update/common/update.config.contribution.ts b/src/vs/platform/update/common/update.config.contribution.ts index a5e0be3bcfc..03bb4897257 100644 --- a/src/vs/platform/update/common/update.config.contribution.ts +++ b/src/vs/platform/update/common/update.config.contribution.ts @@ -9,7 +9,7 @@ import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurati import { Registry } from '../../registry/common/platform.js'; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); -// --- START POSITRON --- +// --- Start Positron --- configurationRegistry.registerConfiguration({ id: 'update', order: 15, @@ -65,5 +65,5 @@ configurationRegistry.registerConfiguration({ included: false } } - // --- END POSITRON --- + // --- End Positron --- }); diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index ce07b28cfe1..17496d6448d 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -14,7 +14,7 @@ import { IProductService } from '../../product/common/productService.js'; import { IRequestService } from '../../request/common/request.js'; import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js'; -//--- START POSITRON +//--- Start Positron --- // eslint-disable-next-line no-duplicate-imports import { asJson } from '../../request/common/request.js'; // eslint-disable-next-line no-duplicate-imports @@ -31,7 +31,7 @@ export const enum UpdateChannel { export function createUpdateURL(platform: string, channel: string, productService: IProductService): string { return `${productService.updateUrl}/${channel}/${platform}`; - //--- END POSITRON + //--- End Positron --- } export type UpdateNotAvailableClassification = { @@ -52,10 +52,10 @@ export abstract class AbstractUpdateService implements IUpdateService { protected url: string | undefined; - // --- START POSITRON --- + // --- Start Positron --- // enable the service to download and apply updates automatically protected enableAutoUpdate: boolean; - // --- END POSITRON --- + // --- End Positron --- private _state: State = State.Uninitialized; @@ -78,7 +78,7 @@ export abstract class AbstractUpdateService implements IUpdateService { @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IRequestService protected requestService: IRequestService, @ILogService protected logService: ILogService, - // --- START POSITRON --- + // --- Start Positron --- @IProductService protected readonly productService: IProductService, @INativeHostMainService protected readonly nativeHostMainService: INativeHostMainService ) { @@ -222,7 +222,7 @@ export abstract class AbstractUpdateService implements IUpdateService { this.setState(State.Idle(this.getUpdateType())); } - // --- END POSITRON --- + // --- End Positron --- async applyUpdate(): Promise { this.logService.trace('update#applyUpdate, state = ', this.state.type); @@ -272,7 +272,7 @@ export abstract class AbstractUpdateService implements IUpdateService { } try { - // --- START POSITRON + // --- Start Positron --- return this.requestService.request({ url: this.url }, CancellationToken.None) .then(asJson) .then(update => { @@ -281,7 +281,7 @@ export abstract class AbstractUpdateService implements IUpdateService { } return Promise.resolve(hasUpdate(update, this.productService.positronVersion)); }); - // --- END POSITRON + // --- End Positron --- } catch (error) { this.logService.error('update#isLatestVersion(): failed to check for updates'); this.logService.error(error); @@ -301,10 +301,10 @@ export abstract class AbstractUpdateService implements IUpdateService { // noop } - // --- START POSITRON --- + // --- Start Positron --- protected abstract buildUpdateFeedUrl(channel: string): string | undefined; protected updateAvailable(context: IUpdate): void { this.setState(State.AvailableForDownload(context)); } - // --- END POSITRON --- + // --- End Positron --- } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 7e5865adce0..5fb2a880bf6 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -80,7 +80,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau this.setState(State.Idle(UpdateType.Archive, message)); } - //--- START POSITRON + //--- Start Positron --- protected buildUpdateFeedUrl(channel: string): string | undefined { const platform = 'mac/universal'; const url = createUpdateURL(platform, channel, this.productService) + '/releases.json'; @@ -111,7 +111,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau electron.autoUpdater.checkForUpdates(); } } - //--- END POSITRON + //--- End Positron --- private onUpdateAvailable(): void { if (this.state.type !== StateType.CheckingForUpdates) { diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index dd73236fe2e..4a24539b826 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -99,7 +99,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun await super.initialize(); } - // --- START POSITRON --- + // --- Start Positron --- protected buildUpdateFeedUrl(channel: string): string | undefined { const platform = `win/${process.arch === 'x64' ? 'x86_64' : 'arm64'}`; const prefix = getUpdateType() === UpdateType.Setup ? 'system-' : 'user-'; @@ -151,7 +151,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun }); }); } - // --- END POSITRON --- + // --- End Positron --- private async getUpdatePackagePath(version: string): Promise { const cachePath = await this.cachePath; From d002b9898e739328420c5610edf55cf1a7a77274 Mon Sep 17 00:00:00 2001 From: Tim Mok Date: Thu, 9 Jan 2025 10:59:45 -0500 Subject: [PATCH 6/8] Restore unused function --- .../electron-main/abstractUpdateService.ts | 22 +++++- .../electron-main/updateService.darwin.ts | 6 ++ .../electron-main/updateService.linux.ts | 1 + .../electron-main/updateService.win32.ts | 79 ++++++++++++++++++- 4 files changed, 101 insertions(+), 7 deletions(-) diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 17496d6448d..3eda5308d71 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -81,8 +81,12 @@ export abstract class AbstractUpdateService implements IUpdateService { // --- Start Positron --- @IProductService protected readonly productService: IProductService, @INativeHostMainService protected readonly nativeHostMainService: INativeHostMainService + // --- End Positron --- ) { + // --- Start Positron --- this.enableAutoUpdate = process.env.POSITRON_AUTO_UPDATE === '1'; + // --- End Positron --- + lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen) .finally(() => this.initialize()); } @@ -91,8 +95,9 @@ export abstract class AbstractUpdateService implements IUpdateService { * This must be called before any other call. This is a performance * optimization, to avoid using extra CPU cycles before first window open. * https://github.com/microsoft/vscode/issues/89784 - */ + */ protected async initialize(): Promise { + // --- Start Positron --- const updateChannel = process.env.POSITRON_UPDATE_CHANNEL ?? UpdateChannel.Prereleases; const autoUpdateFlag = this.configurationService.getValue('update.autoUpdateExperimental'); @@ -124,12 +129,13 @@ export abstract class AbstractUpdateService implements IUpdateService { } this.url = this.buildUpdateFeedUrl(updateChannel); + this.logService.debug('update#ctor - update URL is', this.url); + // --- End Positron --- if (!this.url) { this.setState(State.Disabled(DisablementReason.InvalidConfiguration)); this.logService.info('update#ctor - updates are disabled as the update URL is badly formed'); return; } - this.logService.debug('update#ctor - update URL is', this.url); // hidden setting if (this.configurationService.getValue('_update.prss')) { @@ -156,6 +162,13 @@ export abstract class AbstractUpdateService implements IUpdateService { } } + // --- Start Positron --- + // @ts-ignore + private getProductQuality(updateMode: string): string | undefined { + return updateMode === 'none' ? undefined : this.productService.quality; + } + // --- End Positron --- + private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise { return timeout(delay) .then(() => this.checkForUpdates(false)) @@ -172,6 +185,7 @@ export abstract class AbstractUpdateService implements IUpdateService { return; } + // --- Start Positron --- this.setState(State.CheckingForUpdates(explicit)); this.requestService.request({ url: this.url }, CancellationToken.None) @@ -198,7 +212,7 @@ export abstract class AbstractUpdateService implements IUpdateService { const message: string | undefined = !!explicit ? (err.message || err) : undefined; this.setState(State.Idle(this.getUpdateType(), message)); }); - + // --- End Positron --- } async downloadUpdate(): Promise { @@ -212,6 +226,7 @@ export abstract class AbstractUpdateService implements IUpdateService { } protected async doDownloadUpdate(state: AvailableForDownload): Promise { + // --- Start Positron --- if (this.productService.downloadUrl && this.productService.downloadUrl.length > 0) { // Use the download URL if available as we don't currently detect the package type that was // installed and the website download page is more useful than the tarball generally. @@ -301,6 +316,7 @@ export abstract class AbstractUpdateService implements IUpdateService { // noop } + protected abstract doCheckForUpdates(context: any): void; // --- Start Positron --- protected abstract buildUpdateFeedUrl(channel: string): string | undefined; protected updateAvailable(context: IUpdate): void { diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 5fb2a880bf6..9639157451b 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -94,6 +94,12 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau return url; } + // Unused for Positron + protected doCheckForUpdates(context: any): void { + this.setState(State.CheckingForUpdates(context)); + electron.autoUpdater.checkForUpdates(); + } + /** * Manually check for updates and call Electron to install the update if an update is available. */ diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 8002cf6e627..b198c2df7fa 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -41,6 +41,7 @@ export class LinuxUpdateService extends AbstractUpdateService { } // --- End Positron --- + // Unused for Positron protected doCheckForUpdates(context: any): void { if (!this.url) { return; diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 4a24539b826..a6c9dc3bb9d 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -9,6 +9,7 @@ import { tmpdir } from 'os'; import { timeout } from '../../../base/common/async.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { memoize } from '../../../base/common/decorators.js'; +import { hash } from '../../../base/common/hash.js'; import * as path from '../../../base/common/path.js'; import { URI } from '../../../base/common/uri.js'; import { checksum } from '../../../base/node/crypto.js'; @@ -20,12 +21,12 @@ import { ILifecycleMainService, IRelaunchHandler, IRelaunchOptions } from '../.. import { ILogService } from '../../log/common/log.js'; import { INativeHostMainService } from '../../native/electron-main/nativeHostMainService.js'; import { IProductService } from '../../product/common/productService.js'; -import { DisablementReason, IUpdate, State, StateType, UpdateType } from '../common/update.js'; - +import { asJson, IRequestService } from '../../request/common/request.js'; +import { ITelemetryService } from '../../telemetry/common/telemetry.js'; // --- Start Positron --- -import { IRequestService } from '../../request/common/request.js'; -import { AbstractUpdateService, createUpdateURL } from './abstractUpdateService.js'; +import { DisablementReason, IUpdate, State, StateType, UpdateType } from '../common/update.js'; // --- End Positron --- +import { AbstractUpdateService, createUpdateURL, UpdateErrorClassification, UpdateNotAvailableClassification } from './abstractUpdateService.js'; async function pollUntil(fn: () => boolean, millis = 1000): Promise { while (!fn()) { @@ -62,6 +63,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun constructor( @ILifecycleMainService lifecycleMainService: ILifecycleMainService, @IConfigurationService configurationService: IConfigurationService, + @ITelemetryService private readonly telemetryService: ITelemetryService, @IEnvironmentMainService environmentMainService: IEnvironmentMainService, @IRequestService requestService: IRequestService, @ILogService logService: ILogService, @@ -110,7 +112,76 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun return url; } + // --- End Positron --- + + // Unused for Positron + protected doCheckForUpdates(context: any): void { + if (!this.url) { + return; + } + + this.setState(State.CheckingForUpdates(context)); + + this.requestService.request({ url: this.url }, CancellationToken.None) + .then(asJson) + .then(update => { + const updateType = getUpdateType(); + + if (!update || !update.url || !update.version || !update.productVersion) { + this.telemetryService.publicLog2<{ explicit: boolean }, UpdateNotAvailableClassification>('update:notAvailable', { explicit: !!context }); + this.setState(State.Idle(updateType)); + return Promise.resolve(null); + } + + if (updateType === UpdateType.Archive) { + this.setState(State.AvailableForDownload(update)); + return Promise.resolve(null); + } + + this.setState(State.Downloading); + + return this.cleanup(update.version).then(() => { + return this.getUpdatePackagePath(update.version).then(updatePackagePath => { + return pfs.Promises.exists(updatePackagePath).then(exists => { + if (exists) { + return Promise.resolve(updatePackagePath); + } + + const downloadPath = `${updatePackagePath}.tmp`; + + return this.requestService.request({ url: update.url }, CancellationToken.None) + .then(context => this.fileService.writeFile(URI.file(downloadPath), context.stream)) + .then(update.sha256hash ? () => checksum(downloadPath, update.sha256hash) : () => undefined) + .then(() => pfs.Promises.rename(downloadPath, updatePackagePath, false /* no retry */)) + .then(() => updatePackagePath); + }); + }).then(packagePath => { + this.availableUpdate = { packagePath }; + this.setState(State.Downloaded(update)); + + const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); + if (fastUpdatesEnabled) { + if (this.productService.target === 'user') { + this.doApplyUpdate(); + } + } else { + this.setState(State.Ready(update)); + } + }); + }); + }) + .then(undefined, err => { + this.telemetryService.publicLog2<{ messageHash: string }, UpdateErrorClassification>('update:error', { messageHash: String(hash(String(err))) }); + this.logService.error(err); + + // only show message when explicitly checking for updates + const message: string | undefined = !!context ? (err.message || err) : undefined; + this.setState(State.Idle(getUpdateType(), message)); + }); + } + + // --- Start Positron --- protected override updateAvailable(update: IUpdate): void { // Notify about updates for now. Do not download or install them. if (!this.enableAutoUpdate) { From 3f46834bb8a612542c2976c6b02e7ad2ba5cb686 Mon Sep 17 00:00:00 2001 From: Tim Mok Date: Thu, 9 Jan 2025 11:10:43 -0500 Subject: [PATCH 7/8] Further clarify code fences --- .../update/electron-main/abstractUpdateService.ts | 9 +++++---- .../update/electron-main/updateService.darwin.ts | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 3eda5308d71..ebb03d745f3 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -225,8 +225,8 @@ export abstract class AbstractUpdateService implements IUpdateService { await this.doDownloadUpdate(this.state); } + // --- Start Positron --- protected async doDownloadUpdate(state: AvailableForDownload): Promise { - // --- Start Positron --- if (this.productService.downloadUrl && this.productService.downloadUrl.length > 0) { // Use the download URL if available as we don't currently detect the package type that was // installed and the website download page is more useful than the tarball generally. @@ -286,8 +286,8 @@ export abstract class AbstractUpdateService implements IUpdateService { return false; } + // --- Start Positron --- try { - // --- Start Positron --- return this.requestService.request({ url: this.url }, CancellationToken.None) .then(asJson) .then(update => { @@ -296,12 +296,12 @@ export abstract class AbstractUpdateService implements IUpdateService { } return Promise.resolve(hasUpdate(update, this.productService.positronVersion)); }); - // --- End Positron --- } catch (error) { this.logService.error('update#isLatestVersion(): failed to check for updates'); this.logService.error(error); return undefined; } + // --- End Positron --- } async _applySpecificUpdate(packagePath: string): Promise { @@ -316,8 +316,9 @@ export abstract class AbstractUpdateService implements IUpdateService { // noop } - protected abstract doCheckForUpdates(context: any): void; // --- Start Positron --- + // This isn't actually used for Positron updates but is kept to make future merges from upstream easier + protected abstract doCheckForUpdates(context: any): void; protected abstract buildUpdateFeedUrl(channel: string): string | undefined; protected updateAvailable(context: IUpdate): void { this.setState(State.AvailableForDownload(context)); diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 9639157451b..bdc0d7ca0d1 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -93,6 +93,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau } return url; } + // --- End Positron --- // Unused for Positron protected doCheckForUpdates(context: any): void { @@ -100,6 +101,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau electron.autoUpdater.checkForUpdates(); } + // --- Start Positron --- /** * Manually check for updates and call Electron to install the update if an update is available. */ From d2ea088e0de78320c928d8f589b71ab214475031 Mon Sep 17 00:00:00 2001 From: Tim Mok Date: Thu, 9 Jan 2025 11:14:20 -0500 Subject: [PATCH 8/8] Update abstractUpdateService.ts --- src/vs/platform/update/electron-main/abstractUpdateService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index ebb03d745f3..9293323d58a 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -163,6 +163,8 @@ export abstract class AbstractUpdateService implements IUpdateService { } // --- Start Positron --- + // This is essentially the update 'channel' (aka insiders, stable, etc.). VS Code sets it through the + // product.json. Positron will have it configurable for now. // @ts-ignore private getProductQuality(updateMode: string): string | undefined { return updateMode === 'none' ? undefined : this.productService.quality;