Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…on protocol (eclipse-glsp#79)

* eclipse-glsp#94 eclipse-glsp#104 eclipse-glsp#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 <[email protected]>
  • Loading branch information
2 people authored and holkerveen committed Dec 21, 2024
1 parent d597f7a commit 1237187
Show file tree
Hide file tree
Showing 8 changed files with 345 additions and 21 deletions.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": {
Expand Down
31 changes: 31 additions & 0 deletions src/base/actions/protocol-actions.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion src/model-source/websocket-diagram-server.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
93 changes: 93 additions & 0 deletions src/protocol/glsp-client.ts
Original file line number Diff line number Diff line change
@@ -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<void>;
/**
* 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<Boolean>;
/**
* Send a shutdown notification to the server
*/
shutdownServer(): void
/**
* Stop the client and cleanup/dispose resources
*/
stop(): Promise<void>;
/**
* 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";
}
}
166 changes: 166 additions & 0 deletions src/protocol/glsp-jsonrpc-client.ts
Original file line number Diff line number Diff line change
@@ -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> = T | Promise<T> | PromiseLike<T>;
export type ConnectionProvider = MessageConnection | (() => MaybePromise<MessageConnection>);

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<ActionMessage, void>('process');
export const InitializeRequest = new RequestType<InitializeParameters, Boolean, void, void>('initialize');
export const ShutdownNotification = new NotificationType0<void>('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<MessageConnection>;
protected resolvedConnection?: MessageConnection;
protected state: ClientState;
protected onStop?: Promise<void>;

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<Boolean> {
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<void> {
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<void> {
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<MessageConnection> {
if (!this.connectionPromise) {
this.connectionPromise = this.doCreateConnection();
}
return this.connectionPromise;
}

protected async doCreateConnection(): Promise<MessageConnection> {
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;
}
}
17 changes: 17 additions & 0 deletions src/protocol/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Loading

0 comments on commit 1237187

Please sign in to comment.