From 03349783b106859ba687de909ef82118af10061e Mon Sep 17 00:00:00 2001 From: Anton Kosiakov Date: Wed, 25 Apr 2018 10:20:47 +0500 Subject: [PATCH] multiplex JSON-RPC connections over a single web socket Signed-off-by: Anton Kosiakov --- .../browser/frontend-application-module.ts | 2 +- .../core/src/browser/messaging/connection.ts | 93 ------------ packages/core/src/browser/messaging/index.ts | 4 +- .../messaging/messaging-frontend-module.ts | 2 +- .../messaging/ws-connection-provider.ts | 138 +++++++++++++++++ .../common/messaging/web-socket-channel.ts | 129 ++++++++++++++++ .../node/messaging/messaging-contribution.ts | 141 ++++++++++++------ .../src/node/messaging/messaging-service.ts | 25 +++- .../src/node/shell-terminal-server.spec.ts | 8 +- ...terminal-backend-contribution.slow-spec.ts | 33 ++-- .../terminal/src/node/terminal-server.spec.ts | 12 +- ...c-config.ts => terminal-test-container.ts} | 18 ++- 12 files changed, 431 insertions(+), 174 deletions(-) delete mode 100644 packages/core/src/browser/messaging/connection.ts create mode 100644 packages/core/src/browser/messaging/ws-connection-provider.ts create mode 100644 packages/core/src/common/messaging/web-socket-channel.ts rename packages/terminal/src/node/test/{inversify.spec-config.ts => terminal-test-container.ts} (55%) diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index fc05f5cad12a0..8676702c5fed6 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -40,7 +40,7 @@ import { ThemingCommandContribution, ThemeService } from './theming'; import { ConnectionStatusService, FrontendConnectionStatusService, ApplicationConnectionStatusContribution } from './connection-status-service'; import { DiffUriLabelProviderContribution } from './diff-uris'; import { ApplicationServer, applicationPath } from "../common/application-protocol"; -import { WebSocketConnectionProvider } from "./messaging/connection"; +import { WebSocketConnectionProvider } from "./messaging"; import { AboutDialog, AboutDialogProps } from "./about-dialog"; import { EnvVariablesServer, envVariablesPath } from "./../common/env-variables"; import { FrontendApplicationStateService } from './frontend-application-state'; diff --git a/packages/core/src/browser/messaging/connection.ts b/packages/core/src/browser/messaging/connection.ts deleted file mode 100644 index fa4e81a6cbae8..0000000000000 --- a/packages/core/src/browser/messaging/connection.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2017 TypeFox and others. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - */ - -import { injectable, interfaces } from "inversify"; -import { listen as doListen, Logger, ConsoleLogger } from "vscode-ws-jsonrpc"; -import { ConnectionHandler, JsonRpcProxyFactory, JsonRpcProxy } from "../../common"; -import { Endpoint } from "../endpoint"; -const ReconnectingWebSocket = require('reconnecting-websocket'); - -export interface WebSocketOptions { - /** - * True by default. - */ - reconnecting?: boolean; -} - -@injectable() -export class WebSocketConnectionProvider { - - static createProxy(container: interfaces.Container, path: string, target?: object): JsonRpcProxy { - return container.get(WebSocketConnectionProvider).createProxy(path, target); - } - - /** - * Create a proxy object to remote interface of T type - * over a web socket connection for the given path. - * - * An optional target can be provided to handle - * notifications and requests from a remote side. - */ - createProxy(path: string, target?: object, options?: WebSocketOptions): JsonRpcProxy { - const factory = new JsonRpcProxyFactory(target); - this.listen({ - path, - onConnection: c => factory.listen(c) - }, options); - return factory.createProxy(); - } - - /** - * Install a connection handler for the given path. - */ - listen(handler: ConnectionHandler, options?: WebSocketOptions): void { - const url = this.createWebSocketUrl(handler.path); - const webSocket = this.createWebSocket(url, options); - - const logger = this.createLogger(); - webSocket.onerror = function (error: Event) { - logger.error('' + error); - return; - }; - doListen({ - webSocket, - onConnection: handler.onConnection.bind(handler), - logger - }); - } - - protected createLogger(): Logger { - return new ConsoleLogger(); - } - - /** - * Creates a websocket URL to the current location - */ - protected createWebSocketUrl(path: string): string { - const endpoint = new Endpoint({ path }); - return endpoint.getWebSocketUrl().toString(); - } - - /** - * Creates a web socket for the given url - */ - protected createWebSocket(url: string, options?: WebSocketOptions): WebSocket { - if (options === undefined || options.reconnecting) { - const socketOptions = { - maxReconnectionDelay: 10000, - minReconnectionDelay: 1000, - reconnectionDelayGrowFactor: 1.3, - connectionTimeout: 10000, - maxRetries: Infinity, - debug: false - }; - return new ReconnectingWebSocket(url, undefined, socketOptions); - } - return new WebSocket(url); - } - -} diff --git a/packages/core/src/browser/messaging/index.ts b/packages/core/src/browser/messaging/index.ts index e8d6f2e968a23..5e8447ea1d457 100644 --- a/packages/core/src/browser/messaging/index.ts +++ b/packages/core/src/browser/messaging/index.ts @@ -1,8 +1,8 @@ /* - * Copyright (C) 2017 TypeFox and others. + * Copyright (C) 2018 TypeFox and others. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ -export * from './connection'; +export * from './ws-connection-provider'; diff --git a/packages/core/src/browser/messaging/messaging-frontend-module.ts b/packages/core/src/browser/messaging/messaging-frontend-module.ts index 97abbe9aeb074..4b947777334a6 100644 --- a/packages/core/src/browser/messaging/messaging-frontend-module.ts +++ b/packages/core/src/browser/messaging/messaging-frontend-module.ts @@ -6,7 +6,7 @@ */ import { ContainerModule } from "inversify"; -import { WebSocketConnectionProvider } from './connection'; +import { WebSocketConnectionProvider } from './ws-connection-provider'; export const messagingFrontendModule = new ContainerModule(bind => { bind(WebSocketConnectionProvider).toSelf().inSingletonScope(); diff --git a/packages/core/src/browser/messaging/ws-connection-provider.ts b/packages/core/src/browser/messaging/ws-connection-provider.ts new file mode 100644 index 0000000000000..74768b9b85978 --- /dev/null +++ b/packages/core/src/browser/messaging/ws-connection-provider.ts @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2018 TypeFox and others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + */ + +import { injectable, interfaces } from "inversify"; +import { createWebSocketConnection, Logger, ConsoleLogger } from "vscode-ws-jsonrpc/lib"; +import { ConnectionHandler, JsonRpcProxyFactory, JsonRpcProxy } from "../../common"; +import { WebSocketChannel } from "../../common/messaging/web-socket-channel"; +import { Endpoint } from "../endpoint"; +const ReconnectingWebSocket = require('reconnecting-websocket'); + +export interface WebSocketOptions { + /** + * True by default. + */ + reconnecting?: boolean; +} + +@injectable() +export class WebSocketConnectionProvider { + + static createProxy(container: interfaces.Container, path: string, target?: object): JsonRpcProxy { + return container.get(WebSocketConnectionProvider).createProxy(path, target); + } + + protected channelIdSeq = 0; + protected readonly socket: WebSocket; + protected readonly channels = new Map(); + + constructor() { + const url = this.createWebSocketUrl(WebSocketChannel.wsPath); + const socket = this.createWebSocket(url); + socket.onerror = console.error; + socket.onclose = ({ code, reason }) => { + for (const channel of this.channels.values()) { + channel.fireClose(code, reason); + } + this.channels.clear(); + }; + socket.onmessage = ({ data }) => { + const message: WebSocketChannel.Message = JSON.parse(data); + const channel = this.channels.get(message.id); + if (channel) { + channel.handleMessage(message); + } else { + console.error('The ws channel does not exist', message.id); + } + }; + this.socket = socket; + } + + /** + * Create a proxy object to remote interface of T type + * over a web socket connection for the given path. + * + * An optional target can be provided to handle + * notifications and requests from a remote side. + */ + createProxy(path: string, target?: object): JsonRpcProxy { + const factory = new JsonRpcProxyFactory(target); + this.listen({ + path, + onConnection: c => factory.listen(c) + }); + return factory.createProxy(); + } + + /** + * Install a connection handler for the given path. + */ + listen(handler: ConnectionHandler, options?: WebSocketOptions): void { + if (this.socket.readyState === WebSocket.OPEN) { + this.openChannel(handler, options); + } else { + this.socket.addEventListener('open', () => this.openChannel(handler, options), { once: true }); + } + } + + protected openChannel(handler: ConnectionHandler, options?: WebSocketOptions): void { + const id = this.channelIdSeq++; + const channel = this.createChannel(id); + this.channels.set(id, channel); + channel.onOpen(() => { + const connection = createWebSocketConnection(channel, this.createLogger()); + connection.onDispose(() => this.closeChannel(id, handler, options)); + handler.onConnection(connection); + }); + channel.open(handler.path); + } + + protected createChannel(id: number): WebSocketChannel { + return new WebSocketChannel(id, content => this.socket.send(content)); + } + + protected createLogger(): Logger { + return new ConsoleLogger(); + } + + protected closeChannel(id: number, handler: ConnectionHandler, options?: WebSocketOptions): void { + const channel = this.channels.get(id); + if (channel) { + this.channels.delete(id); + if (this.socket.readyState < WebSocket.CLOSING) { + channel.close(); + } + } + const { reconnecting } = { reconnecting: true, ...options }; + if (reconnecting) { + this.listen(handler, options); + } + } + + /** + * Creates a websocket URL to the current location + */ + protected createWebSocketUrl(path: string): string { + const endpoint = new Endpoint({ path }); + return endpoint.getWebSocketUrl().toString(); + } + + /** + * Creates a web socket for the given url + */ + protected createWebSocket(url: string): WebSocket { + return new ReconnectingWebSocket(url, undefined, { + maxReconnectionDelay: 10000, + minReconnectionDelay: 1000, + reconnectionDelayGrowFactor: 1.3, + connectionTimeout: 10000, + maxRetries: Infinity, + debug: false + }); + } + +} diff --git a/packages/core/src/common/messaging/web-socket-channel.ts b/packages/core/src/common/messaging/web-socket-channel.ts new file mode 100644 index 0000000000000..3338246a81e1b --- /dev/null +++ b/packages/core/src/common/messaging/web-socket-channel.ts @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2018 TypeFox and others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + */ + +// tslint:disable:no-any + +import { IWebSocket } from "vscode-ws-jsonrpc/lib/socket/socket"; +import { Disposable, DisposableCollection } from "../disposable"; + +export class WebSocketChannel implements IWebSocket { + + static wsPath = '/services'; + + protected readonly toDispose = new DisposableCollection(); + + constructor( + readonly id: number, + protected readonly doSend: (content: string) => void + ) { + this.toDispose.push(Disposable.NULL); + } + + dispose(): void { + this.toDispose.dispose(); + } + + protected checkNotDisposed(): void { + if (this.toDispose.disposed) { + throw new Error('The channel has been disposed.'); + } + } + + handleMessage(message: WebSocketChannel.Message) { + if (message.kind === 'ready') { + this.fireOpen(); + } else if (message.kind === 'data') { + this.fireMessage(message.content); + } else if (message.kind === 'close') { + this.fireClose(1000, ''); + } + } + + open(path: string): void { + this.checkNotDisposed(); + this.doSend(JSON.stringify({ + kind: 'open', + id: this.id, + path + })); + } + + ready(): void { + this.checkNotDisposed(); + this.doSend(JSON.stringify({ + kind: 'ready', + id: this.id + })); + } + + send(content: string): void { + this.checkNotDisposed(); + this.doSend(JSON.stringify({ + kind: 'data', + id: this.id, + content + })); + } + + close(): void { + this.checkNotDisposed(); + this.doSend(JSON.stringify({ + kind: 'close', + id: this.id + })); + } + + protected fireOpen: () => void = () => { }; + onOpen(cb: () => void): void { + this.checkNotDisposed(); + this.fireOpen = cb; + this.toDispose.push(Disposable.create(() => this.fireOpen = () => { })); + } + + protected fireMessage: (data: any) => void = () => { }; + onMessage(cb: (data: any) => void): void { + this.checkNotDisposed(); + this.fireMessage = cb; + this.toDispose.push(Disposable.create(() => this.fireMessage = () => { })); + } + + fireError: (reason: any) => void = () => { }; + onError(cb: (reason: any) => void): void { + this.checkNotDisposed(); + this.fireError = cb; + this.toDispose.push(Disposable.create(() => this.fireError = () => { })); + } + + fireClose: (code: number, reason: string) => void = () => { }; + onClose(cb: (code: number, reason: string) => void): void { + this.checkNotDisposed(); + this.fireClose = cb; + this.toDispose.push(Disposable.create(() => this.fireClose = () => { })); + } + +} +export namespace WebSocketChannel { + export interface OpenMessage { + kind: 'open' + id: number + path: string + } + export interface ReadyMessage { + kind: 'ready' + id: number + } + export interface DataMessage { + kind: 'data' + id: number + content: string + } + export interface CloseMessage { + kind: 'close' + id: number + } + export type Message = OpenMessage | ReadyMessage | DataMessage | CloseMessage; +} diff --git a/packages/core/src/node/messaging/messaging-contribution.ts b/packages/core/src/node/messaging/messaging-contribution.ts index b47060f74b349..cde447608baa5 100644 --- a/packages/core/src/node/messaging/messaging-contribution.ts +++ b/packages/core/src/node/messaging/messaging-contribution.ts @@ -14,8 +14,8 @@ import { MessageConnection } from 'vscode-jsonrpc'; import { createWebSocketConnection } from 'vscode-ws-jsonrpc/lib/socket/connection'; import { IConnection } from 'vscode-ws-jsonrpc/lib/server/connection'; import * as launch from 'vscode-ws-jsonrpc/lib/server/launch'; -import { IWebSocket } from 'vscode-ws-jsonrpc/lib/socket/socket'; import { ContributionProvider, ConnectionHandler } from '../../common'; +import { WebSocketChannel } from '../../common/messaging/web-socket-channel'; import { BackendApplicationContribution } from "../backend-application"; import { MessagingService } from './messaging-service'; import { ConsoleLogger } from "./logger"; @@ -31,8 +31,12 @@ export class MessagingContribution implements BackendApplicationContribution, Me @inject(ContributionProvider) @named(MessagingService.Contribution) protected readonly contributions: ContributionProvider; + protected readonly wsHandlers = new MessagingContribution.ConnectionHandlers(); + protected readonly channelHandlers = new MessagingContribution.ConnectionHandlers(); + @postConstruct() protected init(): void { + this.ws(WebSocketChannel.wsPath, (_, socket) => this.handleChannels(socket)); for (const contribution of this.contributions.getContributions()) { contribution.configure(this); } @@ -43,46 +47,22 @@ export class MessagingContribution implements BackendApplicationContribution, Me } } - listen(spec: string, callback: (params: MessagingService.Params, connection: MessageConnection) => void): void { - return this.pushAcceptor(spec, (params, socket) => { - const connection = createWebSocketConnection(this.toIWebSocket(socket), new ConsoleLogger()); + listen(spec: string, callback: (params: MessagingService.PathParams, connection: MessageConnection) => void): void { + return this.channelHandlers.push(spec, (params, channel) => { + const connection = createWebSocketConnection(channel, new ConsoleLogger()); callback(params, connection); }); } - forward(spec: string, callback: (params: MessagingService.Params, connection: IConnection) => void): void { - return this.pushAcceptor(spec, (params, socket) => { - const connection = launch.createWebSocketConnection(this.toIWebSocket(socket)); + forward(spec: string, callback: (params: MessagingService.PathParams, connection: IConnection) => void): void { + return this.channelHandlers.push(spec, (params, channel) => { + const connection = launch.createWebSocketConnection(channel); callback(params, connection); }); } - protected readonly acceptors: ((path: string, socket: ws) => boolean)[] = []; - protected pushAcceptor(spec: string, callback: (params: MessagingService.Params, socket: ws) => void): void { - const route = new Route(spec); - this.acceptors.push((path, socket) => { - const params = route.match(path); - if (!params) { - return false; - } - callback(params, socket); - return true; - }); - } - protected dispatch(socket: ws, request: http.IncomingMessage): void { - const pathname = request.url && url.parse(request.url).pathname; - if (!pathname) { - return; - } - for (const acceptor of this.acceptors) { - try { - if (acceptor(pathname, socket)) { - return; - } - } catch (e) { - console.error(e); - } - } + ws(spec: string, callback: (params: MessagingService.PathParams, socket: ws) => void): void { + return this.wsHandlers.push(spec, callback); } protected checkAliveTimeout = 30000; @@ -97,7 +77,7 @@ export class MessagingContribution implements BackendApplicationContribution, Me wss.on('connection', (socket: CheckAliveWS, request) => { socket.alive = true; socket.on('pong', () => socket.alive = true); - this.dispatch(socket, request); + this.handleConnection(socket, request); }); setInterval(() => { wss.clients.forEach((socket: CheckAliveWS) => { @@ -110,22 +90,89 @@ export class MessagingContribution implements BackendApplicationContribution, Me }, this.checkAliveTimeout); } - protected toIWebSocket(webSocket: ws): IWebSocket { - return { - send: content => webSocket.send(content, error => { - if (error) { - console.log(error); + protected handleConnection(socket: ws, request: http.IncomingMessage): void { + const pathname = request.url && url.parse(request.url).pathname; + if (pathname && !this.wsHandlers.route(pathname, socket)) { + console.error('Cannot find a ws handler for the path: ' + pathname); + } + } + + protected readonly channels = new Map(); + protected handleChannels(socket: ws): void { + socket.on('message', data => { + const message: WebSocketChannel.Message = JSON.parse(data.toString()); + if (message.kind === 'open') { + const { id, path } = message; + const channel = this.createChannel(id, socket); + if (this.channelHandlers.route(path, channel)) { + channel.ready(); + this.channels.set(id, channel); + } else { + console.error('Cannot find a service for the path: ' + path); } - }), - onMessage: cb => webSocket.on('message', cb), - onError: cb => webSocket.on('error', cb), - onClose: cb => webSocket.on('close', cb), - dispose: () => { - if (webSocket.readyState < ws.CLOSING) { - webSocket.close(); + } else { + const { id } = message; + const channel = this.channels.get(id); + if (channel) { + if (message.kind === 'close') { + this.channels.delete(id); + } + channel.handleMessage(message); + } else { + console.error('The ws channel does not exist', id); } } - }; + }); + socket.on('error', err => { + for (const channel of this.channels.values()) { + channel.fireError(err); + } + }); + socket.on('close', (code, reason) => { + for (const channel of this.channels.values()) { + channel.fireClose(code, reason); + } + this.channels.clear(); + }); + } + + protected createChannel(id: number, socket: ws): WebSocketChannel { + return new WebSocketChannel(id, content => socket.send(content, err => { + if (err) { + throw err; + } + })); } } +export namespace MessagingContribution { + export class ConnectionHandlers { + protected readonly handlers: ((path: string, connection: T) => string | false)[] = []; + + push(spec: string, callback: (params: MessagingService.PathParams, connection: T) => void): void { + const route = new Route(spec); + this.handlers.push((path, channel) => { + const params = route.match(path); + if (!params) { + return false; + } + callback(params, channel); + return route.reverse(params); + }); + } + + route(path: string, connection: T): string | false { + for (const handler of this.handlers) { + try { + const result = handler(path, connection); + if (result) { + return result; + } + } catch (e) { + console.error(e); + } + } + return false; + } + } +} diff --git a/packages/core/src/node/messaging/messaging-service.ts b/packages/core/src/node/messaging/messaging-service.ts index f6b8c8f325691..4bd788a37aa64 100644 --- a/packages/core/src/node/messaging/messaging-service.ts +++ b/packages/core/src/node/messaging/messaging-service.ts @@ -1,4 +1,3 @@ - /* * Copyright (C) 2018 TypeFox and others. * @@ -6,15 +5,33 @@ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ +import * as ws from 'ws'; import { MessageConnection } from "vscode-jsonrpc"; import { IConnection } from "vscode-ws-jsonrpc/lib/server/connection"; export interface MessagingService { - listen(path: string, callback: (params: MessagingService.Params, connection: MessageConnection) => void): void; - forward(path: string, callback: (params: MessagingService.Params, connection: IConnection) => void): void; + /** + * Accept a JSON-RPC connection on the given path. + * A path supports the route syntax: https://github.com/rcs/route-parser#what-can-i-use-in-my-routes. + */ + listen(path: string, callback: (params: MessagingService.PathParams, connection: MessageConnection) => void): void; + /** + * Accept a raw JSON-RPC connection on the given path. + * A path supports the route syntax: https://github.com/rcs/route-parser#what-can-i-use-in-my-routes. + */ + forward(path: string, callback: (params: MessagingService.PathParams, connection: IConnection) => void): void; + /** + * Accept a web socket connection on the given path. + * A path supports the route syntax: https://github.com/rcs/route-parser#what-can-i-use-in-my-routes. + * + * #### Important + * Prefer JSON-RPC connections over web sockets. Clients can handle only limited amount of web sockets + * and excessive amount can cause performance degradation. All JSON-RPC connections share the single web socket connection. + */ + ws(path: string, callback: (params: MessagingService.PathParams, socket: ws) => void): void; } export namespace MessagingService { - export interface Params { + export interface PathParams { [name: string]: string } export const Contribution = Symbol('MessagingService.Contribution'); diff --git a/packages/terminal/src/node/shell-terminal-server.spec.ts b/packages/terminal/src/node/shell-terminal-server.spec.ts index 068f85e62319c..055a4f4716504 100644 --- a/packages/terminal/src/node/shell-terminal-server.spec.ts +++ b/packages/terminal/src/node/shell-terminal-server.spec.ts @@ -5,7 +5,7 @@ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ import * as chai from 'chai'; -import { testContainer } from './test/inversify.spec-config'; +import { createTerminalTestContainer } from './test/terminal-test-container'; import { IShellTerminalServer } from '../common/shell-terminal-protocol'; /** @@ -17,7 +17,11 @@ const expect = chai.expect; describe('ShellServer', function () { this.timeout(5000); - const shellTerminalServer = testContainer.get(IShellTerminalServer); + let shellTerminalServer: IShellTerminalServer; + + beforeEach(() => { + shellTerminalServer = createTerminalTestContainer().get(IShellTerminalServer); + }); it('test shell terminal create', async function () { const createResult = shellTerminalServer.create({}); diff --git a/packages/terminal/src/node/terminal-backend-contribution.slow-spec.ts b/packages/terminal/src/node/terminal-backend-contribution.slow-spec.ts index 6c315be4f2ff0..fab3f517931c6 100644 --- a/packages/terminal/src/node/terminal-backend-contribution.slow-spec.ts +++ b/packages/terminal/src/node/terminal-backend-contribution.slow-spec.ts @@ -5,8 +5,9 @@ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ -import { testContainer } from './test/inversify.spec-config'; +import { createTerminalTestContainer } from './test/terminal-test-container'; import { BackendApplication } from '@theia/core/lib/node/backend-application'; +import { WebSocketChannel } from '@theia/core/lib/common/messaging/web-socket-channel'; import { IShellTerminalServer } from '../common/shell-terminal-protocol'; import * as ws from 'ws'; import * as http from 'http'; @@ -19,25 +20,31 @@ describe('Terminal Backend Contribution', function () { let server: http.Server | https.Server; let shellTerminalServer: IShellTerminalServer; - before(async function () { - const application = testContainer.get(BackendApplication); - shellTerminalServer = testContainer.get(IShellTerminalServer); + beforeEach(async () => { + const container = createTerminalTestContainer(); + const application = container.get(BackendApplication); + shellTerminalServer = container.get(IShellTerminalServer); server = await application.start(); }); - it("is data received from the terminal ws server", async function () { + it("is data received from the terminal ws server", async () => { const terminalId = await shellTerminalServer.create({}); - const p = new Promise((resolve, reject) => { - const socket = new ws(`ws://localhost:${server.address().port}${terminalsPath}/${terminalId}`); - socket.on('message', msg => { + await new Promise((resolve, reject) => { + const socket = new ws(`ws://localhost:${server.address().port}/services`); + socket.on('error', reject); + socket.on('close', (code, reason) => reject(`socket is closed with '${code}' code and '${reason}' reason`)); + + const channel = new WebSocketChannel(0, content => socket.send(content)); + channel.onOpen(() => { resolve(); socket.close(); }); - socket.on('error', error => { - reject(error); - }); + socket.on('message', data => + channel.handleMessage(JSON.parse(data.toString())) + ); + socket.on('open', () => + channel.open(`${terminalsPath}/${terminalId}`) + ); }); - - await p; }); }); diff --git a/packages/terminal/src/node/terminal-server.spec.ts b/packages/terminal/src/node/terminal-server.spec.ts index ca6b204df2da3..234fe1be93f64 100644 --- a/packages/terminal/src/node/terminal-server.spec.ts +++ b/packages/terminal/src/node/terminal-server.spec.ts @@ -6,7 +6,7 @@ */ import * as chai from 'chai'; -import { testContainer } from './test/inversify.spec-config'; +import { createTerminalTestContainer } from './test/terminal-test-container'; import { TerminalWatcher } from '../common/terminal-watcher'; import { ITerminalServer } from '../common/terminal-protocol'; import { IBaseTerminalExitEvent } from '../common/base-terminal-protocol'; @@ -21,8 +21,14 @@ const expect = chai.expect; describe('TermninalServer', function () { this.timeout(5000); - const terminalServer = testContainer.get(ITerminalServer); - const terminalWatcher = testContainer.get(TerminalWatcher); + let terminalServer: ITerminalServer; + let terminalWatcher: TerminalWatcher; + + beforeEach(() => { + const container = createTerminalTestContainer(); + terminalServer = container.get(ITerminalServer); + terminalWatcher = container.get(TerminalWatcher); + }); it('test terminal create', async function () { const args = ['--version']; diff --git a/packages/terminal/src/node/test/inversify.spec-config.ts b/packages/terminal/src/node/test/terminal-test-container.ts similarity index 55% rename from packages/terminal/src/node/test/inversify.spec-config.ts rename to packages/terminal/src/node/test/terminal-test-container.ts index 22d71926a9857..102b8bee52b76 100644 --- a/packages/terminal/src/node/test/inversify.spec-config.ts +++ b/packages/terminal/src/node/test/terminal-test-container.ts @@ -8,13 +8,15 @@ import { Container } from 'inversify'; import { loggerBackendModule } from '@theia/core/lib/node/logger-backend-module'; import { backendApplicationModule } from '@theia/core/lib/node/backend-application-module'; import processBackendModule from '@theia/process/lib/node/process-backend-module'; +import { messagingBackendModule } from '@theia/core/lib/node/messaging/messaging-backend-module'; import terminalBackendModule from '../terminal-backend-module'; -const testContainer = new Container(); - -testContainer.load(backendApplicationModule); -testContainer.load(loggerBackendModule); -testContainer.load(processBackendModule); -testContainer.load(terminalBackendModule); - -export { testContainer }; +export function createTerminalTestContainer() { + const terminalTestContainer = new Container(); + terminalTestContainer.load(backendApplicationModule); + terminalTestContainer.load(loggerBackendModule); + terminalTestContainer.load(messagingBackendModule); + terminalTestContainer.load(processBackendModule); + terminalTestContainer.load(terminalBackendModule); + return terminalTestContainer; +}