@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
1,093 lines (1,092 loc) • 42.3 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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();
}
}