src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts

1141 LOC · 190 covered · 951 uncovered · 32 ranges · 5 concepts · 1 introducers · 7 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/platform/extensionManagement/node/extensionDownloader.ts · 300 LOCnode/extensionDownloader…src/vs/platform/extensionManagement/node/extensionManagementUtil.ts · 37 LOCnode/extensionManagement…src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts · 136 LOCnode/extensionSignatureV…extensionDownloader.ts ×10 · 52 introduced LOCextensionDownloader.ts ×…extensionDownloader.ts ×1 · 2 introduced LOCextensionDownloader.ts ×…extensionDownloader.ts ×1 · 2 introduced LOCextensionDownloader.ts ×…extensionDownloader.ts ×1 · 2 introduced LOCextensionDownloader.ts ×…abstractExtensionManagementService.ts ×32 · 395 introduced LOCabstractExtensionManagem…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully for an unsigned extension even when signature verification throws error|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully for an unsigned extension even when signature verification throws error|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully for unsigned extension|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully for unsigned extension|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification fails to execute|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification fails to execute|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification fails|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification fails|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification is disabled because the module is not loaded|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification is disabled because the module is not loaded|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification is disabled by options|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification is disabled by options|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification succeeds|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification succeeds|occurrence=1extensionDownloader.test…Focused file · src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts · 1141 LOCcommon/abstractExtension…

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- abstractExtensionManagementService.ts ×32
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { distinct, isNonEmptyArray } from '../../../base/common/arrays.js';
7 > import { Barrier, CancelablePromise, createCancelablePromise } from '../../../base/common/async.js';
8 > import { CancellationToken } from '../../../base/common/cancellation.js';
9 > import { CancellationError, getErrorMessage, isCancellationError } from '../../../base/common/errors.js';
10 > import { Emitter, Event } from '../../../base/common/event.js';
11 > import { Disposable, toDisposable } from '../../../base/common/lifecycle.js';
12 > import { ResourceMap } from '../../../base/common/map.js';
13 > import { isWeb } from '../../../base/common/platform.js';
14 > import { URI } from '../../../base/common/uri.js';
15 > import * as nls from '../../../nls.js';
16 > import {
17 > ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IGalleryExtension, ILocalExtension, InstallOperation,
18 > IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode,
19 > InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError,
20 > IProductVersion, ExtensionGalleryErrorCode,
21 > EXTENSION_INSTALL_SOURCE_CONTEXT,
22 > DidUpdateExtensionMetadata,
23 > UninstallExtensionInfo,
24 > ExtensionSignatureVerificationCode,
25 > IAllowedExtensionsService
26 > } from './extensionManagement.js';
27 > import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, isMalicious } from './extensionManagementUtil.js';
28 > import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from '../../extensions/common/extensions.js';
29 > import { ILogService } from '../../log/common/log.js';
30 > import { IProductService } from '../../product/common/productService.js';
31 > import { ITelemetryService } from '../../telemetry/common/telemetry.js';
32 > import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
33 > import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
34 > import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js';
35 >
36 > export type InstallableExtension = { readonly manifest: IExtensionManifest; extension: IGalleryExtension | URI; options: InstallOptions };
37 >
38 > export type InstallExtensionTaskOptions = InstallOptions & { readonly profileLocation: URI; readonly productVersion: IProductVersion };
39 > export interface IInstallExtensionTask {
40 > readonly manifest: IExtensionManifest;
41 > readonly identifier: IExtensionIdentifier;
42 > readonly source: IGalleryExtension | URI;
43 > readonly operation: InstallOperation;
44 > readonly options: InstallExtensionTaskOptions;
45 > readonly verificationStatus?: ExtensionSignatureVerificationCode;
46 > run(): Promise<ILocalExtension>;
47 > waitUntilTaskIsFinished(): Promise<ILocalExtension>;
48 > cancel(): void;
49 > }
50 >
51 > export type UninstallExtensionTaskOptions = UninstallOptions & { readonly profileLocation: URI };
52 > export interface IUninstallExtensionTask {
53 > readonly options: UninstallExtensionTaskOptions;
54 > readonly extension: ILocalExtension;
55 > run(): Promise<void>;
56 > waitUntilTaskIsFinished(): Promise<void>;
57 > cancel(): void;
58 > }
59 >
60 > export abstract class CommontExtensionManagementService extends Disposable implements IExtensionManagementService {
61 >
62 > _serviceBrand: undefined;
63 >
64 > readonly preferPreReleases: boolean;
65 >
66 > constructor(
67 @IProductService protected readonly productService: IProductService,
68 @IAllowedExtensionsService protected readonly allowedExtensionsService: IAllowedExtensionsService,
69 ) {
70 super();
71 this.preferPreReleases = this.productService.quality !== 'stable';
72 }
74 > async canInstall(extension: IGalleryExtension): Promise<true | IMarkdownString> {
75 const allowedToInstall = this.allowedExtensionsService.isAllowed({ id: extension.identifier.id, publisherDisplayName: extension.publisherDisplayName });
76 if (allowedToInstall !== true) {
77 return new MarkdownString(nls.localize('not allowed to install', "This extension cannot be installed because {0}", allowedToInstall.value));
78 }
79
80 if (!(await this.isExtensionPlatformCompatible(extension))) {
81 const learnLink = isWeb ? 'https://aka.ms/vscode-web-extensions-guide' : 'https://aka.ms/vscode-platform-specific-extensions';
82 return new MarkdownString(`${nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for the {2} platform.",
83 extension.displayName ?? extension.identifier.id, this.productService.nameLong, TargetPlatformToString(await this.getTargetPlatform()))} [${nls.localize('learn why', "Learn Why")}](${learnLink})`);
84 }
85
86 return true;
87 }
89 > protected async isExtensionPlatformCompatible(extension: IGalleryExtension): Promise<boolean> {
90 const currentTargetPlatform = await this.getTargetPlatform();
91 return extension.allTargetPlatforms.some(targetPlatform => isTargetPlatformCompatible(targetPlatform, extension.allTargetPlatforms, currentTargetPlatform));
92 }
94 > abstract readonly onInstallExtension: Event<InstallExtensionEvent>;
95 > abstract readonly onDidInstallExtensions: Event<readonly InstallExtensionResult[]>;
96 > abstract readonly onUninstallExtension: Event<UninstallExtensionEvent>;
97 > abstract readonly onDidUninstallExtension: Event<DidUninstallExtensionEvent>;
98 > abstract readonly onDidUpdateExtensionMetadata: Event<DidUpdateExtensionMetadata>;
99 > abstract installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise<ILocalExtension>;
100 > abstract installGalleryExtensions(extensions: InstallExtensionInfo[]): Promise<InstallExtensionResult[]>;
101 > abstract uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise<void>;
102 > abstract uninstallExtensions(extensions: UninstallExtensionInfo[]): Promise<void>;
103 > abstract toggleApplicationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise<ILocalExtension>;
104 > abstract getExtensionsControlManifest(): Promise<IExtensionsControlManifest>;
105 > abstract resetPinnedStateForAllUserExtensions(pinned: boolean): Promise<void>;
106 > abstract registerParticipant(pariticipant: IExtensionManagementParticipant): void;
107 > abstract getTargetPlatform(): Promise<TargetPlatform>;
108 > abstract zip(extension: ILocalExtension): Promise<URI>;
109 > abstract getManifest(vsix: URI): Promise<IExtensionManifest>;
110 > abstract install(vsix: URI, options?: InstallOptions): Promise<ILocalExtension>;
111 > abstract installFromLocation(location: URI, profileLocation: URI): Promise<ILocalExtension>;
112 > abstract installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise<ILocalExtension[]>;
113 > abstract getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise<ILocalExtension[]>;
114 > abstract copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise<void>;
115 > abstract download(extension: IGalleryExtension, operation: InstallOperation, donotVerifySignature: boolean): Promise<URI>;
116 > abstract cleanUp(): Promise<void>;
117 > abstract updateMetadata(local: ILocalExtension, metadata: Partial<Metadata>, profileLocation: URI): Promise<ILocalExtension>;
118 > }
119 >
120 > export abstract class AbstractExtensionManagementService extends CommontExtensionManagementService implements IExtensionManagementService {
121 >
122 > declare readonly _serviceBrand: undefined;
123 >
124 > private extensionsControlManifest: Promise<IExtensionsControlManifest> | undefined;
125 > private lastReportTimestamp = 0;
126 > private readonly installingExtensions = new Map<string, { task: IInstallExtensionTask; waitingTasks: IInstallExtensionTask[] }>();
127 > private readonly uninstallingExtensions = new Map<string, IUninstallExtensionTask>();
128 >
129 > private readonly _onInstallExtension = this._register(new Emitter<InstallExtensionEvent>());
130 > get onInstallExtension() { return this._onInstallExtension.event; }
131 >
132 > protected readonly _onDidInstallExtensions = this._register(new Emitter<InstallExtensionResult[]>());
133 > get onDidInstallExtensions() { return this._onDidInstallExtensions.event; }
134 >
135 > protected readonly _onUninstallExtension = this._register(new Emitter<UninstallExtensionEvent>());
136 > get onUninstallExtension() { return this._onUninstallExtension.event; }
137 >
138 > protected _onDidUninstallExtension = this._register(new Emitter<DidUninstallExtensionEvent>());
139 > get onDidUninstallExtension() { return this._onDidUninstallExtension.event; }
140 >
141 > protected readonly _onDidUpdateExtensionMetadata = this._register(new Emitter<DidUpdateExtensionMetadata>());
142 > get onDidUpdateExtensionMetadata() { return this._onDidUpdateExtensionMetadata.event; }
143 >
144 > private readonly participants: IExtensionManagementParticipant[] = [];
145 >
146 > constructor(
147 @IExtensionGalleryService protected readonly galleryService: IExtensionGalleryService,
148 @ITelemetryService protected readonly telemetryService: ITelemetryService,
149 @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,
150 @ILogService protected readonly logService: ILogService,
151 @IProductService productService: IProductService,
152 @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService,
153 @IUserDataProfilesService protected readonly userDataProfilesService: IUserDataProfilesService,
154 ) {
155 super(productService, allowedExtensionsService);
156 this._register(toDisposable(() => {
157 this.installingExtensions.forEach(({ task }) => task.cancel());
158 this.uninstallingExtensions.forEach(promise => promise.cancel());
159 this.installingExtensions.clear();
160 this.uninstallingExtensions.clear();
161 }));
162 }
164 > async installFromGallery(extension: IGalleryExtension, options: InstallOptions = {}): Promise<ILocalExtension> {
165 try {
166 const results = await this.installGalleryExtensions([{ extension, options }]);
167 const result = results.find(({ identifier }) => areSameExtensions(identifier, extension.identifier));
168 if (result?.local) {
169 return result.local;
170 }
171 if (result?.error) {
172 throw result.error;
173 }
174 // Extension might have been redirected due to deprecation (e.g., github.copilot -> github.copilot-chat)
175 // In this case, the result will have the redirected extension's identifier
176 const redirectedResult = results[0];
177 if (redirectedResult?.local) {
178 return redirectedResult.local;
179 }
180 if (redirectedResult?.error) {
181 throw redirectedResult.error;
182 }
183 throw new ExtensionManagementError(`Unknown error while installing extension ${extension.identifier.id}`, ExtensionManagementErrorCode.Unknown);
184 } catch (error) {
185 throw toExtensionManagementError(error);
186 }
187 }
189 > async installGalleryExtensions(extensions: InstallExtensionInfo[]): Promise<InstallExtensionResult[]> {
190 if (!this.galleryService.isEnabled()) {
191 throw new ExtensionManagementError(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"), ExtensionManagementErrorCode.NotAllowed);
192 }
193
194 const results: InstallExtensionResult[] = [];
195 const installableExtensions: InstallableExtension[] = [];
196
197 await Promise.allSettled(extensions.map(async ({ extension, options }) => {
198 try {
199 const compatible = await this.checkAndGetCompatibleVersion(extension, !!options?.installGivenVersion, !!options?.installPreReleaseVersion, options.productVersion ?? { version: this.productService.version, date: this.productService.date });
200 installableExtensions.push({ ...compatible, options });
201 } catch (error) {
202 results.push({ identifier: extension.identifier, operation: InstallOperation.Install, source: extension, error, profileLocation: options.profileLocation ?? this.getCurrentExtensionsManifestLocation() });
203 }
204 }));
205
206 if (installableExtensions.length) {
207 results.push(...await this.installExtensions(installableExtensions));
208 }
209
210 return results;
211 }
213 > async uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise<void> {
214 this.logService.trace('ExtensionManagementService#uninstall', extension.identifier.id);
215 return this.uninstallExtensions([{ extension, options }]);
216 }
218 > async toggleApplicationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise<ILocalExtension> {
219 if (isApplicationScopedExtension(extension.manifest) || extension.isBuiltin) {
220 return extension;
221 }
222
223 if (extension.isApplicationScoped) {
224 let local = await this.updateMetadata(extension, { isApplicationScoped: false }, this.userDataProfilesService.defaultProfile.extensionsResource);
225 if (!this.uriIdentityService.extUri.isEqual(fromProfileLocation, this.userDataProfilesService.defaultProfile.extensionsResource)) {
226 local = await this.copyExtension(extension, this.userDataProfilesService.defaultProfile.extensionsResource, fromProfileLocation);
227 }
228
229 for (const profile of this.userDataProfilesService.profiles) {
230 const existing = (await this.getInstalled(ExtensionType.User, profile.extensionsResource))
231 .find(e => areSameExtensions(e.identifier, extension.identifier));
232 if (existing) {
233 this._onDidUpdateExtensionMetadata.fire({ local: existing, profileLocation: profile.extensionsResource });
234 } else {
235 this._onDidUninstallExtension.fire({ identifier: extension.identifier, profileLocation: profile.extensionsResource });
236 }
237 }
238 return local;
239 }
240
241 else {
242 const local = this.uriIdentityService.extUri.isEqual(fromProfileLocation, this.userDataProfilesService.defaultProfile.extensionsResource)
243 ? await this.updateMetadata(extension, { isApplicationScoped: true }, this.userDataProfilesService.defaultProfile.extensionsResource)
244 : await this.copyExtension(extension, fromProfileLocation, this.userDataProfilesService.defaultProfile.extensionsResource, { isApplicationScoped: true });
245
246 this._onDidInstallExtensions.fire([{ identifier: local.identifier, operation: InstallOperation.Install, local, profileLocation: this.userDataProfilesService.defaultProfile.extensionsResource, applicationScoped: true }]);
247 return local;
248 }
249
250 }
252 > getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
253 const now = new Date().getTime();
254
255 if (!this.extensionsControlManifest || now - this.lastReportTimestamp > 1000 * 60 * 5) { // 5 minute cache freshness
256 this.extensionsControlManifest = this.updateControlCache();
257 this.lastReportTimestamp = now;
258 }
259
260 return this.extensionsControlManifest;
261 }
263 > registerParticipant(participant: IExtensionManagementParticipant): void {
264 this.participants.push(participant);
265 }
267 > async resetPinnedStateForAllUserExtensions(pinned: boolean): Promise<void> {
268 try {
269 await this.joinAllSettled(this.userDataProfilesService.profiles.map(
270 async profile => {
271 const extensions = await this.getInstalled(ExtensionType.User, profile.extensionsResource);
272 await this.joinAllSettled(extensions.map(
273 async extension => {
274 if (extension.pinned !== pinned) {
275 await this.updateMetadata(extension, { pinned }, profile.extensionsResource);
276 }
277 }));
278 }));
279 } catch (error) {
280 this.logService.error('Error while resetting pinned state for all user extensions', getErrorMessage(error));
281 throw error;
282 }
283 }
285 > protected async installExtensions(extensions: InstallableExtension[]): Promise<InstallExtensionResult[]> {
286 const installExtensionResultsMap = new Map<string, InstallExtensionResult & { profileLocation: URI }>();
287 const installingExtensionsMap = new Map<string, { task: IInstallExtensionTask; root: IInstallExtensionTask | undefined; uninstallTaskToWaitFor?: IUninstallExtensionTask }>();
288 const alreadyRequestedInstallations: Promise<ILocalExtension>[] = [];
289
290 const getInstallExtensionTaskKey = (extension: IGalleryExtension, profileLocation: URI) => `${ExtensionKey.create(extension).toString()}-${profileLocation.toString()}`;
291 const createInstallExtensionTask = (manifest: IExtensionManifest, extension: IGalleryExtension | URI, options: InstallExtensionTaskOptions, root: IInstallExtensionTask | undefined): void => {
292 let uninstallTaskToWaitFor;
293 if (!URI.isUri(extension)) {
294 if (installingExtensionsMap.has(`${extension.identifier.id.toLowerCase()}-${options.profileLocation.toString()}`)) {
295 return;
296 }
297 const existingInstallingExtension = this.installingExtensions.get(getInstallExtensionTaskKey(extension, options.profileLocation));
298 if (existingInstallingExtension) {
299 if (root && this.canWaitForTask(root, existingInstallingExtension.task)) {
300 const identifier = existingInstallingExtension.task.identifier;
301 this.logService.info('Waiting for already requested installing extension', identifier.id, root.identifier.id, options.profileLocation.toString());
302 existingInstallingExtension.waitingTasks.push(root);
303 // add promise that waits until the extension is completely installed, ie., onDidInstallExtensions event is triggered for this extension
304 const waitForInstallation = Event.toPromise(
305 Event.filter(this.onDidInstallExtensions, results => results.some(result => areSameExtensions(result.identifier, identifier)))
306 ).then(results => {
307 this.logService.info('Finished waiting for already requested installing extension', identifier.id, root.identifier.id, options.profileLocation.toString());
308 const result = results.find(result => areSameExtensions(result.identifier, identifier));
309 if (!result?.local) {
310 // Extension failed to install
311 throw new Error(`Extension ${identifier.id} is not installed`);
312 }
313 return result.local;
314 });
315 alreadyRequestedInstallations.push(waitForInstallation);
316 // Attach a no-op rejection handler to prevent an unhandledRejection if the
317 // outer try throws before `alreadyRequestedInstallations` is awaited below.
318 // The original promise is still observed via `joinAllSettled` on the happy path,
319 // and the underlying install failure is already reported by the primary task.
320 waitForInstallation.catch(() => { });
321 }
322 return;
323 }
324 uninstallTaskToWaitFor = this.uninstallingExtensions.get(this.getUninstallExtensionTaskKey(extension.identifier, options.profileLocation));
325 }
326 const installExtensionTask = this.createInstallExtensionTask(manifest, extension, options);
327 const key = `${getGalleryExtensionId(manifest.publisher, manifest.name)}-${options.profileLocation.toString()}`;
328 installingExtensionsMap.set(key, { task: installExtensionTask, root, uninstallTaskToWaitFor });
329 this._onInstallExtension.fire({ identifier: installExtensionTask.identifier, source: extension, profileLocation: options.profileLocation });
330 this.logService.info('Installing extension:', installExtensionTask.identifier.id, options);
331 // only cache gallery extensions tasks
332 if (!URI.isUri(extension)) {
333 this.installingExtensions.set(getInstallExtensionTaskKey(extension, options.profileLocation), { task: installExtensionTask, waitingTasks: [] });
334 }
335 };
336
337 try {
338 const systemExtensions = await this.getInstalled(ExtensionType.System);
339 // Start installing extensions
340 for (const { manifest, extension, options } of extensions) {
341 const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name);
342 const isSystemExtension = systemExtensions.some(e => areSameExtensions(e.identifier, { id: extensionId }));
343 const isBuiltin = options.isBuiltin || isSystemExtension;
344 const isApplicationScoped = options.isApplicationScoped || isBuiltin || isApplicationScopedExtension(manifest);
345 const installExtensionTaskOptions: InstallExtensionTaskOptions = {
346 ...options,
347 isBuiltin,
348 isApplicationScoped,
349 profileLocation: isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation ?? this.getCurrentExtensionsManifestLocation(),
350 productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date }
351 };
352
353 const existingInstallExtensionTask = !URI.isUri(extension) ? this.installingExtensions.get(getInstallExtensionTaskKey(extension, installExtensionTaskOptions.profileLocation)) : undefined;
354 if (existingInstallExtensionTask) {
355 const existingTask = existingInstallExtensionTask.task;
356 this.logService.info('Extension is already requested to install', existingTask.identifier.id, installExtensionTaskOptions.profileLocation.toString());
357 // Record the result of the in-flight install into our results map so callers
358 // (e.g. installFromGallery) can find the actual local extension or real error
359 // instead of falling through to a generic "Unknown error".
360 const resultKey = `${existingTask.identifier.id.toLowerCase()}-${installExtensionTaskOptions.profileLocation.toString()}`;
361 const waitForInstallation = existingTask.waitUntilTaskIsFinished().then(local => {
362 installExtensionResultsMap.set(resultKey, {
363 local,
364 identifier: existingTask.identifier,
365 operation: existingTask.operation,
366 source: existingTask.source,
367 context: installExtensionTaskOptions.context,
368 profileLocation: installExtensionTaskOptions.profileLocation,
369 applicationScoped: local.isApplicationScoped,
370 });
371 return local;
372 }, error => {
373 installExtensionResultsMap.set(resultKey, {
374 error: toExtensionManagementError(error),
375 identifier: existingTask.identifier,
376 operation: existingTask.operation,
377 source: existingTask.source,
378 context: installExtensionTaskOptions.context,
379 profileLocation: installExtensionTaskOptions.profileLocation,
380 });
381 throw error;
382 });
383 alreadyRequestedInstallations.push(waitForInstallation);
384 // Attach a no-op rejection handler to prevent an unhandledRejection if the
385 // outer try throws before `alreadyRequestedInstallations` is awaited below.
386 // The original promise is still observed via `joinAllSettled` on the happy path.
387 waitForInstallation.catch(() => { });
388 } else {
389 createInstallExtensionTask(manifest, extension, installExtensionTaskOptions, undefined);
390 }
391 }
392
393 // collect and start installing all dependencies and pack extensions
394 await Promise.all([...installingExtensionsMap.values()].map(async ({ task }) => {
395 if (task.options.donotIncludePackAndDependencies) {
396 this.logService.info('Installing the extension without checking dependencies and pack', task.identifier.id);
397 } else {
398 try {
399 let preferPreRelease = this.preferPreReleases;
400 if (task.options.installPreReleaseVersion) {
401 preferPreRelease = true;
402 } else if (!URI.isUri(task.source) && task.source.hasPreReleaseVersion) {
403 // Explicitly asked to install the release version
404 preferPreRelease = false;
405 }
406 const installed = await this.getInstalled(undefined, task.options.profileLocation, task.options.productVersion);
407 const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, task.manifest, preferPreRelease, task.options.productVersion, installed);
408 const options: InstallExtensionTaskOptions = { ...task.options, pinned: false, installGivenVersion: false, context: { ...task.options.context, [EXTENSION_INSTALL_DEP_PACK_CONTEXT]: true } };
409 for (const { gallery, manifest } of distinct(allDepsAndPackExtensionsToInstall, ({ gallery }) => gallery.identifier.id)) {
410 const existing = installed.find(e => areSameExtensions(e.identifier, gallery.identifier));
411 // Skip if the extension is already installed and has the same application scope
412 if (existing && existing.isApplicationScoped === !!options.isApplicationScoped) {
413 continue;
414 }
415 createInstallExtensionTask(manifest, gallery, options, task);
416 }
417 } catch (error) {
418 // Installing through VSIX
419 if (URI.isUri(task.source)) {
420 // Ignore installing dependencies and packs
421 if (isNonEmptyArray(task.manifest.extensionDependencies)) {
422 this.logService.warn(`Cannot install dependencies of extension:`, task.identifier.id, error.message);
423 }
424 if (isNonEmptyArray(task.manifest.extensionPack)) {
425 this.logService.warn(`Cannot install packed extensions of extension:`, task.identifier.id, error.message);
426 }
427 } else {
428 this.logService.error('Error while preparing to install dependencies and extension packs of the extension:', task.identifier.id);
429 throw error;
430 }
431 }
432 }
433 }));
434
435 const otherProfilesToUpdate = await this.getOtherProfilesToUpdateExtension([...installingExtensionsMap.values()].map(({ task }) => task));
436 for (const [profileLocation, task] of otherProfilesToUpdate) {
437 createInstallExtensionTask(task.manifest, task.source, { ...task.options, profileLocation }, undefined);
438 }
439
440 // Install extensions in parallel and wait until all extensions are installed / failed
441 await this.joinAllSettled([...installingExtensionsMap.entries()].map(async ([key, { task, uninstallTaskToWaitFor }]) => {
442 const startTime = new Date().getTime();
443 let local: ILocalExtension;
444 try {
445 if (uninstallTaskToWaitFor) {
446 this.logService.info('Waiting for existing uninstall task to complete before installing', task.identifier.id);
447 try {
448 await uninstallTaskToWaitFor.waitUntilTaskIsFinished();
449 this.logService.info('Finished waiting for uninstall task, proceeding with install', task.identifier.id);
450 } catch (error) {
451 this.logService.info('Uninstall task failed, proceeding with install anyway', task.identifier.id, getErrorMessage(error));
452 }
453 }
454
455 local = await task.run();
456 await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, task.options, CancellationToken.None)), ExtensionManagementErrorCode.PostInstall);
457 } catch (e) {
458 const error = toExtensionManagementError(e);
459 if (!URI.isUri(task.source)) {
460 reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', {
461 extensionData: getGalleryExtensionTelemetryData(task.source),
462 error,
463 source: task.options.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] as string | undefined
464 });
465 }
466 installExtensionResultsMap.set(key, { error, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.options.profileLocation, applicationScoped: task.options.isApplicationScoped });
467 this.logService.error('Error while installing the extension', task.identifier.id, getErrorMessage(error), task.options.profileLocation.toString());
468 throw error;
469 }
470 if (!URI.isUri(task.source)) {
471 const isUpdate = task.operation === InstallOperation.Update;
472 const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000;
473 reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', {
474 extensionData: getGalleryExtensionTelemetryData(task.source),
475 verificationStatus: task.verificationStatus,
476 duration: new Date().getTime() - startTime,
477 durationSinceUpdate,
478 source: task.options.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] as string | undefined
479 });
480 }
481 installExtensionResultsMap.set(key, { local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.options.profileLocation, applicationScoped: local.isApplicationScoped });
482 }));
483
484 if (alreadyRequestedInstallations.length) {
485 await this.joinAllSettled(alreadyRequestedInstallations);
486 }
487 } catch (error) {
488 const getAllDepsAndPacks = (extension: ILocalExtension, profileLocation: URI, allDepsOrPacks: string[]) => {
489 const depsOrPacks = [];
490 if (extension.manifest.extensionDependencies?.length) {
491 depsOrPacks.push(...extension.manifest.extensionDependencies);
492 }
493 if (extension.manifest.extensionPack?.length) {
494 depsOrPacks.push(...extension.manifest.extensionPack);
495 }
496 for (const id of depsOrPacks) {
497 if (allDepsOrPacks.includes(id.toLowerCase())) {
498 continue;
499 }
500 allDepsOrPacks.push(id.toLowerCase());
501 const installed = installExtensionResultsMap.get(`${id.toLowerCase()}-${profileLocation.toString()}`);
502 if (installed?.local) {
503 allDepsOrPacks = getAllDepsAndPacks(installed.local, profileLocation, allDepsOrPacks);
504 }
505 }
506 return allDepsOrPacks;
507 };
508 const getErrorResult = (task: IInstallExtensionTask) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: task.options.context, profileLocation: task.options.profileLocation, error });
509
510 const rollbackTasks: IUninstallExtensionTask[] = [];
511 for (const [key, { task, root }] of installingExtensionsMap) {
512 const result = installExtensionResultsMap.get(key);
513 if (!result) {
514 task.cancel();
515 installExtensionResultsMap.set(key, getErrorResult(task));
516 }
517 // If the extension is installed by a root task and the root task is failed, then uninstall the extension
518 else if (result.local && root && !installExtensionResultsMap.get(`${root.identifier.id.toLowerCase()}-${task.options.profileLocation.toString()}`)?.local) {
519 rollbackTasks.push(this.createUninstallExtensionTask(result.local, { versionOnly: true, profileLocation: task.options.profileLocation }));
520 installExtensionResultsMap.set(key, getErrorResult(task));
521 }
522 }
523 for (const [key, { task }] of installingExtensionsMap) {
524 const result = installExtensionResultsMap.get(key);
525 if (!result?.local) {
526 continue;
527 }
528 if (task.options.donotIncludePackAndDependencies) {
529 continue;
530 }
531 const depsOrPacks = getAllDepsAndPacks(result.local, task.options.profileLocation, [result.local.identifier.id.toLowerCase()]).slice(1);
532 if (depsOrPacks.some(depOrPack => installingExtensionsMap.has(`${depOrPack.toLowerCase()}-${task.options.profileLocation.toString()}`) && !installExtensionResultsMap.get(`${depOrPack.toLowerCase()}-${task.options.profileLocation.toString()}`)?.local)) {
533 rollbackTasks.push(this.createUninstallExtensionTask(result.local, { versionOnly: true, profileLocation: task.options.profileLocation }));
534 installExtensionResultsMap.set(key, getErrorResult(task));
535 }
536 }
537
538 if (rollbackTasks.length) {
539 await Promise.allSettled(rollbackTasks.map(async rollbackTask => {
540 try {
541 await rollbackTask.run();
542 this.logService.info('Rollback: Uninstalled extension', rollbackTask.extension.identifier.id);
543 } catch (error) {
544 this.logService.warn('Rollback: Error while uninstalling extension', rollbackTask.extension.identifier.id, getErrorMessage(error));
545 }
546 }));
547 }
548 } finally {
549 // Finally, remove all the tasks from the cache
550 for (const { task } of installingExtensionsMap.values()) {
551 if (task.source && !URI.isUri(task.source)) {
552 this.installingExtensions.delete(getInstallExtensionTaskKey(task.source, task.options.profileLocation));
553 }
554 }
555 }
556 const results = [...installExtensionResultsMap.values()];
557 for (const result of results) {
558 if (result.local) {
559 this.logService.info(`Extension installed successfully:`, result.identifier.id, result.profileLocation.toString());
560 }
561 }
562 this._onDidInstallExtensions.fire(results);
563 return results;
564 }
566 > private async getOtherProfilesToUpdateExtension(tasks: IInstallExtensionTask[]): Promise<[URI, IInstallExtensionTask][]> {
567 const otherProfilesToUpdate: [URI, IInstallExtensionTask][] = [];
568 const profileExtensionsCache = new ResourceMap<ILocalExtension[]>();
569 for (const task of tasks) {
570 if (task.operation !== InstallOperation.Update
571 || task.options.isApplicationScoped
572 || task.options.pinned
573 || task.options.installGivenVersion
574 || URI.isUri(task.source)
575 ) {
576 continue;
577 }
578 for (const profile of this.userDataProfilesService.profiles) {
579 if (this.uriIdentityService.extUri.isEqual(profile.extensionsResource, task.options.profileLocation)) {
580 continue;
581 }
582 let installedExtensions = profileExtensionsCache.get(profile.extensionsResource);
583 if (!installedExtensions) {
584 installedExtensions = await this.getInstalled(ExtensionType.User, profile.extensionsResource);
585 profileExtensionsCache.set(profile.extensionsResource, installedExtensions);
586 }
587 const installedExtension = installedExtensions.find(e => areSameExtensions(e.identifier, task.identifier));
588 if (installedExtension && !installedExtension.pinned) {
589 otherProfilesToUpdate.push([profile.extensionsResource, task]);
590 }
591 }
592 }
593 return otherProfilesToUpdate;
594 }
596 > private canWaitForTask(taskToWait: IInstallExtensionTask, taskToWaitFor: IInstallExtensionTask): boolean {
597 for (const [, { task, waitingTasks }] of this.installingExtensions.entries()) {
598 if (task === taskToWait) {
599 // Cannot be waited, If taskToWaitFor is waiting for taskToWait
600 if (waitingTasks.includes(taskToWaitFor)) {
601 return false;
602 }
603 // Cannot be waited, If taskToWaitFor is waiting for tasks waiting for taskToWait
604 if (waitingTasks.some(waitingTask => this.canWaitForTask(waitingTask, taskToWaitFor))) {
605 return false;
606 }
607 }
608 // Cannot be waited, if the taskToWait cannot be waited for the task created the taskToWaitFor
609 // Because, the task waits for the tasks it created
610 if (task === taskToWaitFor && waitingTasks[0] && !this.canWaitForTask(taskToWait, waitingTasks[0])) {
611 return false;
612 }
613 }
614 return true;
615 }
617 > private async joinAllSettled<T>(promises: Promise<T>[], errorCode?: ExtensionManagementErrorCode): Promise<T[]> {
618 const results: T[] = [];
619 const errors: ExtensionManagementError[] = [];
620 const promiseResults = await Promise.allSettled(promises);
621 for (const r of promiseResults) {
622 if (r.status === 'fulfilled') {
623 results.push(r.value);
624 } else {
625 errors.push(toExtensionManagementError(r.reason, errorCode));
626 }
627 }
628
629 if (!errors.length) {
630 return results;
631 }
632
633 // Throw if there are errors
634 if (errors.length === 1) {
635 throw errors[0];
636 }
637
638 let error = new ExtensionManagementError('', ExtensionManagementErrorCode.Unknown);
639 for (const current of errors) {
640 error = new ExtensionManagementError(
641 error.message ? `${error.message}, ${current.message}` : current.message,
642 current.code !== ExtensionManagementErrorCode.Unknown && current.code !== ExtensionManagementErrorCode.Internal ? current.code : error.code
643 );
644 }
645 throw error;
646 }
648 > private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, preferPreRelease: boolean, productVersion: IProductVersion, installed: ILocalExtension[]): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> {
649 if (!this.galleryService.isEnabled()) {
650 return [];
651 }
652
653 const knownIdentifiers: IExtensionIdentifier[] = [];
654
655 const allDependenciesAndPacks: { gallery: IGalleryExtension; manifest: IExtensionManifest }[] = [];
656 const collectDependenciesAndPackExtensionsToInstall = async (extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest): Promise<void> => {
657 knownIdentifiers.push(extensionIdentifier);
658 const dependecies: string[] = manifest.extensionDependencies ? manifest.extensionDependencies.filter(dep => !installed.some(e => areSameExtensions(e.identifier, { id: dep }))) : [];
659 const dependenciesAndPackExtensions = [...dependecies];
660 if (manifest.extensionPack) {
661 const existing = installed.find(e => areSameExtensions(e.identifier, extensionIdentifier));
662 for (const extension of manifest.extensionPack) {
663 // add only those extensions which are new in currently installed extension
664 if (!(existing && existing.manifest.extensionPack && existing.manifest.extensionPack.some(old => areSameExtensions({ id: old }, { id: extension })))) {
665 if (dependenciesAndPackExtensions.every(e => !areSameExtensions({ id: e }, { id: extension }))) {
666 dependenciesAndPackExtensions.push(extension);
667 }
668 }
669 }
670 }
671
672 if (dependenciesAndPackExtensions.length) {
673 // filter out known extensions
674 const ids = dependenciesAndPackExtensions.filter(id => knownIdentifiers.every(galleryIdentifier => !areSameExtensions(galleryIdentifier, { id })));
675 if (ids.length) {
676 const galleryExtensions = await this.galleryService.getExtensions(ids.map(id => ({ id, preRelease: preferPreRelease })), CancellationToken.None);
677 for (const galleryExtension of galleryExtensions) {
678 if (knownIdentifiers.find(identifier => areSameExtensions(identifier, galleryExtension.identifier))) {
679 continue;
680 }
681 const isDependency = dependecies.some(id => areSameExtensions({ id }, galleryExtension.identifier));
682 let compatible;
683 try {
684 compatible = await this.checkAndGetCompatibleVersion(galleryExtension, false, preferPreRelease, productVersion);
685 } catch (error) {
686 if (!isDependency) {
687 this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id, getErrorMessage(error));
688 continue;
689 } else {
690 throw error;
691 }
692 }
693 allDependenciesAndPacks.push({ gallery: compatible.extension, manifest: compatible.manifest });
694 await collectDependenciesAndPackExtensionsToInstall(compatible.extension.identifier, compatible.manifest);
695 }
696 }
697 }
698 };
699
700 await collectDependenciesAndPackExtensionsToInstall(extensionIdentifier, manifest);
701 return allDependenciesAndPacks;
702 }
704 > private async checkAndGetCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, installPreRelease: boolean, productVersion: IProductVersion): Promise<{ extension: IGalleryExtension; manifest: IExtensionManifest }> {
705 let compatibleExtension: IGalleryExtension | null;
706
707 const extensionsControlManifest = await this.getExtensionsControlManifest();
708 if (isMalicious(extension.identifier, extensionsControlManifest.malicious)) {
709 throw new ExtensionManagementError(nls.localize('malicious extension', "Can't install '{0}' extension since it was reported to be problematic.", extension.identifier.id), ExtensionManagementErrorCode.Malicious);
710 }
711
712 const deprecationInfo = extensionsControlManifest.deprecated[extension.identifier.id.toLowerCase()];
713 if (deprecationInfo?.extension?.autoMigrate) {
714 this.logService.info(`The '${extension.identifier.id}' extension is deprecated, fetching the compatible '${deprecationInfo.extension.id}' extension instead.`);
715 compatibleExtension = (await this.galleryService.getExtensions([{ id: deprecationInfo.extension.id, preRelease: deprecationInfo.extension.preRelease }], { targetPlatform: await this.getTargetPlatform(), compatible: true, productVersion }, CancellationToken.None))[0];
716 if (!compatibleExtension) {
717 throw new ExtensionManagementError(nls.localize('notFoundDeprecatedReplacementExtension', "Can't install '{0}' extension since it was deprecated and the replacement extension '{1}' can't be found.", extension.identifier.id, deprecationInfo.extension.id), ExtensionManagementErrorCode.Deprecated);
718 }
719 }
720
721 else {
722 if (await this.canInstall(extension) !== true) {
723 const targetPlatform = await this.getTargetPlatform();
724 throw new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for the {2} platform.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.IncompatibleTargetPlatform);
725 }
726
727 compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease, productVersion);
728 if (!compatibleExtension) {
729 /** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */
730 if (!installPreRelease && extension.hasPreReleaseVersion && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) {
731 throw new ExtensionManagementError(nls.localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.displayName ?? extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound);
732 }
733 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);
734 }
735 }
736
737 this.logService.info('Getting Manifest...', compatibleExtension.identifier.id);
738 const manifest = await this.galleryService.getManifest(compatibleExtension, CancellationToken.None);
739 if (manifest === null) {
740 throw new ExtensionManagementError(`Missing manifest for extension ${compatibleExtension.identifier.id}`, ExtensionManagementErrorCode.Invalid);
741 }
742
743 if (manifest.version !== compatibleExtension.version) {
744 throw new ExtensionManagementError(`Cannot install '${compatibleExtension.identifier.id}' extension because of version mismatch in Marketplace`, ExtensionManagementErrorCode.Invalid);
745 }
746
747 return { extension: compatibleExtension, manifest };
748 }
750 > protected async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean, productVersion: IProductVersion): Promise<IGalleryExtension | null> {
751 const targetPlatform = await this.getTargetPlatform();
752 let compatibleExtension: IGalleryExtension | null = null;
753
754 if (!sameVersion && extension.hasPreReleaseVersion && extension.properties.isPreReleaseVersion !== includePreRelease) {
755 compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: includePreRelease }], { targetPlatform, compatible: true, productVersion }, CancellationToken.None))[0] || null;
756 }
757
758 if (!compatibleExtension && await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform, productVersion)) {
759 compatibleExtension = extension;
760 }
761
762 if (!compatibleExtension) {
763 if (sameVersion) {
764 compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, version: extension.version }], { targetPlatform, compatible: true, productVersion }, CancellationToken.None))[0] || null;
765 } else {
766 compatibleExtension = await this.galleryService.getCompatibleExtension(extension, includePreRelease, targetPlatform, productVersion);
767 }
768 }
769
770 return compatibleExtension;
771 }
773 > private getUninstallExtensionTaskKey(identifier: IExtensionIdentifier, profileLocation: URI, version?: string): string {
774 return `${identifier.id.toLowerCase()}${version ? `-${version}` : ''}@${profileLocation.toString()}`;
775 }
777 > async uninstallExtensions(extensions: UninstallExtensionInfo[]): Promise<void> {
778
779 const getUninstallExtensionTaskKey = (extension: ILocalExtension, uninstallOptions: UninstallExtensionTaskOptions) => this.getUninstallExtensionTaskKey(extension.identifier, uninstallOptions.profileLocation, uninstallOptions.versionOnly ? extension.manifest.version : undefined);
780
781 const createUninstallExtensionTask = (extension: ILocalExtension, uninstallOptions: UninstallExtensionTaskOptions): void => {
782 let installTaskToWaitFor: IInstallExtensionTask | undefined;
783 for (const { task } of this.installingExtensions.values()) {
784 if (!(task.source instanceof URI) && areSameExtensions(task.identifier, extension.identifier) && this.uriIdentityService.extUri.isEqual(task.options.profileLocation, uninstallOptions.profileLocation)) {
785 installTaskToWaitFor = task;
786 break;
787 }
788 }
789 const task = this.createUninstallExtensionTask(extension, uninstallOptions);
790 this.uninstallingExtensions.set(getUninstallExtensionTaskKey(task.extension, uninstallOptions), task);
791 this.logService.info('Uninstalling extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString());
792 this._onUninstallExtension.fire({ identifier: extension.identifier, profileLocation: uninstallOptions.profileLocation, applicationScoped: extension.isApplicationScoped });
793 allTasks.push({ task, installTaskToWaitFor });
794 };
795
796 const postUninstallExtension = (extension: ILocalExtension, uninstallOptions: UninstallExtensionTaskOptions, error?: ExtensionManagementError): void => {
797 if (error) {
798 this.logService.error('Failed to uninstall extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString(), error.message);
799 } else {
800 this.logService.info('Successfully uninstalled extension from the profile', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString());
801 }
802 reportTelemetry(this.telemetryService, 'extensionGallery:uninstall', { extensionData: getLocalExtensionTelemetryData(extension), error });
803 this._onDidUninstallExtension.fire({ identifier: extension.identifier, error: error?.code, profileLocation: uninstallOptions.profileLocation, applicationScoped: extension.isApplicationScoped });
804 };
805
806 const allTasks: { task: IUninstallExtensionTask; installTaskToWaitFor?: IInstallExtensionTask }[] = [];
807 const processedTasks: IUninstallExtensionTask[] = [];
808 const alreadyRequestedUninstalls: Promise<void>[] = [];
809 const extensionsToRemove: ILocalExtension[] = [];
810
811 const installedExtensionsMap = new ResourceMap<ILocalExtension[]>();
812 const getInstalledExtensions = async (profileLocation: URI) => {
813 let installed = installedExtensionsMap.get(profileLocation);
814 if (!installed) {
815 installedExtensionsMap.set(profileLocation, installed = await this.getInstalled(ExtensionType.User, profileLocation));
816 }
817 return installed;
818 };
819
820 for (const { extension, options } of extensions) {
821 const uninstallOptions: UninstallExtensionTaskOptions = {
822 ...options,
823 profileLocation: extension.isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options?.profileLocation ?? this.getCurrentExtensionsManifestLocation()
824 };
825 const uninstallExtensionTask = this.uninstallingExtensions.get(getUninstallExtensionTaskKey(extension, uninstallOptions));
826 if (uninstallExtensionTask) {
827 this.logService.info('Extensions is already requested to uninstall', extension.identifier.id);
828 alreadyRequestedUninstalls.push(uninstallExtensionTask.waitUntilTaskIsFinished());
829 } else {
830 createUninstallExtensionTask(extension, uninstallOptions);
831 }
832
833 if (uninstallOptions.remove || extension.isApplicationScoped) {
834 if (uninstallOptions.remove) {
835 extensionsToRemove.push(extension);
836 }
837 for (const profile of this.userDataProfilesService.profiles) {
838 if (this.uriIdentityService.extUri.isEqual(profile.extensionsResource, uninstallOptions.profileLocation)) {
839 continue;
840 }
841 const installed = await getInstalledExtensions(profile.extensionsResource);
842 const profileExtension = installed.find(e => areSameExtensions(e.identifier, extension.identifier));
843 if (profileExtension) {
844 const uninstallOptionsWithProfile = { ...uninstallOptions, profileLocation: profile.extensionsResource };
845 const uninstallExtensionTask = this.uninstallingExtensions.get(getUninstallExtensionTaskKey(profileExtension, uninstallOptionsWithProfile));
846 if (uninstallExtensionTask) {
847 this.logService.info('Extensions is already requested to uninstall', profileExtension.identifier.id);
848 alreadyRequestedUninstalls.push(uninstallExtensionTask.waitUntilTaskIsFinished());
849 } else {
850 createUninstallExtensionTask(profileExtension, uninstallOptionsWithProfile);
851 }
852 }
853 }
854 }
855 }
856
857 try {
858 for (const { task } of allTasks.slice(0)) {
859 const installed = await getInstalledExtensions(task.options.profileLocation);
860
861 if (task.options.donotIncludePack) {
862 this.logService.info('Uninstalling the extension without including packed extension', `${task.extension.identifier.id}@${task.extension.manifest.version}`);
863 } else {
864 const packedExtensions = this.getAllPackExtensionsToUninstall(task.extension, installed);
865 for (const packedExtension of packedExtensions) {
866 if (this.uninstallingExtensions.has(getUninstallExtensionTaskKey(packedExtension, task.options))) {
867 this.logService.info('Extensions is already requested to uninstall', packedExtension.identifier.id);
868 } else {
869 createUninstallExtensionTask(packedExtension, task.options);
870 }
871 }
872 }
873 if (task.options.donotCheckDependents) {
874 this.logService.info('Uninstalling the extension without checking dependents', `${task.extension.identifier.id}@${task.extension.manifest.version}`);
875 } else {
876 this.checkForDependents(allTasks.map(({ task }) => task.extension), installed, task.extension);
877 }
878 }
879
880 // Uninstall extensions in parallel and wait until all extensions are uninstalled / failed
881 await this.joinAllSettled(allTasks.map(async ({ task, installTaskToWaitFor }) => {
882 try {
883 // Wait for opposite task if it exists
884 if (installTaskToWaitFor) {
885 this.logService.info('Waiting for existing install task to complete before uninstalling', task.extension.identifier.id);
886 try {
887 await installTaskToWaitFor.waitUntilTaskIsFinished();
888 this.logService.info('Finished waiting for install task, proceeding with uninstall', task.extension.identifier.id);
889 } catch (error) {
890 this.logService.info('Install task failed, proceeding with uninstall anyway', task.extension.identifier.id, getErrorMessage(error));
891 }
892 }
893
894 await task.run();
895 await this.joinAllSettled(this.participants.map(participant => participant.postUninstall(task.extension, task.options, CancellationToken.None)));
896 // only report if extension has a mapped gallery extension and not in web. UUID identifies the gallery extension.
897 if (task.extension.identifier.uuid && !isWeb) {
898 try {
899 await this.galleryService.reportStatistic(task.extension.manifest.publisher, task.extension.manifest.name, task.extension.manifest.version, StatisticType.Uninstall);
900 } catch (error) { /* ignore */ }
901 }
902 } catch (e) {
903 const error = toExtensionManagementError(e);
904 postUninstallExtension(task.extension, task.options, error);
905 throw error;
906 } finally {
907 processedTasks.push(task);
908 }
909 }));
910
911 if (alreadyRequestedUninstalls.length) {
912 await this.joinAllSettled(alreadyRequestedUninstalls);
913 }
914
915 for (const { task } of allTasks) {
916 postUninstallExtension(task.extension, task.options);
917 }
918
919 if (extensionsToRemove.length) {
920 await this.joinAllSettled(extensionsToRemove.map(extension => this.deleteExtension(extension)));
921 }
922 } catch (e) {
923 const error = toExtensionManagementError(e);
924 for (const { task } of allTasks) {
925 // cancel the tasks
926 try { task.cancel(); } catch (error) { /* ignore */ }
927 if (!processedTasks.includes(task)) {
928 postUninstallExtension(task.extension, task.options, error);
929 }
930 }
931 throw error;
932 } finally {
933 // Remove tasks from cache
934 for (const { task } of allTasks) {
935 if (!this.uninstallingExtensions.delete(getUninstallExtensionTaskKey(task.extension, task.options))) {
936 this.logService.warn('Uninstallation task is not found in the cache', task.extension.identifier.id);
937 }
938 }
939 }
940 }
942 > private checkForDependents(extensionsToUninstall: ILocalExtension[], installed: ILocalExtension[], extensionToUninstall: ILocalExtension): void {
943 for (const extension of extensionsToUninstall) {
944 const dependents = this.getDependents(extension, installed);
945 if (dependents.length) {
946 const remainingDependents = dependents.filter(dependent => !extensionsToUninstall.some(e => areSameExtensions(e.identifier, dependent.identifier)));
947 if (remainingDependents.length) {
948 throw new Error(this.getDependentsErrorMessage(extension, remainingDependents, extensionToUninstall));
949 }
950 }
951 }
952 }
954 > private getDependentsErrorMessage(dependingExtension: ILocalExtension, dependents: ILocalExtension[], extensionToUninstall: ILocalExtension): string {
955 if (extensionToUninstall === dependingExtension) {
956 if (dependents.length === 1) {
957 return nls.localize('singleDependentError', "Cannot uninstall '{0}' extension. '{1}' extension depends on this.",
958 extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name);
959 }
960 if (dependents.length === 2) {
961 return nls.localize('twoDependentsError', "Cannot uninstall '{0}' extension. '{1}' and '{2}' extensions depend on this.",
962 extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name);
963 }
964 return nls.localize('multipleDependentsError', "Cannot uninstall '{0}' extension. '{1}', '{2}' and other extension depend on this.",
965 extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name);
966 }
967 if (dependents.length === 1) {
968 return nls.localize('singleIndirectDependentError', "Cannot uninstall '{0}' extension . It includes uninstalling '{1}' extension and '{2}' extension depends on this.",
969 extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependingExtension.manifest.displayName
970 || dependingExtension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name);
971 }
972 if (dependents.length === 2) {
973 return nls.localize('twoIndirectDependentsError', "Cannot uninstall '{0}' extension. It includes uninstalling '{1}' extension and '{2}' and '{3}' extensions depend on this.",
974 extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependingExtension.manifest.displayName
975 || dependingExtension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name);
976 }
977 return nls.localize('multipleIndirectDependentsError', "Cannot uninstall '{0}' extension. It includes uninstalling '{1}' extension and '{2}', '{3}' and other extensions depend on this.",
978 extensionToUninstall.manifest.displayName || extensionToUninstall.manifest.name, dependingExtension.manifest.displayName
979 || dependingExtension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name);
980
981 }
983 > private getAllPackExtensionsToUninstall(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[] = []): ILocalExtension[] {
984 if (checked.indexOf(extension) !== -1) {
985 return [];
986 }
987 if (areSameExtensions(extension.identifier, { id: this.productService.defaultChatAgent.extensionId })) {
988 return [];
989 }
990 checked.push(extension);
991 const extensionsPack = extension.manifest.extensionPack ? extension.manifest.extensionPack : [];
992 if (extensionsPack.length) {
993 const packedExtensions = installed.filter(i => !i.isBuiltin && extensionsPack.some(id => areSameExtensions({ id }, i.identifier)));
994 const packOfPackedExtensions: ILocalExtension[] = [];
995 for (const packedExtension of packedExtensions) {
996 packOfPackedExtensions.push(...this.getAllPackExtensionsToUninstall(packedExtension, installed, checked));
997 }
998 return [...packedExtensions, ...packOfPackedExtensions];
999 }
1000 return [];
1001 }
1003 > private getDependents(extension: ILocalExtension, installed: ILocalExtension[]): ILocalExtension[] {
1004 return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.some(id => areSameExtensions({ id }, extension.identifier)));
1005 }
1007 > private async updateControlCache(): Promise<IExtensionsControlManifest> {
1008 try {
1009 this.logService.trace('ExtensionManagementService.updateControlCache');
1010 return await this.galleryService.getExtensionsControlManifest();
1011 } catch (err) {
1012 this.logService.trace('ExtensionManagementService.refreshControlCache - failed to get extension control manifest', getErrorMessage(err));
1013 return { malicious: [], deprecated: {}, search: [] };
1014 }
1015 }
1017 > protected abstract getCurrentExtensionsManifestLocation(): URI;
1018 > protected abstract createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask;
1019 > protected abstract createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask;
1020 > protected abstract copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata?: Partial<Metadata>): Promise<ILocalExtension>;
1021 > protected abstract moveExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata?: Partial<Metadata>): Promise<ILocalExtension>;
1022 > protected abstract removeExtension(extension: ILocalExtension, fromProfileLocation: URI): Promise<void>;
1023 > protected abstract deleteExtension(extension: ILocalExtension): Promise<void>;
1024 > }
1025 >
1026 > export function toExtensionManagementError(error: Error, code?: ExtensionManagementErrorCode): ExtensionManagementError {
1027 if (error instanceof ExtensionManagementError) {
1028 return error;
1029 }
1030 let extensionManagementError: ExtensionManagementError;
1031 if (error instanceof ExtensionGalleryError) {
1032 extensionManagementError = new ExtensionManagementError(error.message, error.code === ExtensionGalleryErrorCode.DownloadFailedWriting ? ExtensionManagementErrorCode.DownloadFailedWriting : ExtensionManagementErrorCode.Gallery);
1033 } else {
1034 extensionManagementError = new ExtensionManagementError(error.message, isCancellationError(error) ? ExtensionManagementErrorCode.Cancelled : (code ?? ExtensionManagementErrorCode.Internal));
1035 }
1036 extensionManagementError.stack = error.stack;
1037 return extensionManagementError;
1038 }
1040 function reportTelemetry(telemetryService: ITelemetryService, eventName: string,
1041 {
1042 extensionData,
1043 verificationStatus,
1044 duration,
1045 error,
1046 source,
1047 durationSinceUpdate
1048 }: {
1049 extensionData: object;
1050 verificationStatus?: ExtensionSignatureVerificationCode;
1051 duration?: number;
1052 durationSinceUpdate?: number;
1053 source?: string;
1054 error?: ExtensionManagementError | ExtensionGalleryError;
1055 }): void {
1056
1057 /* __GDPR__
1058 "extensionGallery:install" : {
1059 "owner": "sandy081",
1060 "success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
1061 "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
1062 "durationSinceUpdate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
1063 "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
1064 "recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
1065 "verificationStatus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
1066 "source": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
1067 "${include}": [
1068 "${GalleryExtensionTelemetryData}"
1069 ]
1070 }
1071 */
1072 /* __GDPR__
1073 "extensionGallery:uninstall" : {
1074 "owner": "sandy081",
1075 "success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
1076 "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
1077 "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
1078 "${include}": [
1079 "${GalleryExtensionTelemetryData}"
1080 ]
1081 }
1082 */
1083 /* __GDPR__
1084 "extensionGallery:update" : {
1085 "owner": "sandy081",
1086 "success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
1087 "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
1088 "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
1089 "verificationStatus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
1090 "source": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
1091 "${include}": [
1092 "${GalleryExtensionTelemetryData}"
1093 ]
1094 }
1095 */
1096 telemetryService.publicLog(eventName, {
1097 ...extensionData,
1098 source,
1099 duration,
1100 durationSinceUpdate,
1101 success: !error,
1102 errorcode: error?.code,
1103 verificationStatus: verificationStatus === ExtensionSignatureVerificationCode.Success ? 'Verified' : (verificationStatus ?? 'Unverified')
1104 });
1105 }
1107 > export abstract class AbstractExtensionTask<T> {
1108
1109 private readonly barrier = new Barrier();
1110 > private cancellablePromise: CancelablePromise<T> | undefined; abstractExtensionManagementService.ts ×32
1111 >
1112 > async waitUntilTaskIsFinished(): Promise<T> {
1113 await this.barrier.wait();
1114 return this.cancellablePromise!;
1115 }
1117 > run(): Promise<T> {
1118 if (!this.cancellablePromise) {
1119 this.cancellablePromise = createCancelablePromise(token => this.doRun(token));
1120 }
1121 this.barrier.open();
1122 return this.cancellablePromise;
1123 }
1125 > cancel(): void {
1126 if (!this.cancellablePromise) {
1127 this.cancellablePromise = createCancelablePromise(token => {
1128 return new Promise((c, e) => {
1129 const disposable = token.onCancellationRequested(() => {
1130 disposable.dispose();
1131 e(new CancellationError());
1132 });
1133 });
1134 });
1135 this.barrier.open();
1136 }
1137 this.cancellablePromise.cancel();
1138 }
1140 > protected abstract doRun(token: CancellationToken): Promise<T>;
1141 > }