/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { isAbsolute, join, resolve } from '../../base/common/path.js'; import * as platform from '../../base/common/platform.js'; import { cwd } from '../../base/common/process.js'; import { URI } from '../../base/common/uri.js'; import * as performance from '../../base/common/performance.js'; import { Event } from '../../base/common/event.js'; import { IURITransformer, transformOutgoingURIs } from '../../base/common/uriIpc.js'; import { IServerChannel } from '../../base/parts/ipc/common/ipc.js'; import { ContextKeyDefinedExpr, ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyExpression, ContextKeyGreaterEqualsExpr, ContextKeyGreaterExpr, ContextKeyInExpr, ContextKeyNotEqualsExpr, ContextKeyNotExpr, ContextKeyNotInExpr, ContextKeyRegexExpr, ContextKeySmallerEqualsExpr, ContextKeySmallerExpr, IContextKeyExprMapper } from '../../platform/contextkey/common/contextkey.js'; import { IExtensionGalleryService, InstallOptions } from '../../platform/extensionManagement/common/extensionManagement.js'; import { ExtensionManagementCLI } from '../../platform/extensionManagement/common/extensionManagementCLI.js'; import { IExtensionsScannerService, toExtensionDescription } from '../../platform/extensionManagement/common/extensionsScannerService.js'; import { ExtensionType, IExtensionDescription } from '../../platform/extensions/common/extensions.js'; import { ILogService } from '../../platform/log/common/log.js'; import { IUserDataProfilesService } from '../../platform/userDataProfile/common/userDataProfile.js'; import { IServerEnvironmentService } from './serverEnvironmentService.js'; import { dedupExtensions } from '../../workbench/services/extensions/common/extensionsUtil.js'; import { Schemas } from '../../base/common/network.js'; import { IRemoteExtensionsScannerService } from '../../platform/remote/common/remoteExtensionsScanner.js'; import { ILanguagePackService } from '../../platform/languagePacks/common/languagePacks.js'; export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService { readonly _serviceBrand: undefined; private readonly _whenBuiltinExtensionsReady = Promise.resolve(); private readonly _whenExtensionsReady = Promise.resolve(); constructor( private readonly _extensionManagementCLI: ExtensionManagementCLI, environmentService: IServerEnvironmentService, private readonly _userDataProfilesService: IUserDataProfilesService, private readonly _extensionsScannerService: IExtensionsScannerService, private readonly _logService: ILogService, private readonly _extensionGalleryService: IExtensionGalleryService, private readonly _languagePackService: ILanguagePackService ) { const builtinExtensionsToInstall = environmentService.args['install-builtin-extension']; if (builtinExtensionsToInstall) { _logService.trace('Installing builtin extensions passed via args...'); const installOptions: InstallOptions = { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] }; performance.mark('code/server/willInstallBuiltinExtensions'); this._whenExtensionsReady = this._whenBuiltinExtensionsReady = _extensionManagementCLI.installExtensions([], this._asExtensionIdOrVSIX(builtinExtensionsToInstall), installOptions, !!environmentService.args['force']) .then(() => { performance.mark('code/server/didInstallBuiltinExtensions'); _logService.trace('Finished installing builtin extensions'); }, error => { _logService.error(error); }); } const extensionsToInstall = environmentService.args['install-extension']; if (extensionsToInstall) { _logService.trace('Installing extensions passed via args...'); this._whenExtensionsReady = this._whenBuiltinExtensionsReady .then(() => _extensionManagementCLI.installExtensions(this._asExtensionIdOrVSIX(extensionsToInstall), [], { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'], isApplicationScoped: true // extensions installed during server startup are available to all profiles }, !!environmentService.args['force'])) .then(() => { _logService.trace('Finished installing extensions'); }, error => { _logService.error(error); }); } } private _asExtensionIdOrVSIX(inputs: string[]): (string | URI)[] { return inputs.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input); } whenExtensionsReady(): Promise { return this._whenExtensionsReady; } async scanExtensions( language?: string, profileLocation?: URI, workspaceExtensionLocations?: URI[], extensionDevelopmentLocations?: URI[], languagePackId?: string ): Promise { performance.mark('code/server/willScanExtensions'); this._logService.trace(`Scanning extensions using UI language: ${language}`); await this._whenBuiltinExtensionsReady; const extensionDevelopmentPaths = extensionDevelopmentLocations ? extensionDevelopmentLocations.filter(url => url.scheme === Schemas.file).map(url => url.fsPath) : undefined; profileLocation = profileLocation ?? this._userDataProfilesService.defaultProfile.extensionsResource; const extensions = await this._scanExtensions(profileLocation, language ?? platform.language, workspaceExtensionLocations, extensionDevelopmentPaths, languagePackId); this._logService.trace('Scanned Extensions', extensions); this._massageWhenConditions(extensions); performance.mark('code/server/didScanExtensions'); return extensions; } private async _scanExtensions(profileLocation: URI, language: string, workspaceInstalledExtensionLocations: URI[] | undefined, extensionDevelopmentPath: string[] | undefined, languagePackId: string | undefined): Promise { await this._ensureLanguagePackIsInstalled(language, languagePackId); const [builtinExtensions, installedExtensions, workspaceInstalledExtensions, developedExtensions] = await Promise.all([ this._scanBuiltinExtensions(language), this._scanInstalledExtensions(profileLocation, language), this._scanWorkspaceInstalledExtensions(language, workspaceInstalledExtensionLocations), this._scanDevelopedExtensions(language, extensionDevelopmentPath) ]); return dedupExtensions(builtinExtensions, installedExtensions, workspaceInstalledExtensions, developedExtensions, this._logService); } private async _scanDevelopedExtensions(language: string, extensionDevelopmentPaths?: string[]): Promise { if (extensionDevelopmentPaths) { return (await Promise.all(extensionDevelopmentPaths.map(extensionDevelopmentPath => this._extensionsScannerService.scanOneOrMultipleExtensions(URI.file(resolve(extensionDevelopmentPath)), ExtensionType.User, { language })))) .flat() .map(e => toExtensionDescription(e, true)); } return []; } private async _scanWorkspaceInstalledExtensions(language: string, workspaceInstalledExtensions?: URI[]): Promise { const result: IExtensionDescription[] = []; if (workspaceInstalledExtensions?.length) { const scannedExtensions = await Promise.all(workspaceInstalledExtensions.map(location => this._extensionsScannerService.scanExistingExtension(location, ExtensionType.User, { language }))); for (const scannedExtension of scannedExtensions) { if (scannedExtension) { result.push(toExtensionDescription(scannedExtension, false)); } } } return result; } private async _scanBuiltinExtensions(language: string): Promise { const scannedExtensions = await this._extensionsScannerService.scanSystemExtensions({ language }); return scannedExtensions.map(e => toExtensionDescription(e, false)); } private async _scanInstalledExtensions(profileLocation: URI, language: string): Promise { const scannedExtensions = await this._extensionsScannerService.scanUserExtensions({ profileLocation, language, useCache: true }); return scannedExtensions.map(e => toExtensionDescription(e, false)); } private async _ensureLanguagePackIsInstalled(language: string, languagePackId: string | undefined): Promise { if ( // No need to install language packs for the default language language === platform.LANGUAGE_DEFAULT || // The extension gallery service needs to be available !this._extensionGalleryService.isEnabled() ) { return; } try { const installed = await this._languagePackService.getInstalledLanguages(); if (installed.find(p => p.id === language)) { this._logService.trace(`Language Pack ${language} is already installed. Skipping language pack installation.`); return; } } catch (err) { // We tried to see what is installed but failed. We can try installing anyway. this._logService.error(err); } if (!languagePackId) { this._logService.trace(`No language pack id provided for language ${language}. Skipping language pack installation.`); return; } this._logService.trace(`Language Pack ${languagePackId} for language ${language} is not installed. It will be installed now.`); try { await this._extensionManagementCLI.installExtensions([languagePackId], [], { isMachineScoped: true }, true); } catch (err) { // We tried to install the language pack but failed. We can continue without it thus using the default language. this._logService.error(err); } } private _massageWhenConditions(extensions: IExtensionDescription[]): void { // Massage "when" conditions which mention `resourceScheme` interface WhenUser { when?: string } interface LocWhenUser { [loc: string]: WhenUser[] } const _mapResourceSchemeValue = (value: string, isRegex: boolean): string => { // console.log(`_mapResourceSchemeValue: ${value}, ${isRegex}`); return value.replace(/file/g, 'vscode-remote'); }; const _mapResourceRegExpValue = (value: RegExp): RegExp => { let flags = ''; flags += value.global ? 'g' : ''; flags += value.ignoreCase ? 'i' : ''; flags += value.multiline ? 'm' : ''; return new RegExp(_mapResourceSchemeValue(value.source, true), flags); }; const _exprKeyMapper = new class implements IContextKeyExprMapper { mapDefined(key: string): ContextKeyExpression { return ContextKeyDefinedExpr.create(key); } mapNot(key: string): ContextKeyExpression { return ContextKeyNotExpr.create(key); } mapEquals(key: string, value: any): ContextKeyExpression { if (key === 'resourceScheme' && typeof value === 'string') { return ContextKeyEqualsExpr.create(key, _mapResourceSchemeValue(value, false)); } else { return ContextKeyEqualsExpr.create(key, value); } } mapNotEquals(key: string, value: any): ContextKeyExpression { if (key === 'resourceScheme' && typeof value === 'string') { return ContextKeyNotEqualsExpr.create(key, _mapResourceSchemeValue(value, false)); } else { return ContextKeyNotEqualsExpr.create(key, value); } } mapGreater(key: string, value: any): ContextKeyExpression { return ContextKeyGreaterExpr.create(key, value); } mapGreaterEquals(key: string, value: any): ContextKeyExpression { return ContextKeyGreaterEqualsExpr.create(key, value); } mapSmaller(key: string, value: any): ContextKeyExpression { return ContextKeySmallerExpr.create(key, value); } mapSmallerEquals(key: string, value: any): ContextKeyExpression { return ContextKeySmallerEqualsExpr.create(key, value); } mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr { if (key === 'resourceScheme' && regexp) { return ContextKeyRegexExpr.create(key, _mapResourceRegExpValue(regexp)); } else { return ContextKeyRegexExpr.create(key, regexp); } } mapIn(key: string, valueKey: string): ContextKeyInExpr { return ContextKeyInExpr.create(key, valueKey); } mapNotIn(key: string, valueKey: string): ContextKeyNotInExpr { return ContextKeyNotInExpr.create(key, valueKey); } }; const _massageWhenUser = (element: WhenUser) => { if (!element || !element.when || !/resourceScheme/.test(element.when)) { return; } const expr = ContextKeyExpr.deserialize(element.when); if (!expr) { return; } const massaged = expr.map(_exprKeyMapper); element.when = massaged.serialize(); }; const _massageWhenUserArr = (elements: WhenUser[] | WhenUser) => { if (Array.isArray(elements)) { for (const element of elements) { _massageWhenUser(element); } } else { _massageWhenUser(elements); } }; const _massageLocWhenUser = (target: LocWhenUser) => { for (const loc in target) { _massageWhenUserArr(target[loc]); } }; extensions.forEach((extension) => { if (extension.contributes) { if (extension.contributes.menus) { _massageLocWhenUser(extension.contributes.menus); } if (extension.contributes.keybindings) { _massageWhenUserArr(extension.contributes.keybindings); } if (extension.contributes.views) { _massageLocWhenUser(extension.contributes.views); } } }); } } export class RemoteExtensionsScannerChannel implements IServerChannel { constructor(private service: RemoteExtensionsScannerService, private getUriTransformer: (requestContext: any) => IURITransformer) { } listen(context: any, event: string): Event { throw new Error('Invalid listen'); } async call(context: any, command: string, args?: any): Promise { const uriTransformer = this.getUriTransformer(context); switch (command) { case 'whenExtensionsReady': return this.service.whenExtensionsReady(); case 'scanExtensions': { const language = args[0]; const profileLocation = args[1] ? URI.revive(uriTransformer.transformIncoming(args[1])) : undefined; const workspaceExtensionLocations = Array.isArray(args[2]) ? args[2].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined; const extensionDevelopmentPath = Array.isArray(args[3]) ? args[3].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined; const languagePackId: string | undefined = args[4]; const extensions = await this.service.scanExtensions( language, profileLocation, workspaceExtensionLocations, extensionDevelopmentPath, languagePackId ); return extensions.map(extension => transformOutgoingURIs(extension, uriTransformer)); } } throw new Error('Invalid call'); } }