Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.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 { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';
7
import { Emitter } from '../../../../base/common/event.js';
8
import * as path from '../../../../base/common/path.js';
9
import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
10
import { promiseWithResolvers } from '../../../../base/common/async.js';
11
12
export class DeltaExtensionsResult {
13
constructor(
14
public readonly versionId: number,
15
public readonly removedDueToLooping: IExtensionDescription[]
16
) { }
17
}
18
19
export interface IReadOnlyExtensionDescriptionRegistry {
20
containsActivationEvent(activationEvent: string): boolean;
21
containsExtension(extensionId: ExtensionIdentifier): boolean;
22
getExtensionDescriptionsForActivationEvent(activationEvent: string): IExtensionDescription[];
23
getAllExtensionDescriptions(): IExtensionDescription[];
24
getExtensionDescription(extensionId: ExtensionIdentifier | string): IExtensionDescription | undefined;
25
getExtensionDescriptionByUUID(uuid: string): IExtensionDescription | undefined;
26
getExtensionDescriptionByIdOrUUID(extensionId: ExtensionIdentifier | string, uuid: string | undefined): IExtensionDescription | undefined;
27
}
28
29
export class ExtensionDescriptionRegistry extends Disposable implements IReadOnlyExtensionDescriptionRegistry {
30
31
public static isHostExtension(extensionId: ExtensionIdentifier | string, myRegistry: ExtensionDescriptionRegistry, globalRegistry: ExtensionDescriptionRegistry): boolean {
32
if (myRegistry.getExtensionDescription(extensionId)) {
33
// I have this extension
34
return false;
35
}
36
const extensionDescription = globalRegistry.getExtensionDescription(extensionId);
37
if (!extensionDescription) {
38
// unknown extension
39
return false;
40
}
41
if ((extensionDescription.main || extensionDescription.browser) && extensionDescription.api === 'none') {
42
return true;
43
}
44
return false;
45
}
46
47
private readonly _onDidChange = this._register(new Emitter<void>());
48
public readonly onDidChange = this._onDidChange.event;
49
50
private _versionId: number = 0;
51
private _extensionDescriptions: IExtensionDescription[];
52
private _extensionsMap!: ExtensionIdentifierMap<IExtensionDescription>;
53
private _extensionsArr!: IExtensionDescription[];
54
private _activationMap!: Map<string, IExtensionDescription[]>;
55
56
constructor(
57
private readonly _activationEventsReader: IActivationEventsReader,
58
extensionDescriptions: IExtensionDescription[]
59
) {
60
super();
61
this._extensionDescriptions = extensionDescriptions;
62
this._initialize();
63
}
64
65
private _initialize(): void {
66
// Ensure extensions are stored in the order: builtin, user, under development
67
this._extensionDescriptions.sort(extensionCmp);
68
69
this._extensionsMap = new ExtensionIdentifierMap<IExtensionDescription>();
70
this._extensionsArr = [];
71
this._activationMap = new Map<string, IExtensionDescription[]>();
72
73
for (const extensionDescription of this._extensionDescriptions) {
74
if (this._extensionsMap.has(extensionDescription.identifier)) {
75
// No overwriting allowed!
76
console.error('Extension `' + extensionDescription.identifier.value + '` is already registered');
77
continue;
78
}
79
80
this._extensionsMap.set(extensionDescription.identifier, extensionDescription);
81
this._extensionsArr.push(extensionDescription);
82
83
const activationEvents = this._activationEventsReader.readActivationEvents(extensionDescription);
84
for (const activationEvent of activationEvents) {
85
if (!this._activationMap.has(activationEvent)) {
86
this._activationMap.set(activationEvent, []);
87
}
88
this._activationMap.get(activationEvent)!.push(extensionDescription);
89
}
90
}
91
}
92
93
public set(extensionDescriptions: IExtensionDescription[]): { versionId: number } {
94
this._extensionDescriptions = extensionDescriptions;
95
this._initialize();
96
this._versionId++;
97
this._onDidChange.fire(undefined);
98
return {
99
versionId: this._versionId
100
};
101
}
102
103
public deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): DeltaExtensionsResult {
104
// It is possible that an extension is removed, only to be added again at a different version
105
// so we will first handle removals
106
this._extensionDescriptions = removeExtensions(this._extensionDescriptions, toRemove);
107
108
// Then, handle the extensions to add
109
this._extensionDescriptions = this._extensionDescriptions.concat(toAdd);
110
111
// Immediately remove looping extensions!
112
const looping = ExtensionDescriptionRegistry._findLoopingExtensions(this._extensionDescriptions);
113
this._extensionDescriptions = removeExtensions(this._extensionDescriptions, looping.map(ext => ext.identifier));
114
115
this._initialize();
116
this._versionId++;
117
this._onDidChange.fire(undefined);
118
return new DeltaExtensionsResult(this._versionId, looping);
119
}
120
121
private static _findLoopingExtensions(extensionDescriptions: IExtensionDescription[]): IExtensionDescription[] {
122
const G = new class {
123
124
private _arcs = new Map<string, string[]>();
125
private _nodesSet = new Set<string>();
126
private _nodesArr: string[] = [];
127
128
addNode(id: string): void {
129
if (!this._nodesSet.has(id)) {
130
this._nodesSet.add(id);
131
this._nodesArr.push(id);
132
}
133
}
134
135
addArc(from: string, to: string): void {
136
this.addNode(from);
137
this.addNode(to);
138
if (this._arcs.has(from)) {
139
this._arcs.get(from)!.push(to);
140
} else {
141
this._arcs.set(from, [to]);
142
}
143
}
144
145
getArcs(id: string): string[] {
146
if (this._arcs.has(id)) {
147
return this._arcs.get(id)!;
148
}
149
return [];
150
}
151
152
hasOnlyGoodArcs(id: string, good: Set<string>): boolean {
153
const dependencies = G.getArcs(id);
154
for (let i = 0; i < dependencies.length; i++) {
155
if (!good.has(dependencies[i])) {
156
return false;
157
}
158
}
159
return true;
160
}
161
162
getNodes(): string[] {
163
return this._nodesArr;
164
}
165
};
166
167
const descs = new ExtensionIdentifierMap<IExtensionDescription>();
168
for (const extensionDescription of extensionDescriptions) {
169
descs.set(extensionDescription.identifier, extensionDescription);
170
if (extensionDescription.extensionDependencies) {
171
for (const depId of extensionDescription.extensionDependencies) {
172
G.addArc(ExtensionIdentifier.toKey(extensionDescription.identifier), ExtensionIdentifier.toKey(depId));
173
}
174
}
175
}
176
177
// initialize with all extensions with no dependencies.
178
const good = new Set<string>();
179
G.getNodes().filter(id => G.getArcs(id).length === 0).forEach(id => good.add(id));
180
181
// all other extensions will be processed below.
182
const nodes = G.getNodes().filter(id => !good.has(id));
183
184
let madeProgress: boolean;
185
do {
186
madeProgress = false;
187
188
// find one extension which has only good deps
189
for (let i = 0; i < nodes.length; i++) {
190
const id = nodes[i];
191
192
if (G.hasOnlyGoodArcs(id, good)) {
193
nodes.splice(i, 1);
194
i--;
195
good.add(id);
196
madeProgress = true;
197
}
198
}
199
} while (madeProgress);
200
201
// The remaining nodes are bad and have loops
202
return nodes.map(id => descs.get(id)!);
203
}
204
205
public containsActivationEvent(activationEvent: string): boolean {
206
return this._activationMap.has(activationEvent);
207
}
208
209
public containsExtension(extensionId: ExtensionIdentifier): boolean {
210
return this._extensionsMap.has(extensionId);
211
}
212
213
public getExtensionDescriptionsForActivationEvent(activationEvent: string): IExtensionDescription[] {
214
const extensions = this._activationMap.get(activationEvent);
215
return extensions ? extensions.slice(0) : [];
216
}
217
218
public getAllExtensionDescriptions(): IExtensionDescription[] {
219
return this._extensionsArr.slice(0);
220
}
221
222
public getSnapshot(): ExtensionDescriptionRegistrySnapshot {
223
return new ExtensionDescriptionRegistrySnapshot(
224
this._versionId,
225
this.getAllExtensionDescriptions()
226
);
227
}
228
229
public getExtensionDescription(extensionId: ExtensionIdentifier | string): IExtensionDescription | undefined {
230
const extension = this._extensionsMap.get(extensionId);
231
return extension ? extension : undefined;
232
}
233
234
public getExtensionDescriptionByUUID(uuid: string): IExtensionDescription | undefined {
235
for (const extensionDescription of this._extensionsArr) {
236
if (extensionDescription.uuid === uuid) {
237
return extensionDescription;
238
}
239
}
240
return undefined;
241
}
242
243
public getExtensionDescriptionByIdOrUUID(extensionId: ExtensionIdentifier | string, uuid: string | undefined): IExtensionDescription | undefined {
244
return (
245
this.getExtensionDescription(extensionId)
246
?? (uuid ? this.getExtensionDescriptionByUUID(uuid) : undefined)
247
);
248
}
249
}
250
251
export class ExtensionDescriptionRegistrySnapshot {
252
constructor(
253
public readonly versionId: number,
254
public readonly extensions: readonly IExtensionDescription[]
255
) { }
256
}
257
258
export interface IActivationEventsReader {
259
readActivationEvents(extensionDescription: IExtensionDescription): string[];
260
}
261
262
export class LockableExtensionDescriptionRegistry implements IReadOnlyExtensionDescriptionRegistry {
263
264
private readonly _actual: ExtensionDescriptionRegistry;
265
private readonly _lock = new Lock();
266
267
constructor(activationEventsReader: IActivationEventsReader) {
268
this._actual = new ExtensionDescriptionRegistry(activationEventsReader, []);
269
}
270
271
public async acquireLock(customerName: string): Promise<ExtensionDescriptionRegistryLock> {
272
const lock = await this._lock.acquire(customerName);
273
return new ExtensionDescriptionRegistryLock(this, lock);
274
}
275
276
public deltaExtensions(acquiredLock: ExtensionDescriptionRegistryLock, toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): DeltaExtensionsResult {
277
if (!acquiredLock.isAcquiredFor(this)) {
278
throw new Error('Lock is not held');
279
}
280
return this._actual.deltaExtensions(toAdd, toRemove);
281
}
282
283
public containsActivationEvent(activationEvent: string): boolean {
284
return this._actual.containsActivationEvent(activationEvent);
285
}
286
public containsExtension(extensionId: ExtensionIdentifier): boolean {
287
return this._actual.containsExtension(extensionId);
288
}
289
public getExtensionDescriptionsForActivationEvent(activationEvent: string): IExtensionDescription[] {
290
return this._actual.getExtensionDescriptionsForActivationEvent(activationEvent);
291
}
292
public getAllExtensionDescriptions(): IExtensionDescription[] {
293
return this._actual.getAllExtensionDescriptions();
294
}
295
public getSnapshot(): ExtensionDescriptionRegistrySnapshot {
296
return this._actual.getSnapshot();
297
}
298
public getExtensionDescription(extensionId: ExtensionIdentifier | string): IExtensionDescription | undefined {
299
return this._actual.getExtensionDescription(extensionId);
300
}
301
public getExtensionDescriptionByUUID(uuid: string): IExtensionDescription | undefined {
302
return this._actual.getExtensionDescriptionByUUID(uuid);
303
}
304
public getExtensionDescriptionByIdOrUUID(extensionId: ExtensionIdentifier | string, uuid: string | undefined): IExtensionDescription | undefined {
305
return this._actual.getExtensionDescriptionByIdOrUUID(extensionId, uuid);
306
}
307
}
308
309
export class ExtensionDescriptionRegistryLock extends Disposable {
310
311
private _isDisposed = false;
312
313
constructor(
314
private readonly _registry: LockableExtensionDescriptionRegistry,
315
lock: IDisposable
316
) {
317
super();
318
this._register(lock);
319
}
320
321
public isAcquiredFor(registry: LockableExtensionDescriptionRegistry): boolean {
322
return !this._isDisposed && this._registry === registry;
323
}
324
}
325
326
class LockCustomer {
327
public readonly promise: Promise<IDisposable>;
328
private readonly _resolve: (value: IDisposable) => void;
329
330
constructor(
331
public readonly name: string
332
) {
333
const withResolvers = promiseWithResolvers<IDisposable>();
334
this.promise = withResolvers.promise;
335
this._resolve = withResolvers.resolve;
336
}
337
338
resolve(value: IDisposable): void {
339
this._resolve(value);
340
}
341
}
342
343
class Lock {
344
private readonly _pendingCustomers: LockCustomer[] = [];
345
private _isLocked = false;
346
347
public async acquire(customerName: string): Promise<IDisposable> {
348
const customer = new LockCustomer(customerName);
349
this._pendingCustomers.push(customer);
350
this._advance();
351
return customer.promise;
352
}
353
354
private _advance(): void {
355
if (this._isLocked) {
356
// cannot advance yet
357
return;
358
}
359
if (this._pendingCustomers.length === 0) {
360
// no more waiting customers
361
return;
362
}
363
364
const customer = this._pendingCustomers.shift()!;
365
366
this._isLocked = true;
367
let customerHoldsLock = true;
368
369
const logLongRunningCustomerTimeout = setTimeout(() => {
370
if (customerHoldsLock) {
371
console.warn(`The customer named ${customer.name} has been holding on to the lock for 30s. This might be a problem.`);
372
}
373
}, 30 * 1000 /* 30 seconds */);
374
375
const releaseLock = () => {
376
if (!customerHoldsLock) {
377
return;
378
}
379
clearTimeout(logLongRunningCustomerTimeout);
380
customerHoldsLock = false;
381
this._isLocked = false;
382
this._advance();
383
};
384
385
customer.resolve(toDisposable(releaseLock));
386
}
387
}
388
389
const enum SortBucket {
390
Builtin = 0,
391
User = 1,
392
Dev = 2
393
}
394
395
/**
396
* Ensure that:
397
* - first are builtin extensions
398
* - second are user extensions
399
* - third are extensions under development
400
*
401
* In each bucket, extensions must be sorted alphabetically by their folder name.
402
*/
403
function extensionCmp(a: IExtensionDescription, b: IExtensionDescription): number {
404
const aSortBucket = (a.isBuiltin ? SortBucket.Builtin : a.isUnderDevelopment ? SortBucket.Dev : SortBucket.User);
405
const bSortBucket = (b.isBuiltin ? SortBucket.Builtin : b.isUnderDevelopment ? SortBucket.Dev : SortBucket.User);
406
if (aSortBucket !== bSortBucket) {
407
return aSortBucket - bSortBucket;
408
}
409
const aLastSegment = path.posix.basename(a.extensionLocation.path);
410
const bLastSegment = path.posix.basename(b.extensionLocation.path);
411
if (aLastSegment < bLastSegment) {
412
return -1;
413
}
414
if (aLastSegment > bLastSegment) {
415
return 1;
416
}
417
return 0;
418
}
419
420
function removeExtensions(arr: IExtensionDescription[], toRemove: ExtensionIdentifier[]): IExtensionDescription[] {
421
const toRemoveSet = new ExtensionIdentifierSet(toRemove);
422
return arr.filter(extension => !toRemoveSet.has(extension.identifier));
423
}
424
425