Skip to content

Commit

Permalink
Auto update notification (#5884)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
timtmok authored Jan 13, 2025
1 parent 968a8ac commit 7f92f2a
Show file tree
Hide file tree
Showing 9 changed files with 383 additions and 63 deletions.
12 changes: 4 additions & 8 deletions build/secrets/.secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -599,31 +595,31 @@
"filename": "product.json",
"hashed_secret": "829eaa1d03499ac3be472b80e2d7a7b7df709fca",
"is_verified": false,
"line_number": 43,
"line_number": 45,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "product.json",
"hashed_secret": "6dddc5671d5ed146759f817632cbb0c2d278fdd0",
"is_verified": false,
"line_number": 59,
"line_number": 61,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "product.json",
"hashed_secret": "98f6b0e364fc315e344dd24ce8ccd59b9a827ccd",
"is_verified": false,
"line_number": 75,
"line_number": 77,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "product.json",
"hashed_secret": "aa5db2e10c46111cbb98fa5d5bf0f7a51937dbfe",
"is_verified": false,
"line_number": 229,
"line_number": 231,
"is_secret": false
}
],
Expand Down
2 changes: 2 additions & 0 deletions product.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 16 additions & 6 deletions src/vs/platform/update/common/update.config.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurati
import { Registry } from '../../registry/common/platform.js';

const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
// --- Start Positron ---
configurationRegistry.registerConfiguration({
id: 'update',
order: 15,
Expand All @@ -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."),
Expand All @@ -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': {
Expand All @@ -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 ---
});
124 changes: 106 additions & 18 deletions src/vs/platform/update/electron-main/abstractUpdateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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<State>();
Expand All @@ -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());
}
Expand All @@ -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<void> {
if (!this.environmentMainService.isBuilt) {
// --- Start Positron ---
const updateChannel = process.env.POSITRON_UPDATE_CHANNEL ?? UpdateChannel.Prereleases;
const autoUpdateFlag = this.configurationService.getValue<boolean>('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');
Expand Down Expand Up @@ -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<void> {
return timeout(delay)
Expand All @@ -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<IUpdate | null>(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<void> {
Expand All @@ -160,9 +227,19 @@ export abstract class AbstractUpdateService implements IUpdateService {
await this.doDownloadUpdate(this.state);
}

// --- Start Positron ---
protected async doDownloadUpdate(state: AvailableForDownload): Promise<void> {
// 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<void> {
this.logService.trace('update#applyUpdate, state = ', this.state.type);
Expand Down Expand Up @@ -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<IUpdate | null>(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<void> {
Expand All @@ -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 ---
}
Loading

0 comments on commit 7f92f2a

Please sign in to comment.