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

Load snippets via plugin resource #948

Merged
merged 7 commits into from
Mar 16, 2021
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
@@ -0,0 +1,131 @@
/**********************************************************************
* Copyright (c) 2021 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 { Disposable, Emitter, Event } from '@theia/core';
import {
FileChange,
FileDeleteOptions,
FileOverwriteOptions,
FileSystemProvider,
FileSystemProviderCapabilities,
FileType,
FileWriteOptions,
Stat,
WatchOptions,
} from '@theia/filesystem/lib/common/files';
import { FileService, FileServiceContribution } from '@theia/filesystem/lib/browser/file-service';

import { ChePluginUri } from '../common/che-plugin-uri';
import { Endpoint } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { injectable } from 'inversify';

/**
* A very basic file system provider that can read plugin resources via http
*/
export class ChePluginFileSystem implements FileSystemProvider {
private readonly _onDidChange = new Emitter<readonly FileChange[]>();

readonly onDidChangeFile: Event<readonly FileChange[]> = this._onDidChange.event;
readonly onFileWatchError: Event<void> = new Emitter<void>().event;

readonly capabilities: FileSystemProviderCapabilities;
readonly onDidChangeCapabilities: Event<void> = Event.None;

constructor() {
this.capabilities = FileSystemProviderCapabilities.FileReadWrite + FileSystemProviderCapabilities.Readonly;
}

delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
throw new Error('Not implemented.');
}

mkdir(resource: URI): Promise<void> {
throw new Error('Not implemented.');
}

private static getUri(path: string): URI {
return new Endpoint({
path: path,
}).getRestUrl();
}

readFile(resource: URI): Promise<Uint8Array> {
const uri = ChePluginFileSystem.getUri(resource.path.toString());
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.responseType = 'arraybuffer';
request.onreadystatechange = function (): void {
if (this.readyState === XMLHttpRequest.DONE) {
if (this.status === 200) {
resolve(new Uint8Array(this.response));
} else {
reject(new Error('Could not fetch plugin resource'));
}
}
};

request.open('GET', uri.toString(), true);
request.send();
});
}

readdir(resource: URI): Promise<[string, FileType][]> {
throw new Error('Not implemented.');
}

rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
throw new Error('Not implemented.');
}

stat(resource: URI): Promise<Stat> {
const uri = ChePluginFileSystem.getUri(resource.path.toString()) + '?request=stat';
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.responseType = 'json';
request.onreadystatechange = function (): void {
if (this.readyState === XMLHttpRequest.DONE) {
if (this.status === 200) {
resolve(request.response);
} else {
reject(new Error('Could not fetch plugin resource'));
}
}
};

request.open('GET', uri.toString(), true);
request.send();
});
}

watch(resource: URI, opts: WatchOptions): Disposable {
// we treat the FS as read-only
return Disposable.NULL;
}

writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
throw new Error('Not implemented.');
}
}

