@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
368 lines (367 loc) • 13.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 '@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);
}