Path: blob/main/src/vs/platform/extensionManagement/common/extensionsScannerService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { coalesce } from '../../../base/common/arrays.js';6import { ThrottledDelayer } from '../../../base/common/async.js';7import * as objects from '../../../base/common/objects.js';8import { VSBuffer } from '../../../base/common/buffer.js';9import { getErrorMessage } from '../../../base/common/errors.js';10import { getNodeType, parse, ParseError } from '../../../base/common/json.js';11import { getParseErrorMessage } from '../../../base/common/jsonErrorMessages.js';12import { Disposable } from '../../../base/common/lifecycle.js';13import { FileAccess, Schemas } from '../../../base/common/network.js';14import * as path from '../../../base/common/path.js';15import * as platform from '../../../base/common/platform.js';16import { basename, isEqual, joinPath } from '../../../base/common/resources.js';17import * as semver from '../../../base/common/semver/semver.js';18import Severity from '../../../base/common/severity.js';19import { URI } from '../../../base/common/uri.js';20import { localize } from '../../../nls.js';21import { IEnvironmentService } from '../../environment/common/environment.js';22import { IProductVersion, Metadata } from './extensionManagement.js';23import { areSameExtensions, computeTargetPlatform, getExtensionId, getGalleryExtensionId } from './extensionManagementUtil.js';24import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap, parseEnabledApiProposalNames } from '../../extensions/common/extensions.js';25import { validateExtensionManifest } from '../../extensions/common/extensionValidator.js';26import { FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js';27import { createDecorator, IInstantiationService } from '../../instantiation/common/instantiation.js';28import { ILogService } from '../../log/common/log.js';29import { IProductService } from '../../product/common/productService.js';30import { Emitter, Event } from '../../../base/common/event.js';31import { revive } from '../../../base/common/marshalling.js';32import { ExtensionsProfileScanningError, ExtensionsProfileScanningErrorCode, IExtensionsProfileScannerService, IProfileExtensionsScanOptions, IScannedProfileExtension } from './extensionsProfileScannerService.js';33import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';34import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';35import { localizeManifest } from './extensionNls.js';3637export type ManifestMetadata = Partial<{38targetPlatform: TargetPlatform;39installedTimestamp: number;40size: number;41}>;4243export type IScannedExtensionManifest = IRelaxedExtensionManifest & { __metadata?: ManifestMetadata };4445interface IRelaxedScannedExtension {46type: ExtensionType;47isBuiltin: boolean;48identifier: IExtensionIdentifier;49manifest: IRelaxedExtensionManifest;50location: URI;51targetPlatform: TargetPlatform;52publisherDisplayName?: string;53metadata: Metadata | undefined;54isValid: boolean;55validations: readonly [Severity, string][];56preRelease: boolean;57}5859export type IScannedExtension = Readonly<IRelaxedScannedExtension> & { manifest: IExtensionManifest };6061export interface Translations {62[id: string]: string;63}6465export namespace Translations {66export function equals(a: Translations, b: Translations): boolean {67if (a === b) {68return true;69}70const aKeys = Object.keys(a);71const bKeys: Set<string> = new Set<string>();72for (const key of Object.keys(b)) {73bKeys.add(key);74}75if (aKeys.length !== bKeys.size) {76return false;77}7879for (const key of aKeys) {80if (a[key] !== b[key]) {81return false;82}83bKeys.delete(key);84}85return bKeys.size === 0;86}87}8889interface MessageBag {90[key: string]: string | { message: string; comment: string[] };91}9293interface TranslationBundle {94contents: {95package: MessageBag;96};97}9899interface LocalizedMessages {100values: MessageBag | undefined;101default: URI | null;102}103104interface IBuiltInExtensionControl {105[name: string]: 'marketplace' | 'disabled' | string;106}107108export type SystemExtensionsScanOptions = {109readonly checkControlFile?: boolean;110readonly language?: string;111};112113export type UserExtensionsScanOptions = {114readonly profileLocation: URI;115readonly includeInvalid?: boolean;116readonly language?: string;117readonly useCache?: boolean;118readonly productVersion?: IProductVersion;119};120121export type ScanOptions = {122readonly includeInvalid?: boolean;123readonly language?: string;124};125126export const IExtensionsScannerService = createDecorator<IExtensionsScannerService>('IExtensionsScannerService');127export interface IExtensionsScannerService {128readonly _serviceBrand: undefined;129130readonly systemExtensionsLocation: URI;131readonly userExtensionsLocation: URI;132readonly onDidChangeCache: Event<ExtensionType>;133134scanAllExtensions(systemScanOptions: SystemExtensionsScanOptions, userScanOptions: UserExtensionsScanOptions): Promise<IScannedExtension[]>;135scanSystemExtensions(scanOptions: SystemExtensionsScanOptions): Promise<IScannedExtension[]>;136scanUserExtensions(scanOptions: UserExtensionsScanOptions): Promise<IScannedExtension[]>;137scanAllUserExtensions(): Promise<IScannedExtension[]>;138139scanExtensionsUnderDevelopment(existingExtensions: IScannedExtension[], scanOptions: ScanOptions): Promise<IScannedExtension[]>;140scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension | null>;141scanMultipleExtensions(extensionLocations: URI[], extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension[]>;142scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension[]>;143144updateManifestMetadata(extensionLocation: URI, metadata: ManifestMetadata): Promise<void>;145initializeDefaultProfileExtensions(): Promise<void>;146}147148export abstract class AbstractExtensionsScannerService extends Disposable implements IExtensionsScannerService {149150readonly _serviceBrand: undefined;151152protected abstract getTranslations(language: string): Promise<Translations>;153154private readonly _onDidChangeCache = this._register(new Emitter<ExtensionType>());155readonly onDidChangeCache = this._onDidChangeCache.event;156157private readonly systemExtensionsCachedScanner: CachedExtensionsScanner;158private readonly userExtensionsCachedScanner: CachedExtensionsScanner;159private readonly extensionsScanner: ExtensionsScanner;160161constructor(162readonly systemExtensionsLocation: URI,163readonly userExtensionsLocation: URI,164private readonly extensionsControlLocation: URI,165currentProfile: IUserDataProfile,166@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,167@IExtensionsProfileScannerService protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService,168@IFileService protected readonly fileService: IFileService,169@ILogService protected readonly logService: ILogService,170@IEnvironmentService private readonly environmentService: IEnvironmentService,171@IProductService private readonly productService: IProductService,172@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,173@IInstantiationService private readonly instantiationService: IInstantiationService,174) {175super();176177this.systemExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, currentProfile));178this.userExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, currentProfile));179this.extensionsScanner = this._register(this.instantiationService.createInstance(ExtensionsScanner));180181this._register(this.systemExtensionsCachedScanner.onDidChangeCache(() => this._onDidChangeCache.fire(ExtensionType.System)));182this._register(this.userExtensionsCachedScanner.onDidChangeCache(() => this._onDidChangeCache.fire(ExtensionType.User)));183}184185private _targetPlatformPromise: Promise<TargetPlatform> | undefined;186private getTargetPlatform(): Promise<TargetPlatform> {187if (!this._targetPlatformPromise) {188this._targetPlatformPromise = computeTargetPlatform(this.fileService, this.logService);189}190return this._targetPlatformPromise;191}192193async scanAllExtensions(systemScanOptions: SystemExtensionsScanOptions, userScanOptions: UserExtensionsScanOptions): Promise<IScannedExtension[]> {194const [system, user] = await Promise.all([195this.scanSystemExtensions(systemScanOptions),196this.scanUserExtensions(userScanOptions),197]);198return this.dedupExtensions(system, user, [], await this.getTargetPlatform(), true);199}200201async scanSystemExtensions(scanOptions: SystemExtensionsScanOptions): Promise<IScannedExtension[]> {202const promises: Promise<IRelaxedScannedExtension[]>[] = [];203promises.push(this.scanDefaultSystemExtensions(scanOptions.language));204promises.push(this.scanDevSystemExtensions(scanOptions.language, !!scanOptions.checkControlFile));205const [defaultSystemExtensions, devSystemExtensions] = await Promise.all(promises);206return this.applyScanOptions([...defaultSystemExtensions, ...devSystemExtensions], ExtensionType.System, { pickLatest: false });207}208209async scanUserExtensions(scanOptions: UserExtensionsScanOptions): Promise<IScannedExtension[]> {210this.logService.trace('Started scanning user extensions', scanOptions.profileLocation);211const profileScanOptions: IProfileExtensionsScanOptions | undefined = this.uriIdentityService.extUri.isEqual(scanOptions.profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource) ? { bailOutWhenFileNotFound: true } : undefined;212const extensionsScannerInput = await this.createExtensionScannerInput(scanOptions.profileLocation, true, ExtensionType.User, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion());213const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode ? this.userExtensionsCachedScanner : this.extensionsScanner;214let extensions: IRelaxedScannedExtension[];215try {216extensions = await extensionsScanner.scanExtensions(extensionsScannerInput);217} catch (error) {218if (error instanceof ExtensionsProfileScanningError && error.code === ExtensionsProfileScanningErrorCode.ERROR_PROFILE_NOT_FOUND) {219await this.doInitializeDefaultProfileExtensions();220extensions = await extensionsScanner.scanExtensions(extensionsScannerInput);221} else {222throw error;223}224}225extensions = await this.applyScanOptions(extensions, ExtensionType.User, { includeInvalid: scanOptions.includeInvalid, pickLatest: true });226this.logService.trace('Scanned user extensions:', extensions.length);227return extensions;228}229230async scanAllUserExtensions(scanOptions: { includeAllVersions?: boolean; includeInvalid: boolean } = { includeInvalid: true, includeAllVersions: true }): Promise<IScannedExtension[]> {231const extensionsScannerInput = await this.createExtensionScannerInput(this.userExtensionsLocation, false, ExtensionType.User, undefined, true, undefined, this.getProductVersion());232const extensions = await this.extensionsScanner.scanExtensions(extensionsScannerInput);233return this.applyScanOptions(extensions, ExtensionType.User, { includeAllVersions: scanOptions.includeAllVersions, includeInvalid: scanOptions.includeInvalid });234}235236async scanExtensionsUnderDevelopment(existingExtensions: IScannedExtension[], scanOptions: ScanOptions): Promise<IScannedExtension[]> {237if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) {238const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file)239.map(async extensionDevelopmentLocationURI => {240const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, scanOptions.language, false /* do not validate */, undefined, this.getProductVersion());241const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(input);242return extensions.map(extension => {243// Override the extension type from the existing extensions244extension.type = existingExtensions.find(e => areSameExtensions(e.identifier, extension.identifier))?.type ?? extension.type;245// Validate the extension246return this.extensionsScanner.validate(extension, input);247});248})))249.flat();250return this.applyScanOptions(extensions, 'development', { includeInvalid: scanOptions.includeInvalid, pickLatest: true });251}252return [];253}254255async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension | null> {256const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, this.getProductVersion());257const extension = await this.extensionsScanner.scanExtension(extensionsScannerInput);258if (!extension) {259return null;260}261if (!scanOptions.includeInvalid && !extension.isValid) {262return null;263}264return extension;265}266267async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension[]> {268const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, this.getProductVersion());269const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput);270return this.applyScanOptions(extensions, extensionType, { includeInvalid: scanOptions.includeInvalid, pickLatest: true });271}272273async scanMultipleExtensions(extensionLocations: URI[], extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension[]> {274const extensions: IRelaxedScannedExtension[] = [];275await Promise.all(extensionLocations.map(async extensionLocation => {276const scannedExtensions = await this.scanOneOrMultipleExtensions(extensionLocation, extensionType, scanOptions);277extensions.push(...scannedExtensions);278}));279return this.applyScanOptions(extensions, extensionType, { includeInvalid: scanOptions.includeInvalid, pickLatest: true });280}281282async updateManifestMetadata(extensionLocation: URI, metaData: ManifestMetadata): Promise<void> {283const manifestLocation = joinPath(extensionLocation, 'package.json');284const content = (await this.fileService.readFile(manifestLocation)).value.toString();285const manifest: IScannedExtensionManifest = JSON.parse(content);286manifest.__metadata = { ...manifest.__metadata, ...metaData };287288await this.fileService.writeFile(joinPath(extensionLocation, 'package.json'), VSBuffer.fromString(JSON.stringify(manifest, null, '\t')));289}290291async initializeDefaultProfileExtensions(): Promise<void> {292try {293await this.extensionsProfileScannerService.scanProfileExtensions(this.userDataProfilesService.defaultProfile.extensionsResource, { bailOutWhenFileNotFound: true });294} catch (error) {295if (error instanceof ExtensionsProfileScanningError && error.code === ExtensionsProfileScanningErrorCode.ERROR_PROFILE_NOT_FOUND) {296await this.doInitializeDefaultProfileExtensions();297} else {298throw error;299}300}301}302303private initializeDefaultProfileExtensionsPromise: Promise<void> | undefined = undefined;304private async doInitializeDefaultProfileExtensions(): Promise<void> {305if (!this.initializeDefaultProfileExtensionsPromise) {306this.initializeDefaultProfileExtensionsPromise = (async () => {307try {308this.logService.info('Started initializing default profile extensions in extensions installation folder.', this.userExtensionsLocation.toString());309const userExtensions = await this.scanAllUserExtensions({ includeInvalid: true });310if (userExtensions.length) {311await this.extensionsProfileScannerService.addExtensionsToProfile(userExtensions.map(e => [e, e.metadata]), this.userDataProfilesService.defaultProfile.extensionsResource);312} else {313try {314await this.fileService.createFile(this.userDataProfilesService.defaultProfile.extensionsResource, VSBuffer.fromString(JSON.stringify([])));315} catch (error) {316if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {317this.logService.warn('Failed to create default profile extensions manifest in extensions installation folder.', this.userExtensionsLocation.toString(), getErrorMessage(error));318}319}320}321this.logService.info('Completed initializing default profile extensions in extensions installation folder.', this.userExtensionsLocation.toString());322} catch (error) {323this.logService.error(error);324} finally {325this.initializeDefaultProfileExtensionsPromise = undefined;326}327})();328}329return this.initializeDefaultProfileExtensionsPromise;330}331332private async applyScanOptions(extensions: IRelaxedScannedExtension[], type: ExtensionType | 'development', scanOptions: { includeAllVersions?: boolean; includeInvalid?: boolean; pickLatest?: boolean } = {}): Promise<IRelaxedScannedExtension[]> {333if (!scanOptions.includeAllVersions) {334extensions = this.dedupExtensions(type === ExtensionType.System ? extensions : undefined, type === ExtensionType.User ? extensions : undefined, type === 'development' ? extensions : undefined, await this.getTargetPlatform(), !!scanOptions.pickLatest);335}336if (!scanOptions.includeInvalid) {337extensions = extensions.filter(extension => extension.isValid);338}339return extensions.sort((a, b) => {340const aLastSegment = path.basename(a.location.fsPath);341const bLastSegment = path.basename(b.location.fsPath);342if (aLastSegment < bLastSegment) {343return -1;344}345if (aLastSegment > bLastSegment) {346return 1;347}348return 0;349});350}351352private dedupExtensions(system: IScannedExtension[] | undefined, user: IScannedExtension[] | undefined, development: IScannedExtension[] | undefined, targetPlatform: TargetPlatform, pickLatest: boolean): IScannedExtension[] {353const pick = (existing: IScannedExtension, extension: IScannedExtension, isDevelopment: boolean): boolean => {354if (!isDevelopment) {355if (existing.metadata?.isApplicationScoped && !extension.metadata?.isApplicationScoped) {356return false;357}358if (!existing.metadata?.isApplicationScoped && extension.metadata?.isApplicationScoped) {359return true;360}361}362if (existing.isValid && !extension.isValid) {363return false;364}365if (existing.isValid === extension.isValid) {366if (pickLatest && semver.gt(existing.manifest.version, extension.manifest.version)) {367this.logService.debug(`Skipping extension ${extension.location.path} with lower version ${extension.manifest.version} in favour of ${existing.location.path} with version ${existing.manifest.version}`);368return false;369}370if (semver.eq(existing.manifest.version, extension.manifest.version)) {371if (existing.type === ExtensionType.System) {372this.logService.debug(`Skipping extension ${extension.location.path} in favour of system extension ${existing.location.path} with same version`);373return false;374}375if (existing.targetPlatform === targetPlatform) {376this.logService.debug(`Skipping extension ${extension.location.path} from different target platform ${extension.targetPlatform}`);377return false;378}379}380}381if (isDevelopment) {382this.logService.warn(`Overwriting user extension ${existing.location.path} with ${extension.location.path}.`);383} else {384this.logService.debug(`Overwriting user extension ${existing.location.path} with ${extension.location.path}.`);385}386return true;387};388const result = new ExtensionIdentifierMap<IScannedExtension>();389system?.forEach((extension) => {390const existing = result.get(extension.identifier.id);391if (!existing || pick(existing, extension, false)) {392result.set(extension.identifier.id, extension);393}394});395user?.forEach((extension) => {396const existing = result.get(extension.identifier.id);397if (!existing && system && extension.type === ExtensionType.System) {398this.logService.debug(`Skipping obsolete system extension ${extension.location.path}.`);399return;400}401if (!existing || pick(existing, extension, false)) {402result.set(extension.identifier.id, extension);403}404});405development?.forEach(extension => {406const existing = result.get(extension.identifier.id);407if (!existing || pick(existing, extension, true)) {408result.set(extension.identifier.id, extension);409}410result.set(extension.identifier.id, extension);411});412return [...result.values()];413}414415private async scanDefaultSystemExtensions(language: string | undefined): Promise<IRelaxedScannedExtension[]> {416this.logService.trace('Started scanning system extensions');417const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, language, true, undefined, this.getProductVersion());418const extensionsScanner = extensionsScannerInput.devMode ? this.extensionsScanner : this.systemExtensionsCachedScanner;419const result = await extensionsScanner.scanExtensions(extensionsScannerInput);420this.logService.trace('Scanned system extensions:', result.length);421return result;422}423424private async scanDevSystemExtensions(language: string | undefined, checkControlFile: boolean): Promise<IRelaxedScannedExtension[]> {425const devSystemExtensionsList = this.environmentService.isBuilt ? [] : this.productService.builtInExtensions;426if (!devSystemExtensionsList?.length) {427return [];428}429430this.logService.trace('Started scanning dev system extensions');431const builtinExtensionControl = checkControlFile ? await this.getBuiltInExtensionControl() : {};432const devSystemExtensionsLocations: URI[] = [];433const devSystemExtensionsLocation = URI.file(path.normalize(path.join(FileAccess.asFileUri('').fsPath, '..', '.build', 'builtInExtensions')));434for (const extension of devSystemExtensionsList) {435const controlState = builtinExtensionControl[extension.name] || 'marketplace';436switch (controlState) {437case 'disabled':438break;439case 'marketplace':440devSystemExtensionsLocations.push(joinPath(devSystemExtensionsLocation, extension.name));441break;442default:443devSystemExtensionsLocations.push(URI.file(controlState));444break;445}446}447const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, language, true, undefined, this.getProductVersion())))));448this.logService.trace('Scanned dev system extensions:', result.length);449return coalesce(result);450}451452private async getBuiltInExtensionControl(): Promise<IBuiltInExtensionControl> {453try {454const content = await this.fileService.readFile(this.extensionsControlLocation);455return JSON.parse(content.value.toString());456} catch (error) {457return {};458}459}460461private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined, productVersion: IProductVersion): Promise<ExtensionScannerInput> {462const translations = await this.getTranslations(language ?? platform.language);463const mtime = await this.getMtime(location);464const applicationExtensionsLocation = profile && !this.uriIdentityService.extUri.isEqual(location, this.userDataProfilesService.defaultProfile.extensionsResource) ? this.userDataProfilesService.defaultProfile.extensionsResource : undefined;465const applicationExtensionsLocationMtime = applicationExtensionsLocation ? await this.getMtime(applicationExtensionsLocation) : undefined;466return new ExtensionScannerInput(467location,468mtime,469applicationExtensionsLocation,470applicationExtensionsLocationMtime,471profile,472profileScanOptions,473type,474validate,475productVersion.version,476productVersion.date,477this.productService.commit,478!this.environmentService.isBuilt,479language,480translations,481);482}483484private async getMtime(location: URI): Promise<number | undefined> {485try {486const stat = await this.fileService.stat(location);487if (typeof stat.mtime === 'number') {488return stat.mtime;489}490} catch (err) {491// That's ok...492}493return undefined;494}495496private getProductVersion(): IProductVersion {497return {498version: this.productService.version,499date: this.productService.date,500};501}502503}504505export class ExtensionScannerInput {506507constructor(508public readonly location: URI,509public readonly mtime: number | undefined,510public readonly applicationExtensionslocation: URI | undefined,511public readonly applicationExtensionslocationMtime: number | undefined,512public readonly profile: boolean,513public readonly profileScanOptions: IProfileExtensionsScanOptions | undefined,514public readonly type: ExtensionType,515public readonly validate: boolean,516public readonly productVersion: string,517public readonly productDate: string | undefined,518public readonly productCommit: string | undefined,519public readonly devMode: boolean,520public readonly language: string | undefined,521public readonly translations: Translations522) {523// Keep empty!! (JSON.parse)524}525526public static createNlsConfiguration(input: ExtensionScannerInput): NlsConfiguration {527return {528language: input.language,529pseudo: input.language === 'pseudo',530devMode: input.devMode,531translations: input.translations532};533}534535public static equals(a: ExtensionScannerInput, b: ExtensionScannerInput): boolean {536return (537isEqual(a.location, b.location)538&& a.mtime === b.mtime539&& isEqual(a.applicationExtensionslocation, b.applicationExtensionslocation)540&& a.applicationExtensionslocationMtime === b.applicationExtensionslocationMtime541&& a.profile === b.profile542&& objects.equals(a.profileScanOptions, b.profileScanOptions)543&& a.type === b.type544&& a.validate === b.validate545&& a.productVersion === b.productVersion546&& a.productDate === b.productDate547&& a.productCommit === b.productCommit548&& a.devMode === b.devMode549&& a.language === b.language550&& Translations.equals(a.translations, b.translations)551);552}553}554555type NlsConfiguration = {556language: string | undefined;557pseudo: boolean;558devMode: boolean;559translations: Translations;560};561562class ExtensionsScanner extends Disposable {563564private readonly extensionsEnabledWithApiProposalVersion: string[];565566constructor(567@IExtensionsProfileScannerService protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService,568@IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,569@IFileService protected readonly fileService: IFileService,570@IProductService productService: IProductService,571@IEnvironmentService private readonly environmentService: IEnvironmentService,572@ILogService protected readonly logService: ILogService573) {574super();575this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? [];576}577578async scanExtensions(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {579return input.profile580? this.scanExtensionsFromProfile(input)581: this.scanExtensionsFromLocation(input);582}583584private async scanExtensionsFromLocation(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {585const stat = await this.fileService.resolve(input.location);586if (!stat.children?.length) {587return [];588}589const extensions = await Promise.all<IRelaxedScannedExtension | null>(590stat.children.map(async c => {591if (!c.isDirectory) {592return null;593}594// Do not consider user extension folder starting with `.`595if (input.type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) {596return null;597}598const extensionScannerInput = new ExtensionScannerInput(c.resource, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations);599return this.scanExtension(extensionScannerInput);600}));601return coalesce(extensions)602// Sort: Make sure extensions are in the same order always. Helps cache invalidation even if the order changes.603.sort((a, b) => a.location.path < b.location.path ? -1 : 1);604}605606private async scanExtensionsFromProfile(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {607let profileExtensions = await this.scanExtensionsFromProfileResource(input.location, () => true, input);608if (input.applicationExtensionslocation && !this.uriIdentityService.extUri.isEqual(input.location, input.applicationExtensionslocation)) {609profileExtensions = profileExtensions.filter(e => !e.metadata?.isApplicationScoped);610const applicationExtensions = await this.scanExtensionsFromProfileResource(input.applicationExtensionslocation, (e) => !!e.metadata?.isBuiltin || !!e.metadata?.isApplicationScoped, input);611profileExtensions.push(...applicationExtensions);612}613return profileExtensions;614}615616private async scanExtensionsFromProfileResource(profileResource: URI, filter: (extensionInfo: IScannedProfileExtension) => boolean, input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {617const scannedProfileExtensions = await this.extensionsProfileScannerService.scanProfileExtensions(profileResource, input.profileScanOptions);618if (!scannedProfileExtensions.length) {619return [];620}621const extensions = await Promise.all<IRelaxedScannedExtension | null>(622scannedProfileExtensions.map(async extensionInfo => {623if (filter(extensionInfo)) {624const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations);625return this.scanExtension(extensionScannerInput, extensionInfo);626}627return null;628}));629return coalesce(extensions);630}631632async scanOneOrMultipleExtensions(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {633try {634if (await this.fileService.exists(joinPath(input.location, 'package.json'))) {635const extension = await this.scanExtension(input);636return extension ? [extension] : [];637} else {638return await this.scanExtensions(input);639}640} catch (error) {641this.logService.error(`Error scanning extensions at ${input.location.path}:`, getErrorMessage(error));642return [];643}644}645646async scanExtension(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension | null>;647async scanExtension(input: ExtensionScannerInput, scannedProfileExtension: IScannedProfileExtension): Promise<IRelaxedScannedExtension>;648async scanExtension(input: ExtensionScannerInput, scannedProfileExtension?: IScannedProfileExtension): Promise<IRelaxedScannedExtension | null> {649const validations: [Severity, string][] = [];650let isValid = true;651let manifest: IScannedExtensionManifest;652try {653manifest = await this.scanExtensionManifest(input.location);654} catch (e) {655if (scannedProfileExtension) {656validations.push([Severity.Error, getErrorMessage(e)]);657isValid = false;658const [publisher, name] = scannedProfileExtension.identifier.id.split('.');659manifest = {660name,661publisher,662version: scannedProfileExtension.version,663engines: { vscode: '' }664};665} else {666if (input.type !== ExtensionType.System) {667this.logService.error(e);668}669return null;670}671}672673// allow publisher to be undefined to make the initial extension authoring experience smoother674if (!manifest.publisher) {675manifest.publisher = UNDEFINED_PUBLISHER;676}677678let metadata: Metadata | undefined;679if (scannedProfileExtension) {680metadata = {681...scannedProfileExtension.metadata,682size: manifest.__metadata?.size,683};684} else if (manifest.__metadata) {685metadata = {686installedTimestamp: manifest.__metadata.installedTimestamp,687size: manifest.__metadata.size,688targetPlatform: manifest.__metadata.targetPlatform,689};690}691692delete manifest.__metadata;693const id = getGalleryExtensionId(manifest.publisher, manifest.name);694const identifier = metadata?.id ? { id, uuid: metadata.id } : { id };695const type = metadata?.isSystem ? ExtensionType.System : input.type;696const isBuiltin = type === ExtensionType.System || !!metadata?.isBuiltin;697try {698manifest = await this.translateManifest(input.location, manifest, ExtensionScannerInput.createNlsConfiguration(input));699} catch (error) {700this.logService.warn('Failed to translate manifest', getErrorMessage(error));701}702let extension: IRelaxedScannedExtension = {703type,704identifier,705manifest,706location: input.location,707isBuiltin,708targetPlatform: metadata?.targetPlatform ?? TargetPlatform.UNDEFINED,709publisherDisplayName: metadata?.publisherDisplayName,710metadata,711isValid,712validations,713preRelease: !!metadata?.preRelease,714};715if (input.validate) {716extension = this.validate(extension, input);717}718if (manifest.enabledApiProposals && (!this.environmentService.isBuilt || this.extensionsEnabledWithApiProposalVersion.includes(id.toLowerCase()))) {719manifest.originalEnabledApiProposals = manifest.enabledApiProposals;720manifest.enabledApiProposals = parseEnabledApiProposalNames([...manifest.enabledApiProposals]);721}722return extension;723}724725validate(extension: IRelaxedScannedExtension, input: ExtensionScannerInput): IRelaxedScannedExtension {726let isValid = extension.isValid;727const validateApiVersion = this.environmentService.isBuilt && this.extensionsEnabledWithApiProposalVersion.includes(extension.identifier.id.toLowerCase());728const validations = validateExtensionManifest(input.productVersion, input.productDate, input.location, extension.manifest, extension.isBuiltin, validateApiVersion);729for (const [severity, message] of validations) {730if (severity === Severity.Error) {731isValid = false;732this.logService.error(this.formatMessage(input.location, message));733}734}735extension.isValid = isValid;736extension.validations = [...extension.validations, ...validations];737return extension;738}739740private async scanExtensionManifest(extensionLocation: URI): Promise<IScannedExtensionManifest> {741const manifestLocation = joinPath(extensionLocation, 'package.json');742let content;743try {744content = (await this.fileService.readFile(manifestLocation)).value.toString();745} catch (error) {746if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {747this.logService.error(this.formatMessage(extensionLocation, localize('fileReadFail', "Cannot read file {0}: {1}.", manifestLocation.path, error.message)));748}749throw error;750}751let manifest: IScannedExtensionManifest;752try {753manifest = JSON.parse(content);754} catch (err) {755// invalid JSON, let's get good errors756const errors: ParseError[] = [];757parse(content, errors);758for (const e of errors) {759this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseFail', "Failed to parse {0}: [{1}, {2}] {3}.", manifestLocation.path, e.offset, e.length, getParseErrorMessage(e.error))));760}761throw err;762}763if (getNodeType(manifest) !== 'object') {764const errorMessage = this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not a JSON object.", manifestLocation.path));765this.logService.error(errorMessage);766throw new Error(errorMessage);767}768return manifest;769}770771private async translateManifest(extensionLocation: URI, extensionManifest: IExtensionManifest, nlsConfiguration: NlsConfiguration): Promise<IExtensionManifest> {772const localizedMessages = await this.getLocalizedMessages(extensionLocation, extensionManifest, nlsConfiguration);773if (localizedMessages) {774try {775const errors: ParseError[] = [];776// resolveOriginalMessageBundle returns null if localizedMessages.default === undefined;777const defaults = await this.resolveOriginalMessageBundle(localizedMessages.default, errors);778if (errors.length > 0) {779errors.forEach((error) => {780this.logService.error(this.formatMessage(extensionLocation, localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localizedMessages.default?.path, getParseErrorMessage(error.error))));781});782return extensionManifest;783} else if (getNodeType(localizedMessages) !== 'object') {784this.logService.error(this.formatMessage(extensionLocation, localize('jsonInvalidFormat', "Invalid format {0}: JSON object expected.", localizedMessages.default?.path)));785return extensionManifest;786}787const localized = localizedMessages.values || Object.create(null);788return localizeManifest(this.logService, extensionManifest, localized, defaults);789} catch (error) {790/*Ignore Error*/791}792}793return extensionManifest;794}795796private async getLocalizedMessages(extensionLocation: URI, extensionManifest: IExtensionManifest, nlsConfiguration: NlsConfiguration): Promise<LocalizedMessages | undefined> {797const defaultPackageNLS = joinPath(extensionLocation, 'package.nls.json');798const reportErrors = (localized: URI | null, errors: ParseError[]): void => {799errors.forEach((error) => {800this.logService.error(this.formatMessage(extensionLocation, localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localized?.path, getParseErrorMessage(error.error))));801});802};803const reportInvalidFormat = (localized: URI | null): void => {804this.logService.error(this.formatMessage(extensionLocation, localize('jsonInvalidFormat', "Invalid format {0}: JSON object expected.", localized?.path)));805};806807const translationId = `${extensionManifest.publisher}.${extensionManifest.name}`;808const translationPath = nlsConfiguration.translations[translationId];809810if (translationPath) {811try {812const translationResource = URI.file(translationPath);813const content = (await this.fileService.readFile(translationResource)).value.toString();814const errors: ParseError[] = [];815const translationBundle: TranslationBundle = parse(content, errors);816if (errors.length > 0) {817reportErrors(translationResource, errors);818return { values: undefined, default: defaultPackageNLS };819} else if (getNodeType(translationBundle) !== 'object') {820reportInvalidFormat(translationResource);821return { values: undefined, default: defaultPackageNLS };822} else {823const values = translationBundle.contents ? translationBundle.contents.package : undefined;824return { values: values, default: defaultPackageNLS };825}826} catch (error) {827return { values: undefined, default: defaultPackageNLS };828}829} else {830const exists = await this.fileService.exists(defaultPackageNLS);831if (!exists) {832return undefined;833}834let messageBundle;835try {836messageBundle = await this.findMessageBundles(extensionLocation, nlsConfiguration);837} catch (error) {838return undefined;839}840if (!messageBundle.localized) {841return { values: undefined, default: messageBundle.original };842}843try {844const messageBundleContent = (await this.fileService.readFile(messageBundle.localized)).value.toString();845const errors: ParseError[] = [];846const messages: MessageBag = parse(messageBundleContent, errors);847if (errors.length > 0) {848reportErrors(messageBundle.localized, errors);849return { values: undefined, default: messageBundle.original };850} else if (getNodeType(messages) !== 'object') {851reportInvalidFormat(messageBundle.localized);852return { values: undefined, default: messageBundle.original };853}854return { values: messages, default: messageBundle.original };855} catch (error) {856return { values: undefined, default: messageBundle.original };857}858}859}860861/**862* Parses original message bundle, returns null if the original message bundle is null.863*/864private async resolveOriginalMessageBundle(originalMessageBundle: URI | null, errors: ParseError[]): Promise<{ [key: string]: string } | undefined> {865if (originalMessageBundle) {866try {867const originalBundleContent = (await this.fileService.readFile(originalMessageBundle)).value.toString();868return parse(originalBundleContent, errors);869} catch (error) {870/* Ignore Error */871}872}873return;874}875876/**877* Finds localized message bundle and the original (unlocalized) one.878* If the localized file is not present, returns null for the original and marks original as localized.879*/880private findMessageBundles(extensionLocation: URI, nlsConfiguration: NlsConfiguration): Promise<{ localized: URI; original: URI | null }> {881return new Promise<{ localized: URI; original: URI | null }>((c, e) => {882const loop = (locale: string): void => {883const toCheck = joinPath(extensionLocation, `package.nls.${locale}.json`);884this.fileService.exists(toCheck).then(exists => {885if (exists) {886c({ localized: toCheck, original: joinPath(extensionLocation, 'package.nls.json') });887}888const index = locale.lastIndexOf('-');889if (index === -1) {890c({ localized: joinPath(extensionLocation, 'package.nls.json'), original: null });891} else {892locale = locale.substring(0, index);893loop(locale);894}895});896};897if (nlsConfiguration.devMode || nlsConfiguration.pseudo || !nlsConfiguration.language) {898return c({ localized: joinPath(extensionLocation, 'package.nls.json'), original: null });899}900loop(nlsConfiguration.language);901});902}903904private formatMessage(extensionLocation: URI, message: string): string {905return `[${extensionLocation.path}]: ${message}`;906}907908}909910interface IExtensionCacheData {911input: ExtensionScannerInput;912result: IRelaxedScannedExtension[];913}914915class CachedExtensionsScanner extends ExtensionsScanner {916917private input: ExtensionScannerInput | undefined;918private readonly cacheValidatorThrottler: ThrottledDelayer<void> = this._register(new ThrottledDelayer(3000));919920private readonly _onDidChangeCache = this._register(new Emitter<void>());921readonly onDidChangeCache = this._onDidChangeCache.event;922923constructor(924private readonly currentProfile: IUserDataProfile,925@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,926@IExtensionsProfileScannerService extensionsProfileScannerService: IExtensionsProfileScannerService,927@IUriIdentityService uriIdentityService: IUriIdentityService,928@IFileService fileService: IFileService,929@IProductService productService: IProductService,930@IEnvironmentService environmentService: IEnvironmentService,931@ILogService logService: ILogService932) {933super(extensionsProfileScannerService, uriIdentityService, fileService, productService, environmentService, logService);934}935936override async scanExtensions(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {937const cacheFile = this.getCacheFile(input);938const cacheContents = await this.readExtensionCache(cacheFile);939this.input = input;940if (cacheContents && cacheContents.input && ExtensionScannerInput.equals(cacheContents.input, this.input)) {941this.logService.debug('Using cached extensions scan result', input.type === ExtensionType.System ? 'system' : 'user', input.location.toString());942this.cacheValidatorThrottler.trigger(() => this.validateCache());943return cacheContents.result.map((extension) => {944// revive URI object945extension.location = URI.revive(extension.location);946return extension;947});948}949const result = await super.scanExtensions(input);950await this.writeExtensionCache(cacheFile, { input, result });951return result;952}953954private async readExtensionCache(cacheFile: URI): Promise<IExtensionCacheData | null> {955try {956const cacheRawContents = await this.fileService.readFile(cacheFile);957const extensionCacheData: IExtensionCacheData = JSON.parse(cacheRawContents.value.toString());958return { result: extensionCacheData.result, input: revive(extensionCacheData.input) };959} catch (error) {960this.logService.debug('Error while reading the extension cache file:', cacheFile.path, getErrorMessage(error));961}962return null;963}964965private async writeExtensionCache(cacheFile: URI, cacheContents: IExtensionCacheData): Promise<void> {966try {967await this.fileService.writeFile(cacheFile, VSBuffer.fromString(JSON.stringify(cacheContents)));968} catch (error) {969this.logService.debug('Error while writing the extension cache file:', cacheFile.path, getErrorMessage(error));970}971}972973private async validateCache(): Promise<void> {974if (!this.input) {975// Input has been unset by the time we get here, so skip validation976return;977}978979const cacheFile = this.getCacheFile(this.input);980const cacheContents = await this.readExtensionCache(cacheFile);981if (!cacheContents) {982// Cache has been deleted by someone else, which is perfectly fine...983return;984}985986const actual = cacheContents.result;987const expected = JSON.parse(JSON.stringify(await super.scanExtensions(this.input)));988if (objects.equals(expected, actual)) {989// Cache is valid and running with it is perfectly fine...990return;991}992993try {994this.logService.info('Invalidating Cache', actual, expected);995// Cache is invalid, delete it996await this.fileService.del(cacheFile);997this._onDidChangeCache.fire();998} catch (error) {999this.logService.error(error);1000}1001}10021003private getCacheFile(input: ExtensionScannerInput): URI {1004const profile = this.getProfile(input);1005return this.uriIdentityService.extUri.joinPath(profile.cacheHome, input.type === ExtensionType.System ? BUILTIN_MANIFEST_CACHE_FILE : USER_MANIFEST_CACHE_FILE);1006}10071008private getProfile(input: ExtensionScannerInput): IUserDataProfile {1009if (input.type === ExtensionType.System) {1010return this.userDataProfilesService.defaultProfile;1011}1012if (!input.profile) {1013return this.userDataProfilesService.defaultProfile;1014}1015if (this.uriIdentityService.extUri.isEqual(input.location, this.currentProfile.extensionsResource)) {1016return this.currentProfile;1017}1018return this.userDataProfilesService.profiles.find(p => this.uriIdentityService.extUri.isEqual(input.location, p.extensionsResource)) ?? this.currentProfile;1019}10201021}10221023export function toExtensionDescription(extension: IScannedExtension, isUnderDevelopment: boolean): IExtensionDescription {1024const id = getExtensionId(extension.manifest.publisher, extension.manifest.name);1025return {1026id,1027identifier: new ExtensionIdentifier(id),1028isBuiltin: extension.type === ExtensionType.System,1029isUserBuiltin: extension.type === ExtensionType.User && extension.isBuiltin,1030isUnderDevelopment,1031extensionLocation: extension.location,1032uuid: extension.identifier.uuid,1033targetPlatform: extension.targetPlatform,1034publisherDisplayName: extension.publisherDisplayName,1035preRelease: extension.preRelease,1036...extension.manifest,1037};1038}10391040export class NativeExtensionsScannerService extends AbstractExtensionsScannerService implements IExtensionsScannerService {10411042private readonly translationsPromise: Promise<Translations>;10431044constructor(1045systemExtensionsLocation: URI,1046userExtensionsLocation: URI,1047userHome: URI,1048currentProfile: IUserDataProfile,1049userDataProfilesService: IUserDataProfilesService,1050extensionsProfileScannerService: IExtensionsProfileScannerService,1051fileService: IFileService,1052logService: ILogService,1053environmentService: IEnvironmentService,1054productService: IProductService,1055uriIdentityService: IUriIdentityService,1056instantiationService: IInstantiationService,1057) {1058super(1059systemExtensionsLocation,1060userExtensionsLocation,1061joinPath(userHome, '.vscode-oss-dev', 'extensions', 'control.json'),1062currentProfile,1063userDataProfilesService, extensionsProfileScannerService, fileService, logService, environmentService, productService, uriIdentityService, instantiationService);1064this.translationsPromise = (async () => {1065if (platform.translationsConfigFile) {1066try {1067const content = await this.fileService.readFile(URI.file(platform.translationsConfigFile));1068return JSON.parse(content.value.toString());1069} catch (err) { /* Ignore Error */ }1070}1071return Object.create(null);1072})();1073}10741075protected getTranslations(language: string): Promise<Translations> {1076return this.translationsPromise;1077}10781079}108010811082