Support overwrite updates on DarwinUpdateService (#288129)

* Update DarwinUpdateService to handle pending updates during checks and errors

* wip: test update server

* refactor: remove pending update handling from DarwinUpdateService

* show action

* feat: enhance update service to support explicit and overwrite states

* feat: add redirect functionality to update server

* fix: when checking for newer update, make the right call against the update service

also add logs since we're not getting the commit id :(

* fix: darwin auto update should use correct commit version

* fix: cleanup the code a bit

* updates

* feat: add Overwriting state to update process and update related logic

* fix: enhance update check with cancellation support and timeout handling

* nit: language

* do not attempt to overwrite updates twice

* remove test update server scripts
This commit is contained in:
João Moreno
2026-01-22 16:50:50 +01:00
committed by GitHub
parent 4ab234336d
commit 88fa47422c
11 changed files with 234 additions and 116 deletions

View File

@@ -28,6 +28,7 @@ Visual Studio Code is built with a layered architecture using TypeScript, web AP
The core architecture follows these principles:
- **Layered architecture** - from `base`, `platform`, `editor`, to `workbench`
- **Dependency injection** - Services are injected through constructor parameters
- If non-service parameters are needed, they need to come after the service parameters
- **Contribution model** - Features contribute to registries and extension points
- **Cross-platform compatibility** - Abstractions separate platform-specific code

View File

@@ -648,6 +648,7 @@ export class Menubar extends Disposable {
})];
case StateType.Downloading:
case StateType.Overwriting:
return [new MenuItem({ label: nls.localize('miDownloadingUpdate', "Downloading Update..."), enabled: false })];
case StateType.Downloaded:

View File

