Path: blob/main/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { localize, localize2 } from '../../../../nls.js';6import { IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js';7import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';8import { ExtensionType, IExtension, isResolverExtension } from '../../../../platform/extensions/common/extensions.js';9import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';10import { INotificationService, IPromptChoice, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js';11import { IHostService } from '../../host/browser/host.js';12import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';13import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';14import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';15import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';16import { LifecyclePhase } from '../../lifecycle/common/lifecycle.js';17import { Registry } from '../../../../platform/registry/common/platform.js';18import { Extensions, IWorkbenchContributionsRegistry } from '../../../common/contributions.js';19import { ICommandService } from '../../../../platform/commands/common/commands.js';20import { ILogService } from '../../../../platform/log/common/log.js';21import { IProductService } from '../../../../platform/product/common/productService.js';22import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';23import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';24import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';25import { IWorkbenchExtensionEnablementService } from '../common/extensionManagement.js';2627// --- bisect service2829export const IExtensionBisectService = createDecorator<IExtensionBisectService>('IExtensionBisectService');3031export interface IExtensionBisectService {3233readonly _serviceBrand: undefined;3435isDisabledByBisect(extension: IExtension): boolean;36isActive: boolean;37disabledCount: number;38start(extensions: ILocalExtension[]): Promise<void>;39next(seeingBad: boolean): Promise<{ id: string; bad: boolean } | undefined>;40reset(): Promise<void>;41}4243class BisectState {4445static fromJSON(raw: string | undefined): BisectState | undefined {46if (!raw) {47return undefined;48}49try {50interface Raw extends BisectState { }51const data: Raw = JSON.parse(raw);52return new BisectState(data.extensions, data.low, data.high, data.mid);53} catch {54return undefined;55}56}5758constructor(59readonly extensions: string[],60readonly low: number,61readonly high: number,62readonly mid: number = ((low + high) / 2) | 063) { }64}6566class ExtensionBisectService implements IExtensionBisectService {6768declare readonly _serviceBrand: undefined;6970private static readonly _storageKey = 'extensionBisectState';7172private readonly _state: BisectState | undefined;73private readonly _disabled = new Map<string, boolean>();7475constructor(76@ILogService logService: ILogService,77@IStorageService private readonly _storageService: IStorageService,78@IWorkbenchEnvironmentService private readonly _envService: IWorkbenchEnvironmentService79) {80const raw = _storageService.get(ExtensionBisectService._storageKey, StorageScope.APPLICATION);81this._state = BisectState.fromJSON(raw);8283if (this._state) {84const { mid, high } = this._state;85for (let i = 0; i < this._state.extensions.length; i++) {86const isDisabled = i >= mid && i < high;87this._disabled.set(this._state.extensions[i], isDisabled);88}89logService.warn('extension BISECT active', [...this._disabled]);90}91}9293get isActive() {94return !!this._state;95}9697get disabledCount() {98return this._state ? this._state.high - this._state.mid : -1;99}100101isDisabledByBisect(extension: IExtension): boolean {102if (!this._state) {103// bisect isn't active104return false;105}106if (isResolverExtension(extension.manifest, this._envService.remoteAuthority)) {107// the current remote resolver extension cannot be disabled108return false;109}110if (this._isEnabledInEnv(extension)) {111// Extension enabled in env cannot be disabled112return false;113}114const disabled = this._disabled.get(extension.identifier.id);115return disabled ?? false;116}117118private _isEnabledInEnv(extension: IExtension): boolean {119return Array.isArray(this._envService.enableExtensions) && this._envService.enableExtensions.some(id => areSameExtensions({ id }, extension.identifier));120}121122async start(extensions: ILocalExtension[]): Promise<void> {123if (this._state) {124throw new Error('invalid state');125}126const extensionIds = extensions.map(ext => ext.identifier.id);127const newState = new BisectState(extensionIds, 0, extensionIds.length, 0);128this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(newState), StorageScope.APPLICATION, StorageTarget.MACHINE);129await this._storageService.flush();130}131132async next(seeingBad: boolean): Promise<{ id: string; bad: boolean } | undefined> {133if (!this._state) {134throw new Error('invalid state');135}136// check if bad when all extensions are disabled137if (seeingBad && this._state.mid === 0 && this._state.high === this._state.extensions.length) {138return { bad: true, id: '' };139}140// check if there is only one left141if (this._state.low === this._state.high - 1) {142await this.reset();143return { id: this._state.extensions[this._state.low], bad: seeingBad };144}145// the second half is disabled so if there is still bad it must be146// in the first half147const nextState = new BisectState(148this._state.extensions,149seeingBad ? this._state.low : this._state.mid,150seeingBad ? this._state.mid : this._state.high,151);152this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(nextState), StorageScope.APPLICATION, StorageTarget.MACHINE);153await this._storageService.flush();154return undefined;155}156157async reset(): Promise<void> {158this._storageService.remove(ExtensionBisectService._storageKey, StorageScope.APPLICATION);159await this._storageService.flush();160}161}162163registerSingleton(IExtensionBisectService, ExtensionBisectService, InstantiationType.Delayed);164165// --- bisect UI166167class ExtensionBisectUi {168169static ctxIsBisectActive = new RawContextKey<boolean>('isExtensionBisectActive', false);170171constructor(172@IContextKeyService contextKeyService: IContextKeyService,173@IExtensionBisectService private readonly _extensionBisectService: IExtensionBisectService,174@INotificationService private readonly _notificationService: INotificationService,175@ICommandService private readonly _commandService: ICommandService,176) {177if (_extensionBisectService.isActive) {178ExtensionBisectUi.ctxIsBisectActive.bindTo(contextKeyService).set(true);179this._showBisectPrompt();180}181}182183private _showBisectPrompt(): void {184185const goodPrompt: IPromptChoice = {186label: localize('I cannot reproduce', "I can't reproduce"),187run: () => this._commandService.executeCommand('extension.bisect.next', false)188};189const badPrompt: IPromptChoice = {190label: localize('This is Bad', "I can reproduce"),191run: () => this._commandService.executeCommand('extension.bisect.next', true)192};193const stop: IPromptChoice = {194label: 'Stop Bisect',195run: () => this._commandService.executeCommand('extension.bisect.stop')196};197198const message = this._extensionBisectService.disabledCount === 1199? localize('bisect.singular', "Extension Bisect is active and has disabled 1 extension. Check if you can still reproduce the problem and proceed by selecting from these options.")200: localize('bisect.plural', "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);201202this._notificationService.prompt(203Severity.Info,204message,205[goodPrompt, badPrompt, stop],206{ sticky: true, priority: NotificationPriority.URGENT }207);208}209}210211Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench).registerWorkbenchContribution(212ExtensionBisectUi,213LifecyclePhase.Restored214);215216registerAction2(class extends Action2 {217constructor() {218super({219id: 'extension.bisect.start',220title: localize2('title.start', 'Start Extension Bisect'),221category: Categories.Help,222f1: true,223precondition: ExtensionBisectUi.ctxIsBisectActive.negate(),224menu: {225id: MenuId.ViewContainerTitle,226when: ContextKeyExpr.equals('viewContainer', 'workbench.view.extensions'),227group: '2_enablement',228order: 4229}230});231}232233async run(accessor: ServicesAccessor): Promise<void> {234const dialogService = accessor.get(IDialogService);235const hostService = accessor.get(IHostService);236const extensionManagement = accessor.get(IExtensionManagementService);237const extensionEnablementService = accessor.get(IWorkbenchExtensionEnablementService);238const extensionsBisect = accessor.get(IExtensionBisectService);239240const extensions = (await extensionManagement.getInstalled(ExtensionType.User)).filter(ext => extensionEnablementService.isEnabled(ext));241242const res = await dialogService.confirm({243message: localize('msg.start', "Extension Bisect"),244detail: 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.", 2 + Math.log2(extensions.length) | 0),245primaryButton: localize({ key: 'msg2', comment: ['&& denotes a mnemonic'] }, "&&Start Extension Bisect")246});247248if (res.confirmed) {249await extensionsBisect.start(extensions);250hostService.reload();251}252}253});254255registerAction2(class extends Action2 {256constructor() {257super({258id: 'extension.bisect.next',259title: localize2('title.isBad', 'Continue Extension Bisect'),260category: Categories.Help,261f1: true,262precondition: ExtensionBisectUi.ctxIsBisectActive263});264}265266async run(accessor: ServicesAccessor, seeingBad: boolean | undefined): Promise<void> {267const dialogService = accessor.get(IDialogService);268const hostService = accessor.get(IHostService);269const bisectService = accessor.get(IExtensionBisectService);270const productService = accessor.get(IProductService);271const extensionEnablementService = accessor.get(IGlobalExtensionEnablementService);272const commandService = accessor.get(ICommandService);273274if (!bisectService.isActive) {275return;276}277if (seeingBad === undefined) {278const goodBadStopCancel = await this._checkForBad(dialogService, bisectService);279if (goodBadStopCancel === null) {280return;281}282seeingBad = goodBadStopCancel;283}284if (seeingBad === undefined) {285await bisectService.reset();286hostService.reload();287return;288}289const done = await bisectService.next(seeingBad);290if (!done) {291hostService.reload();292return;293}294295if (done.bad) {296// DONE but nothing found297await dialogService.info(298localize('done.msg', "Extension Bisect"),299localize('done.detail2', "Extension Bisect is done but no extension has been identified. This might be a problem with {0}.", productService.nameShort)300);301302} else {303// DONE and identified extension304const res = await dialogService.confirm({305type: Severity.Info,306message: localize('done.msg', "Extension Bisect"),307primaryButton: localize({ key: 'report', comment: ['&& denotes a mnemonic'] }, "&&Report Issue & Continue"),308cancelButton: localize('continue', "Continue"),309detail: localize('done.detail', "Extension Bisect is done and has identified {0} as the extension causing the problem.", done.id),310checkbox: { label: localize('done.disbale', "Keep this extension disabled"), checked: true }311});312if (res.checkboxChecked) {313await extensionEnablementService.disableExtension({ id: done.id }, undefined);314}315if (res.confirmed) {316await commandService.executeCommand('workbench.action.openIssueReporter', done.id);317}318}319await bisectService.reset();320hostService.reload();321}322323private async _checkForBad(dialogService: IDialogService, bisectService: IExtensionBisectService): Promise<boolean | undefined | null> {324const { result } = await dialogService.prompt<boolean | undefined | null>({325type: Severity.Info,326message: localize('msg.next', "Extension Bisect"),327detail: 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.", bisectService.disabledCount),328buttons: [329{330label: localize({ key: 'next.good', comment: ['&& denotes a mnemonic'] }, "I ca&&n't reproduce"),331run: () => false // good now332},333{334label: localize({ key: 'next.bad', comment: ['&& denotes a mnemonic'] }, "I can &&reproduce"),335run: () => true // bad336},337{338label: localize({ key: 'next.stop', comment: ['&& denotes a mnemonic'] }, "&&Stop Bisect"),339run: () => undefined // stop340}341],342cancelButton: {343label: localize({ key: 'next.cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel Bisect"),344run: () => null // cancel345}346});347return result;348}349});350351registerAction2(class extends Action2 {352constructor() {353super({354id: 'extension.bisect.stop',355title: localize2('title.stop', 'Stop Extension Bisect'),356category: Categories.Help,357f1: true,358precondition: ExtensionBisectUi.ctxIsBisectActive359});360}361362async run(accessor: ServicesAccessor): Promise<void> {363const extensionsBisect = accessor.get(IExtensionBisectService);364const hostService = accessor.get(IHostService);365await extensionsBisect.reset();366hostService.reload();367}368});369370371