Path: blob/main/src/vs/workbench/contrib/issue/browser/issueTroubleshoot.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 } from '../../../../platform/extensionManagement/common/extensionManagement.js';7import { ExtensionType } from '../../../../platform/extensions/common/extensions.js';8import { IProductService } from '../../../../platform/product/common/productService.js';9import { IWorkbenchIssueService } from '../common/issue.js';10import { Disposable } from '../../../../base/common/lifecycle.js';11import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';12import { IUserDataProfileImportExportService, IUserDataProfileManagementService, IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';13import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';14import { IExtensionBisectService } from '../../../services/extensionManagement/browser/extensionBisect.js';15import { INotificationHandle, INotificationService, IPromptChoice, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js';16import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js';17import { IHostService } from '../../../services/host/browser/host.js';18import { IUserDataProfile, IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';19import { ServicesAccessor, createDecorator } from '../../../../platform/instantiation/common/instantiation.js';20import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';21import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';22import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';23import { Registry } from '../../../../platform/registry/common/platform.js';24import { Extensions, IWorkbenchContributionsRegistry } from '../../../common/contributions.js';25import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js';26import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';27import { IOpenerService } from '../../../../platform/opener/common/opener.js';28import { URI } from '../../../../base/common/uri.js';29import { RemoteNameContext } from '../../../common/contextkeys.js';30import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js';3132const ITroubleshootIssueService = createDecorator<ITroubleshootIssueService>('ITroubleshootIssueService');3334interface ITroubleshootIssueService {35_serviceBrand: undefined;36isActive(): boolean;37start(): Promise<void>;38resume(): Promise<void>;39stop(): Promise<void>;40}4142enum TroubleshootStage {43EXTENSIONS = 1,44WORKBENCH,45}4647type TroubleShootResult = 'good' | 'bad' | 'stop';4849class TroubleShootState {5051static fromJSON(raw: string | undefined): TroubleShootState | undefined {52if (!raw) {53return undefined;54}55try {56interface Raw extends TroubleShootState { }57const data: Raw = JSON.parse(raw);58if (59(data.stage === TroubleshootStage.EXTENSIONS || data.stage === TroubleshootStage.WORKBENCH)60&& typeof data.profile === 'string'61) {62return new TroubleShootState(data.stage, data.profile);63}64} catch { /* ignore */ }65return undefined;66}6768constructor(69readonly stage: TroubleshootStage,70readonly profile: string,71) { }72}7374class TroubleshootIssueService extends Disposable implements ITroubleshootIssueService {7576readonly _serviceBrand: undefined;7778static readonly storageKey = 'issueTroubleshootState';7980private notificationHandle: INotificationHandle | undefined;8182constructor(83@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,84@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,85@IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService,86@IUserDataProfileImportExportService private readonly userDataProfileImportExportService: IUserDataProfileImportExportService,87@IDialogService private readonly dialogService: IDialogService,88@IExtensionBisectService private readonly extensionBisectService: IExtensionBisectService,89@INotificationService private readonly notificationService: INotificationService,90@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,91@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,92@IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService,93@IProductService private readonly productService: IProductService,94@IHostService private readonly hostService: IHostService,95@IStorageService private readonly storageService: IStorageService,96@IOpenerService private readonly openerService: IOpenerService,97) {98super();99}100101isActive(): boolean {102return this.state !== undefined;103}104105async start(): Promise<void> {106if (this.isActive()) {107throw new Error('invalid state');108}109110const res = await this.dialogService.confirm({111message: localize('troubleshoot issue', "Troubleshoot Issue"),112detail: localize('detail.start', "Issue troubleshooting is a process to help you identify the cause for an issue. The cause for an issue can be a misconfiguration, due to an extension, or be {0} itself.\n\nDuring the process the window reloads repeatedly. Each time you must confirm if you are still seeing the issue.", this.productService.nameLong),113primaryButton: localize({ key: 'msg', comment: ['&& denotes a mnemonic'] }, "&&Troubleshoot Issue"),114custom: true115});116117if (!res.confirmed) {118return;119}120121const originalProfile = this.userDataProfileService.currentProfile;122await this.userDataProfileImportExportService.createTroubleshootProfile();123this.state = new TroubleShootState(TroubleshootStage.EXTENSIONS, originalProfile.id);124await this.resume();125}126127async resume(): Promise<void> {128if (!this.isActive()) {129return;130}131132if (this.state?.stage === TroubleshootStage.EXTENSIONS && !this.extensionBisectService.isActive) {133await this.reproduceIssueWithExtensionsDisabled();134}135136if (this.state?.stage === TroubleshootStage.WORKBENCH) {137await this.reproduceIssueWithEmptyProfile();138}139140await this.stop();141}142143async stop(): Promise<void> {144if (!this.isActive()) {145return;146}147148if (this.notificationHandle) {149this.notificationHandle.close();150this.notificationHandle = undefined;151}152153if (this.extensionBisectService.isActive) {154await this.extensionBisectService.reset();155}156157const profile = this.userDataProfilesService.profiles.find(p => p.id === this.state?.profile) ?? this.userDataProfilesService.defaultProfile;158this.state = undefined;159await this.userDataProfileManagementService.switchProfile(profile);160}161162private async reproduceIssueWithExtensionsDisabled(): Promise<void> {163if (!(await this.extensionManagementService.getInstalled(ExtensionType.User)).length) {164this.state = new TroubleShootState(TroubleshootStage.WORKBENCH, this.state!.profile);165return;166}167168const result = await this.askToReproduceIssue(localize('profile.extensions.disabled', "Issue troubleshooting is active and has temporarily disabled all installed extensions. Check if you can still reproduce the problem and proceed by selecting from these options."));169if (result === 'good') {170const profile = this.userDataProfilesService.profiles.find(p => p.id === this.state!.profile) ?? this.userDataProfilesService.defaultProfile;171await this.reproduceIssueWithExtensionsBisect(profile);172}173if (result === 'bad') {174this.state = new TroubleShootState(TroubleshootStage.WORKBENCH, this.state!.profile);175}176if (result === 'stop') {177await this.stop();178}179}180181private async reproduceIssueWithEmptyProfile(): Promise<void> {182await this.userDataProfileManagementService.createAndEnterTransientProfile();183this.updateState(this.state);184const result = await this.askToReproduceIssue(localize('empty.profile', "Issue troubleshooting is active and has temporarily reset your configurations to defaults. Check if you can still reproduce the problem and proceed by selecting from these options."));185if (result === 'stop') {186await this.stop();187}188if (result === 'good') {189await this.askToReportIssue(localize('issue is with configuration', "Issue troubleshooting has identified that the issue is caused by your configurations. Please report the issue by exporting your configurations using \"Export Profile\" command and share the file in the issue report."));190}191if (result === 'bad') {192await this.askToReportIssue(localize('issue is in core', "Issue troubleshooting has identified that the issue is with {0}.", this.productService.nameLong));193}194}195196private async reproduceIssueWithExtensionsBisect(profile: IUserDataProfile): Promise<void> {197await this.userDataProfileManagementService.switchProfile(profile);198const extensions = (await this.extensionManagementService.getInstalled(ExtensionType.User)).filter(ext => this.extensionEnablementService.isEnabled(ext));199await this.extensionBisectService.start(extensions);200await this.hostService.reload();201}202203private askToReproduceIssue(message: string): Promise<TroubleShootResult> {204return new Promise((c, e) => {205const goodPrompt: IPromptChoice = {206label: localize('I cannot reproduce', "I Can't Reproduce"),207run: () => c('good')208};209const badPrompt: IPromptChoice = {210label: localize('This is Bad', "I Can Reproduce"),211run: () => c('bad')212};213const stop: IPromptChoice = {214label: localize('Stop', "Stop"),215run: () => c('stop')216};217this.notificationHandle = this.notificationService.prompt(218Severity.Info,219message,220[goodPrompt, badPrompt, stop],221{ sticky: true, priority: NotificationPriority.URGENT }222);223});224}225226private async askToReportIssue(message: string): Promise<void> {227let isCheckedInInsiders = false;228if (this.productService.quality === 'stable') {229const res = await this.askToReproduceIssueWithInsiders();230if (res === 'good') {231await this.dialogService.prompt({232type: Severity.Info,233message: localize('troubleshoot issue', "Troubleshoot Issue"),234detail: localize('use insiders', "This likely means that the issue has been addressed already and will be available in an upcoming release. You can safely use {0} insiders until the new stable version is available.", this.productService.nameLong),235custom: true236});237return;238}239if (res === 'stop') {240await this.stop();241return;242}243if (res === 'bad') {244isCheckedInInsiders = true;245}246}247248await this.issueService.openReporter({249issueBody: `> ${message} ${isCheckedInInsiders ? `It is confirmed that the issue exists in ${this.productService.nameLong} Insiders` : ''}`,250});251}252253private async askToReproduceIssueWithInsiders(): Promise<TroubleShootResult | undefined> {254const confirmRes = await this.dialogService.confirm({255type: 'info',256message: localize('troubleshoot issue', "Troubleshoot Issue"),257primaryButton: localize('download insiders', "Download {0} Insiders", this.productService.nameLong),258cancelButton: localize('report anyway', "Report Issue Anyway"),259detail: localize('ask to download insiders', "Please try to download and reproduce the issue in {0} insiders.", this.productService.nameLong),260custom: {261disableCloseAction: true,262}263});264265if (!confirmRes.confirmed) {266return undefined;267}268269const opened = await this.openerService.open(URI.parse('https://aka.ms/vscode-insiders'));270if (!opened) {271return undefined;272}273274const res = await this.dialogService.prompt<TroubleShootResult>({275type: 'info',276message: localize('troubleshoot issue', "Troubleshoot Issue"),277buttons: [{278label: localize('good', "I can't reproduce"),279run: () => 'good'280}, {281label: localize('bad', "I can reproduce"),282run: () => 'bad'283}],284cancelButton: {285label: localize('stop', "Stop"),286run: () => 'stop'287},288detail: localize('ask to reproduce issue', "Please try to reproduce the issue in {0} insiders and confirm if the issue exists there.", this.productService.nameLong),289custom: {290disableCloseAction: true,291}292});293294return res.result;295}296297private _state: TroubleShootState | undefined | null;298get state(): TroubleShootState | undefined {299if (this._state === undefined) {300const raw = this.storageService.get(TroubleshootIssueService.storageKey, StorageScope.PROFILE);301this._state = TroubleShootState.fromJSON(raw);302}303return this._state || undefined;304}305306set state(state: TroubleShootState | undefined) {307this._state = state ?? null;308this.updateState(state);309}310311private updateState(state: TroubleShootState | undefined) {312if (state) {313this.storageService.store(TroubleshootIssueService.storageKey, JSON.stringify(state), StorageScope.PROFILE, StorageTarget.MACHINE);314} else {315this.storageService.remove(TroubleshootIssueService.storageKey, StorageScope.PROFILE);316}317}318}319320class IssueTroubleshootUi extends Disposable {321322static ctxIsTroubleshootActive = new RawContextKey<boolean>('isIssueTroubleshootActive', false);323324constructor(325@IContextKeyService private readonly contextKeyService: IContextKeyService,326@ITroubleshootIssueService private readonly troubleshootIssueService: ITroubleshootIssueService,327@IStorageService storageService: IStorageService,328) {329super();330this.updateContext();331if (troubleshootIssueService.isActive()) {332troubleshootIssueService.resume();333}334this._register(storageService.onDidChangeValue(StorageScope.PROFILE, TroubleshootIssueService.storageKey, this._store)(() => {335this.updateContext();336}));337}338339private updateContext(): void {340IssueTroubleshootUi.ctxIsTroubleshootActive.bindTo(this.contextKeyService).set(this.troubleshootIssueService.isActive());341}342343}344345Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench).registerWorkbenchContribution(IssueTroubleshootUi, LifecyclePhase.Restored);346347registerAction2(class TroubleshootIssueAction extends Action2 {348constructor() {349super({350id: 'workbench.action.troubleshootIssue.start',351title: localize2('troubleshootIssue', 'Troubleshoot Issue...'),352category: Categories.Help,353f1: true,354precondition: ContextKeyExpr.and(IssueTroubleshootUi.ctxIsTroubleshootActive.negate(), RemoteNameContext.isEqualTo(''), IsWebContext.negate()),355});356}357run(accessor: ServicesAccessor): Promise<void> {358return accessor.get(ITroubleshootIssueService).start();359}360});361362registerAction2(class extends Action2 {363constructor() {364super({365id: 'workbench.action.troubleshootIssue.stop',366title: localize2('title.stop', 'Stop Troubleshoot Issue'),367category: Categories.Help,368f1: true,369precondition: IssueTroubleshootUi.ctxIsTroubleshootActive370});371}372373async run(accessor: ServicesAccessor): Promise<void> {374return accessor.get(ITroubleshootIssueService).stop();375}376});377378379registerSingleton(ITroubleshootIssueService, TroubleshootIssueService, InstantiationType.Delayed);380381382