@@ -8,9 +8,8 @@ import { upcast } from '../../../base/common/types.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
export interface IUpdate {
// Windows and Linux: 9a19815253d91900be5ec1016e0ecc7cc9a6950 (Commit Hash). Mac: 1.54.0 (Product Version)
version: string;
productVersion?: string;
version: string; // Build commit ID
productVersion?: string; // Product version like 1.2.3
timestamp?: number;
url?: string;
sha256hash?: string;
@@ -25,13 +24,16 @@ export interface IUpdate {
* ↓ ↑
* Checking for Updates → Available for Download
* ↓
* Downloading Ready
*
* DownloadedUpdating
* Overwriting
* Downloading
* Ready
* ↓ ↑ ↓
* Downloaded → Updating Overwriting → Downloading
*
* Available: There is an update available for download (linux).
* Ready: Code will be updated as soon as it restarts (win32, darwin).
* Downloaded: There is an update ready to be installed in the background (win32).
* Overwriting: A newer update is being downloaded to replace the pending update (darwin).
*/
export const enum StateType {
@@ -44,6 +46,7 @@ export const enum StateType {
Downloaded = 'downloaded',
Updating = 'updating',
Ready = 'ready',
Overwriting = 'overwriting',
}
export const enum UpdateType {
@@ -66,12 +69,13 @@ export type Disabled = { type: StateType.Disabled; reason: DisablementReason };
export type Idle = { type: StateType.Idle; updateType: UpdateType; error?: string };
export type CheckingForUpdates = { type: StateType.CheckingForUpdates; explicit: boolean };
export type AvailableForDownload = { type: StateType.AvailableForDownload; update: IUpdate };
export type Downloading = { type: StateType.Downloading };
export type Downloaded = { type: StateType.Downloaded; update: IUpdate };
export type Downloading = { type: StateType.Downloading; explicit: boolean; overwrite: boolean };
export type Downloaded = { type: StateType.Downloaded; update: IUpdate; explicit: boolean; overwrite: boolean };
export type Updating = { type: StateType.Updating; update: IUpdate };
export type Ready = { type: StateType.Ready; update: IUpdate };
export type Ready = { type: StateType.Ready; update: IUpdate; explicit: boolean; overwrite: boolean };
export type Overwriting = { type: StateType.Overwriting; explicit: boolean };
export type State = Uninitialized | Disabled | Idle | CheckingForUpdates | AvailableForDownload | Downloading | Downloaded | Updating | Ready;
export type State = Uninitialized | Disabled | Idle | CheckingForUpdates | AvailableForDownload | Downloading | Downloaded | Updating | Ready | Overwriting;
export const State = {
Uninitialized: upcast<Uninitialized>({ type: StateType.Uninitialized }),
@@ -79,10 +83,11 @@ export const State = {
Idle: (updateType: UpdateType, error?: string): Idle => ({ type: StateType.Idle, updateType, error }),
CheckingForUpdates: (explicit: boolean): CheckingForUpdates => ({ type: StateType.CheckingForUpdates, explicit }),
AvailableForDownload: (update: IUpdate): AvailableForDownload => ({ type: StateType.AvailableForDownload, update }),
Downloading: upcast<Downloading>({ type: StateType.Downloading }),
Downloaded: (update: IUpdate): Downloaded => ({ type: StateType.Downloaded, update }),
Downloading: (explicit: boolean, overwrite: boolean): Downloading => ({ type: StateType.Downloading, explicit, overwrite }),
Downloaded: (update: IUpdate, explicit: boolean, overwrite: boolean): Downloaded => ({ type: StateType.Downloaded, update, explicit, overwrite }),
Updating: (update: IUpdate): Updating => ({ type: StateType.Updating, update }),
Ready: (update: IUpdate): Ready => ({ type: StateType.Ready, update }),
Ready: (update: IUpdate, explicit: boolean, overwrite: boolean): Ready => ({ type: StateType.Ready, update, explicit, overwrite }),
Overwriting: (explicit: boolean): Overwriting => ({ type: StateType.Overwriting, explicit }),
};
export interface IAutoUpdater extends Event.NodeEventEmitter {

View File

@@ -3,8 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { timeout } from '../../../base/common/async.js';
import { CancellationToken } from '../../../base/common/cancellation.js';
import { IntervalTimer, timeout } from '../../../base/common/async.js';
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
@@ -14,8 +14,18 @@ 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}`;
export interface IUpdateURLOptions {
readonly background?: boolean;
}
export function createUpdateURL(baseUpdateUrl: string, platform: string, quality: string, commit: string, options?: IUpdateURLOptions): string {
const url = new URL(`${baseUpdateUrl}/api/update/${platform}/${quality}/${commit}`);
if (options?.background) {
url.searchParams.set('bg', 'true');
}
return url.toString();
}
export type UpdateErrorClassification = {
@@ -28,9 +38,12 @@ export abstract class AbstractUpdateService implements IUpdateService {
declare readonly _serviceBrand: undefined;
protected url: string | undefined;
protected quality: string | undefined;
private _state: State = State.Uninitialized;
protected _overwrite: boolean = false;
private _hasCheckedForOverwriteOnQuit: boolean = false;
private readonly overwriteUpdatesCheckInterval = new IntervalTimer();
private readonly _onStateChange = new Emitter<State>();
readonly onStateChange: Event<State> = this._onStateChange.event;
@@ -43,6 +56,15 @@ export abstract class AbstractUpdateService implements IUpdateService {
this.logService.info('update#setState', state.type);
this._state = state;
this._onStateChange.fire(state);
// Schedule 5-minute checks when in Ready state and overwrite is supported
if (this.supportsUpdateOverwrite) {
if (state.type === StateType.Ready) {
this.overwriteUpdatesCheckInterval.cancelAndSet(() => this.checkForOverwriteUpdates(), 5 * 60 * 1000);
} else {
this.overwriteUpdatesCheckInterval.cancel();
}
}
}
constructor(
@@ -51,7 +73,8 @@ export abstract class AbstractUpdateService implements IUpdateService {
@IEnvironmentMainService protected environmentMainService: IEnvironmentMainService,
@IRequestService protected requestService: IRequestService,
@ILogService protected logService: ILogService,
@IProductService protected readonly productService: IProductService
@IProductService protected readonly productService: IProductService,
protected readonly supportsUpdateOverwrite: boolean,
) {
lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen)
.finally(() => this.initialize());
@@ -89,19 +112,13 @@ export abstract class AbstractUpdateService implements IUpdateService {
return;
}
this.url = this.buildUpdateFeedUrl(quality);
if (!this.url) {
if (!this.buildUpdateFeedUrl(quality, this.productService.commit!)) {
this.setState(State.Disabled(DisablementReason.InvalidConfiguration));
this.logService.info('update#ctor - updates are disabled as the update URL is badly formed');
return;
}
// hidden setting
if (this.configurationService.getValue<boolean>('_update.prss')) {
const url = new URL(this.url);
url.searchParams.set('prss', 'true');
this.url = url.toString();
}
this.quality = quality;
this.setState(State.Idle(this.getUpdateType()));
@@ -174,11 +191,21 @@ export abstract class AbstractUpdateService implements IUpdateService {
// noop
}
quitAndInstall(): Promise<void> {
async quitAndInstall(): Promise<void> {
this.logService.trace('update#quitAndInstall, state = ', this.state.type);
if (this.state.type !== StateType.Ready) {
return Promise.resolve(undefined);
return undefined;
}
if (this.supportsUpdateOverwrite && !this._hasCheckedForOverwriteOnQuit) {
this._hasCheckedForOverwriteOnQuit = true;
const didOverwrite = await this.checkForOverwriteUpdates(true);
if (didOverwrite) {
this.logService.info('update#quitAndInstall(): overwrite update detected, postponing quitAndInstall');
return;
}
}
this.logService.trace('update#quitAndInstall(): before lifecycle quit()');
@@ -196,19 +223,57 @@ export abstract class AbstractUpdateService implements IUpdateService {
return Promise.resolve(undefined);
}
async isLatestVersion(): Promise<boolean | undefined> {
if (!this.url) {
private async checkForOverwriteUpdates(explicit: boolean = false): Promise<boolean> {
if (this._state.type !== StateType.Ready) {
return false;
}
const pendingUpdateCommit = this._state.update.version;
let isLatest: boolean | undefined;
try {
const cts = new CancellationTokenSource();
const timeoutPromise = timeout(2000).then(() => { cts.cancel(); return undefined; });
isLatest = await Promise.race([this.isLatestVersion(pendingUpdateCommit, cts.token), timeoutPromise]);
cts.dispose();
} catch (error) {
this.logService.warn('update#checkForOverwriteUpdates(): failed to check for updates, proceeding with restart');
this.logService.warn(error);
return false;
}
if (isLatest === false && this._state.type === StateType.Ready) {
this.logService.info('update#readyStateCheck: newer update available, restarting update machinery');
await this.cancelPendingUpdate();
this._overwrite = true;
this.setState(State.Overwriting(explicit));
this.doCheckForUpdates(explicit, pendingUpdateCommit);
return true;
}
return false;
}
async isLatestVersion(commit?: string, token: CancellationToken = CancellationToken.None): Promise<boolean | undefined> {
if (!this.quality) {
return undefined;
}
const mode = this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode');
if (mode === 'none') {
return false;
return undefined;
}
const url = this.buildUpdateFeedUrl(this.quality, commit ?? this.productService.commit!);
if (!url) {
return undefined;
}
try {
const context = await this.requestService.request({ url: this.url }, CancellationToken.None);
const context = await this.requestService.request({ url }, token);
// 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;
@@ -236,6 +301,10 @@ export abstract class AbstractUpdateService implements IUpdateService {
// noop
}
protected abstract buildUpdateFeedUrl(quality: string): string | undefined;
protected abstract doCheckForUpdates(explicit: boolean): void;
protected async cancelPendingUpdate(): Promise<void> {
// noop
}
protected abstract buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string | undefined;
protected abstract doCheckForUpdates(explicit: boolean, pendingCommit?: string): void;
}

View File

@@ -16,7 +16,7 @@ import { IProductService } from '../../product/common/productService.js';
import { IRequestService } from '../../request/common/request.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { IUpdate, State, StateType, UpdateType } from '../common/update.js';
import { AbstractUpdateService, createUpdateURL, UpdateErrorClassification } from './abstractUpdateService.js';
import { AbstractUpdateService, createUpdateURL, IUpdateURLOptions, UpdateErrorClassification } from './abstractUpdateService.js';
export class DarwinUpdateService extends AbstractUpdateService implements IRelaunchHandler {
@@ -25,7 +25,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
@memoize private get onRawError(): Event<string> { return Event.fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); }
@memoize private get onRawUpdateNotAvailable(): Event<void> { return Event.fromNodeEventEmitter<void>(electron.autoUpdater, 'update-not-available'); }
@memoize private get onRawUpdateAvailable(): Event<void> { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available'); }
@memoize private get onRawUpdateDownloaded(): Event<IUpdate> { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, timestamp) => ({ version, productVersion: version, timestamp })); }
@memoize private get onRawUpdateDownloaded(): Event<IUpdate> { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, version: string, productVersion: string, timestamp: number) => ({ version, productVersion, timestamp })); }
constructor(
@ILifecycleMainService lifecycleMainService: ILifecycleMainService,
@@ -36,7 +36,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
@ILogService logService: ILogService,
@IProductService productService: IProductService
) {
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService, true);
lifecycleMainService.setRelaunchHandler(this);
}
@@ -73,14 +73,9 @@ 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);
protected buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string | undefined {
const assetID = this.productService.darwinUniversalAssetId ?? (process.arch === 'x64' ? 'darwin' : 'darwin-arm64');
const url = createUpdateURL(this.productService.updateUrl!, assetID, quality, commit, options);
try {
electron.autoUpdater.setFeedURL({ url });
} catch (e) {
@@ -91,24 +86,38 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
return url;
}
protected doCheckForUpdates(explicit: boolean): void {
if (!this.url) {
override async checkForUpdates(explicit: boolean): Promise<void> {
this.logService.trace('update#checkForUpdates, state = ', this.state.type);
if (this.state.type !== StateType.Idle) {
return;
}
this.doCheckForUpdates(explicit);
}
protected doCheckForUpdates(explicit: boolean, pendingCommit?: string): void {
if (!this.quality) {
return;
}
this.setState(State.CheckingForUpdates(explicit));
const url = explicit ? this.url : `${this.url}?bg=true`;
electron.autoUpdater.setFeedURL({ url });
const url = this.buildUpdateFeedUrl(this.quality, pendingCommit ?? this.productService.commit!, { background: !explicit });
if (!url) {
return;
}
electron.autoUpdater.checkForUpdates();
}
private onUpdateAvailable(): void {
if (this.state.type !== StateType.CheckingForUpdates) {
if (this.state.type !== StateType.CheckingForUpdates && this.state.type !== StateType.Overwriting) {
return;
}
this.setState(State.Downloading);
this.setState(State.Downloading(this.state.explicit, this._overwrite));
}
private onUpdateDownloaded(update: IUpdate): void {
@@ -116,16 +125,10 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
return;
}
this.setState(State.Downloaded(update));
this.setState(State.Downloaded(update, this.state.explicit, this._overwrite));
this.logService.info(`Update downloaded: ${JSON.stringify(update)}`);
type UpdateDownloadedClassification = {
owner: 'joaomoreno';
newVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version number of the new VS Code that has been downloaded.' };
comment: 'This is used to know how often VS Code has successfully downloaded the update.';
};
this.telemetryService.publicLog2<{ newVersion: String }, UpdateDownloadedClassification>('update:downloaded', { newVersion: update.version });
this.setState(State.Ready(update));
this.setState(State.Ready(update, this.state.explicit, this._overwrite));
}
private onUpdateNotAvailable(): void {

View File

@@ -12,7 +12,7 @@ import { INativeHostMainService } from '../../native/electron-main/nativeHostMai
import { IProductService } from '../../product/common/productService.js';
import { asJson, IRequestService } from '../../request/common/request.js';
import { AvailableForDownload, IUpdate, State, UpdateType } from '../common/update.js';
import { AbstractUpdateService, createUpdateURL } from './abstractUpdateService.js';
import { AbstractUpdateService, createUpdateURL, IUpdateURLOptions } from './abstractUpdateService.js';
export class LinuxUpdateService extends AbstractUpdateService {
@@ -25,19 +25,19 @@ export class LinuxUpdateService extends AbstractUpdateService {
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
@IProductService productService: IProductService
) {
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService, false);
}
protected buildUpdateFeedUrl(quality: string): string {
return createUpdateURL(`linux-${process.arch}`, quality, this.productService);
protected buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string {
return createUpdateURL(this.productService.updateUrl!, `linux-${process.arch}`, quality, commit, options);
}
protected doCheckForUpdates(explicit: boolean): void {
if (!this.url) {
protected doCheckForUpdates(explicit: boolean, _pendingCommit?: string): void {
if (!this.quality) {
return;
}
const url = explicit ? this.url : `${this.url}?bg=true`;
const url = this.buildUpdateFeedUrl(this.quality, this.productService.commit!, { background: !explicit });
this.setState(State.CheckingForUpdates(explicit));
this.requestService.request({ url }, CancellationToken.None)

View File

@@ -162,7 +162,7 @@ export class SnapUpdateService extends AbstractUpdateService {
this.setState(State.CheckingForUpdates(false));
this.isUpdateAvailable().then(result => {
if (result) {
this.setState(State.Ready({ version: 'something' }));
this.setState(State.Ready({ version: 'something' }, false, false));
} else {
this.setState(State.Idle(UpdateType.Snap));
}

View File

@@ -26,7 +26,7 @@ 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 } from './abstractUpdateService.js';
import { AbstractUpdateService, createUpdateURL, IUpdateURLOptions, UpdateErrorClassification } from './abstractUpdateService.js';
async function pollUntil(fn: () => boolean, millis = 1000): Promise<void> {
while (!fn()) {
@@ -71,7 +71,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
@IProductService productService: IProductService
) {
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService, false);
lifecycleMainService.setRelaunchHandler(this);
}
@@ -152,7 +152,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
}
}
protected buildUpdateFeedUrl(quality: string): string | undefined {
protected buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string | undefined {
let platform = `win32-${process.arch}`;
if (getUpdateType() === UpdateType.Archive) {
@@ -161,15 +161,15 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
platform += '-user';
}
return createUpdateURL(platform, quality, this.productService);
return createUpdateURL(this.productService.updateUrl!, platform, quality, commit, options);
}
protected doCheckForUpdates(explicit: boolean): void {
if (!this.url) {
if (!this.quality) {
return;
}
const url = explicit ? this.url : `${this.url}?bg=true`;
const url = this.buildUpdateFeedUrl(this.quality, this.productService.commit!, { background: !explicit });
this.setState(State.CheckingForUpdates(explicit));
this.requestService.request({ url }, CancellationToken.None)
@@ -187,7 +187,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
return Promise.resolve(null);
}
this.setState(State.Downloading);
this.setState(State.Downloading(explicit, this._overwrite));
return this.cleanup(update.version).then(() => {
return this.getUpdatePackagePath(update.version).then(updatePackagePath => {
@@ -206,7 +206,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
});
}).then(packagePath => {
this.availableUpdate = { packagePath };
this.setState(State.Downloaded(update));
this.setState(State.Downloaded(update, explicit, this._overwrite));
const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates');
if (fastUpdatesEnabled) {
@@ -214,7 +214,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
this.doApplyUpdate();
}
} else {
this.setState(State.Ready(update));
this.setState(State.Ready(update, explicit, this._overwrite));
}
});
});
@@ -268,6 +268,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
}
const update = this.state.update;
const explicit = this.state.explicit;
this.setState(State.Updating(update));
const cachePath = await this.cachePath;
@@ -292,7 +293,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
// poll for mutex-ready
pollUntil(() => mutex.isActive(readyMutexName))
.then(() => this.setState(State.Ready(update)));
.then(() => this.setState(State.Ready(update, explicit, this._overwrite)));
}
protected override doQuitAndInstall(): void {
@@ -324,16 +325,16 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates');
const update: IUpdate = { version: 'unknown', productVersion: 'unknown' };
this.setState(State.Downloading);
this.setState(State.Downloading(true, false));
this.availableUpdate = { packagePath };
this.setState(State.Downloaded(update));
this.setState(State.Downloaded(update, true, false));
if (fastUpdatesEnabled) {
if (this.productService.target === 'user') {
this.doApplyUpdate();
}
} else {
this.setState(State.Ready(update));
this.setState(State.Ready(update, true, false));
}
}
}

View File

@@ -478,6 +478,7 @@ export class CustomMenubarControl extends MenubarControl {
});
case StateType.Downloading:
case StateType.Overwriting:
return toAction({ id: 'update.downloading', label: localize('DownloadingUpdate', "Downloading Update..."), enabled: false, run: () => { } });
case StateType.Downloaded:

View File

@@ -12,8 +12,8 @@ import { IInstantiationService, ServicesAccessor } from '../../../../platform/in
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { IWorkbenchContribution } from '../../../common/contributions.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { IUpdateService, State as UpdateState, StateType, IUpdate, DisablementReason } from '../../../../platform/update/common/update.js';
import { INotificationService, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js';
import { IUpdateService, State as UpdateState, StateType, IUpdate, DisablementReason, Ready, Overwriting } from '../../../../platform/update/common/update.js';
import { INotificationService, INotificationHandle, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js';
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { IBrowserWorkbenchEnvironmentService } from '../../../services/environment/browser/environmentService.js';
import { ReleaseNotesManager } from './releaseNotesEditor.js';
@@ -161,6 +161,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu
private state: UpdateState;
private readonly badgeDisposable = this._register(new MutableDisposable());
private overwriteNotificationHandle: INotificationHandle | undefined;
private updateStateContextKey: IContextKey<string>;
private majorMinorUpdateAvailableContextKey: IContextKey<boolean>;
@@ -244,14 +245,18 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu
this.onUpdateDownloaded(state.update);
break;
case StateType.Overwriting:
this.onUpdateOverwriting(state);
break;
case StateType.Ready: {
const productVersion = state.update.productVersion;
if (productVersion) {
const currentVersion = parseVersion(this.productService.version);
const nextVersion = parseVersion(productVersion);
this.majorMinorUpdateAvailableContextKey.set(Boolean(currentVersion && nextVersion && isMajorMinorUpdate(currentVersion, nextVersion)));
this.onUpdateReady(state.update);
}
this.onUpdateReady(state);
break;
}
}
@@ -262,7 +267,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu
badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", this.productService.nameShort));
} else if (state.type === StateType.CheckingForUpdates) {
badge = new ProgressBadge(() => nls.localize('checkingForUpdates', "Checking for {0} updates...", this.productService.nameShort));
} else if (state.type === StateType.Downloading) {
} else if (state.type === StateType.Downloading || state.type === StateType.Overwriting) {
badge = new ProgressBadge(() => nls.localize('downloading', "Downloading {0} update...", this.productService.nameShort));
} else if (state.type === StateType.Updating) {
badge = new ProgressBadge(() => nls.localize('updating', "Updating {0}...", this.productService.nameShort));
@@ -363,39 +368,71 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu
}
// windows and mac
private onUpdateReady(update: IUpdate): void {
if (!(isWindows && this.productService.target !== 'user') && !this.shouldShowNotification()) {
private onUpdateReady(state: Ready): void {
if (state.overwrite && this.overwriteNotificationHandle) {
const handle = this.overwriteNotificationHandle;
this.overwriteNotificationHandle = undefined;
// Update notification to show completion with restart action
handle.progress.done();
handle.updateMessage(nls.localize('newerUpdateReady', "The newer update is ready to install."));
handle.updateActions({
primary: [
toAction({
id: 'update.restartToUpdate',
label: nls.localize('restartToUpdate2', "Restart to Update"),
run: () => this.updateService.quitAndInstall()
})
]
});
} else if ((isWindows && this.productService.target !== 'user') || this.shouldShowNotification()) {
const actions = [{
label: nls.localize('updateNow', "Update Now"),
run: () => this.updateService.quitAndInstall()
}, {
label: nls.localize('later', "Later"),
run: () => { }
}];
const productVersion = state.update.productVersion;
if (productVersion) {
actions.push({
label: nls.localize('releaseNotes', "Release Notes"),
run: () => {
this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion));
}
});
}
// windows user fast updates and mac
this.notificationService.prompt(
severity.Info,
nls.localize('updateAvailableAfterRestart', "Restart {0} to apply the latest update.", this.productService.nameLong),
actions,
{
sticky: true,
priority: NotificationPriority.OPTIONAL
}
);
}
}
// macOS overwrite update - overwriting
private onUpdateOverwriting(state: Overwriting): void {
if (!state.explicit) {
return;
}
const actions = [{
label: nls.localize('updateNow', "Update Now"),
run: () => this.updateService.quitAndInstall()
}, {
label: nls.localize('later', "Later"),
run: () => { }
}];
// Show notification with progress
this.overwriteNotificationHandle = this.notificationService.notify({
severity: Severity.Info,
sticky: true,
message: nls.localize('newerUpdateDownloading', "We found a newer update available and have started to download it. We'll let you know as soon as it's ready to install."),
source: nls.localize('update service', "Update Service"),
});
const productVersion = update.productVersion;
if (productVersion) {
actions.push({
label: nls.localize('releaseNotes', "Release Notes"),
run: () => {
this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion));
}
});
}
// windows user fast updates and mac
this.notificationService.prompt(
severity.Info,
nls.localize('updateAvailableAfterRestart', "Restart {0} to apply the latest update.", this.productService.nameLong),
actions,
{
sticky: true,
priority: NotificationPriority.OPTIONAL
}
);
this.overwriteNotificationHandle.progress.infinite();
}
private shouldShowNotification(): boolean {

View File

@@ -70,7 +70,7 @@ export class BrowserUpdateService extends Disposable implements IUpdateService {
const update = await updateProvider.checkForUpdate();
if (update) {
// State -> Downloaded
this.state = State.Ready({ version: update.version, productVersion: update.version });
this.state = State.Ready({ version: update.version, productVersion: update.version }, explicit, false);
} else {
// State -> Idle
this.state = State.Idle(UpdateType.Archive);