sussudio
Version:
An unofficial VS Code Internal API
325 lines (324 loc) • 16.4 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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 "../../../base/common/async.mjs";
import { VSBuffer } from "../../../base/common/buffer.mjs";
import { Disposable } from "../../../base/common/lifecycle.mjs";
import { Emitter } from "../../../base/common/event.mjs";
import { ResourceMap } from "../../../base/common/map.mjs";
import { URI } from "../../../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 "../../../base/common/types.mjs";
import { getErrorMessage } from "../../../base/common/errors.mjs";
import { ITelemetryService } from "../../telemetry/common/telemetry.mjs";
export var ExtensionsProfileScanningErrorCode;
(function (ExtensionsProfileScanningErrorCode) {
/**
* Error when trying to scan extensions from a profile that does not exist.
*/
ExtensionsProfileScanningErrorCode["ERROR_PROFILE_NOT_FOUND"] = "ERROR_PROFILE_NOT_FOUND";
/**
* Error when profile file is invalid.
*/
ExtensionsProfileScanningErrorCode["ERROR_INVALID_CONTENT"] = "ERROR_INVALID_CONTENT";
})(ExtensionsProfileScanningErrorCode || (ExtensionsProfileScanningErrorCode = {}));
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);
}