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

410 LOC · 323 covered · 87 uncovered · 85 ranges · 96 concepts · 29 introducers · 55 tests

File neighbourhood

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

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

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

Graph controls are ready.

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

1 > /*--------------------------------------------------------------------------------------------- extensionsProfileScannerService.ts ×14
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) {
48 > }
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, extensionsProfileScannerService.ts ×1
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 > }
117 > scanProfileExtensions(profileLocation: URI, options?: IProfileExtensionsScanOptions): Promise<IScannedProfileExtension[]> {
118 > return this.withProfileExtensions(profileLocation, undefined, options); extensionsProfileScannerService.ts ×4
119 > }
121 > async addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise<IScannedProfileExtension[]> {
122 > const extensionsToRemove: IScannedProfileExtension[] = []; extensionsProfileScannerService.ts ×8
123 > const extensionsToAdd: IScannedProfileExtension[] = [];
124 > try {
125 > await this.withProfileExtensions(profileLocation, existingExtensions => {
126 > const result: IScannedProfileExtension[] = [];
127 > if (keepExistingVersions) {
128 result.push(...existingExtensions);
130 > for (const existing of existingExtensions) {
131 > if (extensions.some(([e]) => areSameExtensions(e.identifier, existing.identifier) && e.manifest.version !== existing.version)) { extensionsProfileScannerService.ts ×3
132 > // Remove the existing extension with different version extensionsProfileScannerService.ts ×3
133 > extensionsToRemove.push(existing);
135 > result.push(existing); extensionsProfileScannerService.ts ×1
136 > }
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); extensionsProfileScannerService.ts ×1
147 > }
149 > if (extensionsToAdd.length) {
150 > this._onAddExtensions.fire({ extensions: extensionsToAdd, profileLocation });
151 > }
152 > if (extensionsToRemove.length) {
153 > this._onRemoveExtensions.fire({ extensions: extensionsToRemove, profileLocation }); extensionsProfileScannerService.ts ×3
154 > }
156 > });
157 > if (extensionsToAdd.length) {
158 > this._onDidAddExtensions.fire({ extensions: extensionsToAdd, profileLocation });
159 > }
160 > if (extensionsToRemove.length) {
161 > this._onDidRemoveExtensions.fire({ extensions: extensionsToRemove, profileLocation }); extensionsProfileScannerService.ts ×3
162 > }
163 > return extensionsToAdd; extensionsProfileScannerService.ts ×8
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 }
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 }
194 > async removeExtensionsFromProfile(extensions: IExtensionIdentifier[], profileLocation: URI): Promise<void> {
195 > const extensionsToRemove: IScannedProfileExtension[] = []; extensionsProfileScannerService.ts ×3
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 }
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 }
222 > private async withProfileExtensions(file: URI, updateFn?: (extensions: Mutable<IScannedProfileExtension>[]) => IScannedProfileExtension[], options?: IProfileExtensionsScanOptions): Promise<IScannedProfileExtension[]> {
223 > return this.getResourceAccessQueue(file).queue(async () => { extensionsProfileScannerService.ts ×4
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) { extensionsProfileScannerService.ts ×4
233 throw error;
234 }
235 > // migrate from old location, remove this after couple of releases extensionsProfileScannerService.ts ×4
236 > if (this.uriIdentityService.extUri.isEqual(file, this.userDataProfilesService.defaultProfile.extensionsResource)) {
237 > storedProfileExtensions = await this.migrateFromOldDefaultProfileExtensionsLocation(); extensionsScannerService.ts ×12
238 > }
239 > if (!storedProfileExtensions && options?.bailOutWhenFileNotFound) { extensionsProfileScannerService.ts ×4
240 > throw new ExtensionsProfileScanningError(getErrorMessage(error), ExtensionsProfileScanningErrorCode.ERROR_PROFILE_NOT_FOUND); extensionsScannerService.ts ×12
241 > }
243 > if (storedProfileExtensions) { extensionsProfileScannerService.ts ×4
244 > if (!Array.isArray(storedProfileExtensions)) { extensionsProfileScannerService.ts ×4
245 this.throwInvalidConentError(file);
246 }
247 > // TODO @sandy081: Remove this migration after couple of releases extensionsProfileScannerService.ts ×4
248 > let migrate = false;
249 > for (const e of storedProfileExtensions) {
250 > if (!isStoredProfileExtension(e)) { extensionsProfileScannerService.ts ×6
251 > this.throwInvalidConentError(file); extensionsProfileScannerService.ts ×2
252 > }
253 > let location: URI; extensionsProfileScannerService.ts ×3
254 > if (isString(e.relativeLocation) && e.relativeLocation) { extensionsProfileScannerService.ts ×6
255 > // Extension in new format. No migration needed. extensionsProfileScannerService.ts ×2
256 > location = this.resolveExtensionLocation(e.relativeLocation);
257 > } else if (isString(e.location)) { extensionsProfileScannerService.ts ×3
258 this.logService.warn(`Extensions profile: Ignoring extension with invalid location: ${e.location}`);
259 continue;
261 > location = URI.revive(e.location);
262 > const relativePath = this.toRelativePath(location);
263 > if (relativePath) {
264 > // Extension in old format. Migrate to new format. extensionsProfileScannerService.ts ×2
265 > migrate = true;
266 > e.relativeLocation = relativePath;
267 > }
269 > if (isUndefined(e.metadata?.hasPreReleaseVersion) && e.metadata?.preRelease) { extensionsProfileScannerService.ts ×6
270 migrate = true;
271 e.metadata.hasPreReleaseVersion = true;
272 }
273 > const uuid = e.metadata?.id ?? e.identifier.uuid; extensionsProfileScannerService.ts ×6
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 > }
282 > await this.fileService.writeFile(file, VSBuffer.fromString(JSON.stringify(storedProfileExtensions))); extensionsProfileScannerService.ts ×2
283 > }
286 > // Update
287 > if (updateFn) {
288 > extensions = updateFn(extensions); extensionsProfileScannerService.ts ×8
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 > }
300 > return extensions;
302 > }
304 > private throwInvalidConentError(file: URI): void {
305 > throw new ExtensionsProfileScanningError(`Invalid extensions content in ${file.toString()}`, ExtensionsProfileScanningErrorCode.ERROR_INVALID_CONTENT); extensionsProfileScannerService.ts ×2
306 > }
308 > private toRelativePath(extensionLocation: URI): string | undefined {
309 > return this.uriIdentityService.extUri.isEqual(this.uriIdentityService.extUri.dirname(extensionLocation), this.extensionsLocation) extensionsProfileScannerService.ts ×2
310 > ? this.uriIdentityService.extUri.basename(extensionLocation) extensionsProfileScannerService.ts ×1
314 > private resolveExtensionLocation(path: string): URI {
315 > return this.uriIdentityService.extUri.joinPath(this.extensionsLocation, path); extensionsProfileScannerService.ts ×2
316 > }
318 > private _migrationPromise: Promise<IStoredProfileExtension[] | undefined> | undefined;
319 > private async migrateFromOldDefaultProfileExtensionsLocation(): Promise<IStoredProfileExtension[] | undefined> {
320 > if (!this._migrationPromise) { extensionsScannerService.ts ×12
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))) { extensionsScannerService.ts ×12
339 storedProfileExtensions = parsedData;
340 } else {
341 this.logService.warn('Skipping migrating from old default profile locaiton: Found invalid data', parsedData);
342 }
343 > } catch (error) { extensionsScannerService.ts ×12
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;
379 > }
380 > return this._migrationPromise;
381 > }
383 > private getResourceAccessQueue(file: URI): Queue<IScannedProfileExtension[]> {
384 > let resourceQueue = this.resourcesAccessQueueMap.get(file); extensionsProfileScannerService.ts ×4
385 > if (!resourceQueue) {
386 > resourceQueue = new Queue<IScannedProfileExtension[]>();
387 > this.resourcesAccessQueueMap.set(file, resourceQueue);
388 > }
389 > return resourceQueue;
390 > }
392 >
393 > function isStoredProfileExtension(obj: unknown): obj is IStoredProfileExtension { extensionsProfileScannerService.ts ×6
394 > const candidate = obj as IStoredProfileExtension | undefined;
395 > return isObject(candidate)
396 > && isIExtensionIdentifier(candidate.identifier)
397 > && (isUriComponents(candidate.location) || (isString(candidate.location) && !!candidate.location)) extensionsProfileScannerService.ts ×4
398 > && (isUndefined(candidate.relativeLocation) || isString(candidate.relativeLocation)) extensionsProfileScannerService.ts ×1
399 > && !!candidate.version
400 > && isString(candidate.version); extensionsProfileScannerService.ts ×3
403 > function isUriComponents(obj: unknown): obj is UriComponents { extensionsProfileScannerService.ts ×4
404 > if (!obj) {
406 > }
407 > const thing = obj as UriComponents | undefined; extensionsProfileScannerService.ts ×1
408 > return typeof thing?.path === 'string' && extensionsProfileScannerService.ts ×4
409 > typeof thing?.scheme === 'string'; extensionsProfileScannerService.ts ×1