Skip to content

Commit

Permalink
Properly scope server connections on a per-frontend basis
Browse files Browse the repository at this point in the history
- Create one ModelServiceImpl per frontend connection
- Improve connection reporting for server clients
- Replace temporary workspace files with port commands
-- Query command contributed by extension
-- Extension forwards command execution to language server
  • Loading branch information
martin-fleck-at committed Dec 15, 2023
1 parent b9adbdb commit 8460854
Show file tree
Hide file tree
Showing 15 changed files with 198 additions and 248 deletions.
3 changes: 3 additions & 0 deletions extensions/crossmodel-lang/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { GLSP_PORT_COMMAND, MODELSERVER_PORT_COMMAND } from '@crossbreeze/protocol';
import * as path from 'path';
import * as vscode from 'vscode';
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node.js';
Expand Down Expand Up @@ -29,6 +30,8 @@ function launchLanguageClient(context: vscode.ExtensionContext): LanguageClient
// Start the client. This will also launch the server
const languageClient = new LanguageClient('cross-model', 'CrossModel', serverOptions, clientOptions);
languageClient.start();
vscode.commands.registerCommand(MODELSERVER_PORT_COMMAND, () => languageClient.sendRequest(MODELSERVER_PORT_COMMAND));
vscode.commands.registerCommand(GLSP_PORT_COMMAND, () => languageClient.sendRequest(GLSP_PORT_COMMAND));
return languageClient;
}

