Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostExtensionActivator.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 type * as vscode from 'vscode';
7
import * as errors from '../../../base/common/errors.js';
8
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
9
import { ExtensionDescriptionRegistry } from '../../services/extensions/common/extensionDescriptionRegistry.js';
10
import { ExtensionIdentifier, ExtensionIdentifierMap } from '../../../platform/extensions/common/extensions.js';
11
import { ExtensionActivationReason, MissingExtensionDependency } from '../../services/extensions/common/extensions.js';
12
import { ILogService } from '../../../platform/log/common/log.js';
13
import { Barrier } from '../../../base/common/async.js';
14
15
/**
16
* Represents the source code (module) of an extension.
17
*/
18
export interface IExtensionModule {
19
activate?(ctx: vscode.ExtensionContext): Promise<IExtensionAPI>;
20
deactivate?(): void;
21
}
22
23
/**
24
* Represents the API of an extension (return value of `activate`).
25
*/
26
export interface IExtensionAPI {
27
// _extensionAPIBrand: any;
28
}
29
30
export type ExtensionActivationTimesFragment = {
31
startup?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Activation occurred during startup' };
32
codeLoadingTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time it took to load the extension\'s code' };
33
activateCallTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time it took to call activate' };
34
activateResolvedTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time it took for async-activation to finish' };
35
};
36
37
export class ExtensionActivationTimes {
38
39
public static readonly NONE = new ExtensionActivationTimes(false, -1, -1, -1);
40
41
public readonly startup: boolean;
42
public readonly codeLoadingTime: number;
43
public readonly activateCallTime: number;
44
public readonly activateResolvedTime: number;
45
46
constructor(startup: boolean, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number) {
47
this.startup = startup;
48
this.codeLoadingTime = codeLoadingTime;
49
this.activateCallTime = activateCallTime;
50
this.activateResolvedTime = activateResolvedTime;
51
}
52
}
53
54
export class ExtensionActivationTimesBuilder {
55
56
private readonly _startup: boolean;
57
private _codeLoadingStart: number;
58
private _codeLoadingStop: number;
59
private _activateCallStart: number;
60
private _activateCallStop: number;
61
private _activateResolveStart: number;
62
private _activateResolveStop: number;
63
64
constructor(startup: boolean) {
65
this._startup = startup;
66
this._codeLoadingStart = -1;
67
this._codeLoadingStop = -1;
68
this._activateCallStart = -1;
69
this._activateCallStop = -1;
70
this._activateResolveStart = -1;
71
this._activateResolveStop = -1;
72
}
73
74
private _delta(start: number, stop: number): number {
75
if (start === -1 || stop === -1) {
76
return -1;
77
}
78
return stop - start;
79
}
80
81
public build(): ExtensionActivationTimes {
82
return new ExtensionActivationTimes(
83
this._startup,
84
this._delta(this._codeLoadingStart, this._codeLoadingStop),
85
this._delta(this._activateCallStart, this._activateCallStop),
86
this._delta(this._activateResolveStart, this._activateResolveStop)
87
);
88
}
89
90
public codeLoadingStart(): void {
91
this._codeLoadingStart = Date.now();
92
}
93
94
public codeLoadingStop(): void {
95
this._codeLoadingStop = Date.now();
96
}
97
98
public activateCallStart(): void {
99
this._activateCallStart = Date.now();
100
}
101
102
public activateCallStop(): void {
103
this._activateCallStop = Date.now();
104
}
105
106
public activateResolveStart(): void {
107
this._activateResolveStart = Date.now();
108
}
109
110
public activateResolveStop(): void {
111
this._activateResolveStop = Date.now();
112
}
113
}
114
115
export class ActivatedExtension {
116
117
public readonly activationFailed: boolean;
118
public readonly activationFailedError: Error | null;
119
public readonly activationTimes: ExtensionActivationTimes;
120
public readonly module: IExtensionModule;
121
public readonly exports: IExtensionAPI | undefined;
122
public readonly disposable: IDisposable;
123
124
constructor(
125
activationFailed: boolean,
126
activationFailedError: Error | null,
127
activationTimes: ExtensionActivationTimes,
128
module: IExtensionModule,
129
exports: IExtensionAPI | undefined,
130
disposable: IDisposable
131
) {
132
this.activationFailed = activationFailed;
133
this.activationFailedError = activationFailedError;
134
this.activationTimes = activationTimes;
135
this.module = module;
136
this.exports = exports;
137
this.disposable = disposable;
138
}
139
}
140
141
export class EmptyExtension extends ActivatedExtension {
142
constructor(activationTimes: ExtensionActivationTimes) {
143
super(false, null, activationTimes, { activate: undefined, deactivate: undefined }, undefined, Disposable.None);
144
}
145
}
146
147
export class HostExtension extends ActivatedExtension {
148
constructor() {
149
super(false, null, ExtensionActivationTimes.NONE, { activate: undefined, deactivate: undefined }, undefined, Disposable.None);
150
}
151
}
152
153
class FailedExtension extends ActivatedExtension {
154
constructor(activationError: Error) {
155
super(true, activationError, ExtensionActivationTimes.NONE, { activate: undefined, deactivate: undefined }, undefined, Disposable.None);
156
}
157
}
158
159
export interface IExtensionsActivatorHost {
160
onExtensionActivationError(extensionId: ExtensionIdentifier, error: Error | null, missingExtensionDependency: MissingExtensionDependency | null): void;
161
actualActivateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<ActivatedExtension>;
162
}
163
164
type ActivationIdAndReason = { id: ExtensionIdentifier; reason: ExtensionActivationReason };
165
166
export class ExtensionsActivator implements IDisposable {
167
168
private readonly _registry: ExtensionDescriptionRegistry;
169
private readonly _globalRegistry: ExtensionDescriptionRegistry;
170
private readonly _host: IExtensionsActivatorHost;
171
private readonly _operations: ExtensionIdentifierMap<ActivationOperation>;
172
/**
173
* A map of already activated events to speed things up if the same activation event is triggered multiple times.
174
*/
175
private readonly _alreadyActivatedEvents: { [activationEvent: string]: boolean };
176
177
constructor(
178
registry: ExtensionDescriptionRegistry,
179
globalRegistry: ExtensionDescriptionRegistry,
180
host: IExtensionsActivatorHost,
181
@ILogService private readonly _logService: ILogService
182
) {
183
this._registry = registry;
184
this._globalRegistry = globalRegistry;
185
this._host = host;
186
this._operations = new ExtensionIdentifierMap<ActivationOperation>();
187
this._alreadyActivatedEvents = Object.create(null);
188
}
189
190
public dispose(): void {
191
for (const [_, op] of this._operations) {
192
op.dispose();
193
}
194
}
195
196
public async waitForActivatingExtensions(): Promise<void> {
197
const res: Promise<boolean>[] = [];
198
for (const [_, op] of this._operations) {
199
res.push(op.wait());
200
}
201
await Promise.all(res);
202
}
203
204
public isActivated(extensionId: ExtensionIdentifier): boolean {
205
const op = this._operations.get(extensionId);
206
return Boolean(op && op.value);
207
}
208
209
public getActivatedExtension(extensionId: ExtensionIdentifier): ActivatedExtension {
210
const op = this._operations.get(extensionId);
211
if (!op || !op.value) {
212
throw new Error(`Extension '${extensionId.value}' is not known or not activated`);
213
}
214
return op.value;
215
}
216
217
public async activateByEvent(activationEvent: string, startup: boolean): Promise<void> {
218
if (this._alreadyActivatedEvents[activationEvent]) {
219
return;
220
}
221
222
const activateExtensions = this._registry.getExtensionDescriptionsForActivationEvent(activationEvent);
223
await this._activateExtensions(activateExtensions.map(e => ({
224
id: e.identifier,
225
reason: { startup, extensionId: e.identifier, activationEvent }
226
})));
227
228
this._alreadyActivatedEvents[activationEvent] = true;
229
}
230
231
public activateById(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void> {
232
const desc = this._registry.getExtensionDescription(extensionId);
233
if (!desc) {
234
throw new Error(`Extension '${extensionId.value}' is not known`);
235
}
236
return this._activateExtensions([{ id: desc.identifier, reason }]);
237
}
238
239
private async _activateExtensions(extensions: ActivationIdAndReason[]): Promise<void> {
240
const operations = extensions
241
.filter((p) => !this.isActivated(p.id))
242
.map(ext => this._handleActivationRequest(ext));
243
await Promise.all(operations.map(op => op.wait()));
244
}
245
246
/**
247
* Handle semantics related to dependencies for `currentExtension`.
248
* We don't need to worry about dependency loops because they are handled by the registry.
249
*/
250
private _handleActivationRequest(currentActivation: ActivationIdAndReason): ActivationOperation {
251
if (this._operations.has(currentActivation.id)) {
252
return this._operations.get(currentActivation.id)!;
253
}
254
255
if (this._isHostExtension(currentActivation.id)) {
256
return this._createAndSaveOperation(currentActivation, null, [], null);
257
}
258
259
const currentExtension = this._registry.getExtensionDescription(currentActivation.id);
260
if (!currentExtension) {
261
// Error condition 0: unknown extension
262
const error = new Error(`Cannot activate unknown extension '${currentActivation.id.value}'`);
263
const result = this._createAndSaveOperation(currentActivation, null, [], new FailedExtension(error));
264
this._host.onExtensionActivationError(
265
currentActivation.id,
266
error,
267
new MissingExtensionDependency(currentActivation.id.value)
268
);
269
return result;
270
}
271
272
const deps: ActivationOperation[] = [];
273
const depIds = (typeof currentExtension.extensionDependencies === 'undefined' ? [] : currentExtension.extensionDependencies);
274
for (const depId of depIds) {
275
276
if (this._isResolvedExtension(depId)) {
277
// This dependency is already resolved
278
continue;
279
}
280
281
const dep = this._operations.get(depId);
282
if (dep) {
283
deps.push(dep);
284
continue;
285
}
286
287
if (this._isHostExtension(depId)) {
288
// must first wait for the dependency to activate
289
deps.push(this._handleActivationRequest({
290
id: this._globalRegistry.getExtensionDescription(depId)!.identifier,
291
reason: currentActivation.reason
292
}));
293
continue;
294
}
295
296
const depDesc = this._registry.getExtensionDescription(depId);
297
if (depDesc) {
298
if (!depDesc.main && !depDesc.browser) {
299
// this dependency does not need to activate because it is descriptive only
300
continue;
301
}
302
303
// must first wait for the dependency to activate
304
deps.push(this._handleActivationRequest({
305
id: depDesc.identifier,
306
reason: currentActivation.reason
307
}));
308
continue;
309
}
310
311
// Error condition 1: unknown dependency
312
const currentExtensionFriendlyName = currentExtension.displayName || currentExtension.identifier.value;
313
const error = new Error(`Cannot activate the '${currentExtensionFriendlyName}' extension because it depends on unknown extension '${depId}'`);
314
const result = this._createAndSaveOperation(currentActivation, currentExtension.displayName, [], new FailedExtension(error));
315
this._host.onExtensionActivationError(
316
currentExtension.identifier,
317
error,
318
new MissingExtensionDependency(depId)
319
);
320
return result;
321
}
322
323
return this._createAndSaveOperation(currentActivation, currentExtension.displayName, deps, null);
324
}
325
326
private _createAndSaveOperation(activation: ActivationIdAndReason, displayName: string | null | undefined, deps: ActivationOperation[], value: ActivatedExtension | null): ActivationOperation {
327
const operation = new ActivationOperation(activation.id, displayName, activation.reason, deps, value, this._host, this._logService);
328
this._operations.set(activation.id, operation);
329
return operation;
330
}
331
332
private _isHostExtension(extensionId: ExtensionIdentifier | string): boolean {
333
return ExtensionDescriptionRegistry.isHostExtension(extensionId, this._registry, this._globalRegistry);
334
}
335
336
private _isResolvedExtension(extensionId: ExtensionIdentifier | string): boolean {
337
const extensionDescription = this._globalRegistry.getExtensionDescription(extensionId);
338
if (!extensionDescription) {
339
// unknown extension
340
return false;
341
}
342
return (!extensionDescription.main && !extensionDescription.browser);
343
}
344
}
345
346
class ActivationOperation {
347
348
private readonly _barrier = new Barrier();
349
private _isDisposed = false;
350
351
public get value(): ActivatedExtension | null {
352
return this._value;
353
}
354
355
public get friendlyName(): string {
356
return this._displayName || this._id.value;
357
}
358
359
constructor(
360
private readonly _id: ExtensionIdentifier,
361
private readonly _displayName: string | null | undefined,
362
private readonly _reason: ExtensionActivationReason,
363
private readonly _deps: ActivationOperation[],
364
private _value: ActivatedExtension | null,
365
private readonly _host: IExtensionsActivatorHost,
366
@ILogService private readonly _logService: ILogService
367
) {
368
this._initialize();
369
}
370
371
public dispose(): void {
372
this._isDisposed = true;
373
}
374
375
public wait() {
376
return this._barrier.wait();
377
}
378
379
private async _initialize(): Promise<void> {
380
await this._waitForDepsThenActivate();
381
this._barrier.open();
382
}
383
384
private async _waitForDepsThenActivate(): Promise<void> {
385
if (this._value) {
386
// this operation is already finished
387
return;
388
}
389
390
while (this._deps.length > 0) {
391
// remove completed deps
392
for (let i = 0; i < this._deps.length; i++) {
393
const dep = this._deps[i];
394
395
if (dep.value && !dep.value.activationFailed) {
396
// the dependency is already activated OK
397
this._deps.splice(i, 1);
398
i--;
399
continue;
400
}
401
402
if (dep.value && dep.value.activationFailed) {
403
// Error condition 2: a dependency has already failed activation
404
const error = new Error(`Cannot activate the '${this.friendlyName}' extension because its dependency '${dep.friendlyName}' failed to activate`);
405
(<any>error).detail = dep.value.activationFailedError;
406
this._value = new FailedExtension(error);
407
this._host.onExtensionActivationError(this._id, error, null);
408
return;
409
}
410
}
411
412
if (this._deps.length > 0) {
413
// wait for one dependency
414
await Promise.race(this._deps.map(dep => dep.wait()));
415
}
416
}
417
418
await this._activate();
419
}
420
421
private async _activate(): Promise<void> {
422
try {
423
this._value = await this._host.actualActivateExtension(this._id, this._reason);
424
} catch (err) {
425
426
const error = new Error();
427
if (err && err.name) {
428
error.name = err.name;
429
}
430
if (err && err.message) {
431
error.message = `Activating extension '${this._id.value}' failed: ${err.message}.`;
432
} else {
433
error.message = `Activating extension '${this._id.value}' failed: ${err}.`;
434
}
435
if (err && err.stack) {
436
error.stack = err.stack;
437
}
438
439
// Treat the extension as being empty
440
this._value = new FailedExtension(error);
441
442
if (this._isDisposed && errors.isCancellationError(err)) {
443
// It is expected for ongoing activations to fail if the extension host is going down
444
// So simply ignore and don't log canceled errors in this case
445
return;
446
}
447
448
this._host.onExtensionActivationError(this._id, error, null);
449
this._logService.error(`Activating extension ${this._id.value} failed due to an error:`);
450
this._logService.error(err);
451
}
452
}
453
}
454
455