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

Implement remote plugin resources retrieving #430

Merged
merged 1 commit into from
Sep 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
mmorhun marked this conversation as resolved.
Show resolved Hide resolved
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);
mmorhun marked this conversation as resolved.
Show resolved Hide resolved
targetWebsocket.send(JSON.stringify({
'internal': {
'method': 'getResource',
'pluginId': pluginId,
'path': resourcePath
}
}));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to call resolve() somewhere in the promise?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to call resolve, but not here. It is called here.

});
}
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);
mmorhun marked this conversation as resolved.
Show resolved Hide resolved
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.
mmorhun marked this conversation as resolved.
Show resolved Hide resolved
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') {
Copy link
Contributor

@AndrienkoAleksandr AndrienkoAleksandr Sep 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, for stop method we have one more check:
jsonParsed.internal.method && jsonParsed.internal.method === 'stop'
Maybe you need to apply jsonParsed.internal.method to your code too?
=> if (jsonParsed.internal.method && jsonParsed.internal.method === 'getResource') {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. We are sure that jsonParsed.internal is present. Then we need to check jsonParsed.internal.method field. Of course, we can firstly check for undefined and then compare to method name, but I see no point because we may just check equality. If method field is undefined it is definitely not equal to 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