Path: blob/main/src/vs/workbench/api/common/extHostExtensionActivator.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 type * as vscode from 'vscode';6import * as errors from '../../../base/common/errors.js';7import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';8import { ExtensionDescriptionRegistry } from '../../services/extensions/common/extensionDescriptionRegistry.js';9import { ExtensionIdentifier, ExtensionIdentifierMap } from '../../../platform/extensions/common/extensions.js';10import { ExtensionActivationReason, MissingExtensionDependency } from '../../services/extensions/common/extensions.js';11import { ILogService } from '../../../platform/log/common/log.js';12import { Barrier } from '../../../base/common/async.js';1314/**15* Represents the source code (module) of an extension.16*/17export interface IExtensionModule {18activate?(ctx: vscode.ExtensionContext): Promise<IExtensionAPI>;19deactivate?(): void;20}2122/**23* Represents the API of an extension (return value of `activate`).24*/25export interface IExtensionAPI {26// _extensionAPIBrand: any;27}2829export type ExtensionActivationTimesFragment = {30startup?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Activation occurred during startup' };31codeLoadingTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time it took to load the extension\'s code' };32activateCallTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time it took to call activate' };33activateResolvedTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time it took for async-activation to finish' };34};3536export class ExtensionActivationTimes {3738public static readonly NONE = new ExtensionActivationTimes(false, -1, -1, -1);3940public readonly startup: boolean;41public readonly codeLoadingTime: number;42public readonly activateCallTime: number;43public readonly activateResolvedTime: number;4445constructor(startup: boolean, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number) {46this.startup = startup;47this.codeLoadingTime = codeLoadingTime;48this.activateCallTime = activateCallTime;49this.activateResolvedTime = activateResolvedTime;50}51}5253export class ExtensionActivationTimesBuilder {5455private readonly _startup: boolean;56private _codeLoadingStart: number;57private _codeLoadingStop: number;58private _activateCallStart: number;59private _activateCallStop: number;60private _activateResolveStart: number;61private _activateResolveStop: number;6263constructor(startup: boolean) {64this._startup = startup;65this._codeLoadingStart = -1;66this._codeLoadingStop = -1;67this._activateCallStart = -1;68this._activateCallStop = -1;69this._activateResolveStart = -1;70this._activateResolveStop = -1;71}7273private _delta(start: number, stop: number): number {74if (start === -1 || stop === -1) {75return -1;76}77return stop - start;78}7980public build(): ExtensionActivationTimes {81return new ExtensionActivationTimes(82this._startup,83this._delta(this._codeLoadingStart, this._codeLoadingStop),84this._delta(this._activateCallStart, this._activateCallStop),85this._delta(this._activateResolveStart, this._activateResolveStop)86);87}8889public codeLoadingStart(): void {90this._codeLoadingStart = Date.now();91}9293public codeLoadingStop(): void {94this._codeLoadingStop = Date.now();95}9697public activateCallStart(): void {98this._activateCallStart = Date.now();99}100101public activateCallStop(): void {102this._activateCallStop = Date.now();103}104105public activateResolveStart(): void {106this._activateResolveStart = Date.now();107}108109public activateResolveStop(): void {110this._activateResolveStop = Date.now();111}112}113114export class ActivatedExtension {115116public readonly activationFailed: boolean;117public readonly activationFailedError: Error | null;118public readonly activationTimes: ExtensionActivationTimes;119public readonly module: IExtensionModule;120public readonly exports: IExtensionAPI | undefined;121public readonly disposable: IDisposable;122123constructor(124activationFailed: boolean,125activationFailedError: Error | null,126activationTimes: ExtensionActivationTimes,127module: IExtensionModule,128exports: IExtensionAPI | undefined,129disposable: IDisposable130) {131this.activationFailed = activationFailed;132this.activationFailedError = activationFailedError;133this.activationTimes = activationTimes;134this.module = module;135this.exports = exports;136this.disposable = disposable;137}138}139140export class EmptyExtension extends ActivatedExtension {141constructor(activationTimes: ExtensionActivationTimes) {142super(false, null, activationTimes, { activate: undefined, deactivate: undefined }, undefined, Disposable.None);143}144}145146export class HostExtension extends ActivatedExtension {147constructor() {148super(false, null, ExtensionActivationTimes.NONE, { activate: undefined, deactivate: undefined }, undefined, Disposable.None);149}150}151152class FailedExtension extends ActivatedExtension {153constructor(activationError: Error) {154super(true, activationError, ExtensionActivationTimes.NONE, { activate: undefined, deactivate: undefined }, undefined, Disposable.None);155}156}157158export interface IExtensionsActivatorHost {159onExtensionActivationError(extensionId: ExtensionIdentifier, error: Error | null, missingExtensionDependency: MissingExtensionDependency | null): void;160actualActivateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<ActivatedExtension>;161}162163type ActivationIdAndReason = { id: ExtensionIdentifier; reason: ExtensionActivationReason };164165export class ExtensionsActivator implements IDisposable {166167private readonly _registry: ExtensionDescriptionRegistry;168private readonly _globalRegistry: ExtensionDescriptionRegistry;169private readonly _host: IExtensionsActivatorHost;170private readonly _operations: ExtensionIdentifierMap<ActivationOperation>;171/**172* A map of already activated events to speed things up if the same activation event is triggered multiple times.173*/174private readonly _alreadyActivatedEvents: { [activationEvent: string]: boolean };175176constructor(177registry: ExtensionDescriptionRegistry,178globalRegistry: ExtensionDescriptionRegistry,179host: IExtensionsActivatorHost,180@ILogService private readonly _logService: ILogService181) {182this._registry = registry;183this._globalRegistry = globalRegistry;184this._host = host;185this._operations = new ExtensionIdentifierMap<ActivationOperation>();186this._alreadyActivatedEvents = Object.create(null);187}188189public dispose(): void {190for (const [_, op] of this._operations) {191op.dispose();192}193}194195public async waitForActivatingExtensions(): Promise<void> {196const res: Promise<boolean>[] = [];197for (const [_, op] of this._operations) {198res.push(op.wait());199}200await Promise.all(res);201}202203public isActivated(extensionId: ExtensionIdentifier): boolean {204const op = this._operations.get(extensionId);205return Boolean(op && op.value);206}207208public getActivatedExtension(extensionId: ExtensionIdentifier): ActivatedExtension {209const op = this._operations.get(extensionId);210if (!op || !op.value) {211throw new Error(`Extension '${extensionId.value}' is not known or not activated`);212}213return op.value;214}215216public async activateByEvent(activationEvent: string, startup: boolean): Promise<void> {217if (this._alreadyActivatedEvents[activationEvent]) {218return;219}220221const activateExtensions = this._registry.getExtensionDescriptionsForActivationEvent(activationEvent);222await this._activateExtensions(activateExtensions.map(e => ({223id: e.identifier,224reason: { startup, extensionId: e.identifier, activationEvent }225})));226227this._alreadyActivatedEvents[activationEvent] = true;228}229230public activateById(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void> {231const desc = this._registry.getExtensionDescription(extensionId);232if (!desc) {233throw new Error(`Extension '${extensionId.value}' is not known`);234}235return this._activateExtensions([{ id: desc.identifier, reason }]);236}237238private async _activateExtensions(extensions: ActivationIdAndReason[]): Promise<void> {239const operations = extensions240.filter((p) => !this.isActivated(p.id))241.map(ext => this._handleActivationRequest(ext));242await Promise.all(operations.map(op => op.wait()));243}244245/**246* Handle semantics related to dependencies for `currentExtension`.247* We don't need to worry about dependency loops because they are handled by the registry.248*/249private _handleActivationRequest(currentActivation: ActivationIdAndReason): ActivationOperation {250if (this._operations.has(currentActivation.id)) {251return this._operations.get(currentActivation.id)!;252}253254if (this._isHostExtension(currentActivation.id)) {255return this._createAndSaveOperation(currentActivation, null, [], null);256}257258const currentExtension = this._registry.getExtensionDescription(currentActivation.id);259if (!currentExtension) {260// Error condition 0: unknown extension261const error = new Error(`Cannot activate unknown extension '${currentActivation.id.value}'`);262const result = this._createAndSaveOperation(currentActivation, null, [], new FailedExtension(error));263this._host.onExtensionActivationError(264currentActivation.id,265error,266new MissingExtensionDependency(currentActivation.id.value)267);268return result;269}270271const deps: ActivationOperation[] = [];272const depIds = (typeof currentExtension.extensionDependencies === 'undefined' ? [] : currentExtension.extensionDependencies);273for (const depId of depIds) {274275if (this._isResolvedExtension(depId)) {276// This dependency is already resolved277continue;278}279280const dep = this._operations.get(depId);281if (dep) {282deps.push(dep);283continue;284}285286if (this._isHostExtension(depId)) {287// must first wait for the dependency to activate288deps.push(this._handleActivationRequest({289id: this._globalRegistry.getExtensionDescription(depId)!.identifier,290reason: currentActivation.reason291}));292continue;293}294295const depDesc = this._registry.getExtensionDescription(depId);296if (depDesc) {297if (!depDesc.main && !depDesc.browser) {298// this dependency does not need to activate because it is descriptive only299continue;300}301302// must first wait for the dependency to activate303deps.push(this._handleActivationRequest({304id: depDesc.identifier,305reason: currentActivation.reason306}));307continue;308}309310// Error condition 1: unknown dependency311const currentExtensionFriendlyName = currentExtension.displayName || currentExtension.identifier.value;312const error = new Error(`Cannot activate the '${currentExtensionFriendlyName}' extension because it depends on unknown extension '${depId}'`);313const result = this._createAndSaveOperation(currentActivation, currentExtension.displayName, [], new FailedExtension(error));314this._host.onExtensionActivationError(315currentExtension.identifier,316error,317new MissingExtensionDependency(depId)318);319return result;320}321322return this._createAndSaveOperation(currentActivation, currentExtension.displayName, deps, null);323}324325private _createAndSaveOperation(activation: ActivationIdAndReason, displayName: string | null | undefined, deps: ActivationOperation[], value: ActivatedExtension | null): ActivationOperation {326const operation = new ActivationOperation(activation.id, displayName, activation.reason, deps, value, this._host, this._logService);327this._operations.set(activation.id, operation);328return operation;329}330331private _isHostExtension(extensionId: ExtensionIdentifier | string): boolean {332return ExtensionDescriptionRegistry.isHostExtension(extensionId, this._registry, this._globalRegistry);333}334335private _isResolvedExtension(extensionId: ExtensionIdentifier | string): boolean {336const extensionDescription = this._globalRegistry.getExtensionDescription(extensionId);337if (!extensionDescription) {338// unknown extension339return false;340}341return (!extensionDescription.main && !extensionDescription.browser);342}343}344345class ActivationOperation {346347private readonly _barrier = new Barrier();348private _isDisposed = false;349350public get value(): ActivatedExtension | null {351return this._value;352}353354public get friendlyName(): string {355return this._displayName || this._id.value;356}357358constructor(359private readonly _id: ExtensionIdentifier,360private readonly _displayName: string | null | undefined,361private readonly _reason: ExtensionActivationReason,362private readonly _deps: ActivationOperation[],363private _value: ActivatedExtension | null,364private readonly _host: IExtensionsActivatorHost,365@ILogService private readonly _logService: ILogService366) {367this._initialize();368}369370public dispose(): void {371this._isDisposed = true;372}373374public wait() {375return this._barrier.wait();376}377378private async _initialize(): Promise<void> {379await this._waitForDepsThenActivate();380this._barrier.open();381}382383private async _waitForDepsThenActivate(): Promise<void> {384if (this._value) {385// this operation is already finished386return;387}388389while (this._deps.length > 0) {390// remove completed deps391for (let i = 0; i < this._deps.length; i++) {392const dep = this._deps[i];393394if (dep.value && !dep.value.activationFailed) {395// the dependency is already activated OK396this._deps.splice(i, 1);397i--;398continue;399}400401if (dep.value && dep.value.activationFailed) {402// Error condition 2: a dependency has already failed activation403const error = new Error(`Cannot activate the '${this.friendlyName}' extension because its dependency '${dep.friendlyName}' failed to activate`);404(<any>error).detail = dep.value.activationFailedError;405this._value = new FailedExtension(error);406this._host.onExtensionActivationError(this._id, error, null);407return;408}409}410411if (this._deps.length > 0) {412// wait for one dependency413await Promise.race(this._deps.map(dep => dep.wait()));414}415}416417await this._activate();418}419420private async _activate(): Promise<void> {421try {422this._value = await this._host.actualActivateExtension(this._id, this._reason);423} catch (err) {424425const error = new Error();426if (err && err.name) {427error.name = err.name;428}429if (err && err.message) {430error.message = `Activating extension '${this._id.value}' failed: ${err.message}.`;431} else {432error.message = `Activating extension '${this._id.value}' failed: ${err}.`;433}434if (err && err.stack) {435error.stack = err.stack;436}437438// Treat the extension as being empty439this._value = new FailedExtension(error);440441if (this._isDisposed && errors.isCancellationError(err)) {442// It is expected for ongoing activations to fail if the extension host is going down443// So simply ignore and don't log canceled errors in this case444return;445}446447this._host.onExtensionActivationError(this._id, error, null);448this._logService.error(`Activating extension ${this._id.value} failed due to an error:`);449this._logService.error(err);450}451}452}453454455