UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

368 lines (367 loc) 13.4 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); }; }; import { Queue } from '@sussudio/base/common/async.mjs'; import { VSBuffer } from '@sussudio/base/common/buffer.mjs'; import { Disposable } from '@sussudio/base/common/lifecycle.mjs'; import { Emitter } from '@sussudio/base/common/event.mjs'; import { ResourceMap } from '@sussudio/base/common/map.mjs'; import { URI } from '@sussudio/base/common/uri.mjs'; import { areSameExtensions } from './extensionManagementUtil.mjs'; import { isIExtensionIdentifier } from '../../extensions/common/extensions.mjs'; import { IFileService, toFileOperationResult } from '../../files/common/files.mjs'; import { createDecorator } from '../../instantiation/common/instantiation.mjs'; import { ILogService } from '../../log/common/log.mjs'; import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.mjs'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.mjs'; import { isObject, isString } from '@sussudio/base/common/types.mjs'; import { getErrorMessage } from '@sussudio/base/common/errors.mjs'; import { ITelemetryService } from '../../telemetry/common/telemetry.mjs'; export class ExtensionsProfileScanningError extends Error { code; constructor(message, code) { super(message); this.code = code; } } export const IExtensionsProfileScannerService = createDecorator('IExtensionsProfileScannerService'); let AbstractExtensionsProfileScannerService = class AbstractExtensionsProfileScannerService extends Disposable { extensionsLocation; fileService; userDataProfilesService; uriIdentityService; telemetryService; logService; _serviceBrand; _onAddExtensions = this._register(new Emitter()); onAddExtensions = this._onAddExtensions.event; _onDidAddExtensions = this._register(new Emitter()); onDidAddExtensions = this._onDidAddExtensions.event; _onRemoveExtensions = this._register(new Emitter()); onRemoveExtensions = this._onRemoveExtensions.event; _onDidRemoveExtensions = this._register(new Emitter()); onDidRemoveExtensions = this._onDidRemoveExtensions.event; resourcesAccessQueueMap = new ResourceMap(); constructor( extensionsLocation, fileService, userDataProfilesService, uriIdentityService, telemetryService, logService, ) { super(); this.extensionsLocation = extensionsLocation; this.fileService = fileService; this.userDataProfilesService = userDataProfilesService; this.uriIdentityService = uriIdentityService; this.telemetryService = telemetryService; this.logService = logService; } scanProfileExtensions(profileLocation, options) { return this.withProfileExtensions(profileLocation, undefined, options); } async addExtensionsToProfile(extensions, profileLocation) { const extensionsToRemove = []; const extensionsToAdd = []; try { await this.withProfileExtensions(profileLocation, (profileExtensions) => { const result = []; for (const extension of profileExtensions) { if ( extensions.some( ([e]) => areSameExtensions(e.identifier, extension.identifier) && e.manifest.version !== extension.version, ) ) { // Remove the existing extension with different version extensionsToRemove.push(extension); } else { result.push(extension); } } for (const [extension, metadata] of extensions) { if ( !result.some( (e) => areSameExtensions(e.identifier, extension.identifier) && e.version === extension.manifest.version, ) ) { // Add only if the same version of the extension is not already added const extensionToAdd = { identifier: extension.identifier, version: extension.manifest.version, location: extension.location, metadata, }; extensionsToAdd.push(extensionToAdd); result.push(extensionToAdd); } } if (extensionsToAdd.length) { this._onAddExtensions.fire({ extensions: extensionsToAdd, profileLocation }); } if (extensionsToRemove.length) { this._onRemoveExtensions.fire({ extensions: extensionsToRemove, profileLocation }); } return result; }); if (extensionsToAdd.length) { this._onDidAddExtensions.fire({ extensions: extensionsToAdd, profileLocation }); } if (extensionsToRemove.length) { this._onDidRemoveExtensions.fire({ extensions: extensionsToRemove, profileLocation }); } return extensionsToAdd; } catch (error) { if (extensionsToAdd.length) { this._onDidAddExtensions.fire({ extensions: extensionsToAdd, error, profileLocation }); } if (extensionsToRemove.length) { this._onDidRemoveExtensions.fire({ extensions: extensionsToRemove, error, profileLocation }); } throw error; } } async removeExtensionFromProfile(extension, profileLocation) { const extensionsToRemove = []; this._onRemoveExtensions.fire({ extensions: extensionsToRemove, profileLocation }); try { await this.withProfileExtensions(profileLocation, (profileExtensions) => { const result = []; for (const e of profileExtensions) { if (areSameExtensions(e.identifier, extension.identifier)) { extensionsToRemove.push(e); } else { result.push(e); } } if (extensionsToRemove.length) { this._onRemoveExtensions.fire({ extensions: extensionsToRemove, profileLocation }); } return result; }); if (extensionsToRemove.length) { this._onDidRemoveExtensions.fire({ extensions: extensionsToRemove, profileLocation }); } } catch (error) { if (extensionsToRemove.length) { this._onDidRemoveExtensions.fire({ extensions: extensionsToRemove, error, profileLocation }); } throw error; } } async withProfileExtensions(file, updateFn, options) { return this.getResourceAccessQueue(file).queue(async () => { let extensions = []; // Read let storedProfileExtensions; try { const content = await this.fileService.readFile(file); storedProfileExtensions = JSON.parse(content.value.toString()); } catch (error) { if (toFileOperationResult(error) !== 1 /* FileOperationResult.FILE_NOT_FOUND */) { throw error; } // migrate from old location, remove this after couple of releases if ( this.uriIdentityService.extUri.isEqual(file, this.userDataProfilesService.defaultProfile.extensionsResource) ) { storedProfileExtensions = await this.migrateFromOldDefaultProfileExtensionsLocation(); } if (!storedProfileExtensions && options?.bailOutWhenFileNotFound) { throw new ExtensionsProfileScanningError( getErrorMessage(error), 'ERROR_PROFILE_NOT_FOUND' /* ExtensionsProfileScanningErrorCode.ERROR_PROFILE_NOT_FOUND */, ); } } if (storedProfileExtensions) { if (!Array.isArray(storedProfileExtensions)) { this.reportAndThrowInvalidConentError(file); } // TODO @sandy081: Remove this migration after couple of releases let migrate = false; for (const e of storedProfileExtensions) { if (!isStoredProfileExtension(e)) { this.reportAndThrowInvalidConentError(file); } let location; if (isString(e.location)) { location = this.resolveExtensionLocation(e.location); } else { location = URI.revive(e.location); const relativePath = this.toRelativePath(location); if (relativePath) { migrate = true; e.location = relativePath; } } extensions.push({ identifier: e.identifier, location, version: e.version, metadata: e.metadata, }); } if (migrate) { await this.fileService.writeFile(file, VSBuffer.fromString(JSON.stringify(storedProfileExtensions))); } } // Update if (updateFn) { extensions = updateFn(extensions); const storedProfileExtensions = extensions.map((e) => ({ identifier: e.identifier, version: e.version, location: this.toRelativePath(e.location) ?? e.location.toJSON(), metadata: e.metadata, })); await this.fileService.writeFile(file, VSBuffer.fromString(JSON.stringify(storedProfileExtensions))); } return extensions; }); } reportAndThrowInvalidConentError(file) { const error = new ExtensionsProfileScanningError( `Invalid extensions content in ${file.toString()}`, 'ERROR_INVALID_CONTENT' /* ExtensionsProfileScanningErrorCode.ERROR_INVALID_CONTENT */, ); this.telemetryService.publicLogError2('extensionsProfileScanningError', { code: error.code }); throw error; } toRelativePath(extensionLocation) { return this.uriIdentityService.extUri.isEqualOrParent(extensionLocation, this.extensionsLocation) ? this.uriIdentityService.extUri.relativePath(this.extensionsLocation, extensionLocation) : undefined; } resolveExtensionLocation(path) { return this.uriIdentityService.extUri.joinPath(this.extensionsLocation, path); } _migrationPromise; async migrateFromOldDefaultProfileExtensionsLocation() { if (!this._migrationPromise) { this._migrationPromise = (async () => { const oldDefaultProfileExtensionsLocation = this.uriIdentityService.extUri.joinPath( this.userDataProfilesService.defaultProfile.location, 'extensions.json', ); let content; try { content = (await this.fileService.readFile(oldDefaultProfileExtensionsLocation)).value.toString(); } catch (error) { if (toFileOperationResult(error) === 1 /* FileOperationResult.FILE_NOT_FOUND */) { return undefined; } throw error; } this.logService.info( 'Migrating extensions from old default profile location', oldDefaultProfileExtensionsLocation.toString(), ); let storedProfileExtensions; try { const parsedData = JSON.parse(content); if (Array.isArray(parsedData) && parsedData.every((candidate) => isStoredProfileExtension(candidate))) { storedProfileExtensions = parsedData; } else { this.logService.warn( 'Skipping migrating from old default profile locaiton: Found invalid data', parsedData, ); } } catch (error) { /* Ignore */ this.logService.error(error); } if (storedProfileExtensions) { try { await this.fileService.createFile( this.userDataProfilesService.defaultProfile.extensionsResource, VSBuffer.fromString(JSON.stringify(storedProfileExtensions)), { overwrite: false }, ); this.logService.info( 'Migrated extensions from old default profile location to new location', oldDefaultProfileExtensionsLocation.toString(), this.userDataProfilesService.defaultProfile.extensionsResource.toString(), ); } catch (error) { if (toFileOperationResult(error) === 3 /* FileOperationResult.FILE_MODIFIED_SINCE */) { this.logService.info( 'Migration from old default profile location to new location is done by another window', oldDefaultProfileExtensionsLocation.toString(), this.userDataProfilesService.defaultProfile.extensionsResource.toString(), ); } else { throw error; } } } try { await this.fileService.del(oldDefaultProfileExtensionsLocation); } catch (error) { if (toFileOperationResult(error) !== 1 /* FileOperationResult.FILE_NOT_FOUND */) { this.logService.error(error); } } return storedProfileExtensions; })(); } return this._migrationPromise; } getResourceAccessQueue(file) { let resourceQueue = this.resourcesAccessQueueMap.get(file); if (!resourceQueue) { resourceQueue = new Queue(); this.resourcesAccessQueueMap.set(file, resourceQueue); } return resourceQueue; } }; AbstractExtensionsProfileScannerService = __decorate( [ __param(1, IFileService), __param(2, IUserDataProfilesService), __param(3, IUriIdentityService), __param(4, ITelemetryService), __param(5, ILogService), ], AbstractExtensionsProfileScannerService, ); export { AbstractExtensionsProfileScannerService }; function isStoredProfileExtension(candidate) { return ( isObject(candidate) && isIExtensionIdentifier(candidate.identifier) && (isUriComponents(candidate.location) || isString(candidate.location)) && candidate.version && isString(candidate.version) ); } function isUriComponents(thing) { if (!thing) { return false; } return isString(thing.path) && isString(thing.scheme); }