Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/issue/browser/issueTroubleshoot.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { localize, localize2 } from '../../../../nls.js';
7
import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js';
8
import { ExtensionType } from '../../../../platform/extensions/common/extensions.js';
9
import { IProductService } from '../../../../platform/product/common/productService.js';
10
import { IWorkbenchIssueService } from '../common/issue.js';
11
import { Disposable } from '../../../../base/common/lifecycle.js';
12
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
13
import { IUserDataProfileImportExportService, IUserDataProfileManagementService, IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';
14
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
15
import { IExtensionBisectService } from '../../../services/extensionManagement/browser/extensionBisect.js';
16
import { INotificationHandle, INotificationService, IPromptChoice, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js';
17
import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js';
18
import { IHostService } from '../../../services/host/browser/host.js';
19
import { IUserDataProfile, IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
20
import { ServicesAccessor, createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
21
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
22
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
23
import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
24
import { Registry } from '../../../../platform/registry/common/platform.js';
25
import { Extensions, IWorkbenchContributionsRegistry } from '../../../common/contributions.js';
26
import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js';
27
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
28
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
29
import { URI } from '../../../../base/common/uri.js';
30
import { RemoteNameContext } from '../../../common/contextkeys.js';
31
import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js';
32
33
const ITroubleshootIssueService = createDecorator<ITroubleshootIssueService>('ITroubleshootIssueService');
34
35
interface ITroubleshootIssueService {
36
_serviceBrand: undefined;
37
isActive(): boolean;
38
start(): Promise<void>;
39
resume(): Promise<void>;
40
stop(): Promise<void>;
41
}
42
43
enum TroubleshootStage {
44
EXTENSIONS = 1,
45
WORKBENCH,
46
}
47
48
type TroubleShootResult = 'good' | 'bad' | 'stop';
49
50
class TroubleShootState {
51
52
static fromJSON(raw: string | undefined): TroubleShootState | undefined {
53
if (!raw) {
54
return undefined;
55
}
56
try {
57
interface Raw extends TroubleShootState { }
58
const data: Raw = JSON.parse(raw);
59
if (
60
(data.stage === TroubleshootStage.EXTENSIONS || data.stage === TroubleshootStage.WORKBENCH)
61
&& typeof data.profile === 'string'
62
) {
63
return new TroubleShootState(data.stage, data.profile);
64
}
65
} catch { /* ignore */ }
66
return undefined;
67
}
68
69
constructor(
70
readonly stage: TroubleshootStage,
71
readonly profile: string,
72
) { }
73
}
74
75
class TroubleshootIssueService extends Disposable implements ITroubleshootIssueService {
76
77
readonly _serviceBrand: undefined;
78
79
static readonly storageKey = 'issueTroubleshootState';
80
81
private notificationHandle: INotificationHandle | undefined;
82
83
constructor(
84
@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,
85
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
86
@IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService,
87
@IUserDataProfileImportExportService private readonly userDataProfileImportExportService: IUserDataProfileImportExportService,
88
@IDialogService private readonly dialogService: IDialogService,
89
@IExtensionBisectService private readonly extensionBisectService: IExtensionBisectService,
90
@INotificationService private readonly notificationService: INotificationService,
91
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
92
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
93
@IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService,
94
@IProductService private readonly productService: IProductService,
95
@IHostService private readonly hostService: IHostService,
96
@IStorageService private readonly storageService: IStorageService,
97
@IOpenerService private readonly openerService: IOpenerService,
98
) {
99
super();
100
}
101
102
isActive(): boolean {
103
return this.state !== undefined;
104
}
105
106
async start(): Promise<void> {
107
if (this.isActive()) {
108
throw new Error('invalid state');
109
}
110
111
const res = await this.dialogService.confirm({
112
message: localize('troubleshoot issue', "Troubleshoot Issue"),
113
detail: 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),
114
primaryButton: localize({ key: 'msg', comment: ['&& denotes a mnemonic'] }, "&&Troubleshoot Issue"),
115
custom: true
116
});
117
118
if (!res.confirmed) {
119
return;
120
}
121
122
const originalProfile = this.userDataProfileService.currentProfile;
123
await this.userDataProfileImportExportService.createTroubleshootProfile();
124
this.state = new TroubleShootState(TroubleshootStage.EXTENSIONS, originalProfile.id);
125
await this.resume();
126
}
127
128
async resume(): Promise<void> {
129
if (!this.isActive()) {
130
return;
131
}
132
133
if (this.state?.stage === TroubleshootStage.EXTENSIONS && !this.extensionBisectService.isActive) {
134
await this.reproduceIssueWithExtensionsDisabled();
135
}
136
137
if (this.state?.stage === TroubleshootStage.WORKBENCH) {
138
await this.reproduceIssueWithEmptyProfile();
139
}
140
141
await this.stop();
142
}
143
144
async stop(): Promise<void> {
145
if (!this.isActive()) {
146
return;
147
}
148
149
if (this.notificationHandle) {
150
this.notificationHandle.close();
151
this.notificationHandle = undefined;
152
}
153
154
if (this.extensionBisectService.isActive) {
155
await this.extensionBisectService.reset();
156
}
157
158
const profile = this.userDataProfilesService.profiles.find(p => p.id === this.state?.profile) ?? this.userDataProfilesService.defaultProfile;
159
this.state = undefined;
160
await this.userDataProfileManagementService.switchProfile(profile);
161
}
162
163
private async reproduceIssueWithExtensionsDisabled(): Promise<void> {
164
if (!(await this.extensionManagementService.getInstalled(ExtensionType.User)).length) {
165
this.state = new TroubleShootState(TroubleshootStage.WORKBENCH, this.state!.profile);
166
return;
167
}
168
169
const 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."));
170
if (result === 'good') {
171
const profile = this.userDataProfilesService.profiles.find(p => p.id === this.state!.profile) ?? this.userDataProfilesService.defaultProfile;
172
await this.reproduceIssueWithExtensionsBisect(profile);
173
}
174
if (result === 'bad') {
175
this.state = new TroubleShootState(TroubleshootStage.WORKBENCH, this.state!.profile);
176
}
177
if (result === 'stop') {
178
await this.stop();
179
}
180
}
181
182
private async reproduceIssueWithEmptyProfile(): Promise<void> {
183
await this.userDataProfileManagementService.createAndEnterTransientProfile();
184
this.updateState(this.state);
185
const 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."));
186
if (result === 'stop') {
187
await this.stop();
188
}
189
if (result === 'good') {
190
await 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."));
191
}
192
if (result === 'bad') {
193
await this.askToReportIssue(localize('issue is in core', "Issue troubleshooting has identified that the issue is with {0}.", this.productService.nameLong));
194
}
195
}
196
197
private async reproduceIssueWithExtensionsBisect(profile: IUserDataProfile): Promise<void> {
198
await this.userDataProfileManagementService.switchProfile(profile);
199
const extensions = (await this.extensionManagementService.getInstalled(ExtensionType.User)).filter(ext => this.extensionEnablementService.isEnabled(ext));
200
await this.extensionBisectService.start(extensions);
201
await this.hostService.reload();
202
}
203
204
private askToReproduceIssue(message: string): Promise<TroubleShootResult> {
205
return new Promise((c, e) => {
206
const goodPrompt: IPromptChoice = {
207
label: localize('I cannot reproduce', "I Can't Reproduce"),
208
run: () => c('good')
209
};
210
const badPrompt: IPromptChoice = {
211
label: localize('This is Bad', "I Can Reproduce"),
212
run: () => c('bad')
213
};
214
const stop: IPromptChoice = {
215
label: localize('Stop', "Stop"),
216
run: () => c('stop')
217
};
218
this.notificationHandle = this.notificationService.prompt(
219
Severity.Info,
220
message,
221
[goodPrompt, badPrompt, stop],
222
{ sticky: true, priority: NotificationPriority.URGENT }
223
);
224
});
225
}
226
227
private async askToReportIssue(message: string): Promise<void> {
228
let isCheckedInInsiders = false;
229
if (this.productService.quality === 'stable') {
230
const res = await this.askToReproduceIssueWithInsiders();
231
if (res === 'good') {
232
await this.dialogService.prompt({
233
type: Severity.Info,
234
message: localize('troubleshoot issue', "Troubleshoot Issue"),
235
detail: 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),
236
custom: true
237
});
238
return;
239
}
240
if (res === 'stop') {
241
await this.stop();
242
return;
243
}
244
if (res === 'bad') {
245
isCheckedInInsiders = true;
246
}
247
}
248
249
await this.issueService.openReporter({
250
issueBody: `> ${message} ${isCheckedInInsiders ? `It is confirmed that the issue exists in ${this.productService.nameLong} Insiders` : ''}`,
251
});
252
}
253
254
private async askToReproduceIssueWithInsiders(): Promise<TroubleShootResult | undefined> {
255
const confirmRes = await this.dialogService.confirm({
256
type: 'info',
257
message: localize('troubleshoot issue', "Troubleshoot Issue"),
258
primaryButton: localize('download insiders', "Download {0} Insiders", this.productService.nameLong),
259
cancelButton: localize('report anyway', "Report Issue Anyway"),
260
detail: localize('ask to download insiders', "Please try to download and reproduce the issue in {0} insiders.", this.productService.nameLong),
261
custom: {
262
disableCloseAction: true,
263
}
264
});
265
266
if (!confirmRes.confirmed) {
267
return undefined;
268
}
269
270
const opened = await this.openerService.open(URI.parse('https://aka.ms/vscode-insiders'));
271
if (!opened) {
272
return undefined;
273
}
274
275
const res = await this.dialogService.prompt<TroubleShootResult>({
276
type: 'info',
277
message: localize('troubleshoot issue', "Troubleshoot Issue"),
278
buttons: [{
279
label: localize('good', "I can't reproduce"),
280
run: () => 'good'
281
}, {
282
label: localize('bad', "I can reproduce"),
283
run: () => 'bad'
284
}],
285
cancelButton: {
286
label: localize('stop', "Stop"),
287
run: () => 'stop'
288
},
289
detail: localize('ask to reproduce issue', "Please try to reproduce the issue in {0} insiders and confirm if the issue exists there.", this.productService.nameLong),
290
custom: {
291
disableCloseAction: true,
292
}
293
});
294
295
return res.result;
296
}
297
298
private _state: TroubleShootState | undefined | null;
299
get state(): TroubleShootState | undefined {
300
if (this._state === undefined) {
301
const raw = this.storageService.get(TroubleshootIssueService.storageKey, StorageScope.PROFILE);
302
this._state = TroubleShootState.fromJSON(raw);
303
}
304
return this._state || undefined;
305
}
306
307
set state(state: TroubleShootState | undefined) {
308
this._state = state ?? null;
309
this.updateState(state);
310
}
311
312
private updateState(state: TroubleShootState | undefined) {
313
if (state) {
314
this.storageService.store(TroubleshootIssueService.storageKey, JSON.stringify(state), StorageScope.PROFILE, StorageTarget.MACHINE);
315
} else {
316
this.storageService.remove(TroubleshootIssueService.storageKey, StorageScope.PROFILE);
317
}
318
}
319
}
320
321
class IssueTroubleshootUi extends Disposable {
322
323
static ctxIsTroubleshootActive = new RawContextKey<boolean>('isIssueTroubleshootActive', false);
324
325
constructor(
326
@IContextKeyService private readonly contextKeyService: IContextKeyService,
327
@ITroubleshootIssueService private readonly troubleshootIssueService: ITroubleshootIssueService,
328
@IStorageService storageService: IStorageService,
329
) {
330
super();
331
this.updateContext();
332
if (troubleshootIssueService.isActive()) {
333
troubleshootIssueService.resume();
334
}
335
this._register(storageService.onDidChangeValue(StorageScope.PROFILE, TroubleshootIssueService.storageKey, this._store)(() => {
336
this.updateContext();
337
}));
338
}
339
340
private updateContext(): void {
341
IssueTroubleshootUi.ctxIsTroubleshootActive.bindTo(this.contextKeyService).set(this.troubleshootIssueService.isActive());
342
}
343
344
}
345
346
Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench).registerWorkbenchContribution(IssueTroubleshootUi, LifecyclePhase.Restored);
347
348
registerAction2(class TroubleshootIssueAction extends Action2 {
349
constructor() {
350
super({
351
id: 'workbench.action.troubleshootIssue.start',
352
title: localize2('troubleshootIssue', 'Troubleshoot Issue...'),
353
category: Categories.Help,
354
f1: true,
355
precondition: ContextKeyExpr.and(IssueTroubleshootUi.ctxIsTroubleshootActive.negate(), RemoteNameContext.isEqualTo(''), IsWebContext.negate()),
356
});
357
}
358
run(accessor: ServicesAccessor): Promise<void> {
359
return accessor.get(ITroubleshootIssueService).start();
360
}
361
});
362
363
registerAction2(class extends Action2 {
364
constructor() {
365
super({
366
id: 'workbench.action.troubleshootIssue.stop',
367
title: localize2('title.stop', 'Stop Troubleshoot Issue'),
368
category: Categories.Help,
369
f1: true,
370
precondition: IssueTroubleshootUi.ctxIsTroubleshootActive
371
});
372
}
373
374
async run(accessor: ServicesAccessor): Promise<void> {
375
return accessor.get(ITroubleshootIssueService).stop();
376
}
377
});
378
379
380
registerSingleton(ITroubleshootIssueService, TroubleshootIssueService, InstantiationType.Delayed);
381
382