From d25bb4b567db4d552555078afbbc339b545292dc Mon Sep 17 00:00:00 2001 From: Tobias Ortmayr Date: Tue, 30 May 2023 23:49:26 +0200 Subject: [PATCH] Update RPC documentation Updates the outdated documentation for setting up RPC services by - Removing any notion of the old vscode json-rpc protocol. The neutral term RPC is used instead - Replacing the outdated loggingServer example with a current version of the taskServer - Removing sections of the documentation that are now obsolete because the underlying framework code has been reworked Note: he code snippets are based on https://github.com/eclipse-theia/theia/pull/12581. So this PR is blocked until the referenced PR is approved and merged (the last section Part of https://github.com/eclipse-theia/theia/issues/12368 --- src/docs/json_rpc.md | 265 ++++++++++++++++--------------------------- 1 file changed, 98 insertions(+), 167 deletions(-) diff --git a/src/docs/json_rpc.md b/src/docs/json_rpc.md index a199e848..fc334e62 100644 --- a/src/docs/json_rpc.md +++ b/src/docs/json_rpc.md @@ -1,13 +1,13 @@ --- -title: Communication via JSON-RPC +title: Communication via RPC --- -# Communication via JSON-RPC +# Communication via RPC -In this section I will explain how you can create a backend service and -then connect to it over JSON-RPC. +In this section we will explain how you can create a backend service and +then connect to it over RPC. -I will use the debug logging system as a small example of that. +We will use the task execution system as a small example of that. ## Overview @@ -19,34 +19,32 @@ then connecting to that over a websocket connection. So the first thing you will want to do is expose your service so that the frontend can connect to it. -You will need to create backend server module file similar to this (logger-server-module.ts): +You will need to create backend server module file similar to this (`task-backend-module.ts`): ``` typescript -import { ContainerModule } from 'inversify'; -import { ConnectionHandler, JsonRpcConnectionHandler } from "../../messaging/common"; -import { ILoggerServer, ILoggerClient } from '../../application/common/logger-protocol'; +import { ContainerModule } from '@theia/core/shared/inversify''; +import { ConnectionHandler, RpcConnectionHandler } from '@theia/core/lib/common/messaging'; +import { TaskClient, TaskServer, taskPath } from '../common/task-protocol'; -export const loggerServerModule = new ContainerModule(bind => { bind(ConnectionHandler).toDynamicValue(ctx => - new JsonRpcConnectionHandler("/services/logger", client => { - const loggerServer = ctx.container.get(ILoggerServer); - loggerServer.setClient(client); - return loggerServer; + new RpcConnectionHandler(taskPath, client => { + const taskServer = ctx.container.get(TaskServer); + taskServer.setClient(client); + return taskServer; }) - ).inSingletonScope() -}); + ).inSingletonScope(); ``` Let's go over that in detail: ``` typescript -import { ConnectionHandler, JsonRpcConnectionHandler } from "../../messaging/common"; +import { ConnectionHandler, RpcConnectionHandler } from '@theia/core/lib/common/messaging'; ``` -This imports the `JsonRpcConnectionHandler`, this factory enables you to create -a connection handler that onConnection creates proxy object to the object that -is called in the backend over JSON-RPC and expose a local object to JSON-RPC. +This imports the `RpcConnectionHandler`, this factory enables you to create +a connection handler that `onConnection` creates a proxy object to the remote object that +is called in the backend over RPC and optionally exposes a local object to RPC. We'll see more on how this is done as we go. @@ -56,82 +54,64 @@ connection and what happens on connection creation. It looks like this: ``` typescript -import { MessageConnection } from "vscode-jsonrpc"; +import { Channel } from '../message-rpc/channel'; export const ConnectionHandler = Symbol('ConnectionHandler'); export interface ConnectionHandler { readonly path: string; - onConnection(connection: MessageConnection): void; + onConnection(connection: Channel): void; } ``` ``` typescript -import { ILoggerServer, ILoggerClient } from '../../application/common/logger-protocol'; +import { TaskClient, TaskServer, taskPath } from '../common/task-protocol'; ``` -The logger-protocol.ts file contains the interfaces that the server and the +The `task-protocol.ts` file contains the interfaces that the server and the client need to implement. -The server here means the backend object that will be called over JSON-RPC +The server here means the backend object that will be called over RPC and the client is a client object that can receive notifications from the backend object. -I'll get more into that later. +We will get more into that later. ``` typescript bind(ConnectionHandler).toDynamicValue(ctx => { ``` -Here a bit of magic happens, at first glance we're just saying here's an -implementation of a ConnectionHandler. - -The magic here is that this ConnectionHandler type is bound to a -ContributionProvider in messaging-module.ts +Here a bit of magic happens, at first glance we're just saying here is an +implementation of a `ConnectionHandler`. -So as the MessagingContribution starts (onStart is called) it creates a -websocket connection for all bound ConnectionHandlers. +The magic here is that this `ConnectionHandler` type is bound to a +ContributionProvider in `messaging-module.ts` -like so (from messaging-module.ts): - -``` typescript -constructor( @inject(ContributionProvider) @named(ConnectionHandler) protected readonly handlers: ContributionProvider) { - } - - onStart(server: http.Server): void { - for (const handler of this.handlers.getContributions()) { - const path = handler.path; - try { - createServerWebSocketConnection({ - server, - path - }, connection => handler.onConnection(connection)); - } catch (error) { - console.error(error) - } - } - } -``` +So as the `MessagingContribution` starts (`onStart` is called) it creates a +websocket channel for all bound `ConnectionHandlers`. +To save resources the hood all `MessagingContributions` are routed over one +websocket connection (multiplexing). To dig more into ContributionProvider see this [section](Services_and_Contributions#contribution-providers). So now: ``` typescript -new JsonRpcConnectionHandler("/services/logger", client => { + new RpcConnectionHandler(taskPath, client => { ``` This does a few things if we look at this class implementation: ``` typescript -export class JsonRpcConnectionHandler implements ConnectionHandler { +export class RpcConnectionHandler implements ConnectionHandler { constructor( readonly path: string, - readonly targetFactory: (proxy: JsonRpcProxy) => any + readonly targetFactory: (proxy: RpcProxy) => any, + readonly factoryConstructor: new () => RpcProxyFactory = RpcProxyFactory ) { } - onConnection(connection: MessageConnection): void { - const factory = new JsonRpcProxyFactory(this.path); + onConnection(connection: Channel): void { + const factory = new this.factoryConstructor(); const proxy = factory.createProxy(); factory.target = this.targetFactory(proxy); factory.listen(connection); @@ -139,67 +119,58 @@ export class JsonRpcConnectionHandler implements ConnectionHan } ``` -We see that a websocket connection is created on path: "logger" by the extension of the ConnectionHandler class with the path attribute set to "logger". +We see that a websocket channel is created on the `taskPath` ("/services/task") by the extension of the `ConnectionHandler`. -And let's look at what it does onConnection : +And let's look at what it does `onConnection` : ``` typescript - onConnection(connection: MessageConnection): void { - const factory = new JsonRpcProxyFactory(this.path); + onConnection(connection: Channel): void { + const factory = new this.factoryConstructor(); const proxy = factory.createProxy(); factory.target = this.targetFactory(proxy); factory.listen(connection); + } ``` - Let's go over this line by line: ``` typescript - const factory = new JsonRpcProxyFactory(this.path); + const factory = new this.factoryConstructor(); ``` -This creates a JsonRpcProxy on path "logger". +This creates a `ProxyFactory` on path "services/task". ``` typescript - const proxy = factory.createProxy(); + const proxy = factory.createProxy(); ``` Here we create a proxy object from the factory, this will be used to call -the other end of the JSON-RPC connection using the ILoggerClient interface. +the other end of the RPC channel using the `TaskClient` interface. ``` typescript - factory.target = this.targetFactory(proxy); + factory.target = this.targetFactory(proxy); ``` This will call the function we've passed in parameter so: ``` typescript - client => { - const loggerServer = ctx.container.get(ILoggerServer); - loggerServer.setClient(client); - return loggerServer; + client => { + const taskServer = ctx.container.get(TaskServer); + taskServer.setClient(client); + return taskServer; } ``` -This sets the client on the loggerServer, in this case this is used to -send notifications to the frontend about a log level change. +This sets the client on the `taskServer`, in this case this is used to +run asynchronous tasks (e.g. a terminal command) in the backend. -And it returns the loggerServer as the object that will be exposed over JSON-RPC. +And it returns the `taskServer` as the object that will be exposed over RPC. ``` typescript - factory.listen(connection); + factory.listen(channel); ``` -This connects the factory to the connection. - -The endpoints with `services/*` path are served by the webpack dev server, see `webpack.config.js`: - -``` javascript - '/services/*': { - target: 'ws://localhost:3000', - ws: true - }, -``` +This connects the factory to the channel and establishes the RPC protocol. ## Connecting to a service @@ -208,22 +179,19 @@ the frontend. To do that you will need something like this: -(From logger-frontend-module.ts) +(From `task-frontend-module`) ``` typescript -import { ContainerModule, Container } from 'inversify'; -import { WebSocketConnectionProvider } from '../../messaging/browser/connection'; -import { ILogger, LoggerFactory, LoggerOptions, Logger } from '../common/logger'; -import { ILoggerServer } from '../common/logger-protocol'; -import { LoggerWatcher } from '../common/logger-watcher'; - -export const loggerFrontendModule = new ContainerModule(bind => { - bind(ILogger).to(Logger).inSingletonScope(); - bind(LoggerWatcher).toSelf().inSingletonScope(); - bind(ILoggerServer).toDynamicValue(ctx => { - const loggerWatcher = ctx.container.get(LoggerWatcher); +import { ContainerModule } from '@theia/core/shared/inversify'; +import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging'; +import { TaskServer, taskPath } from '../common/task-protocol'; +import { TaskWatcher } from '../common/task-watcher'; + +export default new ContainerModule(bind => { + bind(TaskServer).toDynamicValue(ctx => { const connection = ctx.container.get(WebSocketConnectionProvider); - return connection.createProxy("/services/logger", loggerWatcher.getLoggerClient()); + const taskWatcher = ctx.container.get(TaskWatcher); + return connection.createProxy(taskPath, taskWatcher.getTaskClient()); }).inSingletonScope(); }); ``` @@ -231,10 +199,10 @@ export const loggerFrontendModule = new ContainerModule(bind => { The important bit here are those lines: ``` typescript - bind(ILoggerServer).toDynamicValue(ctx => { - const loggerWatcher = ctx.container.get(LoggerWatcher); + bind(TaskServer).toDynamicValue(ctx => { const connection = ctx.container.get(WebSocketConnectionProvider); - return connection.createProxy("/services/logger", loggerWatcher.getLoggerClient()); + const taskWatcher = ctx.container.get(TaskWatcher); + return connection.createProxy(taskPath, taskWatcher.getTaskClient()); }).inSingletonScope(); ``` @@ -242,104 +210,67 @@ The important bit here are those lines: Let's go line by line: ``` typescript - const loggerWatcher = ctx.container.get(LoggerWatcher); + const connection = ctx.container.get(WebSocketConnectionProvider); ``` -Here we're creating a watcher, this is used to get notified about events -from the backend by using the loggerWatcher client -(loggerWatcher.getLoggerClient()) - -See more information about how events work in theia [here](/docs/Events#events). +Here we're getting the websocket connection, this will be used to create a proxy from. ``` typescript - const connection = ctx.container.get(WebSocketConnectionProvider); + const taskWatcher = ctx.container.get(TaskWatcher); ``` -Here we're getting the websocket connection, this will be used to create a proxy from. +Here we're creating a watcher, this is used to get notified about events +from the backend by using the `taskWatcher`'s client +(`taskWatcher.getTaskClient()`) + +See more information about how events work in theia [here](/docs/Events#events). ``` typescript - return connection.createProxy("/services/logger", loggerWatcher.getLoggerClient()); + return connection.createProxy(taskPath, taskWatcher.getTaskClient()); ``` -As the second argument, we pass a local object to handle JSON-RPC messages from the remote object. +As the second argument, we pass a local object to handle RPC messages from the remote object. Sometimes the local object depends on the proxy and cannot be instantiated before the proxy is instantiated. -In such cases, the proxy interface should implement `JsonRpcServer` and the local object should be provided as a client. +In such cases, the proxy interface should implement `RpcServer` and the local object should be provided as a client. ```ts -export type JsonRpcServer = Disposable & { +export type RpcServer = Disposable & { + /** + * If this server is a proxy to a remote server then + * a client is used as a local object + * to handle RPC messages from the remote server. + */ setClient(client: Client | undefined): void; + getClient?(): Client | undefined; }; -export interface ILoggerServer extends JsonRpcServery { +export interface TaskServer extends RpcServer { // ... } -const serverProxy = connection.createProxy("/services/logger"); -const client = loggerWatcher.getLoggerClient(); +const serverProxy = connection.createProxy("/services/task"); +const client = taskWatcher.getTaskClient(); serverProxy.setClient(client); ``` -So here at the last line we're binding the ILoggerServer interface to a -JsonRpc proxy. - -Note that his under the hood calls: - -``` typescript - createProxy(path: string, target?: object, options?: WebSocketOptions): T { - const factory = new JsonRpcProxyFactory(path, target); - this.listen(factory, options); - return factory.createProxy(); - } -``` - -So it's very similar to the backend example. +So here at the last line we're binding the `TaskServer` interface to a +RPC proxy. Maybe you've noticed too but as far as the connection is concerned the frontend is the server and the backend is the client. But that doesn't really matter in our logic. So again there's multiple things going on here what this does is that: - - it creates a JsonRpc Proxy on path "logger". - - it exposes the loggerWatcher.getLoggerClient() object. - - it returns a proxy of type ILoggerServer. -So now instances of ILoggerServer are proxied over JSON-RPC to the -backend's LoggerServer object. +- it creates a JsonRpc Proxy on path "services/task". +- it exposes the `taskWatcher.getTaskClient()` object. +- it returns a proxy of type `TaskServer`. -## Loading the modules in the example backend and frontend - -So now that we have these modules we need to wire them into the example. -We will use the browser example for this, note that it's the same code for -the electron example. - -### Backend - -In examples/browser/src/backend/main.ts you will need something like: - -``` typescript -import { loggerServerModule } from 'theia-core/lib/application/node/logger-server-module'; -``` +So now instances of `TaskServer` are proxied over RPC to the +backend's `TaskServer` object. -And than load that into the main container: - -``` typescript -container.load(loggerServerModule); -``` - -### Frontend - -In examples/browser/src/frontend/main.ts you will need something like: - -``` typescript -import { loggerFrontendModule } from 'theia-core/lib/application/browser/logger-frontend-module'; -``` - -``` typescript -container.load(frontendLanguagesModule); -``` ## Complete example If you wish to see the complete implementation of what I referred too in this documentation see [this commit](https://github.com/eclipse-theia/theia/commit/99d191f19bd2a3e93098470ca1bb7b320ab344a1). -