From f55a32f9ee917901b1ad51f1d3aea6056cbf0e31 Mon Sep 17 00:00:00 2001 From: Tobias Ortmayr Date: Tue, 25 Aug 2020 09:00:51 +0200 Subject: [PATCH] #94 #104 #96 Clean up communication protocol (#79) * #94 #104 #96 Clean up communication protocol - Clean up communication and move implementation into glsp client - Define a "clean" `GLSPClient` interface that is independent from the underlying communication protocol - Provide a base implementation for a jsonrpc-based `GLSPClient` - Update dependencies to sprotty 0.9.0 - Align dependency versions with Theia versions - Add new DisposeClientAction to notify the server if a specific diagram client/widget can be disposed (e.g. on editor tab close) Part of: - eclipse-glsp/glsp/issues/104 - eclipse-glsp/glsp/issues/94 - eclipse-glsp/glsp/issues/96 * Fix minors * Adapt copyright headers Co-authored-by: Philip Langer --- package.json | 7 +- src/base/actions/protocol-actions.ts | 31 ++++ src/index.ts | 2 + src/model-source/websocket-diagram-server.ts | 4 +- src/protocol/glsp-client.ts | 93 +++++++++++ src/protocol/glsp-jsonrpc-client.ts | 166 +++++++++++++++++++ src/protocol/index.ts | 17 ++ yarn.lock | 46 +++-- 8 files changed, 345 insertions(+), 21 deletions(-) create mode 100644 src/base/actions/protocol-actions.ts create mode 100644 src/protocol/glsp-client.ts create mode 100644 src/protocol/glsp-jsonrpc-client.ts create mode 100644 src/protocol/index.ts diff --git a/package.json b/package.json index de332252..00886d25 100644 --- a/package.json +++ b/package.json @@ -28,14 +28,15 @@ }, "dependencies": { "autocompleter": "5.1.0", - "sprotty": "next", + "sprotty": "0.9.0", "uuid": "7.0.3", - "vscode-ws-jsonrpc": "^0.0.2-1" + "vscode-ws-jsonrpc": "0.2.0" }, "devDependencies": { "@types/node": "10.14.18", "@types/uuid": "3.4.5", "@types/mocha": "^5.2.7", + "@babel/runtime": "^7.11.2", "@types/chai": "4.1.3", "mocha": "^6.2.0", "jenkins-mocha": "^8.0.0", @@ -44,7 +45,7 @@ "reflect-metadata": "^0.1.13", "rimraf": "^2.6.1", "tslint": "^5.5.0", - "typescript": "3.6.4", + "typescript": "^3.9.2", "semver": "6.3.0" }, "scripts": { diff --git a/src/base/actions/protocol-actions.ts b/src/base/actions/protocol-actions.ts new file mode 100644 index 00000000..ea74129a --- /dev/null +++ b/src/base/actions/protocol-actions.ts @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2020 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { injectable } from "inversify"; +import { Action } from "sprotty"; + +/** + * Send to the server if the graphical representation (diagram) for a specific + * client/widget id is no longer needed. e.g. the tab containing the diagram has been closed. + */ +@injectable() +export class DisposeClientAction implements Action { + static readonly KIND = "disposeClient"; + readonly kind = DisposeClientAction.KIND; +} + +export function isDisposeClientAction(action: Action): action is DisposeClientAction { + return action.kind === DisposeClientAction.KIND; +} diff --git a/src/index.ts b/src/index.ts index 76b897ba..0c13951d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,6 +37,7 @@ export * from './base/action-dispatcher'; export * from './base/actions/context-actions'; export * from './base/actions/edit-mode-action'; export * from './base/actions/edit-validation-actions'; +export * from './base/actions/protocol-actions'; export * from './base/args'; export * from './base/auto-complete/auto-complete-actions'; export * from './base/auto-complete/auto-complete-widget'; @@ -98,6 +99,7 @@ export * from './utils/array-utils'; export * from './utils/marker'; export * from './utils/smodel-util'; export * from './utils/viewpoint-util'; +export * from './protocol'; export { validationModule, saveModule, executeCommandModule, paletteModule, toolFeedbackModule, defaultGLSPModule, modelHintsModule, glspCommandPaletteModule, glspContextMenuModule, glspServerCopyPasteModule, copyPasteContextMenuModule, glspSelectModule, glspMouseToolModule, layoutCommandsModule, glspEditLabelModule, diff --git a/src/model-source/websocket-diagram-server.ts b/src/model-source/websocket-diagram-server.ts index cc40df99..8fe7f209 100644 --- a/src/model-source/websocket-diagram-server.ts +++ b/src/model-source/websocket-diagram-server.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2019 EclipseSource and others. + * Copyright (c) 2019-2020 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -40,6 +40,7 @@ import { SourceUriAware } from "../base/source-uri-aware"; import { RequestNavigationTargetsAction } from "../features/navigation/navigation-action-handler"; import { ResolveNavigationTargetAction } from "../features/navigation/navigation-target-resolver"; import { SetEditModeAction, isSetEditModeAction } from "../base/actions/edit-mode-action"; +import { DisposeClientAction } from "../base/actions/protocol-actions"; const receivedFromServerProperty = '__receivedFromServer'; @injectable() @@ -128,6 +129,7 @@ export function registerDefaultGLSPServerActions(registry: ActionHandlerRegistry registry.register(ResolveNavigationTargetAction.KIND, diagramServer); registry.register(CompoundOperation.KIND, diagramServer); registry.register(SetEditModeAction.KIND, diagramServer); + registry.register(DisposeClientAction.KIND, diagramServer); // Register an empty handler for SwitchEditMode, to avoid runtime exceptions. // We don't want to support SwitchEditMode, but sprotty still sends some corresponding diff --git a/src/protocol/glsp-client.ts b/src/protocol/glsp-client.ts new file mode 100644 index 00000000..eef49a2d --- /dev/null +++ b/src/protocol/glsp-client.ts @@ -0,0 +1,93 @@ +/******************************************************************************** + * Copyright (c) 2020 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { ActionMessage } from "sprotty"; +import * as uuid from "uuid"; + +export interface InitializeParameters<> { + /** + * Unique identifier for the current client application + */ + applicationId: string; + options?: any +} +export class ApplicationIdProvider { + private static _applicationId?: string; + static get(): string { + if (!ApplicationIdProvider._applicationId) { + ApplicationIdProvider._applicationId = uuid.v4(); + } + return ApplicationIdProvider._applicationId; + } +} +export type ActionMessageHandler = (message: ActionMessage) => void; + +export enum ClientState { + Initial, + Starting, + StartFailed, + Running, + Stopping, + Stopped, + ServerError +} + +export interface GLSPClient { + readonly id: string; + readonly name: string; + currentState(): ClientState; + /** + * Initialize the client and the server connection. + * + */ + start(): Promise; + /** + * Send an initalize request to ther server. The server needs to be initialized + * in order to accept and process action messages + * @param params Initialize parameter + * @returns true if the initialization was successfull + */ + initializeServer(params: InitializeParameters): Promise; + /** + * Send a shutdown notification to the server + */ + shutdownServer(): void + /** + * Stop the client and cleanup/dispose resources + */ + stop(): Promise; + /** + * Set a handler/listener for action messages received from the server + * @param handler The action message handler + */ + onActionMessage(handler: ActionMessageHandler): void; + /** + * Send an action message to the server + * @param message The message + */ + sendActionMessage(message: ActionMessage): void; +} + +export namespace GLSPClient { + export interface Options { + id: string; + name: string; + } + + export function isOptions(object: any): object is Options { + return object !== undefined && "id" in object && typeof object["id"] === "string" + && "name" in object && typeof object["name"] === "string"; + } +} diff --git a/src/protocol/glsp-jsonrpc-client.ts b/src/protocol/glsp-jsonrpc-client.ts new file mode 100644 index 00000000..3e5e1fc1 --- /dev/null +++ b/src/protocol/glsp-jsonrpc-client.ts @@ -0,0 +1,166 @@ +/******************************************************************************** + * Copyright (c) 2019-2020 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { ActionMessage } from "sprotty"; +import { Message, MessageConnection, NotificationType, RequestType } from "vscode-jsonrpc"; +import { NotificationType0 } from "vscode-ws-jsonrpc"; + +import { ActionMessageHandler, ClientState, GLSPClient, InitializeParameters } from "./glsp-client"; + +export type MaybePromise = T | Promise | PromiseLike; +export type ConnectionProvider = MessageConnection | (() => MaybePromise); + +export namespace JsonrpcGLSPClient { + export interface Options extends GLSPClient.Options { + connectionProvider: ConnectionProvider; + } + + export function isOptions(object: any): object is Options { + return GLSPClient.isOptions(object) && "connectionProvider" in object; + } + + export const ActionMessageNotification = new NotificationType('process'); + export const InitializeRequest = new RequestType('initialize'); + export const ShutdownNotification = new NotificationType0('shutdown'); + export const ClientNotReadyMsg = 'JsonrpcGLSPClient is not ready yet'; +} +export class BaseJsonrpcGLSPClient implements GLSPClient { + + readonly name: string; + readonly id: string; + protected readonly connectionProvider: ConnectionProvider; + protected connectionPromise?: Promise; + protected resolvedConnection?: MessageConnection; + protected state: ClientState; + protected onStop?: Promise; + + constructor(options: JsonrpcGLSPClient.Options) { + Object.assign(this, options); + this.state = ClientState.Initial; + } + + shutdownServer(): void { + if (this.checkConnectionState()) { + this.resolvedConnection!.sendNotification(JsonrpcGLSPClient.ShutdownNotification); + } + } + + initializeServer(params: InitializeParameters): Promise { + if (this.checkConnectionState()) { + return this.resolvedConnection!.sendRequest(JsonrpcGLSPClient.InitializeRequest, params); + } + return Promise.resolve(false); + } + + onActionMessage(handler: ActionMessageHandler): void { + if (this.checkConnectionState()) { + this.resolvedConnection!.onNotification(JsonrpcGLSPClient.ActionMessageNotification, handler); + } + } + + sendActionMessage(message: ActionMessage): void { + if (this.checkConnectionState()) { + this.resolvedConnection!.sendNotification(JsonrpcGLSPClient.ActionMessageNotification, message); + } + } + + protected checkConnectionState(): boolean { + if (!this.isConnectionActive()) { + throw new Error(JsonrpcGLSPClient.ClientNotReadyMsg); + } + return true; + } + + async start(): Promise { + try { + this.state = ClientState.Starting; + const connection = await this.resolveConnection(); + connection.listen(); + this.resolvedConnection = connection; + this.state = ClientState.Running; + } catch (error) { + this.error('Failed to start connection to server', error); + this.state = ClientState.StartFailed; + } + } + + stop(): Promise { + if (!this.connectionPromise) { + this.state = ClientState.Stopped; + return Promise.resolve(); + } + if (this.state === ClientState.Stopping && this.onStop) { + return this.onStop; + } + this.state = ClientState.Stopping; + return this.onStop = this.resolveConnection().then(connection => { + connection.dispose(); + this.state = ClientState.Stopped; + this.onStop = undefined; + this.connectionPromise = undefined; + this.resolvedConnection = undefined; + }); + } + + private resolveConnection(): Promise { + if (!this.connectionPromise) { + this.connectionPromise = this.doCreateConnection(); + } + return this.connectionPromise; + } + + protected async doCreateConnection(): Promise { + const connection = typeof this.connectionProvider === 'function' ? await this.connectionProvider() : this.connectionProvider; + connection.onError((data: [Error, Message, number]) => this.handleConnectionError(data[0], data[1], data[2])); + connection.onClose(() => this.handleConnectionClosed()); + return connection; + } + + protected handleConnectionError(error: Error, message: Message, count: number): void { + this.error('Connection to server is erroring. Shutting down server.', error); + this.stop(); + this.state = ClientState.ServerError; + } + + protected handleConnectionClosed(): void { + if (this.state === ClientState.Stopping || this.state === ClientState.Stopped) { + return; + } + try { + if (this.resolvedConnection) { + this.resolvedConnection.dispose(); + this.connectionPromise = undefined; + this.resolvedConnection = undefined; + } + } catch (error) { + // Disposing a connection could fail if error cases. + } + + this.error('Connection to server got closed. Server will not be restarted.'); + this.state = ClientState.ServerError; + } + + protected error(message: string, ...optionalParams: any[]): void { + console.error(`[JsonrpcGLSPClient] ${message}`, optionalParams); + } + + protected isConnectionActive(): boolean { + return this.state === ClientState.Running && !!this.resolvedConnection; + } + + currentState(): ClientState { + return this.state; + } +} diff --git a/src/protocol/index.ts b/src/protocol/index.ts new file mode 100644 index 00000000..c780e235 --- /dev/null +++ b/src/protocol/index.ts @@ -0,0 +1,17 @@ +/******************************************************************************** + * Copyright (c) 2020 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +export * from './glsp-client'; +export * from './glsp-jsonrpc-client'; diff --git a/yarn.lock b/yarn.lock index 6ff2452e..bb0d17e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -56,6 +56,13 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.3.tgz#790874091d2001c9be6ec426c2eed47bc7679081" integrity sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ== +"@babel/runtime@^7.11.2": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.4.0", "@babel/template@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.3.tgz#e02ad04fe262a657809327f578056ca15fd4d1b8" @@ -1062,6 +1069,11 @@ reflect-metadata@^0.1.13: resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== +regenerator-runtime@^0.13.4: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + release-zalgo@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730" @@ -1232,10 +1244,10 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sprotty@next: - version "0.8.0-next.b8a6e44" - resolved "https://registry.yarnpkg.com/sprotty/-/sprotty-0.8.0-next.b8a6e44.tgz#4d05bef1274787555a664520ebd18bc3ce73a90d" - integrity sha512-S6zj7lIJg9RGRMa4bCvsCUR1MM4sqOT6lssfMSy3XYhtwEgWhFzlzm+5poQsEKz9mMVEu4L/QWeLgCHGH4jYnA== +sprotty@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/sprotty/-/sprotty-0.9.0.tgz#5644cdb239c43e878705fe76d71ffc73f27cd27b" + integrity sha512-kPcXVgspNnMq/ysFFOVfjBkrNK/w9LXSiX1mx3mkEiEVbthjEiKicw+l9uwW9RJH+QvqrK8LrXqEZzKGERUQyA== dependencies: autocompleter "5.1.0" file-saver "2.0.2" @@ -1384,10 +1396,10 @@ type-detect@^4.0.0, type-detect@^4.0.5: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -typescript@3.6.4: - version "3.6.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d" - integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg== +typescript@^3.9.2: + version "3.9.7" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" + integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== uuid@7.0.3: version "7.0.3" @@ -1412,17 +1424,17 @@ void-elements@^2.0.1: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= -vscode-jsonrpc@^3.6.0: - version "3.6.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-3.6.2.tgz#3b5eef691159a15556ecc500e9a8a0dd143470c8" - integrity sha512-T24Jb5V48e4VgYliUXMnZ379ItbrXgOimweKaJshD84z+8q7ZOZjJan0MeDe+Ugb+uqERDVV8SBmemaGMSMugA== +vscode-jsonrpc@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794" + integrity sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A== -vscode-ws-jsonrpc@^0.0.2-1: - version "0.0.2-2" - resolved "https://registry.yarnpkg.com/vscode-ws-jsonrpc/-/vscode-ws-jsonrpc-0.0.2-2.tgz#3d977ea40a2f47148ea8cfcbf077196ecd7fe3a2" - integrity sha512-hViHObJHtxD0KX8tvP6QL8fJGfH9mmDrEkdfLKj6Mf1uaxypoMBnjcZDCU3N4l7VriQiNRbohe/FlMrC3/0r7Q== +vscode-ws-jsonrpc@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/vscode-ws-jsonrpc/-/vscode-ws-jsonrpc-0.2.0.tgz#5e9c26e10da54a1a235da7d59e74508bbcb8edd9" + integrity sha512-NE9HNRgPjCaPyTJvIudcpyIWPImxwRDtuTX16yks7SAiZgSXigxAiZOvSvVBGmD1G/OMfrFo6BblOtjVR9DdVA== dependencies: - vscode-jsonrpc "^3.6.0" + vscode-jsonrpc "^5.0.0" which-module@^2.0.0: version "2.0.0"