Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.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 { Queue } from '../../../base/common/async.js';
7
import { VSBuffer } from '../../../base/common/buffer.js';
8
import { Disposable } from '../../../base/common/lifecycle.js';
9
import { Emitter, Event } from '../../../base/common/event.js';
10
import { ResourceMap } from '../../../base/common/map.js';
11
import { URI, UriComponents } from '../../../base/common/uri.js';
12
import { Metadata, isIExtensionIdentifier } from './extensionManagement.js';
13
import { areSameExtensions } from './extensionManagementUtil.js';
14
import { IExtension, IExtensionIdentifier } from '../../extensions/common/extensions.js';
15
import { FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js';
16
import { createDecorator } from '../../instantiation/common/instantiation.js';
17
import { ILogService } from '../../log/common/log.js';
18
import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
19
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
20
import { Mutable, isObject, isString, isUndefined } from '../../../base/common/types.js';
21
import { getErrorMessage } from '../../../base/common/errors.js';
22
23
interface IStoredProfileExtension {
24
identifier: IExtensionIdentifier;
25
location: UriComponents | string;
26
relativeLocation: string | undefined;
27
version: string;
28
metadata?: Metadata;
29
}
30
31
export const enum ExtensionsProfileScanningErrorCode {
32
33
/**
34
* Error when trying to scan extensions from a profile that does not exist.
35
*/
36
ERROR_PROFILE_NOT_FOUND = 'ERROR_PROFILE_NOT_FOUND',
37
38
/**
39
* Error when profile file is invalid.
40
*/
41
ERROR_INVALID_CONTENT = 'ERROR_INVALID_CONTENT',
42
43
}
44
45
export class ExtensionsProfileScanningError extends Error {
46
constructor(message: string, public code: ExtensionsProfileScanningErrorCode) {
47
super(message);
48
}
49
}
50
51
export interface IScannedProfileExtension {
52
readonly identifier: IExtensionIdentifier;
53
readonly version: string;
54
readonly location: URI;
55
readonly metadata?: Metadata;
56
}
57
58
export interface ProfileExtensionsEvent {
59
readonly extensions: readonly IScannedProfileExtension[];
60
readonly profileLocation: URI;
61
}
62
63
export interface DidAddProfileExtensionsEvent extends ProfileExtensionsEvent {
64
readonly error?: Error;
65
}
66
67
export interface DidRemoveProfileExtensionsEvent extends ProfileExtensionsEvent {
68
readonly error?: Error;
69
}
70
71
export interface IProfileExtensionsScanOptions {
72
readonly bailOutWhenFileNotFound?: boolean;
73
}
74
75
export const IExtensionsProfileScannerService = createDecorator<IExtensionsProfileScannerService>('IExtensionsProfileScannerService');
76
export interface IExtensionsProfileScannerService {
77
readonly _serviceBrand: undefined;
78
79
readonly onAddExtensions: Event<ProfileExtensionsEvent>;
80
readonly onDidAddExtensions: Event<DidAddProfileExtensionsEvent>;
81
readonly onRemoveExtensions: Event<ProfileExtensionsEvent>;
82
readonly onDidRemoveExtensions: Event<DidRemoveProfileExtensionsEvent>;
83
84
scanProfileExtensions(profileLocation: URI, options?: IProfileExtensionsScanOptions): Promise<IScannedProfileExtension[]>;
85
addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise<IScannedProfileExtension[]>;
86
updateMetadata(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise<IScannedProfileExtension[]>;
87
removeExtensionsFromProfile(extensions: IExtensionIdentifier[], profileLocation: URI): Promise<void>;
88
}
89
90
export abstract class AbstractExtensionsProfileScannerService extends Disposable implements IExtensionsProfileScannerService {
91
readonly _serviceBrand: undefined;
92
93
private readonly _onAddExtensions = this._register(new Emitter<ProfileExtensionsEvent>());
94
readonly onAddExtensions = this._onAddExtensions.event;
95
96
private readonly _onDidAddExtensions = this._register(new Emitter<DidAddProfileExtensionsEvent>());
97
readonly onDidAddExtensions = this._onDidAddExtensions.event;
98
99
private readonly _onRemoveExtensions = this._register(new Emitter<ProfileExtensionsEvent>());
100
readonly onRemoveExtensions = this._onRemoveExtensions.event;
101
102
private readonly _onDidRemoveExtensions = this._register(new Emitter<DidRemoveProfileExtensionsEvent>());
103
readonly onDidRemoveExtensions = this._onDidRemoveExtensions.event;
104
105
private readonly resourcesAccessQueueMap = new ResourceMap<Queue<IScannedProfileExtension[]>>();
106
107
constructor(
108
private readonly extensionsLocation: URI,
109
@IFileService private readonly fileService: IFileService,
110
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
111
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
112
@ILogService private readonly logService: ILogService,
113
) {
114
super();
115
}
116
117
scanProfileExtensions(profileLocation: URI, options?: IProfileExtensionsScanOptions): Promise<IScannedProfileExtension[]> {
118
return this.withProfileExtensions(profileLocation, undefined, options);
119
}
120
121
async addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise<IScannedProfileExtension[]> {
122
const extensionsToRemove: IScannedProfileExtension[] = [];
123
const extensionsToAdd: IScannedProfileExtension[] = [];
124
try {
125
await this.withProfileExtensions(profileLocation, existingExtensions => {
126
const result: IScannedProfileExtension[] = [];
127
if (keepExistingVersions) {
128
result.push(...existingExtensions);
129
} else {
130
for (const existing of existingExtensions) {
131
if (extensions.some(([e]) => areSameExtensions(e.identifier, existing.identifier) && e.manifest.version !== existing.version)) {
132
// Remove the existing extension with different version
133
extensionsToRemove.push(existing);
134
} else {
135
result.push(existing);
136
}
137
}
138
}
139
for (const [extension, metadata] of extensions) {
140
const index = result.findIndex(e => areSameExtensions(e.identifier, extension.identifier) && e.version === extension.manifest.version);
141
const extensionToAdd = { identifier: extension.identifier, version: extension.manifest.version, location: extension.location, metadata };
142
if (index === -1) {
143
extensionsToAdd.push(extensionToAdd);
144
result.push(extensionToAdd);
145
} else {
146
result.splice(index, 1, extensionToAdd);
147
}
148
}
149
if (extensionsToAdd.length) {
150
this._onAddExtensions.fire({ extensions: extensionsToAdd, profileLocation });
151
}
152
if (extensionsToRemove.length) {
153
this._onRemoveExtensions.fire({ extensions: extensionsToRemove, profileLocation });
154
}
155
return result;
156
});
157
if (extensionsToAdd.length) {
158
this._onDidAddExtensions.fire({ extensions: extensionsToAdd, profileLocation });
159
}
160
if (extensionsToRemove.length) {
161
this._onDidRemoveExtensions.fire({ extensions: extensionsToRemove, profileLocation });
162
}
163
return extensionsToAdd;
164
} catch (error) {
165
if (extensionsToAdd.length) {
166
this._onDidAddExtensions.fire({ extensions: extensionsToAdd, error, profileLocation });
167
}
168
if (extensionsToRemove.length) {
169
this._onDidRemoveExtensions.fire({ extensions: extensionsToRemove, error, profileLocation });
170
}
171
throw error;
172
}
173
}
174
175
async updateMetadata(extensions: [IExtension, Metadata][], profileLocation: URI): Promise<IScannedProfileExtension[]> {
176
const updatedExtensions: IScannedProfileExtension[] = [];
177
await this.withProfileExtensions(profileLocation, profileExtensions => {
178
const result: IScannedProfileExtension[] = [];
179
for (const profileExtension of profileExtensions) {
180
const extension = extensions.find(([e]) => areSameExtensions({ id: e.identifier.id }, { id: profileExtension.identifier.id }) && e.manifest.version === profileExtension.version);
181
if (extension) {
182
profileExtension.metadata = { ...profileExtension.metadata, ...extension[1] };
183
updatedExtensions.push(profileExtension);
184
result.push(profileExtension);
185
} else {
186
result.push(profileExtension);
187
}
188
}
189
return result;
190
});
191
return updatedExtensions;
192
}
193
194
async removeExtensionsFromProfile(extensions: IExtensionIdentifier[], profileLocation: URI): Promise<void> {
195
const extensionsToRemove: IScannedProfileExtension[] = [];
196
try {
197
await this.withProfileExtensions(profileLocation, profileExtensions => {
198
const result: IScannedProfileExtension[] = [];
199
for (const e of profileExtensions) {
200
if (extensions.some(extension => areSameExtensions(e.identifier, extension))) {
201
extensionsToRemove.push(e);
202
} else {
203
result.push(e);
204
}
205
}
206
if (extensionsToRemove.length) {
207
this._onRemoveExtensions.fire({ extensions: extensionsToRemove, profileLocation });
208
}
209
return result;
210
});
211
if (extensionsToRemove.length) {
212
this._onDidRemoveExtensions.fire({ extensions: extensionsToRemove, profileLocation });
213
}
214
} catch (error) {
215
if (extensionsToRemove.length) {
216
this._onDidRemoveExtensions.fire({ extensions: extensionsToRemove, error, profileLocation });
217
}
218
throw error;
219
}
220
}
221
222
private async withProfileExtensions(file: URI, updateFn?: (extensions: Mutable<IScannedProfileExtension>[]) => IScannedProfileExtension[], options?: IProfileExtensionsScanOptions): Promise<IScannedProfileExtension[]> {
223
return this.getResourceAccessQueue(file).queue(async () => {
224
let extensions: IScannedProfileExtension[] = [];
225
226
// Read
227
let storedProfileExtensions: IStoredProfileExtension[] | undefined;
228
try {
229
const content = await this.fileService.readFile(file);
230
storedProfileExtensions = JSON.parse(content.value.toString().trim() || '[]');
231
} catch (error) {
232
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
233
throw error;
234
}
235
// migrate from old location, remove this after couple of releases
236
if (this.uriIdentityService.extUri.isEqual(file, this.userDataProfilesService.defaultProfile.extensionsResource)) {
237
storedProfileExtensions = await this.migrateFromOldDefaultProfileExtensionsLocation();
238
}
239
if (!storedProfileExtensions && options?.bailOutWhenFileNotFound) {
240
throw new ExtensionsProfileScanningError(getErrorMessage(error), ExtensionsProfileScanningErrorCode.ERROR_PROFILE_NOT_FOUND);
241
}
242
}
243
if (storedProfileExtensions) {
244
if (!Array.isArray(storedProfileExtensions)) {
245
this.throwInvalidConentError(file);
246
}
247
// TODO @sandy081: Remove this migration after couple of releases
248
let migrate = false;
249
for (const e of storedProfileExtensions) {
250
if (!isStoredProfileExtension(e)) {
251
this.throwInvalidConentError(file);
252
}
253
let location: URI;
254
if (isString(e.relativeLocation) && e.relativeLocation) {
255
// Extension in new format. No migration needed.
256
location = this.resolveExtensionLocation(e.relativeLocation);
257
} else if (isString(e.location)) {
258
this.logService.warn(`Extensions profile: Ignoring extension with invalid location: ${e.location}`);
259
continue;
260
} else {
261
location = URI.revive(e.location);
262
const relativePath = this.toRelativePath(location);
263
if (relativePath) {
264
// Extension in old format. Migrate to new format.
265
migrate = true;
266
e.relativeLocation = relativePath;
267
}
268
}
269
if (isUndefined(e.metadata?.hasPreReleaseVersion) && e.metadata?.preRelease) {
270
migrate = true;
271
e.metadata.hasPreReleaseVersion = true;
272
}
273
const uuid = e.metadata?.id ?? e.identifier.uuid;
274
extensions.push({
275
identifier: uuid ? { id: e.identifier.id, uuid } : { id: e.identifier.id },
276
location,
277
version: e.version,
278
metadata: e.metadata,
279
});
280
}
281
if (migrate) {
282
await this.fileService.writeFile(file, VSBuffer.fromString(JSON.stringify(storedProfileExtensions)));
283
}
284
}
285
286
// Update
287
if (updateFn) {
288
extensions = updateFn(extensions);
289
const storedProfileExtensions: IStoredProfileExtension[] = extensions.map(e => ({
290
identifier: e.identifier,
291
version: e.version,
292
// retain old format so that old clients can read it
293
location: e.location.toJSON(),
294
relativeLocation: this.toRelativePath(e.location),
295
metadata: e.metadata
296
}));
297
await this.fileService.writeFile(file, VSBuffer.fromString(JSON.stringify(storedProfileExtensions)));
298
}
299
300
return extensions;
301
});
302
}
303
304
private throwInvalidConentError(file: URI): void {
305
throw new ExtensionsProfileScanningError(`Invalid extensions content in ${file.toString()}`, ExtensionsProfileScanningErrorCode.ERROR_INVALID_CONTENT);
306
}
307
308
private toRelativePath(extensionLocation: URI): string | undefined {
309
return this.uriIdentityService.extUri.isEqual(this.uriIdentityService.extUri.dirname(extensionLocation), this.extensionsLocation)
310
? this.uriIdentityService.extUri.basename(extensionLocation)
311
: undefined;
312
}
313
314
private resolveExtensionLocation(path: string): URI {
315
return this.uriIdentityService.extUri.joinPath(this.extensionsLocation, path);
316
}
317
318
private _migrationPromise: Promise<IStoredProfileExtension[] | undefined> | undefined;
319
private async migrateFromOldDefaultProfileExtensionsLocation(): Promise<IStoredProfileExtension[] | undefined> {
320
if (!this._migrationPromise) {
321
this._migrationPromise = (async () => {
322
const oldDefaultProfileExtensionsLocation = this.uriIdentityService.extUri.joinPath(this.userDataProfilesService.defaultProfile.location, 'extensions.json');
323
const oldDefaultProfileExtensionsInitLocation = this.uriIdentityService.extUri.joinPath(this.extensionsLocation, '.init-default-profile-extensions');
324
let content: string;
325
try {
326
content = (await this.fileService.readFile(oldDefaultProfileExtensionsLocation)).value.toString();
327
} catch (error) {
328
if (toFileOperationResult(error) === FileOperationResult.FILE_NOT_FOUND) {
329
return undefined;
330
}
331
throw error;
332
}
333
334
this.logService.info('Migrating extensions from old default profile location', oldDefaultProfileExtensionsLocation.toString());
335
let storedProfileExtensions: IStoredProfileExtension[] | undefined;
336
try {
337
const parsedData = JSON.parse(content);
338
if (Array.isArray(parsedData) && parsedData.every(candidate => isStoredProfileExtension(candidate))) {
339
storedProfileExtensions = parsedData;
340
} else {
341
this.logService.warn('Skipping migrating from old default profile locaiton: Found invalid data', parsedData);
342
}
343
} catch (error) {
344
/* Ignore */
345
this.logService.error(error);
346
}
347
348
if (storedProfileExtensions) {
349
try {
350
await this.fileService.createFile(this.userDataProfilesService.defaultProfile.extensionsResource, VSBuffer.fromString(JSON.stringify(storedProfileExtensions)), { overwrite: false });
351
this.logService.info('Migrated extensions from old default profile location to new location', oldDefaultProfileExtensionsLocation.toString(), this.userDataProfilesService.defaultProfile.extensionsResource.toString());
352
} catch (error) {
353
if (toFileOperationResult(error) === FileOperationResult.FILE_MODIFIED_SINCE) {
354
this.logService.info('Migration from old default profile location to new location is done by another window', oldDefaultProfileExtensionsLocation.toString(), this.userDataProfilesService.defaultProfile.extensionsResource.toString());
355
} else {
356
throw error;
357
}
358
}
359
}
360
361
try {
362
await this.fileService.del(oldDefaultProfileExtensionsLocation);
363
} catch (error) {
364
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
365
this.logService.error(error);
366
}
367
}
368
369
try {
370
await this.fileService.del(oldDefaultProfileExtensionsInitLocation);
371
} catch (error) {
372
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
373
this.logService.error(error);
374
}
375
}
376
377
return storedProfileExtensions;
378
})();
379
}
380
return this._migrationPromise;
381
}
382
383
private getResourceAccessQueue(file: URI): Queue<IScannedProfileExtension[]> {
384
let resourceQueue = this.resourcesAccessQueueMap.get(file);
385
if (!resourceQueue) {
386
resourceQueue = new Queue<IScannedProfileExtension[]>();
387
this.resourcesAccessQueueMap.set(file, resourceQueue);
388
}
389
return resourceQueue;
390
}
391
}
392
393
function isStoredProfileExtension(candidate: any): candidate is IStoredProfileExtension {
394
return isObject(candidate)
395
&& isIExtensionIdentifier(candidate.identifier)
396
&& (isUriComponents(candidate.location) || (isString(candidate.location) && candidate.location))
397
&& (isUndefined(candidate.relativeLocation) || isString(candidate.relativeLocation))
398
&& candidate.version && isString(candidate.version);
399
}
400
401
function isUriComponents(thing: unknown): thing is UriComponents {
402
if (!thing) {
403
return false;
404
}
405
return isString((<any>thing).path) &&
406
isString((<any>thing).scheme);
407
}
408
409