@injectable()
export class ChePluginFileServiceContribution implements FileServiceContribution {
registerFileSystemProviders(service: FileService): void {
service.onWillActivateFileSystemProvider(event => {
if (event.scheme === ChePluginUri.SCHEME) {
event.waitUntil(
(async () => {
service.registerProvider(ChePluginUri.SCHEME, new ChePluginFileSystem());
})()
);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@
***********************************************************************/

import { BrowserRemoteHostedPluginSupport } from './browser-remote-hosted-plugin-support';
import { ChePluginFileServiceContribution } from './che-plugin-file-system';
import { ContainerModule } from 'inversify';
import { FileServiceContribution } from '@theia/filesystem/lib/browser/file-service';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';

export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BrowserRemoteHostedPluginSupport).toSelf().inSingletonScope();
rebind(HostedPluginSupport).toService(BrowserRemoteHostedPluginSupport);
bind(ChePluginFileServiceContribution).toSelf().inSingletonScope();
bind(FileServiceContribution).toService(ChePluginFileServiceContribution);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**********************************************************************
* Copyright (c) 2021 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 URI from '@theia/core/lib/common/uri';

/**
* Share the definition of plugin resource uris between back end and
* front end.
*/
export namespace ChePluginUri {
export const SCHEME = 'chepluginresource';

export function createUri(pluginId: string, relativePath?: string): URI {
return new URI(
`${SCHEME}:///hostedPlugin/${pluginId}/${
relativePath ? encodeURIComponent(relativePath.normalize().toString()) : ''
}`
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**********************************************************************
* Copyright (c) 2021 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 { PluginPackage, getPluginId } from '@theia/plugin-ext/lib/common/plugin-protocol';

import { ChePluginUri } from '../common/che-plugin-uri';
import { PluginUriFactory } from '@theia/plugin-ext/lib/hosted/node/scanners/plugin-uri-factory';
import URI from '@theia/core/lib/common/uri';
import { injectable } from 'inversify';

/**
* Creates plugin resource URIs for plugin-relative files.
*/
@injectable()
export class ChePluginUriFactory implements PluginUriFactory {
createUri(pkg: PluginPackage, pkgRelativePath?: string): URI {
return ChePluginUri.createUri(getPluginId(pkg), pkgRelativePath);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,23 @@
***********************************************************************/

import { DeployedPlugin, HostedPluginClient } from '@theia/plugin-ext';
import {
GetResourceResponse,
GetResourceStatResponse,
InternalMessage,
InternalMessagePayload,
InternalRequestResponsePayload,
} from './internal-protocol';
import { inject, injectable, postConstruct } from 'inversify';

import { HostedPluginMapping } from './plugin-remote-mapping';
import { ILogger } from '@theia/core/lib/common';
import { PluginDiscovery } from './plugin-discovery';
import { Stat } from '@theia/filesystem/lib/common/files';
import { Websocket } from './websocket';
import { getPluginId } from '@theia/plugin-ext/lib/common';

type PromiseResolver = (value?: Buffer) => void;
type PromiseResolver = (value?: unknown) => void;

/**
* Class handling remote connection for executing plug-ins.
Expand All @@ -43,10 +51,14 @@ export class HostedPluginRemote {
*/
private pluginsDeployedPlugins: Map<string, DeployedPlugin[]> = new Map<string, DeployedPlugin[]>();

/**
* Request id's for "internal requests"
*/
private nextMessageId = 0;
/**
* Mapping between resource request id (pluginId_resourcePath) and resource query callback.
*/
private resourceRequests: Map<string, PromiseResolver> = new Map<string, PromiseResolver>();
private pendingInternalRequests: Map<string, PromiseResolver> = new Map<string, PromiseResolver>();

@postConstruct()
protected postConstruct(): void {
Expand Down Expand Up @@ -103,7 +115,7 @@ export class HostedPluginRemote {
this.endpointsSockets.set(endpointAdress, websocket);
websocket.onMessage = (messageRaw: string) => {
const parsed = JSON.parse(messageRaw);
if (parsed.internal) {
if (InternalMessage.is(parsed)) {
this.handleLocalMessage(parsed.internal);
return;
}
Expand Down Expand Up @@ -140,7 +152,7 @@ export class HostedPluginRemote {
const pluginId = jsonMessage.pluginID;

// socket ?
const endpoint = this.hostedPluginMapping.getPluginsEndPoints().get(pluginId);
const endpoint = this.hostedPluginMapping.getEndpoint(pluginId);
if (!endpoint) {
this.logger.error('no endpoint configured for the given plugin', pluginId, 'skipping message');
return;
Expand All @@ -161,8 +173,8 @@ export class HostedPluginRemote {
// add the mapping retreived from external plug-in if not defined
deployedPlugins.forEach(deployedPlugin => {
const entryName = getPluginId(deployedPlugin.metadata.model);
if (!this.hostedPluginMapping.getPluginsEndPoints().has(entryName)) {
this.hostedPluginMapping.getPluginsEndPoints().set(entryName, jsonMessage.endpointName);
if (!this.hostedPluginMapping.hasEndpoint(entryName)) {
this.hostedPluginMapping.setEndpoint(entryName, jsonMessage.endpointName);
if (this.client) {
this.client.onDidDeploy();
}
Expand All @@ -171,14 +183,39 @@ export class HostedPluginRemote {
return;
}

if (jsonMessage.method === 'getResource') {
const resourceBase64 = jsonMessage.data;
const resource = resourceBase64 ? Buffer.from(resourceBase64, 'base64') : undefined;
this.onGetResourceResponse(jsonMessage['pluginId'], jsonMessage['path'], resource);
if (InternalRequestResponsePayload.is(jsonMessage)) {
this.onInternalRequestResponse(jsonMessage);
return;
}
}

requestPluginResourceStat(pluginId: string, resourcePath: string): Promise<Stat> {
Copy link
Contributor

Choose a reason for hiding this comment

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

for readability maybe it would be easier to flag it as async method (and use await instead of chaining promises)

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 used to like the async/await syntax, but t.b.h these days I find it makes following the control flow rather confusing, in particular when debugging. I tend to only use async/await where it really improves readability. Here it does not, IMO:

return this.sendInternalRequest<GetResourceStatResponse>(pluginId, {
method: 'getResourceStat',
pluginId: pluginId,
path: resourcePath,
}).then((value: GetResourceStatResponse) => {
if (value.stat) {
return value.stat;
}
throw new Error(`No stat found for ${pluginId}, ${resourcePath}`);
});
}

requestPluginResource(pluginId: string, resourcePath: string): Promise<Buffer> {
return this.sendInternalRequest<GetResourceResponse>(pluginId, {
method: 'getResource',
pluginId: pluginId,
path: resourcePath,
}).then((value: GetResourceResponse) => {
const resourceBase64 = value.data;
if (resourceBase64) {
return Buffer.from(resourceBase64, 'base64');
}
throw new Error(`No resource found for ${pluginId}, ${resourcePath}`);
});
}

/**
* Send the given message back to the client
* @param message the message to send
Expand Down Expand Up @@ -212,48 +249,50 @@ export class HostedPluginRemote {
* @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> | undefined {
if (this.hasEndpoint(pluginId) && resourcePath) {
return new Promise<Buffer>((resolve, reject) => {
const endpoint = this.hostedPluginMapping.getPluginsEndPoints().get(pluginId);
public sendInternalRequest<T extends InternalMessagePayload>(pluginId: string, message: object): Promise<T> {
return new Promise<T>((resolve, reject) => {
if (this.hasEndpoint(pluginId)) {
const endpoint = this.hostedPluginMapping.getEndpoint(pluginId);
if (!endpoint) {
reject(new Error(`No endpoint for plugin: ${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({
} else {
const requestId = this.getNextMessageId();
this.pendingInternalRequests.set(requestId, resolve);
const msg = {
internal: {
method: 'getResource',
pluginId: pluginId,
path: resourcePath,
requestId: requestId,
...message,
},
})
);
});
}
return undefined;
};
targetWebsocket.send(JSON.stringify(msg));
}
} else {
reject(new Error('No endpoint found for plugin ' + pluginId));
}
});
}

/**
* 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);
onInternalRequestResponse(msg: InternalRequestResponsePayload): void {
const resourceResponsePromiseResolver = this.pendingInternalRequests.get(msg.requestId);
if (resourceResponsePromiseResolver) {
// This response is being waited for
this.resourceRequests.delete(key);
resourceResponsePromiseResolver(resource);
this.pendingInternalRequests.delete(msg.requestId);
resourceResponsePromiseResolver(msg);
} else {
console.error('got response to unknown request: ' + JSON.stringify(msg));
}
}

private getResourceRequestId(pluginId: string, resourcePath: string): string {
return pluginId + '_' + resourcePath;
private getNextMessageId(): string {
this.nextMessageId++;
return this.nextMessageId.toString();
}
}
Loading