UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

1,093 lines (1,092 loc) 42.3 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); }; }; import { distinct, isNonEmptyArray } from '@sussudio/base/common/arrays.mjs'; import { Barrier, createCancelablePromise } from '@sussudio/base/common/async.mjs'; import { CancellationToken } from '@sussudio/base/common/cancellation.mjs'; import { CancellationError, getErrorMessage } from '@sussudio/base/common/errors.mjs'; import { Emitter, Event } from '@sussudio/base/common/event.mjs'; import { Disposable, toDisposable } from '@sussudio/base/common/lifecycle.mjs'; import { isWeb } from '@sussudio/base/common/platform.mjs'; import { URI } from '@sussudio/base/common/uri.mjs'; import * as nls from 'vscode-nls.mjs'; import { ExtensionManagementError, IExtensionGalleryService, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, } from './extensionManagement.mjs'; import { areSameExtensions, ExtensionKey, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, } from './extensionManagementUtil.mjs'; import { isApplicationScopedExtension } from '../../extensions/common/extensions.mjs'; import { ILogService } from '../../log/common/log.mjs'; import { IProductService } from '../../product/common/productService.mjs'; import { ITelemetryService } from '../../telemetry/common/telemetry.mjs'; import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.mjs'; let AbstractExtensionManagementService = class AbstractExtensionManagementService extends Disposable { galleryService; telemetryService; logService; productService; userDataProfilesService; extensionsControlManifest; lastReportTimestamp = 0; installingExtensions = new Map(); uninstallingExtensions = new Map(); _onInstallExtension = this._register(new Emitter()); get onInstallExtension() { return this._onInstallExtension.event; } _onDidInstallExtensions = this._register(new Emitter()); get onDidInstallExtensions() { return this._onDidInstallExtensions.event; } _onUninstallExtension = this._register(new Emitter()); get onUninstallExtension() { return this._onUninstallExtension.event; } _onDidUninstallExtension = this._register(new Emitter()); get onDidUninstallExtension() { return this._onDidUninstallExtension.event; } participants = []; constructor(galleryService, telemetryService, logService, productService, userDataProfilesService) { super(); this.galleryService = galleryService; this.telemetryService = telemetryService; this.logService = logService; this.productService = productService; this.userDataProfilesService = userDataProfilesService; this._register( toDisposable(() => { this.installingExtensions.forEach(({ task }) => task.cancel()); this.uninstallingExtensions.forEach((promise) => promise.cancel()); this.installingExtensions.clear(); this.uninstallingExtensions.clear(); }), ); } async canInstall(extension) { const currentTargetPlatform = await this.getTargetPlatform(); return extension.allTargetPlatforms.some((targetPlatform) => isTargetPlatformCompatible(targetPlatform, extension.allTargetPlatforms, currentTargetPlatform), ); } async installFromGallery(extension, options = {}) { try { if (!this.galleryService.isEnabled()) { throw new ExtensionManagementError( nls.localize('MarketPlaceDisabled', 'Marketplace is not enabled'), ExtensionManagementErrorCode.Internal, ); } const compatible = await this.checkAndGetCompatibleVersion( extension, !!options.installGivenVersion, !!options.installPreReleaseVersion, ); return await this.installExtension(compatible.manifest, compatible.extension, options); } catch (error) { reportTelemetry(this.telemetryService, 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(extension), error, }); this.logService.error(`Failed to install extension.`, extension.identifier.id); this.logService.error(error); throw toExtensionManagementError(error); } } async uninstall(extension, options = {}) { this.logService.trace('ExtensionManagementService#uninstall', extension.identifier.id); return this.uninstallExtension(extension, options); } getExtensionsControlManifest() { const now = new Date().getTime(); if (!this.extensionsControlManifest || now - this.lastReportTimestamp > 1000 * 60 * 5) { // 5 minute cache freshness this.extensionsControlManifest = this.updateControlCache(); this.lastReportTimestamp = now; } return this.extensionsControlManifest; } registerParticipant(participant) { this.participants.push(participant); } async installExtension(manifest, extension, options) { const installExtensionTaskOptions = { ...options, installOnlyNewlyAddedFromExtensionPack: URI.isUri(extension) ? options.installOnlyNewlyAddedFromExtensionPack : true, profileLocation: isApplicationScopedExtension(manifest) ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation ?? this.getCurrentExtensionsManifestLocation(), }; const getInstallExtensionTaskKey = (extension) => `${ExtensionKey.create(extension).toString()}${ installExtensionTaskOptions.profileLocation ? `-${installExtensionTaskOptions.profileLocation.toString()}` : '' }`; // only cache gallery extensions tasks if (!URI.isUri(extension)) { const installingExtension = this.installingExtensions.get(getInstallExtensionTaskKey(extension)); if (installingExtension) { this.logService.info('Extensions is already requested to install', extension.identifier.id); const { local } = await installingExtension.task.waitUntilTaskIsFinished(); return local; } } const allInstallExtensionTasks = []; const alreadyRequestedInstallations = []; const installResults = []; const installExtensionTask = this.createInstallExtensionTask(manifest, extension, installExtensionTaskOptions); if (!URI.isUri(extension)) { this.installingExtensions.set(getInstallExtensionTaskKey(extension), { task: installExtensionTask, waitingTasks: [], }); } this._onInstallExtension.fire({ identifier: installExtensionTask.identifier, source: extension, profileLocation: installExtensionTaskOptions.profileLocation, }); this.logService.info('Installing extension:', installExtensionTask.identifier.id); allInstallExtensionTasks.push({ task: installExtensionTask, manifest }); let installExtensionHasDependents = false; try { if (installExtensionTaskOptions.donotIncludePackAndDependencies) { this.logService.info( 'Installing the extension without checking dependencies and pack', installExtensionTask.identifier.id, ); } else { try { const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions( installExtensionTask.identifier, manifest, !!installExtensionTaskOptions.installOnlyNewlyAddedFromExtensionPack, !!installExtensionTaskOptions.installPreReleaseVersion, installExtensionTaskOptions.profileLocation, ); const installed = await this.getInstalled(undefined, installExtensionTaskOptions.profileLocation); for (const { gallery, manifest } of distinct( allDepsAndPackExtensionsToInstall, ({ gallery }) => gallery.identifier.id, )) { installExtensionHasDependents = installExtensionHasDependents || !!manifest.extensionDependencies?.some((id) => areSameExtensions({ id }, installExtensionTask.identifier), ); const key = getInstallExtensionTaskKey(gallery); const existingInstallingExtension = this.installingExtensions.get(key); if (existingInstallingExtension) { if (this.canWaitForTask(installExtensionTask, existingInstallingExtension.task)) { const identifier = existingInstallingExtension.task.identifier; this.logService.info( 'Waiting for already requested installing extension', identifier.id, installExtensionTask.identifier.id, ); existingInstallingExtension.waitingTasks.push(installExtensionTask); // add promise that waits until the extension is completely installed, ie., onDidInstallExtensions event is triggered for this extension alreadyRequestedInstallations.push( Event.toPromise( Event.filter(this.onDidInstallExtensions, (results) => results.some((result) => areSameExtensions(result.identifier, identifier)), ), ).then((results) => { this.logService.info( 'Finished waiting for already requested installing extension', identifier.id, installExtensionTask.identifier.id, ); const result = results.find((result) => areSameExtensions(result.identifier, identifier)); if (!result?.local) { // Extension failed to install throw new Error(`Extension ${identifier.id} is not installed`); } }), ); } } else if (!installed.some(({ identifier }) => areSameExtensions(identifier, gallery.identifier))) { const task = this.createInstallExtensionTask(manifest, gallery, { ...installExtensionTaskOptions, donotIncludePackAndDependencies: true, }); this.installingExtensions.set(key, { task, waitingTasks: [installExtensionTask] }); this._onInstallExtension.fire({ identifier: task.identifier, source: gallery, profileLocation: installExtensionTaskOptions.profileLocation, }); this.logService.info('Installing extension:', task.identifier.id, installExtensionTask.identifier.id); allInstallExtensionTasks.push({ task, manifest }); } } } catch (error) { // Installing through VSIX if (URI.isUri(installExtensionTask.source)) { // Ignore installing dependencies and packs if (isNonEmptyArray(manifest.extensionDependencies)) { this.logService.warn( `Cannot install dependencies of extension:`, installExtensionTask.identifier.id, error.message, ); } if (isNonEmptyArray(manifest.extensionPack)) { this.logService.warn( `Cannot install packed extensions of extension:`, installExtensionTask.identifier.id, error.message, ); } } else { this.logService.error( 'Error while preparing to install dependencies and extension packs of the extension:', installExtensionTask.identifier.id, ); throw error; } } } const extensionsToInstallMap = allInstallExtensionTasks.reduce((result, { task, manifest }) => { result.set(task.identifier.id.toLowerCase(), { task, manifest }); return result; }, new Map()); while (extensionsToInstallMap.size) { let extensionsToInstall; const extensionsWithoutDepsToInstall = [...extensionsToInstallMap.values()].filter( ({ manifest }) => !manifest.extensionDependencies?.some((id) => extensionsToInstallMap.has(id.toLowerCase())), ); if (extensionsWithoutDepsToInstall.length) { extensionsToInstall = extensionsToInstallMap.size === 1 ? extensionsWithoutDepsToInstall : /* If the main extension has no dependents remove it and install it at the end */ extensionsWithoutDepsToInstall.filter( ({ task }) => !(task === installExtensionTask && !installExtensionHasDependents), ); } else { this.logService.info( 'Found extensions with circular dependencies', extensionsWithoutDepsToInstall.map(({ task }) => task.identifier.id), ); extensionsToInstall = [...extensionsToInstallMap.values()]; } // Install extensions in parallel and wait until all extensions are installed / failed await this.joinAllSettled( extensionsToInstall.map(async ({ task }) => { const startTime = new Date().getTime(); try { const { local } = await task.run(); await this.joinAllSettled( this.participants.map((participant) => participant.postInstall(local, task.source, installExtensionTaskOptions, CancellationToken.None), ), ); if (!URI.isUri(task.source)) { const isUpdate = task.operation === 3; /* InstallOperation.Update */ const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; reportTelemetry( this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), verificationStatus: task.verificationStatus, duration: new Date().getTime() - startTime, durationSinceUpdate, }, ); // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. if (isWeb && task.operation !== 3 /* InstallOperation.Update */) { try { await this.galleryService.reportStatistic( local.manifest.publisher, local.manifest.name, local.manifest.version, 'install' /* StatisticType.Install */, ); } catch (error) { /* ignore */ } } } installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source, context: installExtensionTaskOptions.context, profileLocation: installExtensionTaskOptions.profileLocation, applicationScoped: local.isApplicationScoped, }); } catch (error) { if (!URI.isUri(task.source)) { reportTelemetry( this.telemetryService, task.operation === 3 /* InstallOperation.Update */ ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), verificationStatus: task.verificationStatus, duration: new Date().getTime() - startTime, error, }, ); } this.logService.error('Error while installing the extension:', task.identifier.id); throw error; } finally { extensionsToInstallMap.delete(task.identifier.id.toLowerCase()); } }), ); } if (alreadyRequestedInstallations.length) { await this.joinAllSettled(alreadyRequestedInstallations); } installResults.forEach(({ identifier }) => this.logService.info(`Extension installed successfully:`, identifier.id), ); this._onDidInstallExtensions.fire(installResults); return installResults.filter(({ identifier }) => areSameExtensions(identifier, installExtensionTask.identifier), )[0].local; } catch (error) { // cancel all tasks allInstallExtensionTasks.forEach(({ task }) => task.cancel()); // rollback installed extensions if (installResults.length) { try { const result = await Promise.allSettled( installResults.map(({ local }) => this.createUninstallExtensionTask(local, { versionOnly: true, profileLocation: installExtensionTaskOptions.profileLocation, }).run(), ), ); for (let index = 0; index < result.length; index++) { const r = result[index]; const { identifier } = installResults[index]; if (r.status === 'fulfilled') { this.logService.info('Rollback: Uninstalled extension', identifier.id); } else { this.logService.warn( 'Rollback: Error while uninstalling extension', identifier.id, getErrorMessage(r.reason), ); } } } catch (error) { // ignore error this.logService.warn( 'Error while rolling back extensions', getErrorMessage(error), installResults.map(({ identifier }) => identifier.id), ); } } this._onDidInstallExtensions.fire( allInstallExtensionTasks.map(({ task }) => ({ identifier: task.identifier, operation: 2 /* InstallOperation.Install */, source: task.source, context: installExtensionTaskOptions.context, profileLocation: installExtensionTaskOptions.profileLocation, })), ); throw error; } finally { // Finally, remove all the tasks from the cache for (const { task } of allInstallExtensionTasks) { if (task.source && !URI.isUri(task.source)) { this.installingExtensions.delete(getInstallExtensionTaskKey(task.source)); } } } } canWaitForTask(taskToWait, taskToWaitFor) { for (const [, { task, waitingTasks }] of this.installingExtensions.entries()) { if (task === taskToWait) { // Cannot be waited, If taskToWaitFor is waiting for taskToWait if (waitingTasks.includes(taskToWaitFor)) { return false; } // Cannot be waited, If taskToWaitFor is waiting for tasks waiting for taskToWait if (waitingTasks.some((waitingTask) => this.canWaitForTask(waitingTask, taskToWaitFor))) { return false; } } // Cannot be waited, if the taskToWait cannot be waited for the task created the taskToWaitFor // Because, the task waits for the tasks it created if (task === taskToWaitFor && waitingTasks[0] && !this.canWaitForTask(taskToWait, waitingTasks[0])) { return false; } } return true; } async joinAllSettled(promises) { const results = []; const errors = []; const promiseResults = await Promise.allSettled(promises); for (const r of promiseResults) { if (r.status === 'fulfilled') { results.push(r.value); } else { errors.push(r.reason); } } // If there are errors, throw the error. if (errors.length) { throw joinErrors(errors); } return results; } async getAllDepsAndPackExtensions( extensionIdentifier, manifest, getOnlyNewlyAddedFromExtensionPack, installPreRelease, profile, ) { if (!this.galleryService.isEnabled()) { return []; } const installed = await this.getInstalled(undefined, profile); const knownIdentifiers = []; const allDependenciesAndPacks = []; const collectDependenciesAndPackExtensionsToInstall = async (extensionIdentifier, manifest) => { knownIdentifiers.push(extensionIdentifier); const dependecies = manifest.extensionDependencies || []; const dependenciesAndPackExtensions = [...dependecies]; if (manifest.extensionPack) { const existing = getOnlyNewlyAddedFromExtensionPack ? installed.find((e) => areSameExtensions(e.identifier, extensionIdentifier)) : undefined; for (const extension of manifest.extensionPack) { // add only those extensions which are new in currently installed extension if ( !( existing && existing.manifest.extensionPack && existing.manifest.extensionPack.some((old) => areSameExtensions({ id: old }, { id: extension })) ) ) { if (dependenciesAndPackExtensions.every((e) => !areSameExtensions({ id: e }, { id: extension }))) { dependenciesAndPackExtensions.push(extension); } } } } if (dependenciesAndPackExtensions.length) { // filter out known extensions const ids = dependenciesAndPackExtensions.filter((id) => knownIdentifiers.every((galleryIdentifier) => !areSameExtensions(galleryIdentifier, { id })), ); if (ids.length) { const galleryExtensions = await this.galleryService.getExtensions( ids.map((id) => ({ id, preRelease: installPreRelease })), CancellationToken.None, ); for (const galleryExtension of galleryExtensions) { if (knownIdentifiers.find((identifier) => areSameExtensions(identifier, galleryExtension.identifier))) { continue; } const isDependency = dependecies.some((id) => areSameExtensions({ id }, galleryExtension.identifier)); let compatible; try { compatible = await this.checkAndGetCompatibleVersion(galleryExtension, false, installPreRelease); } catch (error) { if ( error instanceof ExtensionManagementError && error.code === ExtensionManagementErrorCode.IncompatibleTargetPlatform && !isDependency ) { this.logService.info( 'Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id, ); continue; } else { throw error; } } allDependenciesAndPacks.push({ gallery: compatible.extension, manifest: compatible.manifest }); await collectDependenciesAndPackExtensionsToInstall(compatible.extension.identifier, compatible.manifest); } } } }; await collectDependenciesAndPackExtensionsToInstall(extensionIdentifier, manifest); return allDependenciesAndPacks; } async checkAndGetCompatibleVersion(extension, sameVersion, installPreRelease) { const extensionsControlManifest = await this.getExtensionsControlManifest(); if (extensionsControlManifest.malicious.some((identifier) => areSameExtensions(extension.identifier, identifier))) { throw new ExtensionManagementError( nls.localize( 'malicious extension', "Can't install '{0}' extension since it was reported to be problematic.", extension.identifier.id, ), ExtensionManagementErrorCode.Malicious, ); } if (!(await this.canInstall(extension))) { const targetPlatform = await this.getTargetPlatform(); throw new ExtensionManagementError( nls.localize( 'incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform), ), ExtensionManagementErrorCode.IncompatibleTargetPlatform, ); } const compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease); if (compatibleExtension) { if ( installPreRelease && !sameVersion && extension.hasPreReleaseVersion && !compatibleExtension.properties.isPreReleaseVersion ) { throw new ExtensionManagementError( nls.localize( 'notFoundCompatiblePrereleaseDependency', "Can't install pre-release version of '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version, ), ExtensionManagementErrorCode.IncompatiblePreRelease, ); } } else { /** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */ if ( !installPreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0] ) { throw new ExtensionManagementError( nls.localize( 'notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.identifier.id, ), ExtensionManagementErrorCode.ReleaseVersionNotFound, ); } throw new ExtensionManagementError( nls.localize( 'notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version, ), ExtensionManagementErrorCode.Incompatible, ); } this.logService.info('Getting Manifest...', compatibleExtension.identifier.id); const manifest = await this.galleryService.getManifest(compatibleExtension, CancellationToken.None); if (manifest === null) { throw new ExtensionManagementError( `Missing manifest for extension ${extension.identifier.id}`, ExtensionManagementErrorCode.Invalid, ); } if (manifest.version !== compatibleExtension.version) { throw new ExtensionManagementError( `Cannot install '${extension.identifier.id}' extension because of version mismatch in Marketplace`, ExtensionManagementErrorCode.Invalid, ); } return { extension: compatibleExtension, manifest }; } async getCompatibleVersion(extension, sameVersion, includePreRelease) { const targetPlatform = await this.getTargetPlatform(); let compatibleExtension = null; if ( !sameVersion && extension.hasPreReleaseVersion && extension.properties.isPreReleaseVersion !== includePreRelease ) { compatibleExtension = ( await this.galleryService.getExtensions( [{ ...extension.identifier, preRelease: includePreRelease }], { targetPlatform, compatible: true }, CancellationToken.None, ) )[0] || null; } if ( !compatibleExtension && (await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform)) ) { compatibleExtension = extension; } if (!compatibleExtension) { if (sameVersion) { compatibleExtension = ( await this.galleryService.getExtensions( [{ ...extension.identifier, version: extension.version }], { targetPlatform, compatible: true }, CancellationToken.None, ) )[0] || null; } else { compatibleExtension = await this.galleryService.getCompatibleExtension( extension, includePreRelease, targetPlatform, ); } } return compatibleExtension; } async uninstallExtension(extension, options) { const uninstallOptions = { ...options, profileLocation: extension.isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation ?? this.getCurrentExtensionsManifestLocation(), }; const getUninstallExtensionTaskKey = (identifier) => `${identifier.id.toLowerCase()}${uninstallOptions.versionOnly ? `-${extension.manifest.version}` : ''}${ uninstallOptions.profileLocation ? `@${uninstallOptions.profileLocation.toString()}` : '' }`; const uninstallExtensionTask = this.uninstallingExtensions.get(getUninstallExtensionTaskKey(extension.identifier)); if (uninstallExtensionTask) { this.logService.info('Extensions is already requested to uninstall', extension.identifier.id); return uninstallExtensionTask.waitUntilTaskIsFinished(); } const createUninstallExtensionTask = (extension) => { const uninstallExtensionTask = this.createUninstallExtensionTask(extension, uninstallOptions); this.uninstallingExtensions.set( getUninstallExtensionTaskKey(uninstallExtensionTask.extension.identifier), uninstallExtensionTask, ); if (uninstallOptions.profileLocation) { this.logService.info( 'Uninstalling extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString(), ); } else { this.logService.info('Uninstalling extension:', `${extension.identifier.id}@${extension.manifest.version}`); } this._onUninstallExtension.fire({ identifier: extension.identifier, profileLocation: uninstallOptions.profileLocation, applicationScoped: extension.isApplicationScoped, }); return uninstallExtensionTask; }; const postUninstallExtension = (extension, error) => { if (error) { if (uninstallOptions.profileLocation) { this.logService.error( 'Failed to uninstall extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString(), error.message, ); } else { this.logService.error( 'Failed to uninstall extension:', `${extension.identifier.id}@${extension.manifest.version}`, error.message, ); } } else { if (uninstallOptions.profileLocation) { this.logService.info( 'Successfully uninstalled extension from the profile', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString(), ); } else { this.logService.info( 'Successfully uninstalled extension:', `${extension.identifier.id}@${extension.manifest.version}`, ); } } reportTelemetry(this.telemetryService, 'extensionGallery:uninstall', { extensionData: getLocalExtensionTelemetryData(extension), error, }); this._onDidUninstallExtension.fire({ identifier: extension.identifier, error: error?.code, profileLocation: uninstallOptions.profileLocation, applicationScoped: extension.isApplicationScoped, }); }; const allTasks = []; const processedTasks = []; try { allTasks.push(createUninstallExtensionTask(extension)); const installed = await this.getInstalled(1 /* ExtensionType.User */, uninstallOptions.profileLocation); if (uninstallOptions.donotIncludePack) { this.logService.info( 'Uninstalling the extension without including packed extension', `${extension.identifier.id}@${extension.manifest.version}`, ); } else { const packedExtensions = this.getAllPackExtensionsToUninstall(extension, installed); for (const packedExtension of packedExtensions) { if (this.uninstallingExtensions.has(getUninstallExtensionTaskKey(packedExtension.identifier))) { this.logService.info('Extensions is already requested to uninstall', packedExtension.identifier.id); } else { allTasks.push(createUninstallExtensionTask(packedExtension)); } } } if (uninstallOptions.donotCheckDependents) { this.logService.info( 'Uninstalling the extension without checking dependents', `${extension.identifier.id}@${extension.manifest.version}`, ); } else { this.checkForDependents( allTasks.map((task) => task.extension), installed, extension, ); } // Uninstall extensions in parallel and wait until all extensions are uninstalled / failed await this.joinAllSettled( allTasks.map(async (task) => { try { await task.run(); await this.joinAllSettled( this.participants.map((participant) => participant.postUninstall(task.extension, uninstallOptions, CancellationToken.None), ), ); // only report if extension has a mapped gallery extension. UUID identifies the gallery extension. if (task.extension.identifier.uuid) { try { await this.galleryService.reportStatistic( task.extension.manifest.publisher, task.extension.manifest.name, task.extension.manifest.version, 'uninstall' /* StatisticType.Uninstall */, ); } catch (error) { /* ignore */ } } postUninstallExtension(task.extension); } catch (e) { const error = e instanceof ExtensionManagementError ? e : new ExtensionManagementError(getErrorMessage(e), ExtensionManagementErrorCode.Internal); postUninstallExtension(task.extension, error); throw error; } finally { processedTasks.push(task); } }), ); } catch (e) { const error = e instanceof ExtensionManagementError ? e : new ExtensionManagementError(getErrorMessage(e), ExtensionManagementErrorCode.Internal); for (const task of allTasks) { // cancel the tasks try { task.cancel(); } catch (error) { /* ignore */ } if (!processedTasks.includes(task)) { postUninstallExtension(task.extension, error); } } throw error; } finally { // Remove tasks from cache for (const task of allTasks) { if (!this.uninstallingExtensions.delete(getUninstallExtensionTaskKey(task.extension.identifier))) { this.logService.warn('Uninstallation task is not found in the cache', task.extension.identifier.id); } } } } checkForDependents(extensionsToUninstall, installed, extensionToUninstall) { for (const extension of extensionsToUninstall) { const dependents = this.getDependents(extension, installed); if (dependents.length) { const remainingDependents = dependents.filter( (dependent) => !extensionsToUninstall.some((e) => areSameExtensions(e.identifier, dependent.identifier)), ); if (remainingDependents.length) { throw new Error(this.getDependentsErrorMessage(extension, remainingDependents, extensionToUninstall)); } } } } getDependentsErrorMessage(dependingExtension, dependents, extensionToUninstall) { if (extensionToUninstall === dependingExtension) { if (dependents.length === 1) { return nls.localize( 'singleDependentError', "Cannot uninstall '{0}' extension. '{1}' extension depends on this.", extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, ); } if (dependents.length === 2) { return nls.localize( 'twoDependentsError', "Cannot uninstall '{0}' extension. '{1}' and '{2}' extensions depend on this.", extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name, ); } return nls.localize( 'multipleDependentsError', "Cannot uninstall '{0}' extension. '{1}', '{2}' and other extension depend on this.", extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name, ); } if (dependents.length === 1) { return nls.localize( 'singleIndirectDependentError', "Cannot uninstall '{0}' extension . It includes uninstalling '{1}' extension and '{2}' extension depends on this.", extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependingExtension.manifest.displayName || dependingExtension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, ); } if (dependents.length === 2) { return nls.localize( 'twoIndirectDependentsError', "Cannot uninstall '{0}' extension. It includes uninstalling '{1}' extension and '{2}' and '{3}' extensions depend on this.", extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependingExtension.manifest.displayName || dependingExtension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name, ); } return nls.localize( 'multipleIndirectDependentsError', "Cannot uninstall '{0}' extension. It includes uninstalling '{1}' extension and '{2}', '{3}' and other extensions depend on this.", extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependingExtension.manifest.displayName || dependingExtension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name, ); } getAllPackExtensionsToUninstall(extension, installed, checked = []) { if (checked.indexOf(extension) !== -1) { return []; } checked.push(extension); const extensionsPack = extension.manifest.extensionPack ? extension.manifest.extensionPack : []; if (extensionsPack.length) { const packedExtensions = installed.filter( (i) => !i.isBuiltin && extensionsPack.some((id) => areSameExtensions({ id }, i.identifier)), ); const packOfPackedExtensions = []; for (const packedExtension of packedExtensions) { packOfPackedExtensions.push(...this.getAllPackExtensionsToUninstall(packedExtension, installed, checked)); } return [...packedExtensions, ...packOfPackedExtensions]; } return []; } getDependents(extension, installed) { return installed.filter( (e) => e.manifest.extensionDependencies && e.manifest.extensionDependencies.some((id) => areSameExtensions({ id }, extension.identifier)), ); } async updateControlCache() { try { this.logService.trace('ExtensionManagementService.refreshReportedCache'); const manifest = await this.galleryService.getExtensionsControlManifest(); this.logService.trace(`ExtensionManagementService.refreshControlCache`, manifest); return manifest; } catch (err) { this.logService.trace( 'ExtensionManagementService.refreshControlCache - failed to get extension control manifest', ); return { malicious: [], deprecated: {} }; } } }; AbstractExtensionManagementService = __decorate( [ __param(0, IExtensionGalleryService), __param(1, ITelemetryService), __param(2, ILogService), __param(3, IProductService), __param(4, IUserDataProfilesService), ], AbstractExtensionManagementService, ); export { AbstractExtensionManagementService }; export function joinErrors(errorOrErrors) { const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors]; if (errors.length === 1) { return errors[0] instanceof Error ? errors[0] : new Error(errors[0]); } return errors.reduce((previousValue, currentValue) => { return new Error( `${previousValue.message}${previousValue.message ? ',' : ''}${ currentValue instanceof Error ? currentValue.message : currentValue }`, ); }, new Error('')); } function toExtensionManagementError(error) { if (error instanceof ExtensionManagementError) { return error; } const e = new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Internal); e.stack = error.stack; return e; } export function reportTelemetry( telemetryService, eventName, { extensionData, verificationStatus, duration, error, durationSinceUpdate }, ) { let errorcode; let errorcodeDetail; if (error) { if (error instanceof ExtensionManagementError) { errorcode = error.code; if (error.code === ExtensionManagementErrorCode.Signature) { errorcodeDetail = error.message; } } else { errorcode = ExtensionManagementErrorCode.Internal; } } /* __GDPR__ "extensionGallery:install" : { "owner": "sandy081", "success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "durationSinceUpdate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "verificationStatus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${GalleryExtensionTelemetryData}" ] } */ /* __GDPR__ "extensionGallery:uninstall" : { "owner": "sandy081", "success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "${include}": [ "${GalleryExtensionTelemetryData}" ] } */ /* __GDPR__ "extensionGallery:update" : { "owner": "sandy081", "success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "verificationStatus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${GalleryExtensionTelemetryData}" ] } */ telemetryService.publicLog(eventName, { ...extensionData, verificationStatus, success: !error, duration, errorcode, errorcodeDetail, durationSinceUpdate, }); } export class AbstractExtensionTask { barrier = new Barrier(); cancellablePromise; async waitUntilTaskIsFinished() { await this.barrier.wait(); return this.cancellablePromise; } async run() { if (!this.cancellablePromise) { this.cancellablePromise = createCancelablePromise((token) => this.doRun(token)); } this.barrier.open(); return this.cancellablePromise; } cancel() { if (!this.cancellablePromise) { this.cancellablePromise = createCancelablePromise((token) => { return new Promise((c, e) => { const disposable = token.onCancellationRequested(() => { disposable.dispose(); e(new CancellationError()); }); }); }); this.barrier.open(); } this.cancellablePromise.cancel(); } }