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