Skip to content
This repository has been archived by the owner on Apr 4, 2023. It is now read-only.

Commit

Permalink
Implement remote plugin resources retrieving (#430)
Browse files Browse the repository at this point in the history
Signed-off-by: Mykola Morhun <mmorhun@redhat.com>
  • Loading branch information
mmorhun authored Sep 10, 2019
1 parent fc11a43 commit eaaf7db
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string, Websocket>();

/**
* mapping between endpoint's name and the websocket endpoint
* Mapping between endpoint's name and the websocket endpoint
*/
private pluginsMetadata: Map<string, PluginMetadata[]> = new Map<string, PluginMetadata[]>();

/**
* Mapping between resource request id (pluginId_resourcePath) and resource query callback.
*/
private resourceRequests: Map<string, PromiseResolver> = new Map<string, PromiseResolver>();

@postConstruct()
protected postConstruct(): void {
this.setupDiscovery();
Expand Down Expand Up @@ -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;
}
}

Expand All @@ -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<Buffer | undefined> {
if (this.hasEndpoint(pluginId) && resourcePath) {
return new Promise<Buffer | undefined>((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;
}

}
Original file line number Diff line number Diff line change
@@ -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<string, string>;

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<void> {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -57,6 +60,8 @@ export class PluginRemoteInit {
*/
private sessionId = 0;

private pluginReaderExtension: PluginReaderExtension;

constructor(private pluginPort: number) {

}
Expand Down Expand Up @@ -91,6 +96,8 @@ export class PluginRemoteInit {
const pluginDeployer = inversifyContainer.get<PluginDeployer>(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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"`);
}
}

Expand Down

0 comments on commit eaaf7db

Please sign in to comment.