Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/extensionManagement/node/extensionManagementService.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 * as fs from 'fs';
7
import { Promises, Queue } from '../../../base/common/async.js';
8
import { VSBuffer } from '../../../base/common/buffer.js';
9
import { CancellationToken } from '../../../base/common/cancellation.js';
10
import { IStringDictionary } from '../../../base/common/collections.js';
11
import { CancellationError, getErrorMessage } from '../../../base/common/errors.js';
12
import { Emitter } from '../../../base/common/event.js';
13
import { hash } from '../../../base/common/hash.js';
14
import { Disposable } from '../../../base/common/lifecycle.js';
15
import { ResourceMap, ResourceSet } from '../../../base/common/map.js';
16
import { Schemas } from '../../../base/common/network.js';
17
import * as path from '../../../base/common/path.js';
18
import { joinPath } from '../../../base/common/resources.js';
19
import * as semver from '../../../base/common/semver/semver.js';
20
import { isBoolean, isDefined, isUndefined } from '../../../base/common/types.js';
21
import { URI } from '../../../base/common/uri.js';
22
import { generateUuid } from '../../../base/common/uuid.js';
23
import * as pfs from '../../../base/node/pfs.js';
24
import { extract, IFile, zip } from '../../../base/node/zip.js';
25
import * as nls from '../../../nls.js';
26
import { IDownloadService } from '../../download/common/download.js';
27
import { INativeEnvironmentService } from '../../environment/common/environment.js';
28
import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, toExtensionManagementError, UninstallExtensionTaskOptions } from '../common/abstractExtensionManagementService.js';
29
import {
30
ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOperation,
31
Metadata, InstallOptions,
32
IProductVersion,
33
EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT,
34
ExtensionSignatureVerificationCode,
35
computeSize,
36
IAllowedExtensionsService,
37
VerifyExtensionSignatureConfigKey,
38
shouldRequireRepositorySignatureFor,
39
} from '../common/extensionManagement.js';
40
import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from '../common/extensionManagementUtil.js';
41
import { IExtensionsProfileScannerService, IScannedProfileExtension } from '../common/extensionsProfileScannerService.js';
42
import { IExtensionsScannerService, IScannedExtension, ManifestMetadata, UserExtensionsScanOptions } from '../common/extensionsScannerService.js';
43
import { ExtensionsDownloader } from './extensionDownloader.js';
44
import { ExtensionsLifecycle } from './extensionLifecycle.js';
45
import { fromExtractError, getManifest } from './extensionManagementUtil.js';
46
import { ExtensionsManifestCache } from './extensionsManifestCache.js';
47
import { DidChangeProfileExtensionsEvent, ExtensionsWatcher } from './extensionsWatcher.js';
48
import { ExtensionType, IExtension, IExtensionManifest, TargetPlatform } from '../../extensions/common/extensions.js';
49
import { isEngineValid } from '../../extensions/common/extensionValidator.js';
50
import { FileChangesEvent, FileChangeType, FileOperationResult, IFileService, IFileStat, toFileOperationResult } from '../../files/common/files.js';
51
import { IInstantiationService, refineServiceDecorator } from '../../instantiation/common/instantiation.js';
52
import { ILogService } from '../../log/common/log.js';
53
import { IProductService } from '../../product/common/productService.js';
54
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
55
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
56
import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
57
import { IConfigurationService } from '../../configuration/common/configuration.js';
58
import { IExtensionGalleryManifestService } from '../common/extensionGalleryManifest.js';
59
60
export const INativeServerExtensionManagementService = refineServiceDecorator<IExtensionManagementService, INativeServerExtensionManagementService>(IExtensionManagementService);
61
export interface INativeServerExtensionManagementService extends IExtensionManagementService {
62
readonly _serviceBrand: undefined;
63
scanAllUserInstalledExtensions(): Promise<ILocalExtension[]>;
64
scanInstalledExtensionAtLocation(location: URI): Promise<ILocalExtension | null>;
65
deleteExtensions(...extensions: IExtension[]): Promise<void>;
66
}
67
68
type ExtractExtensionResult = { readonly local: ILocalExtension; readonly verificationStatus?: ExtensionSignatureVerificationCode };
69
70
const DELETED_FOLDER_POSTFIX = '.vsctmp';
71
72
export class ExtensionManagementService extends AbstractExtensionManagementService implements INativeServerExtensionManagementService {
73
74
private readonly extensionsScanner: ExtensionsScanner;
75
private readonly manifestCache: ExtensionsManifestCache;
76
private readonly extensionsDownloader: ExtensionsDownloader;
77
78
private readonly extractingGalleryExtensions = new Map<string, Promise<ExtractExtensionResult>>();
79
80
constructor(
81
@IExtensionGalleryService galleryService: IExtensionGalleryService,
82
@ITelemetryService telemetryService: ITelemetryService,
83
@ILogService logService: ILogService,
84
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
85
@IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService,
86
@IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService,
87
@IDownloadService private downloadService: IDownloadService,
88
@IInstantiationService private readonly instantiationService: IInstantiationService,
89
@IFileService private readonly fileService: IFileService,
90
@IConfigurationService private readonly configurationService: IConfigurationService,
91
@IExtensionGalleryManifestService protected readonly extensionGalleryManifestService: IExtensionGalleryManifestService,
92
@IProductService productService: IProductService,
93
@IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService,
94
@IUriIdentityService uriIdentityService: IUriIdentityService,
95
@IUserDataProfilesService userDataProfilesService: IUserDataProfilesService
96
) {
97
super(galleryService, telemetryService, uriIdentityService, logService, productService, allowedExtensionsService, userDataProfilesService);
98
const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle));
99
this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension)));
100
this.manifestCache = this._register(new ExtensionsManifestCache(userDataProfilesService, fileService, uriIdentityService, this, this.logService));
101
this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader));
102
103
const extensionsWatcher = this._register(new ExtensionsWatcher(this, this.extensionsScannerService, userDataProfilesService, extensionsProfileScannerService, uriIdentityService, fileService, logService));
104
this._register(extensionsWatcher.onDidChangeExtensionsByAnotherSource(e => this.onDidChangeExtensionsFromAnotherSource(e)));
105
this.watchForExtensionsNotInstalledBySystem();
106
}
107
108
private _targetPlatformPromise: Promise<TargetPlatform> | undefined;
109
getTargetPlatform(): Promise<TargetPlatform> {
110
if (!this._targetPlatformPromise) {
111
this._targetPlatformPromise = computeTargetPlatform(this.fileService, this.logService);
112
}
113
return this._targetPlatformPromise;
114
}
115
116
async zip(extension: ILocalExtension): Promise<URI> {
117
this.logService.trace('ExtensionManagementService#zip', extension.identifier.id);
118
const files = await this.collectFiles(extension);
119
const location = await zip(joinPath(this.extensionsDownloader.extensionsDownloadDir, generateUuid()).fsPath, files);
120
return URI.file(location);
121
}
122
123
async getManifest(vsix: URI): Promise<IExtensionManifest> {
124
const { location, cleanup } = await this.downloadVsix(vsix);
125
const zipPath = path.resolve(location.fsPath);
126
try {
127
return await getManifest(zipPath);
128
} finally {
129
await cleanup();
130
}
131
}
132
133
getInstalled(type?: ExtensionType, profileLocation: URI = this.userDataProfilesService.defaultProfile.extensionsResource, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }, language?: string): Promise<ILocalExtension[]> {
134
return this.extensionsScanner.scanExtensions(type ?? null, profileLocation, productVersion, language);
135
}
136
137
scanAllUserInstalledExtensions(): Promise<ILocalExtension[]> {
138
return this.extensionsScanner.scanAllUserExtensions();
139
}
140
141
scanInstalledExtensionAtLocation(location: URI): Promise<ILocalExtension | null> {
142
return this.extensionsScanner.scanUserExtensionAtLocation(location);
143
}
144
145
async install(vsix: URI, options: InstallOptions = {}): Promise<ILocalExtension> {
146
this.logService.trace('ExtensionManagementService#install', vsix.toString());
147
148
const { location, cleanup } = await this.downloadVsix(vsix);
149
150
try {
151
const manifest = await getManifest(path.resolve(location.fsPath));
152
const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name);
153
if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode, this.productService.version, this.productService.date)) {
154
throw new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", extensionId, this.productService.version));
155
}
156
157
const allowedToInstall = this.allowedExtensionsService.isAllowed({ id: extensionId, version: manifest.version, publisherDisplayName: undefined });
158
if (allowedToInstall !== true) {
159
throw new Error(nls.localize('notAllowed', "This extension cannot be installed because {0}", allowedToInstall.value));
160
}
161
162
const results = await this.installExtensions([{ manifest, extension: location, options }]);
163
const result = results.find(({ identifier }) => areSameExtensions(identifier, { id: extensionId }));
164
if (result?.local) {
165
return result.local;
166
}
167
if (result?.error) {
168
throw result.error;
169
}
170
throw toExtensionManagementError(new Error(`Unknown error while installing extension ${extensionId}`));
171
} finally {
172
await cleanup();
173
}
174
}
175
176
async installFromLocation(location: URI, profileLocation: URI): Promise<ILocalExtension> {
177
this.logService.trace('ExtensionManagementService#installFromLocation', location.toString());
178
const local = await this.extensionsScanner.scanUserExtensionAtLocation(location);
179
if (!local || !local.manifest.name || !local.manifest.version) {
180
throw new Error(`Cannot find a valid extension from the location ${location.toString()}`);
181
}
182
await this.addExtensionsToProfile([[local, { source: 'resource' }]], profileLocation);
183
this.logService.info('Successfully installed extension', local.identifier.id, profileLocation.toString());
184
return local;
185
}
186
187
async installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise<ILocalExtension[]> {
188
this.logService.trace('ExtensionManagementService#installExtensionsFromProfile', extensions, fromProfileLocation.toString(), toProfileLocation.toString());
189
const extensionsToInstall = (await this.getInstalled(ExtensionType.User, fromProfileLocation)).filter(e => extensions.some(id => areSameExtensions(id, e.identifier)));
190
if (extensionsToInstall.length) {
191
const metadata = await Promise.all(extensionsToInstall.map(e => this.extensionsScanner.scanMetadata(e, fromProfileLocation)));
192
await this.addExtensionsToProfile(extensionsToInstall.map((e, index) => [e, metadata[index]]), toProfileLocation);
193
this.logService.info('Successfully installed extensions', extensionsToInstall.map(e => e.identifier.id), toProfileLocation.toString());
194
}
195
return extensionsToInstall;
196
}
197
198
async updateMetadata(local: ILocalExtension, metadata: Partial<Metadata>, profileLocation: URI): Promise<ILocalExtension> {
199
this.logService.trace('ExtensionManagementService#updateMetadata', local.identifier.id);
200
if (metadata.isPreReleaseVersion) {
201
metadata.preRelease = true;
202
metadata.hasPreReleaseVersion = true;
203
}
204
// unset if false
205
if (metadata.isMachineScoped === false) {
206
metadata.isMachineScoped = undefined;
207
}
208
if (metadata.isBuiltin === false) {
209
metadata.isBuiltin = undefined;
210
}
211
if (metadata.pinned === false) {
212
metadata.pinned = undefined;
213
}
214
local = await this.extensionsScanner.updateMetadata(local, metadata, profileLocation);
215
this.manifestCache.invalidate(profileLocation);
216
this._onDidUpdateExtensionMetadata.fire({ local, profileLocation });
217
return local;
218
}
219
220
protected deleteExtension(extension: ILocalExtension): Promise<void> {
221
return this.extensionsScanner.deleteExtension(extension, 'remove');
222
}
223
224
protected copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial<Metadata>): Promise<ILocalExtension> {
225
return this.extensionsScanner.copyExtension(extension, fromProfileLocation, toProfileLocation, metadata);
226
}
227
228
protected moveExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial<Metadata>): Promise<ILocalExtension> {
229
return this.extensionsScanner.moveExtension(extension, fromProfileLocation, toProfileLocation, metadata);
230
}
231
232
protected removeExtension(extension: ILocalExtension, fromProfileLocation: URI): Promise<void> {
233
return this.extensionsScanner.removeExtension(extension.identifier, fromProfileLocation);
234
}
235
236
copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise<void> {
237
return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation, { version: this.productService.version, date: this.productService.date });
238
}
239
240
deleteExtensions(...extensions: IExtension[]): Promise<void> {
241
return this.extensionsScanner.setExtensionsForRemoval(...extensions);
242
}
243
244
async cleanUp(): Promise<void> {
245
this.logService.trace('ExtensionManagementService#cleanUp');
246
try {
247
await this.extensionsScanner.cleanUp();
248
} catch (error) {
249
this.logService.error(error);
250
}
251
}
252
253
async download(extension: IGalleryExtension, operation: InstallOperation, donotVerifySignature: boolean): Promise<URI> {
254
const { location } = await this.downloadExtension(extension, operation, !donotVerifySignature);
255
return location;
256
}
257
258
private async downloadVsix(vsix: URI): Promise<{ location: URI; cleanup: () => Promise<void> }> {
259
if (vsix.scheme === Schemas.file) {
260
return { location: vsix, async cleanup() { } };
261
}
262
this.logService.trace('Downloading extension from', vsix.toString());
263
const location = joinPath(this.extensionsDownloader.extensionsDownloadDir, generateUuid());
264
await this.downloadService.download(vsix, location);
265
this.logService.info('Downloaded extension to', location.toString());
266
const cleanup = async () => {
267
try {
268
await this.fileService.del(location);
269
} catch (error) {
270
this.logService.error(error);
271
}
272
};
273
return { location, cleanup };
274
}
275
276
protected getCurrentExtensionsManifestLocation(): URI {
277
return this.userDataProfilesService.defaultProfile.extensionsResource;
278
}
279
280
protected createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask {
281
const extensionKey = extension instanceof URI ? new ExtensionKey({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, manifest.version) : ExtensionKey.create(extension);
282
return this.instantiationService.createInstance(InstallExtensionInProfileTask, extensionKey, manifest, extension, options, (operation, token) => {
283
if (extension instanceof URI) {
284
return this.extractVSIX(extensionKey, extension, options, token);
285
}
286
let promise = this.extractingGalleryExtensions.get(extensionKey.toString());
287
if (!promise) {
288
this.extractingGalleryExtensions.set(extensionKey.toString(), promise = this.downloadAndExtractGalleryExtension(extensionKey, extension, operation, options, token));
289
promise.finally(() => this.extractingGalleryExtensions.delete(extensionKey.toString()));
290
}
291
return promise;
292
}, this.extensionsScanner);
293
}
294
295
protected createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask {
296
return new UninstallExtensionInProfileTask(extension, options, this.extensionsProfileScannerService);
297
}
298
299
private async downloadAndExtractGalleryExtension(extensionKey: ExtensionKey, gallery: IGalleryExtension, operation: InstallOperation, options: InstallExtensionTaskOptions, token: CancellationToken): Promise<ExtractExtensionResult> {
300
const { verificationStatus, location } = await this.downloadExtension(gallery, operation, !options.donotVerifySignature, options.context?.[EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT]);
301
try {
302
303
if (token.isCancellationRequested) {
304
throw new CancellationError();
305
}
306
307
// validate manifest
308
const manifest = await getManifest(location.fsPath);
309
if (!new ExtensionKey(gallery.identifier, gallery.version).equals(new ExtensionKey({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, manifest.version))) {
310
throw new ExtensionManagementError(nls.localize('invalidManifest', "Cannot install '{0}' extension because of manifest mismatch with Marketplace", gallery.identifier.id), ExtensionManagementErrorCode.Invalid);
311
}
312
313
const local = await this.extensionsScanner.extractUserExtension(
314
extensionKey,
315
location.fsPath,
316
false,
317
token);
318
319
if (verificationStatus !== ExtensionSignatureVerificationCode.Success && this.environmentService.isBuilt) {
320
try {
321
await this.extensionsDownloader.delete(location);
322
} catch (e) {
323
/* Ignore */
324
this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(e));
325
}
326
}
327
328
return { local, verificationStatus };
329
} catch (error) {
330
try {
331
await this.extensionsDownloader.delete(location);
332
} catch (e) {
333
/* Ignore */
334
this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(e));
335
}
336
throw toExtensionManagementError(error);
337
}
338
}
339
340
private async downloadExtension(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean, clientTargetPlatform?: TargetPlatform): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionSignatureVerificationCode | undefined }> {
341
if (verifySignature) {
342
const value = this.configurationService.getValue(VerifyExtensionSignatureConfigKey);
343
verifySignature = isBoolean(value) ? value : true;
344
}
345
const { location, verificationStatus } = await this.extensionsDownloader.download(extension, operation, verifySignature, clientTargetPlatform);
346
const shouldRequireSignature = shouldRequireRepositorySignatureFor(extension.private, await this.extensionGalleryManifestService.getExtensionGalleryManifest());
347
348
if (
349
verificationStatus !== ExtensionSignatureVerificationCode.Success
350
&& !(verificationStatus === ExtensionSignatureVerificationCode.NotSigned && !shouldRequireSignature)
351
&& verifySignature
352
&& this.environmentService.isBuilt
353
&& (await this.getTargetPlatform()) !== TargetPlatform.LINUX_ARMHF
354
) {
355
try {
356
await this.extensionsDownloader.delete(location);
357
} catch (e) {
358
/* Ignore */
359
this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(e));
360
}
361
362
if (!verificationStatus) {
363
throw new ExtensionManagementError(nls.localize('signature verification not executed', "Signature verification was not executed."), ExtensionManagementErrorCode.SignatureVerificationInternal);
364
}
365
366
switch (verificationStatus) {
367
case ExtensionSignatureVerificationCode.PackageIntegrityCheckFailed:
368
case ExtensionSignatureVerificationCode.SignatureIsInvalid:
369
case ExtensionSignatureVerificationCode.SignatureManifestIsInvalid:
370
case ExtensionSignatureVerificationCode.SignatureIntegrityCheckFailed:
371
case ExtensionSignatureVerificationCode.EntryIsMissing:
372
case ExtensionSignatureVerificationCode.EntryIsTampered:
373
case ExtensionSignatureVerificationCode.Untrusted:
374
case ExtensionSignatureVerificationCode.CertificateRevoked:
375
case ExtensionSignatureVerificationCode.SignatureIsNotValid:
376
case ExtensionSignatureVerificationCode.SignatureArchiveHasTooManyEntries:
377
case ExtensionSignatureVerificationCode.NotSigned:
378
throw new ExtensionManagementError(nls.localize('signature verification failed', "Signature verification failed with '{0}' error.", verificationStatus), ExtensionManagementErrorCode.SignatureVerificationFailed);
379
}
380
381
throw new ExtensionManagementError(nls.localize('signature verification failed', "Signature verification failed with '{0}' error.", verificationStatus), ExtensionManagementErrorCode.SignatureVerificationInternal);
382
}
383
384
return { location, verificationStatus };
385
}
386
387
private async extractVSIX(extensionKey: ExtensionKey, location: URI, options: InstallExtensionTaskOptions, token: CancellationToken): Promise<ExtractExtensionResult> {
388
const local = await this.extensionsScanner.extractUserExtension(
389
extensionKey,
390
path.resolve(location.fsPath),
391
isBoolean(options.keepExisting) ? !options.keepExisting : true,
392
token);
393
return { local };
394
}
395
396
private async collectFiles(extension: ILocalExtension): Promise<IFile[]> {
397
398
const collectFilesFromDirectory = async (dir: string): Promise<string[]> => {
399
let entries = await pfs.Promises.readdir(dir);
400
entries = entries.map(e => path.join(dir, e));
401
const stats = await Promise.all(entries.map(e => fs.promises.stat(e)));
402
let promise: Promise<string[]> = Promise.resolve([]);
403
stats.forEach((stat, index) => {
404
const entry = entries[index];
405
if (stat.isFile()) {
406
promise = promise.then(result => ([...result, entry]));
407
}
408
if (stat.isDirectory()) {
409
promise = promise
410
.then(result => collectFilesFromDirectory(entry)
411
.then(files => ([...result, ...files])));
412
}
413
});
414
return promise;
415
};
416
417
const files = await collectFilesFromDirectory(extension.location.fsPath);
418
return files.map(f => ({ path: `extension/${path.relative(extension.location.fsPath, f)}`, localPath: f }));
419
}
420
421
private async onDidChangeExtensionsFromAnotherSource({ added, removed }: DidChangeProfileExtensionsEvent): Promise<void> {
422
if (removed) {
423
const removedExtensions = added && this.uriIdentityService.extUri.isEqual(removed.profileLocation, added.profileLocation)
424
? removed.extensions.filter(e => added.extensions.every(identifier => !areSameExtensions(identifier, e)))
425
: removed.extensions;
426
for (const identifier of removedExtensions) {
427
this.logService.info('Extensions removed from another source', identifier.id, removed.profileLocation.toString());
428
this._onDidUninstallExtension.fire({ identifier, profileLocation: removed.profileLocation });
429
}
430
}
431
if (added) {
432
const extensions = await this.getInstalled(ExtensionType.User, added.profileLocation);
433
const addedExtensions = extensions.filter(e => added.extensions.some(identifier => areSameExtensions(identifier, e.identifier)));
434
this._onDidInstallExtensions.fire(addedExtensions.map(local => {
435
this.logService.info('Extensions added from another source', local.identifier.id, added.profileLocation.toString());
436
return { identifier: local.identifier, local, profileLocation: added.profileLocation, operation: InstallOperation.None };
437
}));
438
}
439
}
440
441
private readonly knownDirectories = new ResourceSet();
442
private async watchForExtensionsNotInstalledBySystem(): Promise<void> {
443
this._register(this.extensionsScanner.onExtract(resource => this.knownDirectories.add(resource)));
444
const stat = await this.fileService.resolve(this.extensionsScannerService.userExtensionsLocation);
445
for (const childStat of stat.children ?? []) {
446
if (childStat.isDirectory) {
447
this.knownDirectories.add(childStat.resource);
448
}
449
}
450
this._register(this.fileService.watch(this.extensionsScannerService.userExtensionsLocation));
451
this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e)));
452
}
453
454
private async onDidFilesChange(e: FileChangesEvent): Promise<void> {
455
if (!e.affects(this.extensionsScannerService.userExtensionsLocation, FileChangeType.ADDED)) {
456
return;
457
}
458
459
const added: ILocalExtension[] = [];
460
for (const resource of e.rawAdded) {
461
// Check if this is a known directory
462
if (this.knownDirectories.has(resource)) {
463
continue;
464
}
465
466
// Is not immediate child of extensions resource
467
if (!this.uriIdentityService.extUri.isEqual(this.uriIdentityService.extUri.dirname(resource), this.extensionsScannerService.userExtensionsLocation)) {
468
continue;
469
}
470
471
// .obsolete file changed
472
if (this.uriIdentityService.extUri.isEqual(resource, this.uriIdentityService.extUri.joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete'))) {
473
continue;
474
}
475
476
// Ignore changes to files starting with `.`
477
if (this.uriIdentityService.extUri.basename(resource).startsWith('.')) {
478
continue;
479
}
480
481
// Ignore changes to the deleted folder
482
if (this.uriIdentityService.extUri.basename(resource).endsWith(DELETED_FOLDER_POSTFIX)) {
483
continue;
484
}
485
486
try {
487
// Check if this is a directory
488
if (!(await this.fileService.stat(resource)).isDirectory) {
489
continue;
490
}
491
} catch (error) {
492
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
493
this.logService.error(error);
494
}
495
continue;
496
}
497
498
// Check if this is an extension added by another source
499
// Extension added by another source will not have installed timestamp
500
const extension = await this.extensionsScanner.scanUserExtensionAtLocation(resource);
501
if (extension && extension.installedTimestamp === undefined) {
502
this.knownDirectories.add(resource);
503
added.push(extension);
504
}
505
}
506
507
if (added.length) {
508
await this.addExtensionsToProfile(added.map(e => [e, undefined]), this.userDataProfilesService.defaultProfile.extensionsResource);
509
this.logService.info('Added extensions to default profile from external source', added.map(e => e.identifier.id));
510
}
511
}
512
513
private async addExtensionsToProfile(extensions: [ILocalExtension, Metadata | undefined][], profileLocation: URI): Promise<void> {
514
const localExtensions = extensions.map(e => e[0]);
515
await this.extensionsScanner.unsetExtensionsForRemoval(...localExtensions.map(extension => ExtensionKey.create(extension)));
516
await this.extensionsProfileScannerService.addExtensionsToProfile(extensions, profileLocation);
517
this._onDidInstallExtensions.fire(localExtensions.map(local => ({ local, identifier: local.identifier, operation: InstallOperation.None, profileLocation })));
518
}
519
}
520
521
type UpdateMetadataErrorClassification = {
522
owner: 'sandy081';
523
comment: 'Update metadata error';
524
extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension identifier' };
525
code?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'error code' };
526
isProfile?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Is writing into profile' };
527
};
528
type UpdateMetadataErrorEvent = {
529
extensionId: string;
530
code?: string;
531
isProfile?: boolean;
532
};
533
534
export class ExtensionsScanner extends Disposable {
535
536
private readonly obsoletedResource: URI;
537
private readonly obsoleteFileLimiter: Queue<any>;
538
539
private readonly _onExtract = this._register(new Emitter<URI>());
540
readonly onExtract = this._onExtract.event;
541
542
private scanAllExtensionPromise = new ResourceMap<Promise<IScannedExtension[]>>();
543
private scanUserExtensionsPromise = new ResourceMap<Promise<IScannedExtension[]>>();
544
545
constructor(
546
private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise<void>,
547
@IFileService private readonly fileService: IFileService,
548
@IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService,
549
@IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService,
550
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
551
@ITelemetryService private readonly telemetryService: ITelemetryService,
552
@ILogService private readonly logService: ILogService,
553
) {
554
super();
555
this.obsoletedResource = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete');
556
this.obsoleteFileLimiter = new Queue();
557
}
558
559
async cleanUp(): Promise<void> {
560
await this.removeTemporarilyDeletedFolders();
561
await this.deleteExtensionsMarkedForRemoval();
562
//TODO: Remove this initiialization after coupe of releases
563
await this.initializeExtensionSize();
564
}
565
566
async scanExtensions(type: ExtensionType | null, profileLocation: URI, productVersion: IProductVersion, language?: string): Promise<ILocalExtension[]> {
567
try {
568
const cacheKey: URI = profileLocation.with({ query: language });
569
const userScanOptions: UserExtensionsScanOptions = { includeInvalid: true, profileLocation, productVersion, language };
570
let scannedExtensions: IScannedExtension[] = [];
571
if (type === null || type === ExtensionType.System) {
572
let scanAllExtensionsPromise = this.scanAllExtensionPromise.get(cacheKey);
573
if (!scanAllExtensionsPromise) {
574
scanAllExtensionsPromise = this.extensionsScannerService.scanAllExtensions({ language }, userScanOptions)
575
.finally(() => this.scanAllExtensionPromise.delete(cacheKey));
576
this.scanAllExtensionPromise.set(cacheKey, scanAllExtensionsPromise);
577
}
578
scannedExtensions.push(...await scanAllExtensionsPromise);
579
} else if (type === ExtensionType.User) {
580
let scanUserExtensionsPromise = this.scanUserExtensionsPromise.get(cacheKey);
581
if (!scanUserExtensionsPromise) {
582
scanUserExtensionsPromise = this.extensionsScannerService.scanUserExtensions(userScanOptions)
583
.finally(() => this.scanUserExtensionsPromise.delete(cacheKey));
584
this.scanUserExtensionsPromise.set(cacheKey, scanUserExtensionsPromise);
585
}
586
scannedExtensions.push(...await scanUserExtensionsPromise);
587
}
588
scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions;
589
return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension)));
590
} catch (error) {
591
throw toExtensionManagementError(error, ExtensionManagementErrorCode.Scanning);
592
}
593
}
594
595
async scanAllUserExtensions(): Promise<ILocalExtension[]> {
596
try {
597
const scannedExtensions = await this.extensionsScannerService.scanAllUserExtensions();
598
return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension)));
599
} catch (error) {
600
throw toExtensionManagementError(error, ExtensionManagementErrorCode.Scanning);
601
}
602
}
603
604
async scanUserExtensionAtLocation(location: URI): Promise<ILocalExtension | null> {
605
try {
606
const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, ExtensionType.User, { includeInvalid: true });
607
if (scannedExtension) {
608
return await this.toLocalExtension(scannedExtension);
609
}
610
} catch (error) {
611
this.logService.error(error);
612
}
613
return null;
614
}
615
616
async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, removeIfExists: boolean, token: CancellationToken): Promise<ILocalExtension> {
617
const folderName = extensionKey.toString();
618
const tempLocation = URI.file(path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, `.${generateUuid()}`));
619
const extensionLocation = URI.file(path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, folderName));
620
621
if (await this.fileService.exists(extensionLocation)) {
622
if (!removeIfExists) {
623
try {
624
return await this.scanLocalExtension(extensionLocation, ExtensionType.User);
625
} catch (error) {
626
this.logService.warn(`Error while scanning the existing extension at ${extensionLocation.path}. Deleting the existing extension and extracting it.`, getErrorMessage(error));
627
}
628
}
629
630
try {
631
await this.deleteExtensionFromLocation(extensionKey.id, extensionLocation, 'removeExisting');
632
} catch (error) {
633
throw new ExtensionManagementError(nls.localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionLocation.fsPath, extensionKey.id), ExtensionManagementErrorCode.Delete);
634
}
635
}
636
637
try {
638
if (token.isCancellationRequested) {
639
throw new CancellationError();
640
}
641
642
// Extract
643
try {
644
this.logService.trace(`Started extracting the extension from ${zipPath} to ${extensionLocation.fsPath}`);
645
await extract(zipPath, tempLocation.fsPath, { sourcePath: 'extension', overwrite: true }, token);
646
this.logService.info(`Extracted extension to ${extensionLocation}:`, extensionKey.id);
647
} catch (e) {
648
throw fromExtractError(e);
649
}
650
651
const metadata: ManifestMetadata = { installedTimestamp: Date.now(), targetPlatform: extensionKey.targetPlatform };
652
try {
653
metadata.size = await computeSize(tempLocation, this.fileService);
654
} catch (error) {
655
// Log & ignore
656
this.logService.warn(`Error while getting the size of the extracted extension : ${tempLocation.fsPath}`, getErrorMessage(error));
657
}
658
659
try {
660
await this.extensionsScannerService.updateManifestMetadata(tempLocation, metadata);
661
} catch (error) {
662
this.telemetryService.publicLog2<UpdateMetadataErrorEvent, UpdateMetadataErrorClassification>('extension:extract', { extensionId: extensionKey.id, code: `${toFileOperationResult(error)}` });
663
throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata);
664
}
665
666
if (token.isCancellationRequested) {
667
throw new CancellationError();
668
}
669
670
// Rename
671
try {
672
this.logService.trace(`Started renaming the extension from ${tempLocation.fsPath} to ${extensionLocation.fsPath}`);
673
await this.rename(tempLocation.fsPath, extensionLocation.fsPath);
674
this.logService.info('Renamed to', extensionLocation.fsPath);
675
} catch (error) {
676
if (error.code === 'ENOTEMPTY') {
677
this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, extensionKey.id);
678
try { await this.fileService.del(tempLocation, { recursive: true }); } catch (e) { /* ignore */ }
679
} else {
680
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempLocation);
681
throw error;
682
}
683
}
684
685
this._onExtract.fire(extensionLocation);
686
687
} catch (error) {
688
try { await this.fileService.del(tempLocation, { recursive: true }); } catch (e) { /* ignore */ }
689
throw error;
690
}
691
692
return this.scanLocalExtension(extensionLocation, ExtensionType.User);
693
}
694
695
async scanMetadata(local: ILocalExtension, profileLocation: URI): Promise<Metadata | undefined> {
696
const extension = await this.getScannedExtension(local, profileLocation);
697
return extension?.metadata;
698
}
699
700
private async getScannedExtension(local: ILocalExtension, profileLocation: URI): Promise<IScannedProfileExtension | undefined> {
701
const extensions = await this.extensionsProfileScannerService.scanProfileExtensions(profileLocation);
702
return extensions.find(e => areSameExtensions(e.identifier, local.identifier));
703
}
704
705
async updateMetadata(local: ILocalExtension, metadata: Partial<Metadata>, profileLocation: URI): Promise<ILocalExtension> {
706
try {
707
await this.extensionsProfileScannerService.updateMetadata([[local, metadata]], profileLocation);
708
} catch (error) {
709
this.telemetryService.publicLog2<UpdateMetadataErrorEvent, UpdateMetadataErrorClassification>('extension:extract', { extensionId: local.identifier.id, code: `${toFileOperationResult(error)}`, isProfile: !!profileLocation });
710
throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata);
711
}
712
return this.scanLocalExtension(local.location, local.type, profileLocation);
713
}
714
715
async setExtensionsForRemoval(...extensions: IExtension[]): Promise<void> {
716
const extensionsToRemove = [];
717
for (const extension of extensions) {
718
if (await this.fileService.exists(extension.location)) {
719
extensionsToRemove.push(extension);
720
}
721
}
722
const extensionKeys: ExtensionKey[] = extensionsToRemove.map(e => ExtensionKey.create(e));
723
await this.withRemovedExtensions(removedExtensions =>
724
extensionKeys.forEach(extensionKey => {
725
removedExtensions[extensionKey.toString()] = true;
726
this.logService.info('Marked extension as removed', extensionKey.toString());
727
}));
728
}
729
730
async unsetExtensionsForRemoval(...extensionKeys: ExtensionKey[]): Promise<boolean[]> {
731
try {
732
const results: boolean[] = [];
733
await this.withRemovedExtensions(removedExtensions =>
734
extensionKeys.forEach(extensionKey => {
735
if (removedExtensions[extensionKey.toString()]) {
736
results.push(true);
737
delete removedExtensions[extensionKey.toString()];
738
} else {
739
results.push(false);
740
}
741
}));
742
return results;
743
} catch (error) {
744
throw toExtensionManagementError(error, ExtensionManagementErrorCode.UnsetRemoved);
745
}
746
}
747
748
async deleteExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise<void> {
749
if (this.uriIdentityService.extUri.isEqualOrParent(extension.location, this.extensionsScannerService.userExtensionsLocation)) {
750
await this.deleteExtensionFromLocation(extension.identifier.id, extension.location, type);
751
await this.unsetExtensionsForRemoval(ExtensionKey.create(extension));
752
}
753
}
754
755
async copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial<Metadata>): Promise<ILocalExtension> {
756
const source = await this.getScannedExtension(extension, fromProfileLocation);
757
const target = await this.getScannedExtension(extension, toProfileLocation);
758
metadata = { ...source?.metadata, ...metadata };
759
760
if (target) {
761
if (this.uriIdentityService.extUri.isEqual(target.location, extension.location)) {
762
await this.extensionsProfileScannerService.updateMetadata([[extension, { ...target.metadata, ...metadata }]], toProfileLocation);
763
} else {
764
const targetExtension = await this.scanLocalExtension(target.location, extension.type, toProfileLocation);
765
await this.extensionsProfileScannerService.removeExtensionsFromProfile([targetExtension.identifier], toProfileLocation);
766
await this.extensionsProfileScannerService.addExtensionsToProfile([[extension, { ...target.metadata, ...metadata }]], toProfileLocation);
767
}
768
} else {
769
await this.extensionsProfileScannerService.addExtensionsToProfile([[extension, metadata]], toProfileLocation);
770
}
771
772
return this.scanLocalExtension(extension.location, extension.type, toProfileLocation);
773
}
774
775
async moveExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial<Metadata>): Promise<ILocalExtension> {
776
const source = await this.getScannedExtension(extension, fromProfileLocation);
777
const target = await this.getScannedExtension(extension, toProfileLocation);
778
metadata = { ...source?.metadata, ...metadata };
779
780
if (target) {
781
if (this.uriIdentityService.extUri.isEqual(target.location, extension.location)) {
782
await this.extensionsProfileScannerService.updateMetadata([[extension, { ...target.metadata, ...metadata }]], toProfileLocation);
783
} else {
784
const targetExtension = await this.scanLocalExtension(target.location, extension.type, toProfileLocation);
785
await this.removeExtension(targetExtension.identifier, toProfileLocation);
786
await this.extensionsProfileScannerService.addExtensionsToProfile([[extension, { ...target.metadata, ...metadata }]], toProfileLocation);
787
}
788
} else {
789
await this.extensionsProfileScannerService.addExtensionsToProfile([[extension, metadata]], toProfileLocation);
790
if (source) {
791
await this.removeExtension(source.identifier, fromProfileLocation);
792
}
793
}
794
795
return this.scanLocalExtension(extension.location, extension.type, toProfileLocation);
796
}
797
798
async removeExtension(identifier: IExtensionIdentifier, fromProfileLocation: URI): Promise<void> {
799
await this.extensionsProfileScannerService.removeExtensionsFromProfile([identifier], fromProfileLocation);
800
}
801
802
async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI, productVersion: IProductVersion): Promise<void> {
803
const fromExtensions = await this.scanExtensions(ExtensionType.User, fromProfileLocation, productVersion);
804
const extensions: [ILocalExtension, Metadata | undefined][] = await Promise.all(fromExtensions
805
.filter(e => !e.isApplicationScoped) /* remove application scoped extensions */
806
.map(async e => ([e, await this.scanMetadata(e, fromProfileLocation)])));
807
await this.extensionsProfileScannerService.addExtensionsToProfile(extensions, toProfileLocation);
808
}
809
810
private async deleteExtensionFromLocation(id: string, location: URI, type: string): Promise<void> {
811
this.logService.trace(`Deleting ${type} extension from disk`, id, location.fsPath);
812
const renamedLocation = this.uriIdentityService.extUri.joinPath(this.uriIdentityService.extUri.dirname(location), `${this.uriIdentityService.extUri.basename(location)}.${hash(generateUuid()).toString(16)}${DELETED_FOLDER_POSTFIX}`);
813
await this.rename(location.fsPath, renamedLocation.fsPath);
814
await this.fileService.del(renamedLocation, { recursive: true });
815
this.logService.info(`Deleted ${type} extension from disk`, id, location.fsPath);
816
}
817
818
private withRemovedExtensions(updateFn?: (removed: IStringDictionary<boolean>) => void): Promise<IStringDictionary<boolean>> {
819
return this.obsoleteFileLimiter.queue(async () => {
820
let raw: string | undefined;
821
try {
822
const content = await this.fileService.readFile(this.obsoletedResource, 'utf8');
823
raw = content.value.toString();
824
} catch (error) {
825
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
826
throw error;
827
}
828
}
829
830
let removed = {};
831
if (raw) {
832
try {
833
removed = JSON.parse(raw);
834
} catch (e) { /* ignore */ }
835
}
836
837
if (updateFn) {
838
updateFn(removed);
839
if (Object.keys(removed).length) {
840
await this.fileService.writeFile(this.obsoletedResource, VSBuffer.fromString(JSON.stringify(removed)));
841
} else {
842
try {
843
await this.fileService.del(this.obsoletedResource);
844
} catch (error) {
845
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
846
throw error;
847
}
848
}
849
}
850
}
851
852
return removed;
853
});
854
}
855
856
private async rename(extractPath: string, renamePath: string): Promise<void> {
857
try {
858
await pfs.Promises.rename(extractPath, renamePath, 2 * 60 * 1000 /* Retry for 2 minutes */);
859
} catch (error) {
860
throw toExtensionManagementError(error, ExtensionManagementErrorCode.Rename);
861
}
862
}
863
864
async scanLocalExtension(location: URI, type: ExtensionType, profileLocation?: URI): Promise<ILocalExtension> {
865
try {
866
if (profileLocation) {
867
const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation });
868
const scannedExtension = scannedExtensions.find(e => this.uriIdentityService.extUri.isEqual(e.location, location));
869
if (scannedExtension) {
870
return await this.toLocalExtension(scannedExtension);
871
}
872
} else {
873
const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true });
874
if (scannedExtension) {
875
return await this.toLocalExtension(scannedExtension);
876
}
877
}
878
throw new ExtensionManagementError(nls.localize('cannot read', "Cannot read the extension from {0}", location.path), ExtensionManagementErrorCode.ScanningExtension);
879
} catch (error) {
880
throw toExtensionManagementError(error, ExtensionManagementErrorCode.ScanningExtension);
881
}
882
}
883
884
private async toLocalExtension(extension: IScannedExtension): Promise<ILocalExtension> {
885
let stat: IFileStat | undefined;
886
try {
887
stat = await this.fileService.resolve(extension.location);
888
} catch (error) {/* ignore */ }
889
890
let readmeUrl: URI | undefined;
891
let changelogUrl: URI | undefined;
892
if (stat?.children) {
893
readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource;
894
changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource;
895
}
896
return {
897
identifier: extension.identifier,
898
type: extension.type,
899
isBuiltin: extension.isBuiltin || !!extension.metadata?.isBuiltin,
900
location: extension.location,
901
manifest: extension.manifest,
902
targetPlatform: extension.targetPlatform,
903
validations: extension.validations,
904
isValid: extension.isValid,
905
readmeUrl,
906
changelogUrl,
907
publisherDisplayName: extension.metadata?.publisherDisplayName,
908
publisherId: extension.metadata?.publisherId || null,
909
isApplicationScoped: !!extension.metadata?.isApplicationScoped,
910
isMachineScoped: !!extension.metadata?.isMachineScoped,
911
isPreReleaseVersion: !!extension.metadata?.isPreReleaseVersion,
912
hasPreReleaseVersion: !!extension.metadata?.hasPreReleaseVersion,
913
preRelease: extension.preRelease,
914
installedTimestamp: extension.metadata?.installedTimestamp,
915
updated: !!extension.metadata?.updated,
916
pinned: !!extension.metadata?.pinned,
917
private: !!extension.metadata?.private,
918
isWorkspaceScoped: false,
919
source: extension.metadata?.source ?? (extension.identifier.uuid ? 'gallery' : 'vsix'),
920
size: extension.metadata?.size ?? 0,
921
};
922
}
923
924
private async initializeExtensionSize(): Promise<void> {
925
const extensions = await this.extensionsScannerService.scanAllUserExtensions();
926
await Promise.all(extensions.map(async extension => {
927
// set size if not set before
928
if (isDefined(extension.metadata?.installedTimestamp) && isUndefined(extension.metadata?.size)) {
929
const size = await computeSize(extension.location, this.fileService);
930
await this.extensionsScannerService.updateManifestMetadata(extension.location, { size });
931
}
932
}));
933
}
934
935
private async deleteExtensionsMarkedForRemoval(): Promise<void> {
936
let removed: IStringDictionary<boolean>;
937
try {
938
removed = await this.withRemovedExtensions();
939
} catch (error) {
940
throw toExtensionManagementError(error, ExtensionManagementErrorCode.ReadRemoved);
941
}
942
943
if (Object.keys(removed).length === 0) {
944
this.logService.debug(`No extensions are marked as removed.`);
945
return;
946
}
947
948
this.logService.debug(`Deleting extensions marked as removed:`, Object.keys(removed));
949
950
const extensions = await this.scanAllUserExtensions();
951
const installed: Set<string> = new Set<string>();
952
for (const e of extensions) {
953
if (!removed[ExtensionKey.create(e).toString()]) {
954
installed.add(e.identifier.id.toLowerCase());
955
}
956
}
957
958
try {
959
// running post uninstall tasks for extensions that are not installed anymore
960
const byExtension = groupByExtension(extensions, e => e.identifier);
961
await Promises.settled(byExtension.map(async e => {
962
const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0];
963
if (!installed.has(latest.identifier.id.toLowerCase())) {
964
await this.beforeRemovingExtension(latest);
965
}
966
}));
967
} catch (error) {
968
this.logService.error(error);
969
}
970
971
const toRemove = extensions.filter(e => e.installedTimestamp /* Installed by System */ && removed[ExtensionKey.create(e).toString()]);
972
await Promise.allSettled(toRemove.map(e => this.deleteExtension(e, 'marked for removal')));
973
}
974
975
private async removeTemporarilyDeletedFolders(): Promise<void> {
976
this.logService.trace('ExtensionManagementService#removeTempDeleteFolders');
977
978
let stat;
979
try {
980
stat = await this.fileService.resolve(this.extensionsScannerService.userExtensionsLocation);
981
} catch (error) {
982
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
983
this.logService.error(error);
984
}
985
return;
986
}
987
988
if (!stat?.children) {
989
return;
990
}
991
992
try {
993
await Promise.allSettled(stat.children.map(async child => {
994
if (!child.isDirectory || !child.name.endsWith(DELETED_FOLDER_POSTFIX)) {
995
return;
996
}
997
this.logService.trace('Deleting the temporarily deleted folder', child.resource.toString());
998
try {
999
await this.fileService.del(child.resource, { recursive: true });
1000
this.logService.trace('Deleted the temporarily deleted folder', child.resource.toString());
1001
} catch (error) {
1002
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
1003
this.logService.error(error);
1004
}
1005
}
1006
}));
1007
} catch (error) { /* ignore */ }
1008
}
1009
1010
}
1011
1012
class InstallExtensionInProfileTask extends AbstractExtensionTask<ILocalExtension> implements IInstallExtensionTask {
1013
1014
private _operation = InstallOperation.Install;
1015
get operation() { return this.options.operation ?? this._operation; }
1016
1017
private _verificationStatus: ExtensionSignatureVerificationCode | undefined;
1018
get verificationStatus() { return this._verificationStatus; }
1019
1020
readonly identifier: IExtensionIdentifier;
1021
1022
constructor(
1023
private readonly extensionKey: ExtensionKey,
1024
readonly manifest: IExtensionManifest,
1025
readonly source: IGalleryExtension | URI,
1026
readonly options: InstallExtensionTaskOptions,
1027
private readonly extractExtensionFn: (operation: InstallOperation, token: CancellationToken) => Promise<ExtractExtensionResult>,
1028
private readonly extensionsScanner: ExtensionsScanner,
1029
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
1030
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
1031
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
1032
@IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService,
1033
@IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService,
1034
@ILogService private readonly logService: ILogService,
1035
) {
1036
super();
1037
this.identifier = this.extensionKey.identifier;
1038
}
1039
1040
protected async doRun(token: CancellationToken): Promise<ILocalExtension> {
1041
const installed = await this.extensionsScanner.scanExtensions(ExtensionType.User, this.options.profileLocation, this.options.productVersion);
1042
const existingExtension = installed.find(i => areSameExtensions(i.identifier, this.identifier));
1043
if (existingExtension) {
1044
this._operation = InstallOperation.Update;
1045
}
1046
1047
const metadata: Metadata = {
1048
isApplicationScoped: this.options.isApplicationScoped || existingExtension?.isApplicationScoped,
1049
isMachineScoped: this.options.isMachineScoped || existingExtension?.isMachineScoped,
1050
isBuiltin: this.options.isBuiltin || existingExtension?.isBuiltin,
1051
isSystem: existingExtension?.type === ExtensionType.System ? true : undefined,
1052
installedTimestamp: Date.now(),
1053
pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existingExtension?.pinned),
1054
source: this.source instanceof URI ? 'vsix' : 'gallery',
1055
};
1056
1057
let local: ILocalExtension | undefined;
1058
1059
// VSIX
1060
if (this.source instanceof URI) {
1061
if (existingExtension) {
1062
if (this.extensionKey.equals(new ExtensionKey(existingExtension.identifier, existingExtension.manifest.version))) {
1063
try {
1064
await this.extensionsScanner.deleteExtension(existingExtension, 'existing');
1065
} catch (e) {
1066
throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name));
1067
}
1068
}
1069
}
1070
// Remove the extension with same version if it is already uninstalled.
1071
// Installing a VSIX extension shall replace the existing extension always.
1072
const existingWithSameVersion = await this.unsetIfRemoved(this.extensionKey);
1073
if (existingWithSameVersion) {
1074
try {
1075
await this.extensionsScanner.deleteExtension(existingWithSameVersion, 'existing');
1076
} catch (e) {
1077
throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name));
1078
}
1079
}
1080
1081
}
1082
1083
// Gallery
1084
else {
1085
metadata.id = this.source.identifier.uuid;
1086
metadata.publisherId = this.source.publisherId;
1087
metadata.publisherDisplayName = this.source.publisherDisplayName;
1088
metadata.targetPlatform = this.source.properties.targetPlatform;
1089
metadata.updated = !!existingExtension;
1090
metadata.private = this.source.private;
1091
metadata.isPreReleaseVersion = this.source.properties.isPreReleaseVersion;
1092
metadata.hasPreReleaseVersion = existingExtension?.hasPreReleaseVersion || this.source.properties.isPreReleaseVersion;
1093
metadata.preRelease = isBoolean(this.options.preRelease)
1094
? this.options.preRelease
1095
: this.options.installPreReleaseVersion || this.source.properties.isPreReleaseVersion || existingExtension?.preRelease;
1096
1097
if (existingExtension && existingExtension.type !== ExtensionType.System && existingExtension.manifest.version === this.source.version) {
1098
return this.extensionsScanner.updateMetadata(existingExtension, metadata, this.options.profileLocation);
1099
}
1100
1101
// Unset if the extension is uninstalled and return the unset extension.
1102
local = await this.unsetIfRemoved(this.extensionKey);
1103
}
1104
1105
if (token.isCancellationRequested) {
1106
throw toExtensionManagementError(new CancellationError());
1107
}
1108
1109
if (!local) {
1110
const result = await this.extractExtensionFn(this.operation, token);
1111
local = result.local;
1112
this._verificationStatus = result.verificationStatus;
1113
}
1114
1115
if (this.uriIdentityService.extUri.isEqual(this.userDataProfilesService.defaultProfile.extensionsResource, this.options.profileLocation)) {
1116
try {
1117
await this.extensionsScannerService.initializeDefaultProfileExtensions();
1118
} catch (error) {
1119
throw toExtensionManagementError(error, ExtensionManagementErrorCode.IntializeDefaultProfile);
1120
}
1121
}
1122
1123
if (token.isCancellationRequested) {
1124
throw toExtensionManagementError(new CancellationError());
1125
}
1126
1127
try {
1128
await this.extensionsProfileScannerService.addExtensionsToProfile([[local, metadata]], this.options.profileLocation, !local.isValid);
1129
} catch (error) {
1130
throw toExtensionManagementError(error, ExtensionManagementErrorCode.AddToProfile);
1131
}
1132
1133
const result = await this.extensionsScanner.scanLocalExtension(local.location, ExtensionType.User, this.options.profileLocation);
1134
if (!result) {
1135
throw new ExtensionManagementError('Cannot find the installed extension', ExtensionManagementErrorCode.InstalledExtensionNotFound);
1136
}
1137
1138
if (this.source instanceof URI) {
1139
this.updateMetadata(local, token);
1140
}
1141
1142
return result;
1143
}
1144
1145
private async unsetIfRemoved(extensionKey: ExtensionKey): Promise<ILocalExtension | undefined> {
1146
// If the same version of extension is marked as removed, remove it from there and return the local.
1147
const [removed] = await this.extensionsScanner.unsetExtensionsForRemoval(extensionKey);
1148
if (removed) {
1149
this.logService.info('Removed the extension from removed list:', extensionKey.id);
1150
const userExtensions = await this.extensionsScanner.scanAllUserExtensions();
1151
return userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey));
1152
}
1153
return undefined;
1154
}
1155
1156
private async updateMetadata(extension: ILocalExtension, token: CancellationToken): Promise<void> {
1157
try {
1158
let [galleryExtension] = await this.galleryService.getExtensions([{ id: extension.identifier.id, version: extension.manifest.version }], token);
1159
if (!galleryExtension) {
1160
[galleryExtension] = await this.galleryService.getExtensions([{ id: extension.identifier.id }], token);
1161
}
1162
if (galleryExtension) {
1163
const metadata = {
1164
id: galleryExtension.identifier.uuid,
1165
publisherDisplayName: galleryExtension.publisherDisplayName,
1166
publisherId: galleryExtension.publisherId,
1167
isPreReleaseVersion: galleryExtension.properties.isPreReleaseVersion,
1168
hasPreReleaseVersion: extension.hasPreReleaseVersion || galleryExtension.properties.isPreReleaseVersion,
1169
preRelease: galleryExtension.properties.isPreReleaseVersion || this.options.installPreReleaseVersion
1170
};
1171
await this.extensionsScanner.updateMetadata(extension, metadata, this.options.profileLocation);
1172
}
1173
} catch (error) {
1174
/* Ignore Error */
1175
}
1176
}
1177
}
1178
1179
class UninstallExtensionInProfileTask extends AbstractExtensionTask<void> implements IUninstallExtensionTask {
1180
1181
constructor(
1182
readonly extension: ILocalExtension,
1183
readonly options: UninstallExtensionTaskOptions,
1184
private readonly extensionsProfileScannerService: IExtensionsProfileScannerService,
1185
) {
1186
super();
1187
}
1188
1189
protected doRun(token: CancellationToken): Promise<void> {
1190
return this.extensionsProfileScannerService.removeExtensionsFromProfile([this.extension.identifier], this.options.profileLocation);
1191
}
1192
1193
}
1194
1195