Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/extensionManagement/browser/extensionBisect.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, IGlobalExtensionEnablementService, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js';
8
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
9
import { ExtensionType, IExtension, isResolverExtension } from '../../../../platform/extensions/common/extensions.js';
10
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
11
import { INotificationService, IPromptChoice, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js';
12
import { IHostService } from '../../host/browser/host.js';
13
import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
14
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
15
import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
16
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
17
import { LifecyclePhase } from '../../lifecycle/common/lifecycle.js';
18
import { Registry } from '../../../../platform/registry/common/platform.js';
19
import { Extensions, IWorkbenchContributionsRegistry } from '../../../common/contributions.js';
20
import { ICommandService } from '../../../../platform/commands/common/commands.js';
21
import { ILogService } from '../../../../platform/log/common/log.js';
22
import { IProductService } from '../../../../platform/product/common/productService.js';
23
import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
24
import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
25
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
26
import { IWorkbenchExtensionEnablementService } from '../common/extensionManagement.js';
27
28
// --- bisect service
29
30
export const IExtensionBisectService = createDecorator<IExtensionBisectService>('IExtensionBisectService');
31
32
export interface IExtensionBisectService {
33
34
readonly _serviceBrand: undefined;
35
36
isDisabledByBisect(extension: IExtension): boolean;
37
isActive: boolean;
38
disabledCount: number;
39
start(extensions: ILocalExtension[]): Promise<void>;
40
next(seeingBad: boolean): Promise<{ id: string; bad: boolean } | undefined>;
41
reset(): Promise<void>;
42
}
43
44
class BisectState {
45
46
static fromJSON(raw: string | undefined): BisectState | undefined {
47
if (!raw) {
48
return undefined;
49
}
50
try {
51
interface Raw extends BisectState { }
52
const data: Raw = JSON.parse(raw);
53
return new BisectState(data.extensions, data.low, data.high, data.mid);
54
} catch {
55
return undefined;
56
}
57
}
58
59
constructor(
60
readonly extensions: string[],
61
readonly low: number,
62
readonly high: number,
63
readonly mid: number = ((low + high) / 2) | 0
64
) { }
65
}
66
67
class ExtensionBisectService implements IExtensionBisectService {
68
69
declare readonly _serviceBrand: undefined;
70
71
private static readonly _storageKey = 'extensionBisectState';
72
73
private readonly _state: BisectState | undefined;
74
private readonly _disabled = new Map<string, boolean>();
75
76
constructor(
77
@ILogService logService: ILogService,
78
@IStorageService private readonly _storageService: IStorageService,
79
@IWorkbenchEnvironmentService private readonly _envService: IWorkbenchEnvironmentService
80
) {
81
const raw = _storageService.get(ExtensionBisectService._storageKey, StorageScope.APPLICATION);
82
this._state = BisectState.fromJSON(raw);
83
84
if (this._state) {
85
const { mid, high } = this._state;
86
for (let i = 0; i < this._state.extensions.length; i++) {
87
const isDisabled = i >= mid && i < high;
88
this._disabled.set(this._state.extensions[i], isDisabled);
89
}
90
logService.warn('extension BISECT active', [...this._disabled]);
91
}
92
}
93
94
get isActive() {
95
return !!this._state;
96
}
97
98
get disabledCount() {
99
return this._state ? this._state.high - this._state.mid : -1;
100
}
101
102
isDisabledByBisect(extension: IExtension): boolean {
103
if (!this._state) {
104
// bisect isn't active
105
return false;
106
}
107
if (isResolverExtension(extension.manifest, this._envService.remoteAuthority)) {
108
// the current remote resolver extension cannot be disabled
109
return false;
110
}
111
if (this._isEnabledInEnv(extension)) {
112
// Extension enabled in env cannot be disabled
113
return false;
114
}
115
const disabled = this._disabled.get(extension.identifier.id);
116
return disabled ?? false;
117
}
118
119
private _isEnabledInEnv(extension: IExtension): boolean {
120
return Array.isArray(this._envService.enableExtensions) && this._envService.enableExtensions.some(id => areSameExtensions({ id }, extension.identifier));
121
}
122
123
async start(extensions: ILocalExtension[]): Promise<void> {
124
if (this._state) {
125
throw new Error('invalid state');
126
}
127
const extensionIds = extensions.map(ext => ext.identifier.id);
128
const newState = new BisectState(extensionIds, 0, extensionIds.length, 0);
129
this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(newState), StorageScope.APPLICATION, StorageTarget.MACHINE);
130
await this._storageService.flush();
131
}
132
133
async next(seeingBad: boolean): Promise<{ id: string; bad: boolean } | undefined> {
134
if (!this._state) {
135
throw new Error('invalid state');
136
}
137
// check if bad when all extensions are disabled
138
if (seeingBad && this._state.mid === 0 && this._state.high === this._state.extensions.length) {
139
return { bad: true, id: '' };
140
}
141
// check if there is only one left
142
if (this._state.low === this._state.high - 1) {
143
await this.reset();
144
return { id: this._state.extensions[this._state.low], bad: seeingBad };
145
}
146
// the second half is disabled so if there is still bad it must be
147
// in the first half
148
const nextState = new BisectState(
149
this._state.extensions,
150
seeingBad ? this._state.low : this._state.mid,
151
seeingBad ? this._state.mid : this._state.high,
152
);
153
this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(nextState), StorageScope.APPLICATION, StorageTarget.MACHINE);
154
await this._storageService.flush();
155
return undefined;
156
}
157
158
async reset(): Promise<void> {
159
this._storageService.remove(ExtensionBisectService._storageKey, StorageScope.APPLICATION);
160
await this._storageService.flush();
161
}
162
}
163
164
registerSingleton(IExtensionBisectService, ExtensionBisectService, InstantiationType.Delayed);
165
166
// --- bisect UI
167
168
class ExtensionBisectUi {
169
170
static ctxIsBisectActive = new RawContextKey<boolean>('isExtensionBisectActive', false);
171
172
constructor(
173
@IContextKeyService contextKeyService: IContextKeyService,
174
@IExtensionBisectService private readonly _extensionBisectService: IExtensionBisectService,
175
@INotificationService private readonly _notificationService: INotificationService,
176
@ICommandService private readonly _commandService: ICommandService,
177
) {
178
if (_extensionBisectService.isActive) {
179
ExtensionBisectUi.ctxIsBisectActive.bindTo(contextKeyService).set(true);
180
this._showBisectPrompt();
181
}
182
}
183
184
private _showBisectPrompt(): void {
185
186
const goodPrompt: IPromptChoice = {
187
label: localize('I cannot reproduce', "I can't reproduce"),
188
run: () => this._commandService.executeCommand('extension.bisect.next', false)
189
};
190
const badPrompt: IPromptChoice = {
191
label: localize('This is Bad', "I can reproduce"),
192
run: () => this._commandService.executeCommand('extension.bisect.next', true)
193
};
194
const stop: IPromptChoice = {
195
label: 'Stop Bisect',
196
run: () => this._commandService.executeCommand('extension.bisect.stop')
197
};
198
199
const message = this._extensionBisectService.disabledCount === 1
200
? 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.")
201
: 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);
202
203
this._notificationService.prompt(
204
Severity.Info,
205
message,
206
[goodPrompt, badPrompt, stop],
207
{ sticky: true, priority: NotificationPriority.URGENT }
208
);
209
}
210
}
211
212
Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench).registerWorkbenchContribution(
213
ExtensionBisectUi,
214
LifecyclePhase.Restored
215
);
216
217
registerAction2(class extends Action2 {
218
constructor() {
219
super({
220
id: 'extension.bisect.start',
221
title: localize2('title.start', 'Start Extension Bisect'),
222
category: Categories.Help,
223
f1: true,
224
precondition: ExtensionBisectUi.ctxIsBisectActive.negate(),
225
menu: {
226
id: MenuId.ViewContainerTitle,
227
when: ContextKeyExpr.equals('viewContainer', 'workbench.view.extensions'),
228
group: '2_enablement',
229
order: 4
230
}
231
});
232
}
233
234
async run(accessor: ServicesAccessor): Promise<void> {
235
const dialogService = accessor.get(IDialogService);
236
const hostService = accessor.get(IHostService);
237
const extensionManagement = accessor.get(IExtensionManagementService);
238
const extensionEnablementService = accessor.get(IWorkbenchExtensionEnablementService);
239
const extensionsBisect = accessor.get(IExtensionBisectService);
240
241
const extensions = (await extensionManagement.getInstalled(ExtensionType.User)).filter(ext => extensionEnablementService.isEnabled(ext));
242
243
const res = await dialogService.confirm({
244
message: localize('msg.start', "Extension Bisect"),
245
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.", 2 + Math.log2(extensions.length) | 0),
246
primaryButton: localize({ key: 'msg2', comment: ['&& denotes a mnemonic'] }, "&&Start Extension Bisect")
247
});
248
249
if (res.confirmed) {
250
await extensionsBisect.start(extensions);
251
hostService.reload();
252
}
253
}
254
});
255
256
registerAction2(class extends Action2 {
257
constructor() {
258
super({
259
id: 'extension.bisect.next',
260
title: localize2('title.isBad', 'Continue Extension Bisect'),
261
category: Categories.Help,
262
f1: true,
263
precondition: ExtensionBisectUi.ctxIsBisectActive
264
});
265
}
266
267
async run(accessor: ServicesAccessor, seeingBad: boolean | undefined): Promise<void> {
268
const dialogService = accessor.get(IDialogService);
269
const hostService = accessor.get(IHostService);
270
const bisectService = accessor.get(IExtensionBisectService);
271
const productService = accessor.get(IProductService);
272
const extensionEnablementService = accessor.get(IGlobalExtensionEnablementService);
273
const commandService = accessor.get(ICommandService);
274
275
if (!bisectService.isActive) {
276
return;
277
}
278
if (seeingBad === undefined) {
279
const goodBadStopCancel = await this._checkForBad(dialogService, bisectService);
280
if (goodBadStopCancel === null) {
281
return;
282
}
283
seeingBad = goodBadStopCancel;
284
}
285
if (seeingBad === undefined) {
286
await bisectService.reset();
287
hostService.reload();
288
return;
289
}
290
const done = await bisectService.next(seeingBad);
291
if (!done) {
292
hostService.reload();
293
return;
294
}
295
296
if (done.bad) {
297
// DONE but nothing found
298
await dialogService.info(
299
localize('done.msg', "Extension Bisect"),
300
localize('done.detail2', "Extension Bisect is done but no extension has been identified. This might be a problem with {0}.", productService.nameShort)
301
);
302
303
} else {
304
// DONE and identified extension
305
const res = await dialogService.confirm({
306
type: Severity.Info,
307
message: localize('done.msg', "Extension Bisect"),
308
primaryButton: localize({ key: 'report', comment: ['&& denotes a mnemonic'] }, "&&Report Issue & Continue"),
309
cancelButton: localize('continue', "Continue"),
310
detail: localize('done.detail', "Extension Bisect is done and has identified {0} as the extension causing the problem.", done.id),
311
checkbox: { label: localize('done.disbale', "Keep this extension disabled"), checked: true }
312
});
313
if (res.checkboxChecked) {
314
await extensionEnablementService.disableExtension({ id: done.id }, undefined);
315
}
316
if (res.confirmed) {
317
await commandService.executeCommand('workbench.action.openIssueReporter', done.id);
318
}
319
}
320
await bisectService.reset();
321
hostService.reload();
322
}
323
324
private async _checkForBad(dialogService: IDialogService, bisectService: IExtensionBisectService): Promise<boolean | undefined | null> {
325
const { result } = await dialogService.prompt<boolean | undefined | null>({
326
type: Severity.Info,
327
message: localize('msg.next', "Extension Bisect"),
328
detail: 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),
329
buttons: [
330
{
331
label: localize({ key: 'next.good', comment: ['&& denotes a mnemonic'] }, "I ca&&n't reproduce"),
332
run: () => false // good now
333
},
334
{
335
label: localize({ key: 'next.bad', comment: ['&& denotes a mnemonic'] }, "I can &&reproduce"),
336
run: () => true // bad
337
},
338
{
339
label: localize({ key: 'next.stop', comment: ['&& denotes a mnemonic'] }, "&&Stop Bisect"),
340
run: () => undefined // stop
341
}
342
],
343
cancelButton: {
344
label: localize({ key: 'next.cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel Bisect"),
345
run: () => null // cancel
346
}
347
});
348
return result;
349
}
350
});
351
352
registerAction2(class extends Action2 {
353
constructor() {
354
super({
355
id: 'extension.bisect.stop',
356
title: localize2('title.stop', 'Stop Extension Bisect'),
357
category: Categories.Help,
358
f1: true,
359
precondition: ExtensionBisectUi.ctxIsBisectActive
360
});
361
}
362
363
async run(accessor: ServicesAccessor): Promise<void> {
364
const extensionsBisect = accessor.get(IExtensionBisectService);
365
const hostService = accessor.get(IHostService);
366
await extensionsBisect.reset();
367
hostService.reload();
368
}
369
});
370
371