Path: blob/main/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.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 { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';6import { Emitter } from '../../../../base/common/event.js';7import * as path from '../../../../base/common/path.js';8import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';9import { promiseWithResolvers } from '../../../../base/common/async.js';1011export class DeltaExtensionsResult {12constructor(13public readonly versionId: number,14public readonly removedDueToLooping: IExtensionDescription[]15) { }16}1718export interface IReadOnlyExtensionDescriptionRegistry {19containsActivationEvent(activationEvent: string): boolean;20containsExtension(extensionId: ExtensionIdentifier): boolean;21getExtensionDescriptionsForActivationEvent(activationEvent: string): IExtensionDescription[];22getAllExtensionDescriptions(): IExtensionDescription[];23getExtensionDescription(extensionId: ExtensionIdentifier | string): IExtensionDescription | undefined;24getExtensionDescriptionByUUID(uuid: string): IExtensionDescription | undefined;25getExtensionDescriptionByIdOrUUID(extensionId: ExtensionIdentifier | string, uuid: string | undefined): IExtensionDescription | undefined;26}2728export class ExtensionDescriptionRegistry extends Disposable implements IReadOnlyExtensionDescriptionRegistry {2930public static isHostExtension(extensionId: ExtensionIdentifier | string, myRegistry: ExtensionDescriptionRegistry, globalRegistry: ExtensionDescriptionRegistry): boolean {31if (myRegistry.getExtensionDescription(extensionId)) {32// I have this extension33return false;34}35const extensionDescription = globalRegistry.getExtensionDescription(extensionId);36if (!extensionDescription) {37// unknown extension38return false;39}40if ((extensionDescription.main || extensionDescription.browser) && extensionDescription.api === 'none') {41return true;42}43return false;44}4546private readonly _onDidChange = this._register(new Emitter<void>());47public readonly onDidChange = this._onDidChange.event;4849private _versionId: number = 0;50private _extensionDescriptions: IExtensionDescription[];51private _extensionsMap!: ExtensionIdentifierMap<IExtensionDescription>;52private _extensionsArr!: IExtensionDescription[];53private _activationMap!: Map<string, IExtensionDescription[]>;5455constructor(56private readonly _activationEventsReader: IActivationEventsReader,57extensionDescriptions: IExtensionDescription[]58) {59super();60this._extensionDescriptions = extensionDescriptions;61this._initialize();62}6364private _initialize(): void {65// Ensure extensions are stored in the order: builtin, user, under development66this._extensionDescriptions.sort(extensionCmp);6768this._extensionsMap = new ExtensionIdentifierMap<IExtensionDescription>();69this._extensionsArr = [];70this._activationMap = new Map<string, IExtensionDescription[]>();7172for (const extensionDescription of this._extensionDescriptions) {73if (this._extensionsMap.has(extensionDescription.identifier)) {74// No overwriting allowed!75console.error('Extension `' + extensionDescription.identifier.value + '` is already registered');76continue;77}7879this._extensionsMap.set(extensionDescription.identifier, extensionDescription);80this._extensionsArr.push(extensionDescription);8182const activationEvents = this._activationEventsReader.readActivationEvents(extensionDescription);83for (const activationEvent of activationEvents) {84if (!this._activationMap.has(activationEvent)) {85this._activationMap.set(activationEvent, []);86}87this._activationMap.get(activationEvent)!.push(extensionDescription);88}89}90}9192public set(extensionDescriptions: IExtensionDescription[]): { versionId: number } {93this._extensionDescriptions = extensionDescriptions;94this._initialize();95this._versionId++;96this._onDidChange.fire(undefined);97return {98versionId: this._versionId99};100}101102public deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): DeltaExtensionsResult {103// It is possible that an extension is removed, only to be added again at a different version104// so we will first handle removals105this._extensionDescriptions = removeExtensions(this._extensionDescriptions, toRemove);106107// Then, handle the extensions to add108this._extensionDescriptions = this._extensionDescriptions.concat(toAdd);109110// Immediately remove looping extensions!111const looping = ExtensionDescriptionRegistry._findLoopingExtensions(this._extensionDescriptions);112this._extensionDescriptions = removeExtensions(this._extensionDescriptions, looping.map(ext => ext.identifier));113114this._initialize();115this._versionId++;116this._onDidChange.fire(undefined);117return new DeltaExtensionsResult(this._versionId, looping);118}119120private static _findLoopingExtensions(extensionDescriptions: IExtensionDescription[]): IExtensionDescription[] {121const G = new class {122123private _arcs = new Map<string, string[]>();124private _nodesSet = new Set<string>();125private _nodesArr: string[] = [];126127addNode(id: string): void {128if (!this._nodesSet.has(id)) {129this._nodesSet.add(id);130this._nodesArr.push(id);131}132}133134addArc(from: string, to: string): void {135this.addNode(from);136this.addNode(to);137if (this._arcs.has(from)) {138this._arcs.get(from)!.push(to);139} else {140this._arcs.set(from, [to]);141}142}143144getArcs(id: string): string[] {145if (this._arcs.has(id)) {146return this._arcs.get(id)!;147}148return [];149}150151hasOnlyGoodArcs(id: string, good: Set<string>): boolean {152const dependencies = G.getArcs(id);153for (let i = 0; i < dependencies.length; i++) {154if (!good.has(dependencies[i])) {155return false;156}157}158return true;159}160161getNodes(): string[] {162return this._nodesArr;163}164};165166const descs = new ExtensionIdentifierMap<IExtensionDescription>();167for (const extensionDescription of extensionDescriptions) {168descs.set(extensionDescription.identifier, extensionDescription);169if (extensionDescription.extensionDependencies) {170for (const depId of extensionDescription.extensionDependencies) {171G.addArc(ExtensionIdentifier.toKey(extensionDescription.identifier), ExtensionIdentifier.toKey(depId));172}173}174}175176// initialize with all extensions with no dependencies.177const good = new Set<string>();178G.getNodes().filter(id => G.getArcs(id).length === 0).forEach(id => good.add(id));179180// all other extensions will be processed below.181const nodes = G.getNodes().filter(id => !good.has(id));182183let madeProgress: boolean;184do {185madeProgress = false;186187// find one extension which has only good deps188for (let i = 0; i < nodes.length; i++) {189const id = nodes[i];190191if (G.hasOnlyGoodArcs(id, good)) {192nodes.splice(i, 1);193i--;194good.add(id);195madeProgress = true;196}197}198} while (madeProgress);199200// The remaining nodes are bad and have loops201return nodes.map(id => descs.get(id)!);202}203204public containsActivationEvent(activationEvent: string): boolean {205return this._activationMap.has(activationEvent);206}207208public containsExtension(extensionId: ExtensionIdentifier): boolean {209return this._extensionsMap.has(extensionId);210}211212public getExtensionDescriptionsForActivationEvent(activationEvent: string): IExtensionDescription[] {213const extensions = this._activationMap.get(activationEvent);214return extensions ? extensions.slice(0) : [];215}216217public getAllExtensionDescriptions(): IExtensionDescription[] {218return this._extensionsArr.slice(0);219}220221public getSnapshot(): ExtensionDescriptionRegistrySnapshot {222return new ExtensionDescriptionRegistrySnapshot(223this._versionId,224this.getAllExtensionDescriptions()225);226}227228public getExtensionDescription(extensionId: ExtensionIdentifier | string): IExtensionDescription | undefined {229const extension = this._extensionsMap.get(extensionId);230return extension ? extension : undefined;231}232233public getExtensionDescriptionByUUID(uuid: string): IExtensionDescription | undefined {234for (const extensionDescription of this._extensionsArr) {235if (extensionDescription.uuid === uuid) {236return extensionDescription;237}238}239return undefined;240}241242public getExtensionDescriptionByIdOrUUID(extensionId: ExtensionIdentifier | string, uuid: string | undefined): IExtensionDescription | undefined {243return (244this.getExtensionDescription(extensionId)245?? (uuid ? this.getExtensionDescriptionByUUID(uuid) : undefined)246);247}248}249250export class ExtensionDescriptionRegistrySnapshot {251constructor(252public readonly versionId: number,253public readonly extensions: readonly IExtensionDescription[]254) { }255}256257export interface IActivationEventsReader {258readActivationEvents(extensionDescription: IExtensionDescription): string[];259}260261export class LockableExtensionDescriptionRegistry implements IReadOnlyExtensionDescriptionRegistry {262263private readonly _actual: ExtensionDescriptionRegistry;264private readonly _lock = new Lock();265266constructor(activationEventsReader: IActivationEventsReader) {267this._actual = new ExtensionDescriptionRegistry(activationEventsReader, []);268}269270public async acquireLock(customerName: string): Promise<ExtensionDescriptionRegistryLock> {271const lock = await this._lock.acquire(customerName);272return new ExtensionDescriptionRegistryLock(this, lock);273}274275public deltaExtensions(acquiredLock: ExtensionDescriptionRegistryLock, toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): DeltaExtensionsResult {276if (!acquiredLock.isAcquiredFor(this)) {277throw new Error('Lock is not held');278}279return this._actual.deltaExtensions(toAdd, toRemove);280}281282public containsActivationEvent(activationEvent: string): boolean {283return this._actual.containsActivationEvent(activationEvent);284}285public containsExtension(extensionId: ExtensionIdentifier): boolean {286return this._actual.containsExtension(extensionId);287}288public getExtensionDescriptionsForActivationEvent(activationEvent: string): IExtensionDescription[] {289return this._actual.getExtensionDescriptionsForActivationEvent(activationEvent);290}291public getAllExtensionDescriptions(): IExtensionDescription[] {292return this._actual.getAllExtensionDescriptions();293}294public getSnapshot(): ExtensionDescriptionRegistrySnapshot {295return this._actual.getSnapshot();296}297public getExtensionDescription(extensionId: ExtensionIdentifier | string): IExtensionDescription | undefined {298return this._actual.getExtensionDescription(extensionId);299}300public getExtensionDescriptionByUUID(uuid: string): IExtensionDescription | undefined {301return this._actual.getExtensionDescriptionByUUID(uuid);302}303public getExtensionDescriptionByIdOrUUID(extensionId: ExtensionIdentifier | string, uuid: string | undefined): IExtensionDescription | undefined {304return this._actual.getExtensionDescriptionByIdOrUUID(extensionId, uuid);305}306}307308export class ExtensionDescriptionRegistryLock extends Disposable {309310private _isDisposed = false;311312constructor(313private readonly _registry: LockableExtensionDescriptionRegistry,314lock: IDisposable315) {316super();317this._register(lock);318}319320public isAcquiredFor(registry: LockableExtensionDescriptionRegistry): boolean {321return !this._isDisposed && this._registry === registry;322}323}324325class LockCustomer {326public readonly promise: Promise<IDisposable>;327private readonly _resolve: (value: IDisposable) => void;328329constructor(330public readonly name: string331) {332const withResolvers = promiseWithResolvers<IDisposable>();333this.promise = withResolvers.promise;334this._resolve = withResolvers.resolve;335}336337resolve(value: IDisposable): void {338this._resolve(value);339}340}341342class Lock {343private readonly _pendingCustomers: LockCustomer[] = [];344private _isLocked = false;345346public async acquire(customerName: string): Promise<IDisposable> {347const customer = new LockCustomer(customerName);348this._pendingCustomers.push(customer);349this._advance();350return customer.promise;351}352353private _advance(): void {354if (this._isLocked) {355// cannot advance yet356return;357}358if (this._pendingCustomers.length === 0) {359// no more waiting customers360return;361}362363const customer = this._pendingCustomers.shift()!;364365this._isLocked = true;366let customerHoldsLock = true;367368const logLongRunningCustomerTimeout = setTimeout(() => {369if (customerHoldsLock) {370console.warn(`The customer named ${customer.name} has been holding on to the lock for 30s. This might be a problem.`);371}372}, 30 * 1000 /* 30 seconds */);373374const releaseLock = () => {375if (!customerHoldsLock) {376return;377}378clearTimeout(logLongRunningCustomerTimeout);379customerHoldsLock = false;380this._isLocked = false;381this._advance();382};383384customer.resolve(toDisposable(releaseLock));385}386}387388const enum SortBucket {389Builtin = 0,390User = 1,391Dev = 2392}393394/**395* Ensure that:396* - first are builtin extensions397* - second are user extensions398* - third are extensions under development399*400* In each bucket, extensions must be sorted alphabetically by their folder name.401*/402function extensionCmp(a: IExtensionDescription, b: IExtensionDescription): number {403const aSortBucket = (a.isBuiltin ? SortBucket.Builtin : a.isUnderDevelopment ? SortBucket.Dev : SortBucket.User);404const bSortBucket = (b.isBuiltin ? SortBucket.Builtin : b.isUnderDevelopment ? SortBucket.Dev : SortBucket.User);405if (aSortBucket !== bSortBucket) {406return aSortBucket - bSortBucket;407}408const aLastSegment = path.posix.basename(a.extensionLocation.path);409const bLastSegment = path.posix.basename(b.extensionLocation.path);410if (aLastSegment < bLastSegment) {411return -1;412}413if (aLastSegment > bLastSegment) {414return 1;415}416return 0;417}418419function removeExtensions(arr: IExtensionDescription[], toRemove: ExtensionIdentifier[]): IExtensionDescription[] {420const toRemoveSet = new ExtensionIdentifierSet(toRemove);421return arr.filter(extension => !toRemoveSet.has(extension.identifier));422}423424425