mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-19 22:42:35 +01:00
Show installation progress in update status bar entry on Windows (#292970)
This commit is contained in:
@@ -1365,6 +1365,36 @@ end;
|
||||
|
||||
// Updates
|
||||
|
||||
function GetUpdateProgressFilePath(): String;
|
||||
begin
|
||||
Result := ExpandConstant('{param:progress}');
|
||||
end;
|
||||
|
||||
var
|
||||
LastReportedProgressPct: Integer;
|
||||
|
||||
procedure CurInstallProgressChanged(CurProgress, MaxProgress: Integer);
|
||||
var
|
||||
ProgressFilePath: String;
|
||||
ProgressContent: String;
|
||||
CurrentPct: Integer;
|
||||
begin
|
||||
if IsBackgroundUpdate() then begin
|
||||
ProgressFilePath := GetUpdateProgressFilePath();
|
||||
if ProgressFilePath <> '' then begin
|
||||
if MaxProgress > 0 then
|
||||
CurrentPct := (CurProgress * 100) div MaxProgress
|
||||
else
|
||||
CurrentPct := 0;
|
||||
if CurrentPct <> LastReportedProgressPct then begin
|
||||
LastReportedProgressPct := CurrentPct;
|
||||
ProgressContent := IntToStr(CurProgress) + ',' + IntToStr(MaxProgress);
|
||||
SaveStringToFile(ProgressFilePath, ProgressContent, False);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
var
|
||||
ShouldRestartTunnelService: Boolean;
|
||||
|
||||
@@ -1658,6 +1688,7 @@ begin
|
||||
begin
|
||||
SaveStringToFile(ExpandConstant('{app}\updating_version'), '{#Commit}', False);
|
||||
CreateMutex('{#AppMutex}-ready');
|
||||
DeleteFile(GetUpdateProgressFilePath());
|
||||
|
||||
Log('Checking whether application is still running...');
|
||||
while (CheckForMutexes('{#AppMutex}')) do
|
||||
|
||||
@@ -71,7 +71,7 @@ export type CheckingForUpdates = { type: StateType.CheckingForUpdates; explicit:
|
||||
export type AvailableForDownload = { type: StateType.AvailableForDownload; update: IUpdate };
|
||||
export type Downloading = { type: StateType.Downloading; update?: IUpdate; explicit: boolean; overwrite: boolean; downloadedBytes?: number; totalBytes?: number; startTime?: number };
|
||||
export type Downloaded = { type: StateType.Downloaded; update: IUpdate; explicit: boolean; overwrite: boolean };
|
||||
export type Updating = { type: StateType.Updating; update: IUpdate };
|
||||
export type Updating = { type: StateType.Updating; update: IUpdate; currentProgress?: number; maxProgress?: number };
|
||||
export type Ready = { type: StateType.Ready; update: IUpdate; explicit: boolean; overwrite: boolean };
|
||||
export type Overwriting = { type: StateType.Overwriting; update: IUpdate; explicit: boolean };
|
||||
|
||||
@@ -85,7 +85,7 @@ export const State = {
|
||||
AvailableForDownload: (update: IUpdate): AvailableForDownload => ({ type: StateType.AvailableForDownload, update }),
|
||||
Downloading: (update: IUpdate | undefined, explicit: boolean, overwrite: boolean, downloadedBytes?: number, totalBytes?: number, startTime?: number): Downloading => ({ type: StateType.Downloading, update, explicit, overwrite, downloadedBytes, totalBytes, startTime }),
|
||||
Downloaded: (update: IUpdate, explicit: boolean, overwrite: boolean): Downloaded => ({ type: StateType.Downloaded, update, explicit, overwrite }),
|
||||
Updating: (update: IUpdate): Updating => ({ type: StateType.Updating, update }),
|
||||
Updating: (update: IUpdate, currentProgress?: number, maxProgress?: number): Updating => ({ type: StateType.Updating, update, currentProgress, maxProgress }),
|
||||
Ready: (update: IUpdate, explicit: boolean, overwrite: boolean): Ready => ({ type: StateType.Ready, update, explicit, overwrite }),
|
||||
Overwriting: (update: IUpdate, explicit: boolean): Overwriting => ({ type: StateType.Overwriting, update, explicit }),
|
||||
};
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChildProcess, spawn } from 'child_process';
|
||||
import { app } from 'electron';
|
||||
import { existsSync, unlinkSync } from 'fs';
|
||||
import { mkdir, readFile, unlink } from 'fs/promises';
|
||||
import { tmpdir } from 'os';
|
||||
import { app } from 'electron';
|
||||
import { Delayer, timeout } from '../../../base/common/async.js';
|
||||
import { VSBuffer } from '../../../base/common/buffer.js';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
|
||||
import { memoize } from '../../../base/common/decorators.js';
|
||||
import { hash } from '../../../base/common/hash.js';
|
||||
import * as path from '../../../base/common/path.js';
|
||||
@@ -24,19 +24,13 @@ import { IEnvironmentMainService } from '../../environment/electron-main/environ
|
||||
import { IFileService } from '../../files/common/files.js';
|
||||
import { ILifecycleMainService, IRelaunchHandler, IRelaunchOptions } from '../../lifecycle/electron-main/lifecycleMainService.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import { IMeteredConnectionService } from '../../meteredConnection/common/meteredConnection.js';
|
||||
import { INativeHostMainService } from '../../native/electron-main/nativeHostMainService.js';
|
||||
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, IUpdateURLOptions, UpdateErrorClassification } from './abstractUpdateService.js';
|
||||
import { IMeteredConnectionService } from '../../meteredConnection/common/meteredConnection.js';
|
||||
|
||||
async function pollUntil(fn: () => boolean, millis = 1000): Promise<void> {
|
||||
while (!fn()) {
|
||||
await timeout(millis);
|
||||
}
|
||||
}
|
||||
|
||||
interface IAvailableUpdate {
|
||||
packagePath: string;
|
||||
@@ -61,6 +55,7 @@ function getUpdateType(): UpdateType {
|
||||
export class Win32UpdateService extends AbstractUpdateService implements IRelaunchHandler {
|
||||
|
||||
private availableUpdate: IAvailableUpdate | undefined;
|
||||
private updateCancellationTokenSource: CancellationTokenSource | undefined;
|
||||
|
||||
@memoize
|
||||
get cachePath(): Promise<string> {
|
||||
@@ -342,6 +337,13 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
// ignore
|
||||
}
|
||||
|
||||
const progressFilePath = path.join(cachePath, `update-progress`);
|
||||
try {
|
||||
await unlink(progressFilePath);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
this.availableUpdate.updateFilePath = path.join(cachePath, `CodeSetup-${this.productService.quality}-${update.version}.flag`);
|
||||
this.availableUpdate.cancelFilePath = cancelFilePath;
|
||||
|
||||
@@ -351,6 +353,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
'/verysilent',
|
||||
'/log',
|
||||
`/update="${this.availableUpdate.updateFilePath}"`,
|
||||
`/progress="${progressFilePath}"`,
|
||||
`/sessionend="${sessionEndFlagPath}"`,
|
||||
`/cancel="${cancelFilePath}"`,
|
||||
'/nocloseapplications',
|
||||
@@ -374,9 +377,53 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
const readyMutexName = `${this.productService.win32MutexName}-ready`;
|
||||
const mutex = await import('@vscode/windows-mutex');
|
||||
|
||||
// poll for mutex-ready
|
||||
pollUntil(() => mutex.isActive(readyMutexName))
|
||||
.then(() => this.setState(State.Ready(update, explicit, this._overwrite)));
|
||||
// Poll for progress and ready mutex (timeout after 30 minutes)
|
||||
const pollTimeoutMs = 30 * 60 * 1000;
|
||||
const pollStartTime = Date.now();
|
||||
|
||||
this.updateCancellationTokenSource?.dispose(true);
|
||||
const cts = this.updateCancellationTokenSource = new CancellationTokenSource();
|
||||
const token = cts.token;
|
||||
|
||||
const poll = async () => {
|
||||
while (this.state.type === StateType.Updating && !token.isCancellationRequested) {
|
||||
if (mutex.isActive(readyMutexName)) {
|
||||
this.setState(State.Ready(update, explicit, this._overwrite));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Date.now() - pollStartTime > pollTimeoutMs) {
|
||||
this.logService.warn('update#doApplyUpdate: polling timed out waiting for update to be ready');
|
||||
this.setState(State.Idle(getUpdateType(), 'Update did not complete within expected time'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const progressContent = await readFile(progressFilePath, 'utf8');
|
||||
if (!token.isCancellationRequested) {
|
||||
const [currentStr, maxStr] = progressContent.split(',');
|
||||
const currentProgress = parseInt(currentStr, 10);
|
||||
const maxProgress = parseInt(maxStr, 10);
|
||||
if (!isNaN(currentProgress) && !isNaN(maxProgress) && this.state.type === StateType.Updating) {
|
||||
if (this.state.currentProgress !== currentProgress || this.state.maxProgress !== maxProgress) {
|
||||
this.setState(State.Updating(update, currentProgress, maxProgress));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Progress file may not exist yet or be locked, ignore
|
||||
}
|
||||
|
||||
await timeout(500);
|
||||
}
|
||||
};
|
||||
|
||||
poll().finally(() => {
|
||||
if (this.updateCancellationTokenSource === cts) {
|
||||
this.updateCancellationTokenSource = undefined;
|
||||
}
|
||||
cts.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
protected override async cancelPendingUpdate(): Promise<void> {
|
||||
@@ -384,6 +431,10 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel the polling loop
|
||||
this.updateCancellationTokenSource?.dispose(true);
|
||||
this.updateCancellationTokenSource = undefined;
|
||||
|
||||
this.logService.trace('update#cancelPendingUpdate: cancelling pending update');
|
||||
const { updateProcess, updateFilePath, cancelFilePath } = this.availableUpdate;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IHoverService, nativeHoverDelegate } from '../../../../platform/hover/browser/hover.js';
|
||||
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||
import { Downloading, IUpdate, IUpdateService, Overwriting, StateType, State as UpdateState } from '../../../../platform/update/common/update.js';
|
||||
import { Downloading, IUpdate, IUpdateService, Overwriting, StateType, State as UpdateState, Updating } from '../../../../platform/update/common/update.js';
|
||||
import { IWorkbenchContribution } from '../../../common/contributions.js';
|
||||
import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, TooltipContent } from '../../../services/statusbar/browser/statusbar.js';
|
||||
import './media/updateStatusBarEntry.css';
|
||||
@@ -127,9 +127,9 @@ export class UpdateStatusBarEntryContribution extends Disposable implements IWor
|
||||
case StateType.Updating:
|
||||
this.updateStatusBarEntry({
|
||||
name: UpdateStatusBarEntryContribution.NAME,
|
||||
text: nls.localize('updateStatus.installingUpdateStatus', "$(sync~spin) Installing update..."),
|
||||
ariaLabel: nls.localize('updateStatus.installingUpdateAria', "Installing update"),
|
||||
tooltip: this.getUpdatingTooltip(state.update),
|
||||
text: this.getUpdatingText(state),
|
||||
ariaLabel: this.getUpdatingText(state),
|
||||
tooltip: this.getUpdatingTooltip(state),
|
||||
command: ShowTooltipCommand
|
||||
});
|
||||
break;
|
||||
@@ -178,8 +178,8 @@ export class UpdateStatusBarEntryContribution extends Disposable implements IWor
|
||||
this.appendHeader(container, nls.localize('updateStatus.checkingForUpdatesTitle', "Checking for Updates"), store);
|
||||
this.appendProductInfo(container);
|
||||
|
||||
const waitMessage = dom.append(container, dom.$('.progress-details'));
|
||||
waitMessage.textContent = nls.localize('updateStatus.checkingPleaseWait', "Checking for updates, please wait...");
|
||||
const message = dom.append(container, dom.$('.progress-details'));
|
||||
message.textContent = nls.localize('updateStatus.checkingPleaseWait', "Checking for updates, please wait...");
|
||||
|
||||
return container;
|
||||
}
|
||||
@@ -206,7 +206,7 @@ export class UpdateStatusBarEntryContribution extends Disposable implements IWor
|
||||
return nls.localize('updateStatus.downloadUpdateProgressStatus', "$(sync~spin) Downloading update: {0} / {1} • {2}%",
|
||||
formatBytes(downloadedBytes),
|
||||
formatBytes(totalBytes),
|
||||
Math.round((downloadedBytes / totalBytes) * 100));
|
||||
getProgressPercent(downloadedBytes, totalBytes) ?? 0);
|
||||
} else {
|
||||
return nls.localize('updateStatus.downloadUpdateStatus', "$(sync~spin) Downloading update...");
|
||||
}
|
||||
@@ -223,7 +223,7 @@ export class UpdateStatusBarEntryContribution extends Disposable implements IWor
|
||||
|
||||
const { downloadedBytes, totalBytes } = state;
|
||||
if (downloadedBytes !== undefined && totalBytes !== undefined && totalBytes > 0) {
|
||||
const percentage = Math.round((downloadedBytes / totalBytes) * 100);
|
||||
const percentage = getProgressPercent(downloadedBytes, totalBytes) ?? 0;
|
||||
|
||||
const progressContainer = dom.append(container, dom.$('.progress-container'));
|
||||
const progressBar = dom.append(progressContainer, dom.$('.progress-bar'));
|
||||
@@ -249,8 +249,8 @@ export class UpdateStatusBarEntryContribution extends Disposable implements IWor
|
||||
timeRemainingNode.textContent = `~${formatTimeRemaining(timeRemaining)} ${nls.localize('updateStatus.timeRemaining', "remaining")}`;
|
||||
}
|
||||
} else {
|
||||
const waitMessage = dom.append(container, dom.$('.progress-details'));
|
||||
waitMessage.textContent = nls.localize('updateStatus.downloadingPleaseWait', "Downloading, please wait...");
|
||||
const message = dom.append(container, dom.$('.progress-details'));
|
||||
message.textContent = nls.localize('updateStatus.downloadingPleaseWait', "Downloading, please wait...");
|
||||
}
|
||||
|
||||
return container;
|
||||
@@ -288,17 +288,39 @@ export class UpdateStatusBarEntryContribution extends Disposable implements IWor
|
||||
};
|
||||
}
|
||||
|
||||
private getUpdatingTooltip(update: IUpdate): TooltipContent {
|
||||
private getUpdatingText({ currentProgress, maxProgress }: Updating): string {
|
||||
const percentage = getProgressPercent(currentProgress, maxProgress);
|
||||
if (percentage !== undefined) {
|
||||
return nls.localize('updateStatus.installingUpdateProgressStatus', "$(sync~spin) Installing update: {0}%", percentage);
|
||||
} else {
|
||||
return nls.localize('updateStatus.installingUpdateStatus', "$(sync~spin) Installing update...");
|
||||
}
|
||||
}
|
||||
|
||||
private getUpdatingTooltip(state: Updating): TooltipContent {
|
||||
return {
|
||||
element: (token: CancellationToken) => {
|
||||
const store = this.createTooltipDisposableStore(token);
|
||||
const container = dom.$('.update-status-tooltip');
|
||||
|
||||
this.appendHeader(container, nls.localize('updateStatus.installingUpdateTitle', "Installing Update"), store);
|
||||
this.appendProductInfo(container, update);
|
||||
this.appendProductInfo(container, state.update);
|
||||
|
||||
const message = dom.append(container, dom.$('.progress-details'));
|
||||
message.textContent = nls.localize('updateStatus.installingPleaseWait', "Installing update, please wait...");
|
||||
const { currentProgress, maxProgress } = state;
|
||||
const percentage = getProgressPercent(currentProgress, maxProgress);
|
||||
if (percentage !== undefined) {
|
||||
const progressContainer = dom.append(container, dom.$('.progress-container'));
|
||||
const progressBar = dom.append(progressContainer, dom.$('.progress-bar'));
|
||||
const progressFill = dom.append(progressBar, dom.$('.progress-fill'));
|
||||
progressFill.style.width = `${percentage}%`;
|
||||
|
||||
const progressText = dom.append(progressContainer, dom.$('.progress-text'));
|
||||
const percentageSpan = dom.append(progressText, dom.$('span'));
|
||||
percentageSpan.textContent = `${percentage}%`;
|
||||
} else {
|
||||
const message = dom.append(container, dom.$('.progress-details'));
|
||||
message.textContent = nls.localize('updateStatus.installingPleaseWait', "Installing update, please wait...");
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
@@ -418,6 +440,17 @@ export class UpdateStatusBarEntryContribution extends Disposable implements IWor
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the progress percentage based on the current and maximum progress values.
|
||||
*/
|
||||
export function getProgressPercent(current: number | undefined, max: number | undefined): number | undefined {
|
||||
if (current === undefined || max === undefined || max <= 0) {
|
||||
return undefined;
|
||||
} else {
|
||||
return Math.max(Math.min(Math.round((current / max) * 100), 100), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to parse a date string and returns the timestamp or undefined if parsing fails.
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,7 @@ import assert from 'assert';
|
||||
import * as sinon from 'sinon';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { Downloading, StateType } from '../../../../../platform/update/common/update.js';
|
||||
import { computeDownloadSpeed, computeDownloadTimeRemaining, formatBytes, formatDate, formatTimeRemaining, tryParseDate } from '../../browser/updateStatusBarEntry.js';
|
||||
import { computeDownloadSpeed, computeDownloadTimeRemaining, formatBytes, formatDate, formatTimeRemaining, getProgressPercent, tryParseDate } from '../../browser/updateStatusBarEntry.js';
|
||||
|
||||
suite('UpdateStatusBarEntry', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
@@ -26,6 +26,29 @@ suite('UpdateStatusBarEntry', () => {
|
||||
return { type: StateType.Downloading, explicit: true, overwrite: false, downloadedBytes, totalBytes, startTime };
|
||||
}
|
||||
|
||||
suite('getProgressPercent', () => {
|
||||
test('handles invalid values', () => {
|
||||
assert.strictEqual(getProgressPercent(undefined, 100), undefined);
|
||||
assert.strictEqual(getProgressPercent(50, undefined), undefined);
|
||||
assert.strictEqual(getProgressPercent(undefined, undefined), undefined);
|
||||
assert.strictEqual(getProgressPercent(50, 0), undefined);
|
||||
assert.strictEqual(getProgressPercent(50, -10), undefined);
|
||||
});
|
||||
|
||||
test('computes correct percentage', () => {
|
||||
assert.strictEqual(getProgressPercent(0, 100), 0);
|
||||
assert.strictEqual(getProgressPercent(50, 100), 50);
|
||||
assert.strictEqual(getProgressPercent(100, 100), 100);
|
||||
assert.strictEqual(getProgressPercent(1, 3), 33);
|
||||
assert.strictEqual(getProgressPercent(2, 3), 67);
|
||||
});
|
||||
|
||||
test('clamps to 0-100 range', () => {
|
||||
assert.strictEqual(getProgressPercent(-10, 100), 0);
|
||||
assert.strictEqual(getProgressPercent(200, 100), 100);
|
||||
});
|
||||
});
|
||||
|
||||
suite('computeDownloadTimeRemaining', () => {
|
||||
test('returns undefined for invalid or incomplete input', () => {
|
||||
const now = Date.now();
|
||||
|
||||
Reference in New Issue
Block a user