diff --git a/extensions/eclipse-che-theia-plugin-remote/src/node/hosted-plugin-remote.ts b/extensions/eclipse-che-theia-plugin-remote/src/node/hosted-plugin-remote.ts index 10d537841..3073b0ffa 100644 --- a/extensions/eclipse-che-theia-plugin-remote/src/node/hosted-plugin-remote.ts +++ b/extensions/eclipse-che-theia-plugin-remote/src/node/hosted-plugin-remote.ts @@ -16,6 +16,8 @@ import { Websocket } from './websocket'; import { getPluginId } from '@theia/plugin-ext/lib/common'; import { PluginDiscovery } from './plugin-discovery'; +type PromiseResolver = (value?: Buffer) => void; + /** * Class handling remote connection for executing plug-ins. * @author Florent Benoit @@ -32,15 +34,20 @@ export class HostedPluginRemote { protected hostedPluginMapping: HostedPluginMapping; /** - * mapping between endpoint name and the websockets + * Mapping between endpoint name and the websockets */ private endpointsSockets = new Map(); /** - * mapping between endpoint's name and the websocket endpoint + * Mapping between endpoint's name and the websocket endpoint */ private pluginsMetadata: Map = new Map(); + /** + * Mapping between resource request id (pluginId_resourcePath) and resource query callback. + */ + private resourceRequests: Map = new Map(); + @postConstruct() protected postConstruct(): void { this.setupDiscovery(); @@ -155,6 +162,14 @@ export class HostedPluginRemote { this.hostedPluginMapping.getPluginsEndPoints().set(entryName, jsonMessage.endpointName); } }); + return; + } + + if (jsonMessage.method === 'getResource') { + const resourceBase64 = jsonMessage.data; + const resource = resourceBase64 ? Buffer.from(resourceBase64, 'base64') : undefined; + this.onGetResourceResponse(jsonMessage['pluginId'], jsonMessage['path'], resource); + return; } } @@ -177,4 +192,50 @@ export class HostedPluginRemote { return [].concat.apply([], [...this.pluginsMetadata.values()]); } + /** + * Sends request to retreive plugin resource from its sidecar. + * Returns undefined if plugin doesn't run in sidecar or doesn't exist. + * @param pluginId id of the plugin for which resource should be retreived + * @param resourcePath relative path of the requested resource based on plugin root directory + */ + public requestPluginResource(pluginId: string, resourcePath: string): Promise { + if (this.hasEndpoint(pluginId) && resourcePath) { + return new Promise((resolve, reject) => { + const endpoint = this.hostedPluginMapping.getPluginsEndPoints().get(pluginId); + const targetWebsocket = this.endpointsSockets.get(endpoint); + if (!targetWebsocket) { + reject(new Error(`No websocket connection for plugin: ${pluginId}`)); + } + + this.resourceRequests.set(this.getResourceRequestId(pluginId, resourcePath), resolve); + targetWebsocket.send(JSON.stringify({ + 'internal': { + 'method': 'getResource', + 'pluginId': pluginId, + 'path': resourcePath + } + })); + }); + } + return undefined; + } + + /** + * Handles all responses from all remote plugins. + * Resolves promise from getResource method with requested data. + */ + onGetResourceResponse(pluginId: string, resourcePath: string, resource: Buffer | undefined): void { + const key = this.getResourceRequestId(pluginId, resourcePath); + const resourceResponsePromiseResolver = this.resourceRequests.get(key); + if (resourceResponsePromiseResolver) { + // This response is being waited for + this.resourceRequests.delete(key); + resourceResponsePromiseResolver(resource); + } + } + + private getResourceRequestId(pluginId: string, resourcePath: string): string { + return pluginId + '_' + resourcePath; + } + } diff --git a/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-reader-extension.ts b/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-reader-extension.ts new file mode 100644 index 000000000..eaf06a37d --- /dev/null +++ b/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-reader-extension.ts @@ -0,0 +1,69 @@ +/********************************************************************* + * Copyright (c) 2019 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ + +import * as path from 'path'; +import * as express from 'express'; +import * as escape_html from 'escape-html'; +import { injectable, inject } from 'inversify'; +import { HostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader'; +import { HostedPluginRemote } from './hosted-plugin-remote'; + +/** + * Patches original plugin reader to be able to retrieve remote plugin resources. + */ +@injectable() +export class PluginReaderExtension { + + // To be set on connection creation + // If there are more than one cnnection, the last one will be used. + private hostedPluginRemote: HostedPluginRemote; + + setRemotePluginConnection(hostedPluginRemote: HostedPluginRemote): void { + this.hostedPluginRemote = hostedPluginRemote; + } + + // Map between a plugin id and its local resources storage + private pluginsStorage: Map; + + constructor(@inject(HostedPluginReader) hostedPluginReader: HostedPluginReader) { + // tslint:disable-next-line:no-any + const disclosedPluginReader = (hostedPluginReader as any); + // Get link to plugins storages info + this.pluginsStorage = disclosedPluginReader.pluginsIdsFiles; + // Replace handleMissingResource method, but preserve this of current class + const contextedHandleMissingResource = this.handleMissingResource.bind(this); + disclosedPluginReader.handleMissingResource = contextedHandleMissingResource; + } + + // Handles retrieving of remote resource for plugins. + private async handleMissingResource(req: express.Request, res: express.Response): Promise { + const pluginId = req.params.pluginId; + if (this.hostedPluginRemote) { + const resourcePath = req.params.path; + try { + const resource = await this.hostedPluginRemote.requestPluginResource(pluginId, resourcePath); + if (resource) { + res.type(path.extname(resourcePath)); + res.send(resource); + return; + } + } catch (e) { + console.error('Failed to get plugin resource from sidecar. Error:', e); + } + } + + res.status(404).send(`The plugin with id '${escape_html(pluginId)}' does not exist.`); + } + + // Exposes paths of plugin resources for other components. + public getPluginRootDirectory(pluginId: string): string | undefined { + return this.pluginsStorage.get(pluginId); + } +} diff --git a/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-remote-backend-module.ts b/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-remote-backend-module.ts index 283342698..a33b9623c 100644 --- a/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-remote-backend-module.ts +++ b/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-remote-backend-module.ts @@ -8,22 +8,28 @@ * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ -import { ContainerModule } from 'inversify'; +import { ContainerModule, interfaces } from 'inversify'; import { HostedPluginRemote } from './hosted-plugin-remote'; import { ServerPluginProxyRunner } from './server-plugin-proxy-runner'; import { MetadataProcessor, ServerPluginRunner } from '@theia/plugin-ext/lib/common'; import { RemoteMetadataProcessor } from './remote-metadata-processor'; import { HostedPluginMapping } from './plugin-remote-mapping'; import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module'; +import { PluginReaderExtension } from './plugin-reader-extension'; const localModule = ConnectionContainerModule.create(({ bind }) => { - bind(HostedPluginRemote).toSelf().inSingletonScope(); + bind(HostedPluginRemote).toSelf().inSingletonScope().onActivation((ctx: interfaces.Context, hostedPluginRemote: HostedPluginRemote) => { + const pluginReaderExtension = ctx.container.parent.get(PluginReaderExtension); + pluginReaderExtension.setRemotePluginConnection(hostedPluginRemote); + return hostedPluginRemote; + }); bind(ServerPluginRunner).to(ServerPluginProxyRunner).inSingletonScope(); }); export default new ContainerModule(bind => { bind(HostedPluginMapping).toSelf().inSingletonScope(); bind(MetadataProcessor).to(RemoteMetadataProcessor).inSingletonScope(); + bind(PluginReaderExtension).toSelf().inSingletonScope(); + bind(ConnectionContainerModule).toConstantValue(localModule); -} -); +}); diff --git a/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-remote-init.ts b/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-remote-init.ts index 80eaeb94d..4c4d10a4e 100644 --- a/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-remote-init.ts +++ b/extensions/eclipse-che-theia-plugin-remote/src/node/plugin-remote-init.ts @@ -11,6 +11,8 @@ import 'reflect-metadata'; import * as http from 'http'; import * as ws from 'ws'; +import * as fs from 'fs'; +import * as path from 'path'; import { logger } from '@theia/core'; import { ILogger } from '@theia/core/lib/common'; import { Emitter } from '@theia/core/lib/common/event'; @@ -26,6 +28,7 @@ import { DummyTraceLogger } from './dummy-trace-logger'; import pluginRemoteBackendModule from './plugin-remote-backend-module'; import { TerminalContainerAware } from './terminal-container-aware'; import { PluginDiscovery } from './plugin-discovery'; +import { PluginReaderExtension } from './plugin-reader-extension'; interface CheckAliveWS extends ws { alive: boolean; @@ -57,6 +60,8 @@ export class PluginRemoteInit { */ private sessionId = 0; + private pluginReaderExtension: PluginReaderExtension; + constructor(private pluginPort: number) { } @@ -91,6 +96,8 @@ export class PluginRemoteInit { const pluginDeployer = inversifyContainer.get(PluginDeployer); pluginDeployer.start(); + this.pluginReaderExtension = inversifyContainer.get(PluginReaderExtension); + // display message about process being started console.log(`Theia Endpoint ${process.pid}/pid listening on port`, this.pluginPort); } @@ -214,6 +221,32 @@ to pick-up automatically a free port`)); return; } + // asked to send plugin resource + if (jsonParsed.internal.method === 'getResource') { + const pluginId: string = jsonParsed.internal['pluginId']; + const resourcePath: string = jsonParsed.internal['path']; + + const pluginRootDirectory = this.pluginReaderExtension.getPluginRootDirectory(pluginId); + const resourceFilePath = path.join(pluginRootDirectory!, resourcePath); + + let resourceBase64: string | undefined; + if (fs.existsSync(resourceFilePath)) { + const resourceBinary = fs.readFileSync(resourceFilePath); + resourceBase64 = resourceBinary.toString('base64'); + } + + client.send({ + 'internal': { + 'method': 'getResource', + 'pluginId': pluginId, + 'path': resourcePath, + 'data': resourceBase64 + } + }); + + return; + } + // asked to grab metadata, send them if (jsonParsed.internal.metadata && 'request' === jsonParsed.internal.metadata) { // apply host on all local metadata @@ -314,8 +347,8 @@ class PluginDeployerHandlerImpl implements PluginDeployerHandler { const metadata = await this.reader.getPluginMetadata(plugin.path()); if (metadata) { currentBackendPluginsMetadata.push(metadata); - const path = metadata.model.entryPoint.backend || plugin.path(); - this.logger.info(`Backend plug-in "${metadata.model.name}@${metadata.model.version}" from "${path} is now available"`); + const pluginPath = metadata.model.entryPoint.backend || plugin.path(); + this.logger.info(`Backend plug-in "${metadata.model.name}@${metadata.model.version}" from "${pluginPath} is now available"`); } }