diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a100345f459..8dbae58b45f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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 diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 772e21dcb0e..e32d1b03b4f 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -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: diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 199f433a462..38ad531a08c 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -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 - * ↓ ↑ - * Downloaded → Updating + * ← 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({ 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({ 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 { diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index ed8043f2623..cf250380b3b 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -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(); readonly onStateChange: Event = 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('_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 { + async quitAndInstall(): Promise { 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 { - if (!this.url) { + private async checkForOverwriteUpdates(explicit: boolean = false): Promise { + 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 { + 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 { + // noop + } + + protected abstract buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string | undefined; + protected abstract doCheckForUpdates(explicit: boolean, pendingCommit?: string): void; } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index b78ebc526fc..537b46b1a86 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -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 { return Event.fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); } @memoize private get onRawUpdateNotAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-not-available'); } @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 })); } + @memoize private get onRawUpdateDownloaded(): Event { 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 { + 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 { diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 8550ace2f43..6845ba25693 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -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) diff --git a/src/vs/platform/update/electron-main/updateService.snap.ts b/src/vs/platform/update/electron-main/updateService.snap.ts index 4ca2a34a7a9..2a394e2bde4 100644 --- a/src/vs/platform/update/electron-main/updateService.snap.ts +++ b/src/vs/platform/update/electron-main/updateService.snap.ts @@ -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)); } diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 5bf91910dd7..c55bf9c08ac 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -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 { 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)); } } } diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 07137d30ebb..92fe6a17377 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -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: diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index cc12ca62fbb..a7450ed6942 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -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; private majorMinorUpdateAvailableContextKey: IContextKey; @@ -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 { diff --git a/src/vs/workbench/services/update/browser/updateService.ts b/src/vs/workbench/services/update/browser/updateService.ts index 65016e9067b..cced4f2d183 100644 --- a/src/vs/workbench/services/update/browser/updateService.ts +++ b/src/vs/workbench/services/update/browser/updateService.ts @@ -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);