Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto update notification #5884

Merged
merged 8 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading