未验证 提交 ab942342 编写于 作者: J Johannes Rieken 提交者: GitHub

Merge pull request #109740 from microsoft/joh/extbisect

Joh/extbisect
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { ExtensionType, IExtension } from 'vs/platform/extensions/common/extensions';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
// --- bisect service
export const IExtensionBisectService = createDecorator<IExtensionBisectService>('IExtensionBisectService');
export interface IExtensionBisectService {
readonly _serviceBrand: undefined;
isDisabledByBisect(extension: IExtension): boolean;
isActive: boolean;
disabledCount: number;
start(extensions: ILocalExtension[]): void;
next(seeingBad: boolean): { id: string, bad: boolean } | undefined;
reset(): void;
}
class BisectState {
static fromJSON(raw: string | undefined): BisectState | undefined {
if (!raw) {
return undefined;
}
try {
interface Raw extends BisectState { }
const data: Raw = JSON.parse(raw);
return new BisectState(data.extensions, data.low, data.high);
} catch {
return undefined;
}
}
readonly mid: number;
constructor(
readonly extensions: string[],
readonly low: number,
readonly high: number,
) {
this.mid = ((low + high) / 2) | 0;
}
}
class ExtensionBisectService implements IExtensionBisectService {
declare readonly _serviceBrand: undefined;
private static readonly _storageKey = 'extensionBisectState';
private readonly _state: BisectState | undefined;
private readonly _disabled = new Map<string, boolean>();
constructor(
@ILogService logService: ILogService,
@IStorageService private readonly _storageService: IStorageService,
) {
const raw = _storageService.get(ExtensionBisectService._storageKey, StorageScope.GLOBAL);
this._state = BisectState.fromJSON(raw);
if (this._state) {
const { mid, high } = this._state;
for (let i = 0; i < this._state.extensions.length; i++) {
const isDisabled = i >= mid && i < high;
this._disabled.set(this._state.extensions[i], isDisabled);
}
logService.warn('extension BISECT active', [...this._disabled]);
}
}
get isActive() {
return !!this._state;
}
get disabledCount() {
return this._state ? this._state.high - this._state.mid : -1;
}
isDisabledByBisect(extension: IExtension): boolean {
if (!this._state) {
return false;
}
const disabled = this._disabled.get(extension.identifier.id);
return disabled ?? false;
}
start(extensions: ILocalExtension[]): void {
if (this._state) {
throw new Error('invalid state');
}
const extensionIds = extensions.map(ext => ext.identifier.id);
const newState = new BisectState(extensionIds, 0, extensionIds.length);
this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(newState), StorageScope.GLOBAL);
this._storageService.flush();
}
next(seeingBad: boolean): { id: string, bad: boolean } | undefined {
if (!this._state) {
throw new Error('invalid state');
}
// check if there is only one left
if (this._state.low === this._state.high - 1) {
this.reset();
return { id: this._state.extensions[this._state.low], bad: seeingBad };
}
// the second half is disabled so if there is still bad it must be
// in the first half
const nextState = new BisectState(
this._state.extensions,
seeingBad ? this._state.low : this._state.mid,
seeingBad ? this._state.mid : this._state.high,
);
this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(nextState), StorageScope.GLOBAL);
this._storageService.flush();
return undefined;
}
reset(): void {
this._storageService.remove(ExtensionBisectService._storageKey, StorageScope.GLOBAL);
this._storageService.flush();
}
}
registerSingleton(IExtensionBisectService, ExtensionBisectService, true);
// --- bisect UI
class ExtensionBisectUi {
static ctxIsBisectActive = new RawContextKey('isExtensionBisectActive', false);
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@IExtensionBisectService private readonly _extensionBisectService: IExtensionBisectService,
@INotificationService private readonly _notificationService: INotificationService,
@ICommandService private readonly _commandService: ICommandService,
) {
if (_extensionBisectService.isActive) {
ExtensionBisectUi.ctxIsBisectActive.bindTo(contextKeyService).set(true);
this._showBisectPrompt();
}
}
private _showBisectPrompt(): void {
const goodPrompt: IPromptChoice = {
label: 'Good now',
run: () => this._commandService.executeCommand('extension.bisect.next', false)
};
const badPrompt: IPromptChoice = {
label: 'This is bad',
run: () => this._commandService.executeCommand('extension.bisect.next', true)
};
const stop: IPromptChoice = {
label: 'Stop Bisect',
run: () => this._commandService.executeCommand('extension.bisect.stop')
};
this._notificationService.prompt(
Severity.Info,
localize('bisect', "Extension Bisect is active and has disabled {0} extensions. Check if you can still reproduce the problem and proceed by selecting from these options.", this._extensionBisectService.disabledCount),
[goodPrompt, badPrompt, stop],
{ sticky: true }
);
}
}
Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench).registerWorkbenchContribution(
ExtensionBisectUi,
LifecyclePhase.Restored
);
registerAction2(class extends Action2 {
constructor() {
super({
id: 'extension.bisect.start',
title: localize('title.start', "Start Extension Bisect"),
category: localize('help', "Help"),
f1: true,
precondition: ExtensionBisectUi.ctxIsBisectActive.negate()
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const dialogService = accessor.get(IDialogService);
const hostService = accessor.get(IHostService);
const extensionManagement = accessor.get(IExtensionManagementService);
const extensionEnablementService = accessor.get(IGlobalExtensionEnablementService);
const extensionsBisect = accessor.get(IExtensionBisectService);
const disabled = new Set(extensionEnablementService.getDisabledExtensions().map(id => id.id));
const extensions = (await extensionManagement.getInstalled(ExtensionType.User)).filter(ext => !disabled.has(ext.identifier.id));
const res = await dialogService.confirm({
message: localize('msg.start', "Extension Bisect"),
detail: localize('detail.start', "Extension Bisect will use binary search to find an extension that causes a problem. During the process the window reloads repeatedly (~{0} times). Each time you must confirm if you are still seeing problems.", 1 + Math.log2(extensions.length) | 0),
primaryButton: localize('msg2', "Start Extension Bisect")
});
if (res.confirmed) {
extensionsBisect.start(extensions);
hostService.reload();
}
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'extension.bisect.next',
title: localize('title.isBad', "Continue Extension Bisect"),
category: localize('help', "Help"),
f1: true,
precondition: ExtensionBisectUi.ctxIsBisectActive
});
}
async run(accessor: ServicesAccessor, seeingBad: boolean | undefined): Promise<void> {
const dialogService = accessor.get(IDialogService);
const hostService = accessor.get(IHostService);
const bisectService = accessor.get(IExtensionBisectService);
const productService = accessor.get(IProductService);
const extensionEnablementService = accessor.get(IGlobalExtensionEnablementService);
if (!bisectService.isActive) {
return;
}
if (seeingBad === undefined) {
seeingBad = await this._checkForBad(dialogService);
}
if (seeingBad === undefined) {
bisectService.reset();
hostService.reload();
return;
}
const done = bisectService.next(seeingBad);
if (!done) {
hostService.reload();
return;
}
if (done.bad) {
// DONE but nothing found
await dialogService.show(Severity.Info, localize('done.msg', "Extension Bisect"), [], {
detail: localize('done.detail2', "Extension Bisect is done but no extension has been identified. This might be a problem with {0}", productService.nameShort)
});
} else {
// DONE and identified extension
const res = await dialogService.show(Severity.Info, localize('done.msg', "Extension Bisect"),
// [localize('report', "Report Issue & Continue"), localize('done', "Continue")],
[],
{
detail: localize('done.detail', "Extension Bisect is done and has identified {0} as the extension causing the problem.", done.id),
checkbox: { label: localize('done.disbale', "Keep this extension disabled"), checked: true },
cancelId: 1
}
);
if (res.checkboxChecked) {
await extensionEnablementService.disableExtension({ id: done.id }, undefined);
}
// if (res.choice === 0) {
// issueService.openReport({...});
// }
}
bisectService.reset();
hostService.reload();
}
private async _checkForBad(dialogService: IDialogService) {
const options = {
cancelId: 2,
detail: localize('detail.next', "Are you still seeing the problem for which you have started extension bisect?")
};
const res = await dialogService.show(
Severity.Info,
localize('msg.next', "Extension Bisect"),
[localize('next.good', "Good now"), localize('next.bad', "This is bad"), localize('next.stop', "Stop Bisect")],
options
);
if (res.choice === options.cancelId) {
return undefined;
}
return res.choice === 1;
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'extension.bisect.stop',
title: localize('title.stop', "Stop Extension Bisect"),
category: localize('help', "Help"),
f1: true,
precondition: ExtensionBisectUi.ctxIsBisectActive
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const extensionsBisect = accessor.get(IExtensionBisectService);
const hostService = accessor.get(IHostService);
extensionsBisect.reset();
hostService.reload(); //todo@jrieken reloadExtensionHost instead? update ext viewlet etc?
}
});
......@@ -24,6 +24,7 @@ import { IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/com
import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IExtensionBisectService } from 'vs/workbench/services/extensionManagement/browser/extensionBisect';
const SOURCE = 'IWorkbenchExtensionEnablementService';
......@@ -50,6 +51,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@INotificationService private readonly notificationService: INotificationService,
@IHostService hostService: IHostService,
@IExtensionBisectService private readonly extensionBisectService: IExtensionBisectService,
) {
super();
this.storageManger = this._register(new StorageManager(storageService));
......@@ -76,6 +78,9 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
}
getEnablementState(extension: IExtension): EnablementState {
if (this.extensionBisectService.isDisabledByBisect(extension)) {
return EnablementState.DisabledByEnvironemt;
}
if (this._isDisabledInEnv(extension)) {
return EnablementState.DisabledByEnvironemt;
}
......
......@@ -28,6 +28,8 @@ import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecy
import { INotificationService } from 'vs/platform/notification/common/notification';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { mock } from 'vs/base/test/common/mock';
import { IExtensionBisectService } from 'vs/workbench/services/extensionManagement/browser/extensionBisect';
function createStorageService(instantiationService: TestInstantiationService): IStorageService {
let service = instantiationService.get(IStorageService);
......@@ -61,7 +63,8 @@ export class TestExtensionEnablementService extends ExtensionEnablementService {
instantiationService.get(IUserDataSyncAccountService) || instantiationService.stub(IUserDataSyncAccountService, UserDataSyncAccountService),
instantiationService.get(ILifecycleService) || instantiationService.stub(ILifecycleService, new TestLifecycleService()),
instantiationService.get(INotificationService) || instantiationService.stub(INotificationService, new TestNotificationService()),
instantiationService.get(IHostService)
instantiationService.get(IHostService),
new class extends mock<IExtensionBisectService>() { isDisabledByBisect() { return false; } }
);
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册