mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-12 22:08:04 -05:00
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:
1
.github/copilot-instructions.md
vendored
1
.github/copilot-instructions.md
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user