Expand Down
15 changes: 10 additions & 5 deletions extensions/crossmodel-lang/src/glsp-server/launch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { GLSP_PORT_FILE } from '@crossbreeze/protocol';
import { GLSP_PORT_COMMAND } from '@crossbreeze/protocol';
import { configureELKLayoutModule } from '@eclipse-glsp/layout-elk';
import {
LogLevel,
Expand All @@ -14,8 +14,9 @@ import {
defaultSocketLaunchOptions
} from '@eclipse-glsp/server/node.js';
import { Container, ContainerModule } from 'inversify';
import { AddressInfo } from 'net';
import { URI } from 'vscode-uri';
import { CrossModelLSPServices, writePortFileToWorkspace } from '../integration.js';
import { CrossModelLSPServices } from '../integration.js';
import { CrossModelServices, CrossModelSharedServices } from '../language-server/cross-model-module.js';
import { CrossModelDiagramModule } from './diagram/cross-model-module.js';
import { CrossModelLayoutConfigurator } from './layout/cross-model-layout-configurator.js';
Expand Down Expand Up @@ -49,16 +50,20 @@ export function startGLSPServer(services: CrossModelLSPServices, workspaceFolder
launcher.configure(serverModule);
try {
const stop = launcher.start(launchOptions);
launcher['netServer'].on('listening', () =>
// write dynamically assigned port to workspace folder to let clients know we are ready to accept connections
writePortFileToWorkspace(workspaceFolder, GLSP_PORT_FILE, launcher['netServer'].address())
launcher['netServer'].on(
'listening',
() => services.shared.lsp.Connection?.onRequest(GLSP_PORT_COMMAND, () => getPort(launcher['netServer'].address()))
);
return stop;
} catch (error) {
logger.error('Error in GLSP server launcher:', error);
}
}

function getPort(address: AddressInfo | string | null): number | undefined {
return address && !(typeof address === 'string') ? address.port : undefined;
}

/**
* Custom module to bind language services so that they can be injected in other classes created through DI.
*
Expand Down
19 changes: 0 additions & 19 deletions extensions/crossmodel-lang/src/integration.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { PORT_FOLDER } from '@crossbreeze/protocol';
import * as fs from 'fs';
import { AddressInfo } from 'net';
import { join } from 'path';
import { URI } from 'vscode-uri';
import { CrossModelServices, CrossModelSharedServices } from './language-server/cross-model-module.js';

/**
Expand All @@ -18,17 +13,3 @@ export interface CrossModelLSPServices {
/** CrossModel language-specific services. */
language: CrossModelServices;
}

export function writePortFileToWorkspace(workspace: URI, fileName: string, address: AddressInfo | string | null): void {
if (address && !(typeof address === 'string')) {
const portFolder = join(workspace.fsPath, PORT_FOLDER);
fs.mkdirSync(portFolder, { recursive: true });
fs.writeFileSync(join(portFolder, fileName), address.port.toString());
} else {
console.error(
'Could not write file ' + fileName + ' to workspace as no workspace is set or no port was provided.',
fileName,
address
);
}
}
9 changes: 4 additions & 5 deletions extensions/crossmodel-lang/src/model-server/launch.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { MODELSERVER_PORT_FILE } from '@crossbreeze/protocol';
import { MODELSERVER_PORT_COMMAND } from '@crossbreeze/protocol';
import console from 'console';
import * as net from 'net';
import * as rpc from 'vscode-jsonrpc/node.js';
import { URI } from 'vscode-uri';
import { CrossModelLSPServices, writePortFileToWorkspace } from '../integration.js';
import { CrossModelLSPServices } from '../integration.js';
import { ModelServer } from './model-server.js';

const currentConnections: rpc.MessageConnection[] = [];
Expand All @@ -31,9 +32,7 @@ export function startModelServer(services: CrossModelLSPServices, workspaceFolde
return;
}
console.log(`[ModelServer] Ready to accept new client requests on port: ${addressInfo.port}`);

// Write dynamically assigned port to workspace folder to let clients know we are ready to accept connections
writePortFileToWorkspace(workspaceFolder, MODELSERVER_PORT_FILE, addressInfo);
services.shared.lsp.Connection?.onRequest(MODELSERVER_PORT_COMMAND, () => addressInfo.port);
});
netServer.on('error', err => {
console.error('[ModelServer] Error: ', err);
Expand Down
4 changes: 0 additions & 4 deletions packages/core/src/browser/core-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { MenuContribution } from '@theia/core';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { ContainerModule } from '@theia/core/shared/inversify';
import { FileNavigatorWidget } from '@theia/navigator/lib/browser';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { WorkspaceCommandContribution } from '@theia/workspace/lib/browser/workspace-commands';
import '../../style/index.css';
import { createCrossModelFileNavigatorWidget } from './cm-file-navigator-tree-widget';
import { DynamicPortCleanup } from './dynamic-port-cleanup';
import { CrossModelFileNavigatorContribution, CrossModelWorkspaceContribution } from './new-element-contribution';

export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
bind(FrontendApplicationContribution).to(DynamicPortCleanup);

bind(CrossModelWorkspaceContribution).toSelf().inSingletonScope();
rebind(WorkspaceCommandContribution).toService(CrossModelWorkspaceContribution);
bind(MenuContribution).toService(CrossModelWorkspaceContribution);
Expand Down
50 changes: 0 additions & 50 deletions packages/core/src/browser/dynamic-port-cleanup.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/core/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
export * from './cm-env-variable-server';
export * from './integration-util';
37 changes: 0 additions & 37 deletions packages/core/src/node/integration-util.ts

This file was deleted.

15 changes: 11 additions & 4 deletions packages/glsp-client/src/node/crossmodel-backend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/

import { bindAsService } from '@eclipse-glsp/protocol';
import { GLSPServerContribution } from '@eclipse-glsp/theia-integration/lib/node/glsp-server-contribution';
import { ConnectionHandler } from '@theia/core';
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
import { ContainerModule } from '@theia/core/shared/inversify/index';
import { CrossModelDiagramServerContribution } from './crossmodel-diagram-server-contribution';
import { CrossModelDiagramGLSPConnectionHandler } from './crossmodel-diagram-connection-handler';

const frontendScopedConnectionModule = ConnectionContainerModule.create(({ bind }) => {
bind(CrossModelDiagramGLSPConnectionHandler).toSelf().inSingletonScope();
bind(ConnectionHandler)
.toDynamicValue(context => context.container.get(CrossModelDiagramGLSPConnectionHandler))
.inSingletonScope();
});

export default new ContainerModule(bind => {
bindAsService(bind, GLSPServerContribution, CrossModelDiagramServerContribution);
bind(ConnectionContainerModule).toConstantValue(frontendScopedConnectionModule);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { GLSP_PORT_COMMAND } from '@crossbreeze/protocol';
import { GLSPContribution } from '@eclipse-glsp/theia-integration/lib/common';
import { SocketConnectionForwarder } from '@eclipse-glsp/theia-integration/lib/node';
import { Channel, CommandService, ConnectionHandler, Disposable, MessageService } from '@theia/core';
import { ForwardingChannel } from '@theia/core/lib/common/message-rpc/channel';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as net from 'net';
import { CrossModelDiagramLanguage } from '../common/crossmodel-diagram-language';

@injectable()
export class CrossModelDiagramGLSPConnectionHandler implements ConnectionHandler {
path = GLSPContribution.servicePath + '/' + CrossModelDiagramLanguage.contributionId;

@inject(MessageService) protected messageService: MessageService;
@inject(CommandService) protected commandService: CommandService;

onConnection(connection: Channel): void {
this.initializeServerConnection(connection);
}

protected async initializeServerConnection(channel: Channel): Promise<void> {
const progress = await this.messageService.showProgress({
text: 'Connecting to Graphical Server',
options: { cancelable: false }
});
try {
progress.report({ message: 'Waiting for port information...' });
const port = await this.findPort();
progress.report({ message: 'Waiting for connection on port ' + port + '...' });
await this.connectToServer(channel, port);
progress.cancel();
this.messageService.info('Connected to Graphical Server on port ' + port, { timeout: 3000 });
} catch (error) {
progress.cancel();
this.messageService.error('Could not connect to Graphical Server: ' + error);
}
}

protected async findPort(timeout = 500, attempts = -1): Promise<number> {
const pendingContent = new Deferred<number>();
let counter = 0;
const tryQueryingPort = (): void => {
setTimeout(async () => {
try {
const port = await this.commandService.executeCommand<number>(GLSP_PORT_COMMAND);
if (port) {
pendingContent.resolve(port);
}
} catch (error) {
counter++;
if (attempts >= 0 && counter > attempts) {
pendingContent.reject(error);
} else {
tryQueryingPort();
}
}
}, timeout);
};
tryQueryingPort();
return pendingContent.promise;
}

protected async connectToServer(channel: Channel, port: number): Promise<any> {
// Create the deferred object which exposes the Promise of the connection with the ModelServer.
const connected = new Deferred<void>();

// Create the socket, reader, writer and rpc-connection.
const socket = new net.Socket();

// Configure connection promise results for the socket.
socket.on('ready', () => connected.resolve());
socket.on('close', () => connected.reject('Socket from Graphical Client to Graphical Server was closed.'));
socket.on('error', error => console.error('Error occurred with the Graphical socket: %s; %s', error.name, error.message));

this.forwardToSocketConnection(channel, socket);
if (channel instanceof ForwardingChannel) {
socket.on('error', error => channel.onErrorEmitter.fire(error));
}

// Connect to the ModelServer on the given port.
socket.connect({ port });

setTimeout(() => connected.reject('Timeout reached.'), 10000);
return connected.promise;
}

protected forwardToSocketConnection(clientChannel: Channel, socket: net.Socket): Disposable {
return new SocketConnectionForwarder(clientChannel, socket);
}
}

This file was deleted.

Loading

0 comments on commit 8460854

Please sign in to comment.