From 7f92f2a9465a5237d882486b01314528c22c44ff Mon Sep 17 00:00:00 2001 From: Tim Mok Date: Mon, 13 Jan 2025 09:13:05 -0500 Subject: [PATCH] Auto update notification (#5884) Checks for updates hourly and notifies via badge decoration on gear icon. The update action opens the releases page to allow manually downloading the update. --- build/secrets/.secrets.baseline | 12 +- product.json | 2 + .../common/update.config.contribution.ts | 22 +++- .../electron-main/abstractUpdateService.ts | 124 +++++++++++++++--- .../update/electron-main/positronVersion.ts | 97 ++++++++++++++ .../electron-main/updateService.darwin.ts | 47 +++++-- .../electron-main/updateService.linux.ts | 16 ++- .../electron-main/updateService.win32.ts | 72 +++++++--- .../electron-main/positronVersions.test.ts | 54 ++++++++ 9 files changed, 383 insertions(+), 63 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 731a1d2975b..25f7ca15b1f 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..03bb4897257 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..9293323d58a 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,15 @@ 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 + // --- End Positron --- ) { + // --- Start Positron --- + this.enableAutoUpdate = process.env.POSITRON_AUTO_UPDATE === '1'; + // --- End Positron --- + lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen) .finally(() => this.initialize()); } @@ -67,35 +95,42 @@ 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 { - if (!this.environmentMainService.isBuilt) { + // --- Start Positron --- + 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); + 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'); @@ -127,9 +162,14 @@ 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; } + // --- End Positron --- private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise { return timeout(delay) @@ -147,7 +187,34 @@ export abstract class AbstractUpdateService implements IUpdateService { return; } - this.doCheckForUpdates(explicit); + // --- Start Positron --- + 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)); + }); + // --- End Positron --- } async downloadUpdate(): Promise { @@ -160,9 +227,19 @@ export abstract class AbstractUpdateService implements IUpdateService { await this.doDownloadUpdate(this.state); } + // --- Start Positron --- 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); + } 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); @@ -211,17 +288,22 @@ export abstract class AbstractUpdateService implements IUpdateService { return false; } + // --- Start Positron --- 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; - + 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)); + }); } 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 { @@ -236,6 +318,12 @@ export abstract class AbstractUpdateService implements IUpdateService { // noop } - protected abstract buildUpdateFeedUrl(quality: string): string | undefined; + // --- 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)); + } + // --- 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..a3d8af6c303 --- /dev/null +++ b/src/vs/platform/update/electron-main/positronVersion.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 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..bdc0d7ca0d1 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); @@ -90,12 +93,34 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau } return url; } + // --- End Positron --- + // Unused for Positron protected doCheckForUpdates(context: any): void { this.setState(State.CheckingForUpdates(context)); electron.autoUpdater.checkForUpdates(); } + // --- Start Positron --- + /** + * 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) { return; diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 6e076c72ed8..b198c2df7fa 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,16 +25,23 @@ 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); } - 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 --- + // 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 61109e54741..a6c9dc3bb9d 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -23,7 +23,9 @@ import { INativeHostMainService } from '../../native/electron-main/nativeHostMai 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'; +// --- Start Positron --- +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 { @@ -66,10 +68,10 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun @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); } @@ -99,18 +101,20 @@ 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-'; + const baseUrl = createUpdateURL(platform, channel, this.productService); - if (getUpdateType() === UpdateType.Archive) { - platform += '-archive'; - } else if (this.productService.target === 'user') { - platform += '-user'; - } + // TODO: properly determine if this is a user or system install + const url = `${baseUrl}/${prefix}releases.json`; - return createUpdateURL(platform, quality, this.productService); + return url; } + // --- End Positron --- + // Unused for Positron protected doCheckForUpdates(context: any): void { if (!this.url) { return; @@ -177,12 +181,48 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun }); } - protected override async doDownloadUpdate(state: AvailableForDownload): Promise { - if (state.update.url) { - this.nativeHostMainService.openExternal(undefined, state.update.url); + // --- Start Positron --- + protected override updateAvailable(update: IUpdate): void { + // Notify about updates for now. Do not download or install them. + if (!this.enableAutoUpdate) { + this.setState(State.AvailableForDownload(update)); + return; } - this.setState(State.Idle(getUpdateType())); + + // 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); + } + + 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)); + } + }); + }); } + // --- End Positron --- private async getUpdatePackagePath(version: string): Promise { const cachePath = await this.cachePath; 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..09319386d9e --- /dev/null +++ b/src/vs/platform/update/test/electron-main/positronVersions.test.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 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'); + }); + +});