From 5b16961ac32efbc74aa663381577306307ade8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=A4der?= Date: Mon, 21 Feb 2022 11:41:08 +0100 Subject: [PATCH 1/3] Protoype of a faster RPC protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Mäder --- .../core/src/common/message-rpc/README.md | 10 + .../array-buffer-message-buffer.spec.ts | 41 ++ .../array-buffer-message-buffer.ts | 124 ++++++ .../src/common/message-rpc/channel.spec.ts | 67 +++ .../core/src/common/message-rpc/channel.ts | 215 ++++++++++ .../common/message-rpc/connection-handler.ts | 38 ++ .../src/common/message-rpc/experiments.ts | 56 +++ .../src/common/message-rpc/message-buffer.ts | 70 +++ .../message-rpc/message-encoder.spec.ts | 39 ++ .../src/common/message-rpc/message-encoder.ts | 400 ++++++++++++++++++ .../src/common/message-rpc/rpc-protocol.ts | 163 +++++++ .../core/src/common/message-rpc/rpc-proxy.ts | 93 ++++ .../message-rpc/websocket-client-channel.ts | 229 ++++++++++ 13 files changed, 1545 insertions(+) create mode 100644 packages/core/src/common/message-rpc/README.md create mode 100644 packages/core/src/common/message-rpc/array-buffer-message-buffer.spec.ts create mode 100644 packages/core/src/common/message-rpc/array-buffer-message-buffer.ts create mode 100644 packages/core/src/common/message-rpc/channel.spec.ts create mode 100644 packages/core/src/common/message-rpc/channel.ts create mode 100644 packages/core/src/common/message-rpc/connection-handler.ts create mode 100644 packages/core/src/common/message-rpc/experiments.ts create mode 100644 packages/core/src/common/message-rpc/message-buffer.ts create mode 100644 packages/core/src/common/message-rpc/message-encoder.spec.ts create mode 100644 packages/core/src/common/message-rpc/message-encoder.ts create mode 100644 packages/core/src/common/message-rpc/rpc-protocol.ts create mode 100644 packages/core/src/common/message-rpc/rpc-proxy.ts create mode 100644 packages/core/src/common/message-rpc/websocket-client-channel.ts diff --git a/packages/core/src/common/message-rpc/README.md b/packages/core/src/common/message-rpc/README.md new file mode 100644 index 0000000000000..d94e3170c0906 --- /dev/null +++ b/packages/core/src/common/message-rpc/README.md @@ -0,0 +1,10 @@ +# message-rpc + +An attempt to rewrite the theia RPC infrastructure with a couple of changes: + +1. "Zero-copy" message writing and reading +2. Support for binary buffers without ever encoding them +3. Separate RPC server from RPC client +4. Use a unified "Channel" interface + +A lot of this code is more or less copied from the current Theia code. diff --git a/packages/core/src/common/message-rpc/array-buffer-message-buffer.spec.ts b/packages/core/src/common/message-rpc/array-buffer-message-buffer.spec.ts new file mode 100644 index 0000000000000..9c84a7ba7558a --- /dev/null +++ b/packages/core/src/common/message-rpc/array-buffer-message-buffer.spec.ts @@ -0,0 +1,41 @@ +/******************************************************************************** + * Copyright (C) 2021 Red Hat, Inc. 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 { expect } from 'chai'; +import { ArrayBufferReadBuffer, ArrrayBufferWriteBuffer } from './array-buffer-message-buffer'; + +describe('array message buffer tests', () => { + it('basic read write test', () => { + const buffer = new ArrayBuffer(1024); + const writer = new ArrrayBufferWriteBuffer(buffer); + + writer.writeByte(8); + writer.writeInt(10000); + writer.writeBytes(new Uint8Array([1, 2, 3, 4])); + writer.writeString('this is a string'); + writer.writeString('another string'); + writer.commit(); + + const written = writer.getCurrentContents(); + + const reader = new ArrayBufferReadBuffer(written); + + expect(reader.readByte()).equal(8); + expect(reader.readInt()).equal(10000); + expect(reader.readBytes()).deep.equal(new Uint8Array([1, 2, 3, 4]).buffer); + expect(reader.readString()).equal('this is a string'); + expect(reader.readString()).equal('another string'); + }); +}); diff --git a/packages/core/src/common/message-rpc/array-buffer-message-buffer.ts b/packages/core/src/common/message-rpc/array-buffer-message-buffer.ts new file mode 100644 index 0000000000000..cf5d8832705f5 --- /dev/null +++ b/packages/core/src/common/message-rpc/array-buffer-message-buffer.ts @@ -0,0 +1,124 @@ +/******************************************************************************** + * Copyright (C) 2021 Red Hat, Inc. 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 { Emitter, Event } from '../event'; +import { ReadBuffer, WriteBuffer } from './message-buffer'; + +export class ArrrayBufferWriteBuffer implements WriteBuffer { + constructor(private buffer: ArrayBuffer = new ArrayBuffer(1024), private offset: number = 0) { + } + + private get msg(): DataView { + return new DataView(this.buffer); + } + + ensureCapacity(value: number): WriteBuffer { + let newLength = this.buffer.byteLength; + while (newLength < this.offset + value) { + newLength *= 2; + } + if (newLength !== this.buffer.byteLength) { + const newBuffer = new ArrayBuffer(newLength); + new Uint8Array(newBuffer).set(new Uint8Array(this.buffer)); + this.buffer = newBuffer; + } + return this; + } + + writeByte(value: number): WriteBuffer { + this.ensureCapacity(1); + this.msg.setUint8(this.offset++, value); + return this; + } + + writeInt(value: number): WriteBuffer { + this.ensureCapacity(4); + this.msg.setUint32(this.offset, value); + this.offset += 4; + return this; + } + + writeString(value: string): WriteBuffer { + const encoded = this.encodeString(value); + this.writeBytes(encoded); + return this; + } + + private encodeString(value: string): Uint8Array { + return new TextEncoder().encode(value); + } + + writeBytes(value: ArrayBuffer): WriteBuffer { + this.ensureCapacity(value.byteLength + 4); + this.writeInt(value.byteLength); + new Uint8Array(this.buffer).set(new Uint8Array(value), this.offset); + this.offset += value.byteLength; + return this; + } + + private onCommitEmitter = new Emitter(); + get onCommit(): Event { + return this.onCommitEmitter.event; + } + + commit(): void { + this.onCommitEmitter.fire(this.getCurrentContents()); + } + + getCurrentContents(): ArrayBuffer { + return this.buffer.slice(0, this.offset); + } +} + +export class ArrayBufferReadBuffer implements ReadBuffer { + private offset: number = 0; + + constructor(private readonly buffer: ArrayBuffer) { + } + + private get msg(): DataView { + return new DataView(this.buffer); + } + + readByte(): number { + return this.msg.getUint8(this.offset++); + } + + readInt(): number { + const result = this.msg.getInt32(this.offset); + this.offset += 4; + return result; + } + + readString(): string { + const len = this.msg.getUint32(this.offset); + this.offset += 4; + const result = this.decodeString(this.buffer.slice(this.offset, this.offset + len)); + this.offset += len; + return result; + } + + private decodeString(buf: ArrayBuffer): string { + return new TextDecoder().decode(buf); + } + + readBytes(): ArrayBuffer { + const length = this.msg.getUint32(this.offset); + this.offset += 4; + const result = this.buffer.slice(this.offset, this.offset + length); + this.offset += length; + return result; + } +} diff --git a/packages/core/src/common/message-rpc/channel.spec.ts b/packages/core/src/common/message-rpc/channel.spec.ts new file mode 100644 index 0000000000000..6c372ffb64a06 --- /dev/null +++ b/packages/core/src/common/message-rpc/channel.spec.ts @@ -0,0 +1,67 @@ +/******************************************************************************** + * Copyright (C) 2021 Red Hat, Inc. 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 { assert, expect, spy, use } from 'chai'; +import * as spies from 'chai-spies'; + +import { ChannelMultiplexer, ChannelPipe } from './channel'; +import { ReadBuffer } from './message-buffer'; + +use(spies); + +describe('multiplexer test', () => { + it('multiplex message', async () => { + const pipe = new ChannelPipe(); + + const leftMultiplexer = new ChannelMultiplexer(pipe.left); + const rightMultiplexer = new ChannelMultiplexer(pipe.right); + const openChannelSpy = spy(() => { + }); + + rightMultiplexer.onDidOpenChannel(openChannelSpy); + leftMultiplexer.onDidOpenChannel(openChannelSpy); + + const leftFirst = await leftMultiplexer.open('first'); + const leftSecond = await leftMultiplexer.open('second'); + + const rightFirst = rightMultiplexer.getOpenChannel('first'); + const rightSecond = rightMultiplexer.getOpenChannel('second'); + + assert.isNotNull(rightFirst); + assert.isNotNull(rightSecond); + + const leftSecondSpy = spy((buf: ReadBuffer) => { + const message = buf.readString(); + expect(message).equal('message for second'); + }); + + leftSecond.onMessage(leftSecondSpy); + + const rightFirstSpy = spy((buf: ReadBuffer) => { + const message = buf.readString(); + expect(message).equal('message for first'); + }); + + rightFirst!.onMessage(rightFirstSpy); + + leftFirst.getWriteBuffer().writeString('message for first').commit(); + rightSecond!.getWriteBuffer().writeString('message for second').commit(); + + expect(leftSecondSpy).to.be.called(); + expect(rightFirstSpy).to.be.called(); + + expect(openChannelSpy).to.be.called.exactly(4); + }) +}); diff --git a/packages/core/src/common/message-rpc/channel.ts b/packages/core/src/common/message-rpc/channel.ts new file mode 100644 index 0000000000000..da7342251c291 --- /dev/null +++ b/packages/core/src/common/message-rpc/channel.ts @@ -0,0 +1,215 @@ +/******************************************************************************** + * Copyright (C) 2021 Red Hat, Inc. 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 { ArrayBufferReadBuffer, ArrrayBufferWriteBuffer } from './array-buffer-message-buffer'; +import { Emitter, Event } from '../event'; +import { ReadBuffer, WriteBuffer } from './message-buffer'; + +/** + * A channel is a bidirectinal communications channel with lifecycle and + * error signalling. Note that creation of channels is specific to particular + * implementations and thus not part of the protocol. + */ +export interface Channel { + /** + * The remote side has closed the channel + */ + onClose: Event; + /** + * An error has occurred while writing to or reading from the channel + */ + onError: Event; + /** + * A message has arrived and can be read using the given {@link ReadBuffer} + */ + onMessage: Event; + /** + * Obtain a {@link WriteBuffer} to write a message to the channel. + */ + getWriteBuffer(): WriteBuffer; + /** + * Close this channel. No {@link onClose} event should be sent + */ + close(): void; +} + +enum MessageTypes { + Open = 1, + Close = 2, + AckOpen = 3, + Data = 4 +} + +/** + * Helper class to implement the single channels on a {@link ChannelMultiplexer} + */ +class ForwardingChannel implements Channel { + constructor(private readonly closeHander: () => void, private readonly writeBufferSource: () => WriteBuffer) { + } + + onCloseEmitter: Emitter = new Emitter(); + get onClose(): Event { + return this.onCloseEmitter.event; + }; + onErrorEmitter: Emitter = new Emitter(); + get onError(): Event { + return this.onErrorEmitter.event; + }; + onMessageEmitter: Emitter = new Emitter(); + get onMessage(): Event { + return this.onMessageEmitter.event; + }; + + getWriteBuffer(): WriteBuffer { + return this.writeBufferSource(); + } + + close(): void { + this.closeHander(); + } +} + +/** + * A class to encode/decode multiple channels over a single underlying {@link Channel} + * The write buffers in this implementation immediately write to the underlying + * channel, so we rely on writers to the multiplexed channels to always commit their + * messages and always in one go. + */ +export class ChannelMultiplexer { + protected pendingOpen: Map void> = new Map(); + protected openChannels: Map = new Map(); + + protected readonly onOpenChannelEmitter: Emitter = new Emitter(); + get onDidOpenChannel(): Event { + return this.onOpenChannelEmitter.event; + } + + constructor(protected readonly underlyingChannel: Channel) { + this.underlyingChannel.onMessage(buffer => this.handleMessage(buffer)); + this.underlyingChannel.onClose(() => this.handleClose()); + this.underlyingChannel.onError(error => this.handleError(error)); + } + + protected handleError(error: unknown): void { + this.openChannels.forEach(channel => { + channel.onErrorEmitter.fire(error); + }); + } + + protected handleClose(): void { + this.pendingOpen.clear(); + this.openChannels.forEach(channel => { + channel.close(); + }); + this.openChannels.clear(); + } + + protected handleMessage(buffer: ReadBuffer): void { + const type = buffer.readByte(); + const id = buffer.readString(); + switch (type) { + case MessageTypes.AckOpen: { + // edge case: both side try to open a channel at the same time. + const resolve = this.pendingOpen.get(id); + if (resolve) { + const channel = this.createChannel(id); + this.pendingOpen.delete(id); + this.openChannels.set(id, channel); + resolve!(channel); + this.onOpenChannelEmitter.fire(channel); + } + break; + } + case MessageTypes.Open: { + if (!this.openChannels.has(id)) { + const channel = this.createChannel(id); + this.openChannels.set(id, channel); + const resolve = this.pendingOpen.get(id); + if (resolve) { + // edge case: both side try to open a channel at the same time. + resolve(channel); + } + this.underlyingChannel.getWriteBuffer().writeByte(MessageTypes.AckOpen).writeString(id).commit(); + this.onOpenChannelEmitter.fire(channel); + } + + break; + } + case MessageTypes.Close: { + const channel = this.openChannels.get(id); + if (channel) { + channel.onCloseEmitter.fire(); + this.openChannels.delete(id); + } + break; + } + case MessageTypes.Data: { + const channel = this.openChannels.get(id); + if (channel) { + channel.onMessageEmitter.fire(buffer); + } + break; + } + + } + } + + protected createChannel(id: string): ForwardingChannel { + return new ForwardingChannel(() => this.closeChannel(id), () => { + const underlying = this.underlyingChannel.getWriteBuffer(); + underlying.writeByte(MessageTypes.Data); + underlying.writeString(id); + return underlying; + }); + } + + protected closeChannel(id: string): void { + this.underlyingChannel.getWriteBuffer().writeByte(MessageTypes.Close).writeString(id).commit(); + this.openChannels.get(id)!.onCloseEmitter.fire(); + this.openChannels.delete(id); + } + + open(id: string): Promise { + const result = new Promise((resolve, reject) => { + this.pendingOpen.set(id, resolve); + }); + this.underlyingChannel.getWriteBuffer().writeByte(MessageTypes.Open).writeString(id).commit(); + return result; + } + + getOpenChannel(id: string): Channel | undefined { + return this.openChannels.get(id); + } +} + +/** + * A pipe with two channels at each end for testing. + */ +export class ChannelPipe { + readonly left: ForwardingChannel = new ForwardingChannel(() => this.right.onCloseEmitter.fire(), () => { + const leftWrite = new ArrrayBufferWriteBuffer(); + leftWrite.onCommit(buffer => { + this.right.onMessageEmitter.fire(new ArrayBufferReadBuffer(buffer)); + }); + return leftWrite; + }); + readonly right: ForwardingChannel = new ForwardingChannel(() => this.left.onCloseEmitter.fire(), () => { + const rightWrite = new ArrrayBufferWriteBuffer(); + rightWrite.onCommit(buffer => { + this.left.onMessageEmitter.fire(new ArrayBufferReadBuffer(buffer)); + }); + return rightWrite; + }); +} diff --git a/packages/core/src/common/message-rpc/connection-handler.ts b/packages/core/src/common/message-rpc/connection-handler.ts new file mode 100644 index 0000000000000..d5fbfa277224a --- /dev/null +++ b/packages/core/src/common/message-rpc/connection-handler.ts @@ -0,0 +1,38 @@ +/******************************************************************************** + * Copyright (C) 2021 Red Hat, Inc. 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 { Channel } from './channel'; +import { RpcHandler, RpcProxyHandler } from './rpc-proxy'; + +interface ConnectionHandler { + onConnection(connection: Channel): void; +} + +export class JsonRpcConnectionHandler implements ConnectionHandler { + constructor( + readonly path: string, + readonly targetFactory: (proxy: T) => unknown, + ) { } + + onConnection(connection: Channel): void { + const proxyHandler = new RpcProxyHandler(); + // eslint-disable-next-line no-null/no-null + const proxy = new Proxy(Object.create(null), proxyHandler); + const target = this.targetFactory(proxy); + + new RpcHandler(target).onChannelOpen(connection); + proxyHandler.onChannelOpen(connection); + } +} diff --git a/packages/core/src/common/message-rpc/experiments.ts b/packages/core/src/common/message-rpc/experiments.ts new file mode 100644 index 0000000000000..60285ea3d7907 --- /dev/null +++ b/packages/core/src/common/message-rpc/experiments.ts @@ -0,0 +1,56 @@ +/******************************************************************************** + * Copyright (C) 2021 Red Hat, Inc. 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 { ChannelPipe } from './channel'; +import { RpcHandler, RpcProxyHandler } from './rpc-proxy'; +import * as fs from 'fs'; + +/** + * This file is for fiddling around and testing. Not production code. + */ + +const pipe = new ChannelPipe(); + +interface ReadFile { + read(path: string): Promise; +} + +class Server implements ReadFile { + read(path: string): Promise { + const bytes = fs.readFileSync(path); + const result = new ArrayBuffer(bytes.byteLength); + bytes.copy(new Uint8Array(result)); + return Promise.resolve(result); + } +} + +const handler = new RpcHandler(new Server()); +handler.onChannelOpen(pipe.right); + +const proxyHandler = new RpcProxyHandler(); +// eslint-disable-next-line no-null/no-null +const proxy: ReadFile = new Proxy(Object.create(null), proxyHandler); +proxyHandler.onChannelOpen(pipe.left); + +const t0 = new Date().getTime(); + +proxy.read(process.argv[2]).then(value => { + const t1 = new Date().getTime(); + console.log(`read file of length: ${value.byteLength} in ${t1 - t0}ms`); + console.log(value.slice(0, 20)); +}).catch(e => { + console.log(e); +}); + diff --git a/packages/core/src/common/message-rpc/message-buffer.ts b/packages/core/src/common/message-rpc/message-buffer.ts new file mode 100644 index 0000000000000..79466424512b5 --- /dev/null +++ b/packages/core/src/common/message-rpc/message-buffer.ts @@ -0,0 +1,70 @@ +/******************************************************************************** + * Copyright (C) 2021 Red Hat, Inc. 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 + ********************************************************************************/ + +/** + * A buffer maintaining a write position capable of writing primitive values + */ +export interface WriteBuffer { + writeByte(byte: number): WriteBuffer + writeInt(value: number): WriteBuffer; + writeString(value: string): WriteBuffer; + writeBytes(value: ArrayBuffer): WriteBuffer; + + /** + * Makes any writes to the buffer permanent, for example by sending the writes over a channel. + * You must obtain a new write buffer after committing + */ + commit(): void; +} + +export class ForwardingWriteBuffer implements WriteBuffer { + constructor(protected readonly underlying: WriteBuffer) { + } + writeByte(byte: number): WriteBuffer { + this.underlying.writeByte(byte); + return this; + } + + writeInt(value: number): WriteBuffer { + this.underlying.writeInt(value); + return this; + } + + writeString(value: string): WriteBuffer { + this.underlying.writeString(value); + return this; + } + + writeBytes(value: ArrayBuffer): WriteBuffer { + this.underlying.writeBytes(value); + return this; + } + + commit(): void { + this.underlying.commit(); + } +} + +/** + * A buffer maintaining a read position in a buffer containing a received message capable of + * reading primitive values. + */ +export interface ReadBuffer { + readByte(): number; + readInt(): number; + readString(): string; + readBytes(): ArrayBuffer; +} diff --git a/packages/core/src/common/message-rpc/message-encoder.spec.ts b/packages/core/src/common/message-rpc/message-encoder.spec.ts new file mode 100644 index 0000000000000..0f6108052c0a5 --- /dev/null +++ b/packages/core/src/common/message-rpc/message-encoder.spec.ts @@ -0,0 +1,39 @@ +/******************************************************************************** + * Copyright (C) 2021 Red Hat, Inc. 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 { expect } from 'chai'; +import { ArrayBufferReadBuffer, ArrrayBufferWriteBuffer } from './array-buffer-message-buffer'; +import { MessageDecoder, MessageEncoder } from './message-encoder'; + +describe('message buffer test', () => { + it('encode object', () => { + const buffer = new ArrayBuffer(1024); + const writer = new ArrrayBufferWriteBuffer(buffer); + + const encoder = new MessageEncoder(); + const jsonMangled = JSON.parse(JSON.stringify(encoder)); + + encoder.writeTypedValue(writer, encoder); + + const written = writer.getCurrentContents(); + + const reader = new ArrayBufferReadBuffer(written); + + const decoder = new MessageDecoder(); + const decoded = decoder.readTypedValue(reader); + + expect(decoded).deep.equal(jsonMangled); + }); +}); diff --git a/packages/core/src/common/message-rpc/message-encoder.ts b/packages/core/src/common/message-rpc/message-encoder.ts new file mode 100644 index 0000000000000..16e3a55593a30 --- /dev/null +++ b/packages/core/src/common/message-rpc/message-encoder.ts @@ -0,0 +1,400 @@ +/******************************************************************************** + * Copyright (C) 2021 Red Hat, Inc. 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 { ReadBuffer, WriteBuffer } from './message-buffer'; + +/** + * This code lets you encode rpc protocol messages (request/reply/notification/error/cancel) + * into a channel write buffer and decode the same messages from a read buffer. + * Custom encoders/decoders can be registered to specially handling certain types of values + * to be encoded. Clients are responsible for ensuring that the set of tags for encoders + * is distinct and the same at both ends of a channel. + */ + +export interface SerializedError { + readonly $isError: true; + readonly name: string; + readonly message: string; + readonly stack: string; +} + +export const enum MessageType { + Request = 1, + Notification = 2, + Reply = 3, + ReplyErr = 4, + Cancel = 5, +} + +export interface CancelMessage { + type: MessageType.Cancel; + id: number; +} + +export interface RequestMessage { + type: MessageType.Request; + id: number; + method: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args: any[]; +} + +export interface NotificationMessage { + type: MessageType.Notification; + id: number; + method: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args: any[]; +} + +export interface ReplyMessage { + type: MessageType.Reply; + id: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + res: any; +} + +export interface ReplyErrMessage { + type: MessageType.ReplyErr; + id: number; + err: SerializedError; +} + +export type RPCMessage = RequestMessage | ReplyMessage | ReplyErrMessage | CancelMessage | NotificationMessage; + +enum ObjectType { + JSON = 0, + ByteArray = 1, + ObjectArray = 2, + Undefined = 3, + Object = 4 +} +/** + * A value encoder writes javascript values to a write buffer. Encoders will be asked + * in turn (ordered by their tag value, descending) whether they can encode a given value + * This means encoders with higher tag values have priority. Since the default encoders + * have tag values from 0-4, they can be easily overridden. + */ +export interface ValueEncoder { + /** + * Returns true if this encoder wants to encode this value. + * @param value the value to be encoded + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + is(value: any): boolean; + /** + * Write the given value to the buffer. Will only be called if {@link is(value)} returns true. + * @param buf The buffer to write to + * @param value The value to be written + * @param recursiveEncode A function that will use the encoders registered on the {@link MessageEncoder} + * to write a value to the underlying buffer. This is used mostly to write structures like an array + * without having to know how to encode the values in the array + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + write(buf: WriteBuffer, value: any, recursiveEncode: (buf: WriteBuffer, value: any) => void): void; +} + +/** + * Reads javascript values from a read buffer + */ +export interface ValueDecoder { + /** + * Reads a value from a read buffer. This method will be called for the decoder that is + * registered for the tag associated with the value encoder that encoded this value. + * @param buf The read buffer to read from + * @param recursiveDecode A function that will use the decoders registered on the {@link MessageEncoder} + * to read values from the underlying read buffer. This is used mostly to decode structures like an array + * without having to know how to decode the values in the aray. + */ + read(buf: ReadBuffer, recursiveDecode: (buf: ReadBuffer) => unknown): unknown; +} + +/** + * A MessageDecoder parses a ReadBuffer into a RCPMessage + */ + +export class MessageDecoder { + protected decoders: Map = new Map(); + + constructor() { + this.registerDecoder(ObjectType.JSON, { + read: buf => { + const json = buf.readString(); + return JSON.parse(json); + } + }); + this.registerDecoder(ObjectType.ByteArray, { + read: buf => buf.readBytes() + }); + this.registerDecoder(ObjectType.ObjectArray, { + read: buf => this.readArray(buf) + }); + + this.registerDecoder(ObjectType.Undefined, { + read: () => undefined + }); + + this.registerDecoder(ObjectType.Object, { + read: (buf, recursiveRead) => { + const propertyCount = buf.readInt(); + const result = Object.create({}); + for (let i = 0; i < propertyCount; i++) { + const key = buf.readString(); + const value = recursiveRead(buf); + result[key] = value; + } + return result; + } + }); + } + + registerDecoder(tag: number, decoder: ValueDecoder): void { + if (this.decoders.has(tag)) { + throw new Error(`Decoder already registered: ${tag}`); + } + this.decoders.set(tag, decoder); + } + + parse(buf: ReadBuffer): RPCMessage { + try { + const msgType = buf.readByte(); + + switch (msgType) { + case MessageType.Request: + return this.parseRequest(buf); + case MessageType.Notification: + return this.parseNotification(buf); + case MessageType.Reply: + return this.parseReply(buf); + case MessageType.ReplyErr: + return this.parseReplyErr(buf); + case MessageType.Cancel: + return this.parseCancel(buf); + } + throw new Error(`Unknown message type: ${msgType}`); + } catch (e) { + // exception does not show problematic content: log it! + console.log('failed to parse message: ' + buf); + throw e; + } + } + + protected parseCancel(msg: ReadBuffer): CancelMessage { + const callId = msg.readInt(); + return { + type: MessageType.Cancel, + id: callId + }; + } + + protected parseRequest(msg: ReadBuffer): RequestMessage { + const callId = msg.readInt(); + const method = msg.readString(); + let args = this.readArray(msg); + // convert `null` to `undefined`, since we don't use `null` in internal plugin APIs + args = args.map(arg => arg === null ? undefined : arg); // eslint-disable-line no-null/no-null + + return { + type: MessageType.Request, + id: callId, + method: method, + args: args + }; + } + + protected parseNotification(msg: ReadBuffer): NotificationMessage { + const callId = msg.readInt(); + const method = msg.readString(); + let args = this.readArray(msg); + // convert `null` to `undefined`, since we don't use `null` in internal plugin APIs + args = args.map(arg => arg === null ? undefined : arg); // eslint-disable-line no-null/no-null + + return { + type: MessageType.Notification, + id: callId, + method: method, + args: args + }; + } + + parseReply(msg: ReadBuffer): ReplyMessage { + const callId = msg.readInt(); + const value = this.readTypedValue(msg); + return { + type: MessageType.Reply, + id: callId, + res: value + }; + } + + parseReplyErr(msg: ReadBuffer): ReplyErrMessage { + const callId = msg.readInt(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let err: any = this.readTypedValue(msg); + if (err && err.$isError) { + err = new Error(); + err.name = err.name; + err.message = err.message; + err.stack = err.stack; + } + return { + type: MessageType.ReplyErr, + id: callId, + err: err + }; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readArray(buf: ReadBuffer): any[] { + const length = buf.readInt(); + const result = new Array(length); + for (let i = 0; i < length; i++) { + result[i] = this.readTypedValue(buf); + } + return result; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readTypedValue(buf: ReadBuffer): any { + const type = buf.readInt(); + const decoder = this.decoders.get(type); + if (!decoder) { + throw new Error(`No decoder for tag ${type}`); + } + return decoder.read(buf, innerBuffer => this.readTypedValue(innerBuffer)); + } +} +/** + * A MessageEncoder writes RCPMessage objects to a WriteBuffer. Note that it is + * up to clients to commit the message. This allows for multiple messages being + * encoded before sending. + */ +export class MessageEncoder { + protected readonly encoders: [number, ValueEncoder][] = []; + protected readonly registeredTags: Set = new Set(); + + constructor() { + // encoders will be consulted in reverse order of registration, so the JSON fallback needs to be last + this.registerEncoder(ObjectType.JSON, { + is: () => true, + write: (buf, value) => { + buf.writeString(JSON.stringify(value)); + } + }); + this.registerEncoder(ObjectType.Object, { + is: value => typeof value === 'object', + write: (buf, object, recursiveEncode) => { + const properties = Object.keys(object); + const relevant = []; + for (const property of properties) { + const value = object[property]; + if (typeof value !== 'function') { + relevant.push([property, value]); + } + } + + buf.writeInt(relevant.length); + for (const [property, value] of relevant) { + buf.writeString(property); + recursiveEncode(buf, value); + } + } + }); + this.registerEncoder(ObjectType.Undefined, { + is: value => (typeof value === 'undefined'), + write: () => { } + }); + + this.registerEncoder(ObjectType.ObjectArray, { + is: value => Array.isArray(value), + write: (buf, value) => { + this.writeArray(buf, value); + } + }); + + this.registerEncoder(ObjectType.ByteArray, { + is: value => value instanceof ArrayBuffer, + write: (buf, value) => { + buf.writeBytes(value); + } + }); + } + + registerEncoder(tag: number, encoder: ValueEncoder): void { + if (this.registeredTags.has(tag)) { + throw new Error(`Tag already registered: ${tag}`); + } + this.registeredTags.add(tag); + this.encoders.push([tag, encoder]); + } + + cancel(buf: WriteBuffer, requestId: number): void { + buf.writeByte(MessageType.Cancel); + buf.writeInt(requestId); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + notification(buf: WriteBuffer, requestId: number, method: string, args: any[]): void { + buf.writeByte(MessageType.Notification); + buf.writeInt(requestId); + buf.writeString(method); + this.writeArray(buf, args); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request(buf: WriteBuffer, requestId: number, method: string, args: any[]): void { + buf.writeByte(MessageType.Request); + buf.writeInt(requestId); + buf.writeString(method); + this.writeArray(buf, args); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + replyOK(buf: WriteBuffer, requestId: number, res: any): void { + buf.writeByte(MessageType.Reply); + buf.writeInt(requestId); + this.writeTypedValue(buf, res); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + replyErr(buf: WriteBuffer, requestId: number, err: any): void { + buf.writeByte(MessageType.ReplyErr); + buf.writeInt(requestId); + this.writeTypedValue(buf, err); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + writeTypedValue(buf: WriteBuffer, value: any): void { + for (let i: number = this.encoders.length - 1; i >= 0; i--) { + if (this.encoders[i][1].is(value)) { + buf.writeInt(this.encoders[i][0]); + this.encoders[i][1].write(buf, value, (innerBuffer, innerValue) => { + this.writeTypedValue(innerBuffer, innerValue); + }); + return; + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + writeArray(buf: WriteBuffer, value: any[]): void { + buf.writeInt(value.length); + for (let i = 0; i < value.length; i++) { + this.writeTypedValue(buf, value[i]); + } + } + +} diff --git a/packages/core/src/common/message-rpc/rpc-protocol.ts b/packages/core/src/common/message-rpc/rpc-protocol.ts new file mode 100644 index 0000000000000..f9ea1c93cc044 --- /dev/null +++ b/packages/core/src/common/message-rpc/rpc-protocol.ts @@ -0,0 +1,163 @@ +/******************************************************************************** + * Copyright (C) 2021 Red Hat, Inc. 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 + ********************************************************************************/ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Emitter, Event } from '../event'; +import { Deferred } from '../promise-util'; +import { Channel } from './channel'; +import { ReadBuffer } from './message-buffer'; +import { MessageDecoder, MessageEncoder, MessageType } from './message-encoder'; +/** + * A RCPServer reads rcp request and notification messages and sends the reply values or + * errors from the request to the channel. + */ +export class RPCServer { + protected readonly encoder: MessageEncoder = new MessageEncoder(); + protected readonly decoder: MessageDecoder = new MessageDecoder(); + protected onNotificationEmitter: Emitter<{ method: string; args: any[]; }> = new Emitter(); + + get onNotification(): Event<{ method: string; args: any[]; }> { + return this.onNotificationEmitter.event; + } + + constructor(protected channel: Channel, public readonly requestHandler: (method: string, args: any[]) => Promise) { + const registration = channel.onMessage((data: ReadBuffer) => this.handleMessage(data)); + channel.onClose(() => registration.dispose()); + } + + handleMessage(data: ReadBuffer): void { + const message = this.decoder.parse(data); + switch (message.type) { + case MessageType.Cancel: { + this.handleCancel(message.id); + break; + } + case MessageType.Request: { + this.handleRequest(message.id, message.method, message.args); + break; + } + case MessageType.Notification: { + this.handleNotify(message.id, message.method, message.args); + break; + } + } + } + + protected handleCancel(id: number): void { + // implement cancellation + /* const token = this.cancellationTokens.get(id); + if (token) { + this.cancellationTokens.delete(id); + token.cancel(); + } else { + console.warn(`cancel: no token for message: ${id}`); + }*/ + } + + protected async handleRequest(id: number, method: string, args: any[]): Promise { + const output = this.channel.getWriteBuffer(); + try { + // console.log(`handling request ${method} with id ${id}`); + const result = await this.requestHandler(method, args); + this.encoder.replyOK(output, id, result); + // console.log(`handled request ${method} with id ${id}`); + } catch (err) { + this.encoder.replyErr(output, id, err); + console.log(`error on request ${method} with id ${id}`); + } + output.commit(); + } + + protected async handleNotify(id: number, method: string, args: any[]): Promise { + // console.log(`handling notification ${method} with id ${id}`); + this.onNotificationEmitter.fire({ method, args }); + } +} + +/** + * An RpcClient sends requests and notifications to a remote server. + * Clients can get a promise for the request result that will be either resolved or + * rejected depending on the success of the request. + * The RpcClient keeps track of outstanding requests and matches replies to the appropriate request + * Currently, there is no timeout handling implemented in the client. + */ +export class RpcClient { + protected readonly pendingRequests: Map> = new Map(); + protected nextMessageId: number = 0; + + protected readonly encoder: MessageEncoder = new MessageEncoder(); + protected readonly decoder: MessageDecoder = new MessageDecoder(); + + constructor(protected channel: Channel) { + const registration = channel.onMessage((data: ReadBuffer) => this.handleMessage(data)); + channel.onClose(() => registration.dispose()); + } + + handleMessage(data: ReadBuffer): void { + const message = this.decoder.parse(data); + switch (message.type) { + case MessageType.Reply: { + this.handleReply(message.id, message.res); + break; + } + case MessageType.ReplyErr: { + this.handleReplyErr(message.id, message.err); + break; + } + } + } + + protected handleReply(id: number, value: any): void { + const replyHandler = this.pendingRequests.get(id); + // console.log(`received reply with id ${id}`); + if (replyHandler) { + this.pendingRequests.delete(id); + replyHandler.resolve(value); + } else { + console.warn(`reply: no handler for message: ${id}`); + } + } + + protected handleReplyErr(id: number, error: any): void { + const replyHandler = this.pendingRequests.get(id); + if (replyHandler) { + this.pendingRequests.delete(id); + // console.log(`received error id ${id}`); + replyHandler.reject(error); + } else { + console.warn(`error: no handler for message: ${id}`); + } + } + + sendRequest(method: string, args: any[]): Promise { + const id = this.nextMessageId++; + const reply = new Deferred(); + // console.log(`sending request ${method} with id ${id}`); + + this.pendingRequests.set(id, reply); + const output = this.channel.getWriteBuffer(); + this.encoder.request(output, id, method, args); + output.commit(); + return reply.promise; + } + + sendNotification(method: string, args: any[]): void { + // console.log(`sending notification ${method} with id ${this.nextMessageId + 1}`); + const output = this.channel.getWriteBuffer(); + this.encoder.notification(output, this.nextMessageId++, method, args); + output.commit(); + } +} diff --git a/packages/core/src/common/message-rpc/rpc-proxy.ts b/packages/core/src/common/message-rpc/rpc-proxy.ts new file mode 100644 index 0000000000000..3578f64560942 --- /dev/null +++ b/packages/core/src/common/message-rpc/rpc-proxy.ts @@ -0,0 +1,93 @@ +/******************************************************************************** + * Copyright (C) 2021 Red Hat, Inc. 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 + ********************************************************************************/ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Deferred } from '../promise-util'; +import { Channel } from './channel'; +import { RpcClient, RPCServer } from './rpc-protocol'; + +/** + * A proxy handler that will send any method invocation on the proxied object + * as a rcp protocol message over a channel. + */ +export class RpcProxyHandler implements ProxyHandler { + private channelDeferred: Deferred = new Deferred(); + + onChannelOpen(channel: Channel): void { + const client = new RpcClient(channel); + this.channelDeferred.resolve(client); + } + + get?(target: T, p: string | symbol, receiver: any): any { + const isNotify = this.isNotification(p); + return (...args: any[]) => { + const method = p.toString(); + return this.channelDeferred.promise.then((connection: RpcClient) => + new Promise((resolve, reject) => { + try { + if (isNotify) { + // console.info(`Send notification ${method}`); + connection.sendNotification(method, args); + resolve(undefined); + } else { + // console.info(`Send request ${method}`); + const resultPromise = connection.sendRequest(method, args) as Promise; + resultPromise.then((result: any) => { + // console.info(`request succeeded: ${method}`); + resolve(result); + }).catch(e => { + reject(e); + }); + } + } catch (err) { + reject(err); + } + }) + ); + }; + } + + /** + * Return whether the given property represents a notification. If true, + * the promise returned from the invocation will resolve immediatey to `undefined` + * + * A property leads to a notification rather than a method call if its name + * begins with `notify` or `on`. + * + * @param p - The property being called on the proxy. + * @return Whether `p` represents a notification. + */ + protected isNotification(p: PropertyKey): boolean { + return p.toString().startsWith('notify') || p.toString().startsWith('on'); + } +} + +export class RpcHandler { + constructor(readonly target: any) { + } + + onChannelOpen(channel: Channel): void { + const server = new RPCServer(channel, (method: string, args: any[]) => this.handleRequest(method, args)); + server.onNotification((e: { method: string, args: any }) => this.onNotification(e.method, e.args)); + } + + protected async handleRequest(method: string, args: any[]): Promise { + return this.target[method](...args); + } + + protected onNotification(method: string, args: any[]): void { + this.target[method](args); + } +} diff --git a/packages/core/src/common/message-rpc/websocket-client-channel.ts b/packages/core/src/common/message-rpc/websocket-client-channel.ts new file mode 100644 index 0000000000000..bf07088f18448 --- /dev/null +++ b/packages/core/src/common/message-rpc/websocket-client-channel.ts @@ -0,0 +1,229 @@ +/******************************************************************************** + * Copyright (C) 2018 TypeFox 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 + ********************************************************************************/ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import ReconnectingWebSocket from 'reconnecting-websocket'; +import { v4 as uuid } from 'uuid'; +import { Channel } from './channel'; +import { ReadBuffer, WriteBuffer } from './message-buffer'; +import { ArrayBufferReadBuffer, ArrrayBufferWriteBuffer } from './array-buffer-message-buffer'; +import { Deferred } from '../promise-util'; +import { Emitter, Event } from '../event'; +import { Endpoint } from 'src/browser'; + +/** + * An attempt at a channel implementation over a websocket with fallback to http. + */ + +export interface WebSocketOptions { + /** + * True by default. + */ + reconnecting?: boolean; +} + +export const HttpFallbackOptions = Symbol('HttpFallbackOptions'); + +export interface HttpFallbackOptions { + /** Determines whether Theia is allowed to use the http fallback. True by default. */ + allowed: boolean; + /** Number of failed websocket connection attempts before the fallback is triggered. 2 by default. */ + maxAttempts: number; + /** The maximum duration (in ms) after which the http request should timeout. 5000 by default. */ + pollingTimeout: number; + /** The timeout duration (in ms) after a request was answered with an error code. 5000 by default. */ + errorTimeout: number; + /** The minimum timeout duration (in ms) between two http requests. 0 by default. */ + requestTimeout: number; +} + +export const DEFAULT_HTTP_FALLBACK_OPTIONS: HttpFallbackOptions = { + allowed: true, + maxAttempts: 2, + errorTimeout: 5000, + pollingTimeout: 5000, + requestTimeout: 0 +}; + +export class WebSocketClientChannel implements Channel { + + protected readonly readyDeferred: Deferred = new Deferred(); + + protected readonly onCloseEmitter: Emitter = new Emitter(); + get onClose(): Event { + return this.onCloseEmitter.event; + } + + protected readonly onMessageEmitter: Emitter = new Emitter(); + get onMessage(): Event { + return this.onMessageEmitter.event; + } + + protected readonly onErrorEmitter: Emitter = new Emitter(); + get onError(): Event { + return this.onErrorEmitter.event; + } + + protected readonly socket: ReconnectingWebSocket; + protected useHttpFallback = false; + protected websocketErrorCounter = 0; + protected httpFallbackId = uuid(); + protected httpFallbackDisconnected = true; + + constructor(protected readonly httpFallbackOptions: HttpFallbackOptions | undefined) { + const url = this.createWebSocketUrl('/services'); + const socket = this.createWebSocket(url); + socket.onerror = event => this.handleSocketError(event); + socket.onopen = () => { + this.fireSocketDidOpen(); + }; + socket.onclose = ({ code, reason }) => { + this.onCloseEmitter.fire(); + }; + socket.onmessage = ({ data }) => { + this.onMessageEmitter.fire(new ArrayBufferReadBuffer(data)); + }; + this.socket = socket; + window.addEventListener('offline', () => this.tryReconnect()); + window.addEventListener('online', () => this.tryReconnect()); + } + + getWriteBuffer(): WriteBuffer { + const result = new ArrrayBufferWriteBuffer(); + const httpUrl = this.createHttpWebSocketUrl('/services'); + if (this.useHttpFallback) { + result.writeString(this.httpFallbackId); + result.writeString('true'); + result.onCommit(buffer => { + fetch(httpUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream' + }, + body: buffer + }); + }); + + } else if (this.socket.readyState < WebSocket.CLOSING) { + result.onCommit(buffer => { + this.socket.send(buffer); + }); + } + return result; + + } + + close(): void { + this.socket.close(); + } + + get ready(): Promise { + return this.readyDeferred.promise; + } + + handleSocketError(event: unknown): void { + this.websocketErrorCounter += 1; + if (this.httpFallbackOptions?.allowed && this.websocketErrorCounter >= this.httpFallbackOptions?.maxAttempts) { + this.useHttpFallback = true; + this.socket.close(); + const httpUrl = this.createHttpWebSocketUrl('/services'); + this.readyDeferred.resolve(); + this.doLongPolling(httpUrl); + console.warn( + 'Could not establish a websocket connection. The application will be using the HTTP fallback mode. This may affect performance and the behavior of some features.' + ); + } + this.onErrorEmitter.fire(event); + console.error(event); + } + + async doLongPolling(url: string): Promise { + let timeoutDuration = this.httpFallbackOptions?.requestTimeout || 0; + const controller = new AbortController(); + const pollingId = window.setTimeout(() => controller.abort(), this.httpFallbackOptions?.pollingTimeout); + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + signal: controller.signal, + keepalive: true, + body: JSON.stringify({ id: this.httpFallbackId, polling: true }) + }); + if (response.status === 200) { + window.clearTimeout(pollingId); + if (this.httpFallbackDisconnected) { + this.fireSocketDidOpen(); + } + const bytes = await response.arrayBuffer(); + this.onMessageEmitter.fire(new ArrayBufferReadBuffer(bytes)); + } else { + timeoutDuration = this.httpFallbackOptions?.errorTimeout || 0; + this.httpFallbackDisconnected = true; + this.onCloseEmitter.fire(); + throw new Error('Response has error code: ' + response.status); + } + } catch (e) { + console.error('Error occurred during long polling', e); + } + setTimeout(() => this.doLongPolling(url), timeoutDuration); + } + + /** + * Creates a websocket URL to the current location + */ + protected createWebSocketUrl(path: string): string { + const endpoint = new Endpoint({ path }); + return endpoint.getWebSocketUrl().toString(); + } + + protected createHttpWebSocketUrl(path: string): string { + const endpoint = new Endpoint({ path }); + return endpoint.getRestUrl().toString(); + } + + /** + * Creates a web socket for the given url + */ + protected createWebSocket(url: string): ReconnectingWebSocket { + const socket = new ReconnectingWebSocket(url, undefined, { + maxReconnectionDelay: 10000, + minReconnectionDelay: 1000, + reconnectionDelayGrowFactor: 1.3, + connectionTimeout: 10000, + maxRetries: Infinity, + debug: false + }); + socket.binaryType = 'arraybuffer'; + return socket; + } + + protected fireSocketDidOpen(): void { + // Once a websocket connection has opened, disable the http fallback + if (this.httpFallbackOptions?.allowed) { + this.httpFallbackOptions.allowed = false; + } + this.readyDeferred.resolve(); + } + + protected tryReconnect(): void { + if (!this.useHttpFallback && this.socket.readyState !== WebSocket.CONNECTING) { + this.socket.reconnect(); + } + } + +} From cedee258c1f1b1ca4d9565569d583daf7241bdc8 Mon Sep 17 00:00:00 2001 From: Tobias Ortmayr Date: Fri, 25 Feb 2022 04:51:40 +0100 Subject: [PATCH 2/3] [WIP] Integrate new message-rpc prototype into core messaging API (extensions) Integrates the new message-rpc prototype into the core messaging API (replacing vscode-ws-jsonrpc). This has major impacts and the Messaging API as we no longer expose a `Connection` object (which was provided by vscode-ws-jsonrpc) and directly rely on the generic `Channel` implementation instead. Note: This is a WIP PR that is not ready to be reviewed yet and contains a lot of temporary hacks and workarounds (e.g. commenting out of *.spec.ts* files that have compilation errors) and is currently only working in browser-applications. It's intention is to present the current state of #10684 to interested parties. Contributed on behalf of STMicroelectronics. Closes #10684 --- package.json | 4 + .../messaging/ws-connection-provider.ts | 84 +++++--- .../array-buffer-message-buffer.spec.ts | 34 ++-- .../array-buffer-message-buffer.ts | 66 +++++-- .../src/common/message-rpc/channel.spec.ts | 44 ++--- .../core/src/common/message-rpc/channel.ts | 91 +++++---- .../common/message-rpc/connection-handler.ts | 30 +-- .../src/common/message-rpc/experiments.ts | 137 ++++++++----- .../src/common/message-rpc/message-buffer.ts | 33 ++-- .../message-rpc/message-encoder.spec.ts | 34 ++-- .../src/common/message-rpc/message-encoder.ts | 84 ++++---- .../src/common/message-rpc/rpc-protocol.ts | 164 ++++++++++++--- .../core/src/common/message-rpc/rpc-proxy.ts | 30 +-- .../message-rpc/websocket-client-channel.ts | 52 ++--- .../messaging/abstract-connection-provider.ts | 48 ++--- packages/core/src/common/messaging/handler.ts | 4 +- .../common/messaging/proxy-factory.spec.ts | 186 +++++++++--------- .../src/common/messaging/proxy-factory.ts | 28 +-- .../common/messaging/web-socket-channel.ts | 153 +------------- .../electron-ipc-connection-provider.ts | 25 ++- .../electron-ws-connection-provider.ts | 12 +- .../electron-messaging-contribution.ts | 113 ++++++----- .../messaging/electron-messaging-service.ts | 4 +- .../core/src/node/messaging/ipc-bootstrap.ts | 49 +++-- .../node/messaging/ipc-connection-provider.ts | 65 +++--- .../core/src/node/messaging/ipc-protocol.ts | 4 +- .../node/messaging/messaging-contribution.ts | 122 ++++++------ .../src/node/messaging/messaging-service.ts | 22 +-- .../messaging/test/test-web-socket-channel.ts | 56 +++--- .../src/browser/debug-session-connection.ts | 12 +- .../src/browser/debug-session-contribution.ts | 3 +- packages/debug/src/browser/debug-session.tsx | 38 ++-- .../debug/src/node/debug-adapter-session.ts | 6 +- packages/debug/src/node/debug-model.ts | 2 +- .../src/browser/notifications-manager.ts | 2 +- packages/plugin-ext/src/common/connection.ts | 40 ++-- .../debug/plugin-debug-session-factory.ts | 2 +- .../debug/plugin-debug-adapter-session.ts | 2 +- .../task/src/node/task-server.slow-spec.ts | 103 +++++----- .../src/browser/terminal-widget-impl.ts | 57 +++--- ...terminal-backend-contribution.slow-spec.ts | 70 ++++--- .../src/node/terminal-backend-contribution.ts | 22 ++- yarn.lock | 28 ++- 43 files changed, 1150 insertions(+), 1015 deletions(-) diff --git a/package.json b/package.json index 52350494e2f90..39e5bc7fc9f81 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "**/@types/node": "12" }, "devDependencies": { + "@types/chai": "4.3.0", + "@types/chai-spies": "1.0.3", "@types/chai-string": "^1.4.0", "@types/jsdom": "^11.0.4", "@types/node": "12", @@ -20,6 +22,8 @@ "@typescript-eslint/eslint-plugin": "^4.8.1", "@typescript-eslint/eslint-plugin-tslint": "^4.8.1", "@typescript-eslint/parser": "^4.8.1", + "chai": "4.3.4", + "chai-spies": "1.0.0", "chai-string": "^1.4.0", "chalk": "4.0.0", "concurrently": "^3.5.0", diff --git a/packages/core/src/browser/messaging/ws-connection-provider.ts b/packages/core/src/browser/messaging/ws-connection-provider.ts index f83aabda22826..6b5136af01a52 100644 --- a/packages/core/src/browser/messaging/ws-connection-provider.ts +++ b/packages/core/src/browser/messaging/ws-connection-provider.ts @@ -14,12 +14,15 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable, interfaces, decorate, unmanaged } from 'inversify'; -import { JsonRpcProxyFactory, JsonRpcProxy, Emitter, Event } from '../../common'; +import { decorate, injectable, interfaces, unmanaged } from 'inversify'; +import { io, Socket } from 'socket.io-client'; +import { Emitter, Event, JsonRpcProxy, JsonRpcProxyFactory } from '../../common'; +import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from '../../common/message-rpc/array-buffer-message-buffer'; +import { Channel, ReadBufferConstructor } from '../../common/message-rpc/channel'; +import { WriteBuffer } from '../../common/message-rpc/message-buffer'; +import { AbstractConnectionProvider } from '../../common/messaging/abstract-connection-provider'; import { WebSocketChannel } from '../../common/messaging/web-socket-channel'; import { Endpoint } from '../endpoint'; -import { AbstractConnectionProvider } from '../../common/messaging/abstract-connection-provider'; -import { io, Socket } from 'socket.io-client'; decorate(injectable(), JsonRpcProxyFactory); decorate(unmanaged(), JsonRpcProxyFactory, 0); @@ -35,6 +38,8 @@ export interface WebSocketOptions { export class WebSocketConnectionProvider extends AbstractConnectionProvider { protected readonly onSocketDidOpenEmitter: Emitter = new Emitter(); + // Socket that is used by the main channel + protected socket: Socket; get onSocketDidOpen(): Event { return this.onSocketDidOpenEmitter.event; } @@ -48,31 +53,23 @@ export class WebSocketConnectionProvider extends AbstractConnectionProvider(path, arg); } - protected readonly socket: Socket; - - constructor() { - super(); + protected createMainChannel(): Channel { const url = this.createWebSocketUrl(WebSocketChannel.wsPath); const socket = this.createWebSocket(url); + const channel = new SocketIOChannel(socket); socket.on('connect', () => { this.fireSocketDidOpen(); }); - socket.on('disconnect', reason => { - for (const channel of [...this.channels.values()]) { - channel.close(undefined, reason); - } - this.fireSocketDidClose(); - }); - socket.on('message', data => { - this.handleIncomingRawMessage(data); - }); + channel.onClose(() => this.fireSocketDidClose()); socket.connect(); this.socket = socket; + + return channel; } - override openChannel(path: string, handler: (channel: WebSocketChannel) => void, options?: WebSocketOptions): void { + override async openChannel(path: string, handler: (channel: Channel) => void, options?: WebSocketOptions): Promise { if (this.socket.connected) { - super.openChannel(path, handler, options); + return super.openChannel(path, handler, options); } else { const openChannel = () => { this.socket.off('connect', openChannel); @@ -82,14 +79,6 @@ export class WebSocketConnectionProvider extends AbstractConnectionProvider { - if (this.socket.connected) { - this.socket.send(content); - } - }); - } - /** * @param path The handler to reach in the backend. */ @@ -143,3 +132,44 @@ export class WebSocketConnectionProvider extends AbstractConnectionProvider = new Emitter(); + get onClose(): Event { + return this.onCloseEmitter.event; + } + + protected readonly onMessageEmitter: Emitter = new Emitter(); + get onMessage(): Event { + return this.onMessageEmitter.event; + } + + protected readonly onErrorEmitter: Emitter = new Emitter(); + get onError(): Event { + return this.onErrorEmitter.event; + } + + readonly id: string; + + constructor(protected readonly socket: Socket) { + socket.on('error', error => this.onErrorEmitter.fire(error)); + socket.on('disconnect', reason => this.onCloseEmitter.fire()); + socket.on('message', buffer => this.onMessageEmitter.fire(() => new ArrayBufferReadBuffer(buffer))); + this.id = socket.id; + } + + getWriteBuffer(): WriteBuffer { + const result = new ArrayBufferWriteBuffer(); + if (this.socket.connected) { + result.onCommit(buffer => { + this.socket.emit('message', buffer); + }); + } + return result; + } + + close(): void { + this.socket.close(); + } + +} diff --git a/packages/core/src/common/message-rpc/array-buffer-message-buffer.spec.ts b/packages/core/src/common/message-rpc/array-buffer-message-buffer.spec.ts index 9c84a7ba7558a..8d72251856176 100644 --- a/packages/core/src/common/message-rpc/array-buffer-message-buffer.spec.ts +++ b/packages/core/src/common/message-rpc/array-buffer-message-buffer.spec.ts @@ -1,25 +1,25 @@ -/******************************************************************************** - * Copyright (C) 2021 Red Hat, Inc. 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 - ********************************************************************************/ +// ***************************************************************************** +// Copyright (C) 2021 Red Hat, Inc. 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 { expect } from 'chai'; -import { ArrayBufferReadBuffer, ArrrayBufferWriteBuffer } from './array-buffer-message-buffer'; +import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from './array-buffer-message-buffer'; describe('array message buffer tests', () => { it('basic read write test', () => { const buffer = new ArrayBuffer(1024); - const writer = new ArrrayBufferWriteBuffer(buffer); + const writer = new ArrayBufferWriteBuffer(buffer); writer.writeByte(8); writer.writeInt(10000); diff --git a/packages/core/src/common/message-rpc/array-buffer-message-buffer.ts b/packages/core/src/common/message-rpc/array-buffer-message-buffer.ts index cf5d8832705f5..2b2c1a9e0eeae 100644 --- a/packages/core/src/common/message-rpc/array-buffer-message-buffer.ts +++ b/packages/core/src/common/message-rpc/array-buffer-message-buffer.ts @@ -1,22 +1,39 @@ -/******************************************************************************** - * Copyright (C) 2021 Red Hat, Inc. 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 - ********************************************************************************/ +// ***************************************************************************** +// Copyright (C) 2021 Red Hat, Inc. 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 { Emitter, Event } from '../event'; import { ReadBuffer, WriteBuffer } from './message-buffer'; -export class ArrrayBufferWriteBuffer implements WriteBuffer { +/** + * Converts the given node {@link Buffer} to an {@link ArrayBuffer}. The node buffer implementation is backed by an `Uint8Array` + * so the conversion can be efficiently achieved by slicing the section that is represented by the `Buffer` from the underlying + * array buffer. + * @param buffer The buffer that should be converted. + * @returns an `ArrayBuffer`representation of the given buffer. + */ +export function toArrayBuffer(buffer: Buffer | ArrayBuffer): ArrayBuffer { + if (buffer instanceof ArrayBuffer) { + return buffer; + } + if (buffer.byteOffset === 0 && buffer.byteLength === buffer.buffer.byteLength) { + return buffer.buffer; + } + return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); +} + +export class ArrayBufferWriteBuffer implements WriteBuffer { constructor(private buffer: ArrayBuffer = new ArrayBuffer(1024), private offset: number = 0) { } @@ -85,7 +102,8 @@ export class ArrrayBufferWriteBuffer implements WriteBuffer { export class ArrayBufferReadBuffer implements ReadBuffer { private offset: number = 0; - constructor(private readonly buffer: ArrayBuffer) { + constructor(private readonly buffer: ArrayBuffer, readPosition = 0) { + this.offset = readPosition; } private get msg(): DataView { @@ -97,9 +115,13 @@ export class ArrayBufferReadBuffer implements ReadBuffer { } readInt(): number { - const result = this.msg.getInt32(this.offset); - this.offset += 4; - return result; + try { + const result = this.msg.getInt32(this.offset); + this.offset += 4; + return result; + } catch (err) { + throw err; + } } readString(): string { @@ -121,4 +143,8 @@ export class ArrayBufferReadBuffer implements ReadBuffer { this.offset += length; return result; } + + copy(): ReadBuffer { + return new ArrayBufferReadBuffer(this.buffer, this.offset); + } } diff --git a/packages/core/src/common/message-rpc/channel.spec.ts b/packages/core/src/common/message-rpc/channel.spec.ts index 6c372ffb64a06..1ed41c76e7fa2 100644 --- a/packages/core/src/common/message-rpc/channel.spec.ts +++ b/packages/core/src/common/message-rpc/channel.spec.ts @@ -1,23 +1,21 @@ -/******************************************************************************** - * Copyright (C) 2021 Red Hat, Inc. 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 - ********************************************************************************/ +// ***************************************************************************** +// Copyright (C) 2021 Red Hat, Inc. 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 { assert, expect, spy, use } from 'chai'; import * as spies from 'chai-spies'; - -import { ChannelMultiplexer, ChannelPipe } from './channel'; -import { ReadBuffer } from './message-buffer'; +import { ChannelMultiplexer, ChannelPipe, ReadBufferConstructor } from './channel'; use(spies); @@ -42,15 +40,15 @@ describe('multiplexer test', () => { assert.isNotNull(rightFirst); assert.isNotNull(rightSecond); - const leftSecondSpy = spy((buf: ReadBuffer) => { - const message = buf.readString(); + const leftSecondSpy = spy((buf: ReadBufferConstructor) => { + const message = buf().readString(); expect(message).equal('message for second'); }); leftSecond.onMessage(leftSecondSpy); - const rightFirstSpy = spy((buf: ReadBuffer) => { - const message = buf.readString(); + const rightFirstSpy = spy((buf: ReadBufferConstructor) => { + const message = buf().readString(); expect(message).equal('message for first'); }); @@ -63,5 +61,5 @@ describe('multiplexer test', () => { expect(rightFirstSpy).to.be.called(); expect(openChannelSpy).to.be.called.exactly(4); - }) + }); }); diff --git a/packages/core/src/common/message-rpc/channel.ts b/packages/core/src/common/message-rpc/channel.ts index da7342251c291..abf835d0c52ee 100644 --- a/packages/core/src/common/message-rpc/channel.ts +++ b/packages/core/src/common/message-rpc/channel.ts @@ -1,22 +1,24 @@ -/******************************************************************************** - * Copyright (C) 2021 Red Hat, Inc. 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 { ArrayBufferReadBuffer, ArrrayBufferWriteBuffer } from './array-buffer-message-buffer'; +// ***************************************************************************** +// Copyright (C) 2021 Red Hat, Inc. 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 { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from './array-buffer-message-buffer'; import { Emitter, Event } from '../event'; import { ReadBuffer, WriteBuffer } from './message-buffer'; +export type ReadBufferConstructor = () => ReadBuffer; + /** * A channel is a bidirectinal communications channel with lifecycle and * error signalling. Note that creation of channels is specific to particular @@ -32,9 +34,11 @@ export interface Channel { */ onError: Event; /** - * A message has arrived and can be read using the given {@link ReadBuffer} + * A message has arrived and can be read using a {@link ReadBuffer}. Since one `ReadBuffer` cannot be reused + * by multiple listener to read the same message again, each lister has to construct its + * own buffer using the given {@link ReadBufferConstructor} */ - onMessage: Event; + onMessage: Event; /** * Obtain a {@link WriteBuffer} to write a message to the channel. */ @@ -43,9 +47,11 @@ export interface Channel { * Close this channel. No {@link onClose} event should be sent */ close(): void; + + readonly id: string; } -enum MessageTypes { +export enum MessageTypes { Open = 1, Close = 2, AckOpen = 3, @@ -55,8 +61,8 @@ enum MessageTypes { /** * Helper class to implement the single channels on a {@link ChannelMultiplexer} */ -class ForwardingChannel implements Channel { - constructor(private readonly closeHander: () => void, private readonly writeBufferSource: () => WriteBuffer) { +export class ForwardingChannel implements Channel { + constructor(readonly id: string, private readonly closeHander: () => void, private readonly writeBufferSource: () => WriteBuffer) { } onCloseEmitter: Emitter = new Emitter(); @@ -67,8 +73,8 @@ class ForwardingChannel implements Channel { get onError(): Event { return this.onErrorEmitter.event; }; - onMessageEmitter: Emitter = new Emitter(); - get onMessage(): Event { + onMessageEmitter: Emitter = new Emitter(); + get onMessage(): Event { return this.onMessageEmitter.event; }; @@ -76,6 +82,12 @@ class ForwardingChannel implements Channel { return this.writeBufferSource(); } + send(message: ArrayBuffer): void { + const writeBuffer = this.getWriteBuffer(); + writeBuffer.writeBytes(message); + writeBuffer.commit(); + } + close(): void { this.closeHander(); } @@ -91,13 +103,13 @@ export class ChannelMultiplexer { protected pendingOpen: Map void> = new Map(); protected openChannels: Map = new Map(); - protected readonly onOpenChannelEmitter: Emitter = new Emitter(); - get onDidOpenChannel(): Event { + protected readonly onOpenChannelEmitter = new Emitter<{ id: string, channel: Channel }>(); + get onDidOpenChannel(): Event<{ id: string, channel: Channel }> { return this.onOpenChannelEmitter.event; } constructor(protected readonly underlyingChannel: Channel) { - this.underlyingChannel.onMessage(buffer => this.handleMessage(buffer)); + this.underlyingChannel.onMessage(buffer => this.handleMessage(buffer())); this.underlyingChannel.onClose(() => this.handleClose()); this.underlyingChannel.onError(error => this.handleError(error)); } @@ -128,7 +140,7 @@ export class ChannelMultiplexer { this.pendingOpen.delete(id); this.openChannels.set(id, channel); resolve!(channel); - this.onOpenChannelEmitter.fire(channel); + this.onOpenChannelEmitter.fire({ id, channel }); } break; } @@ -142,7 +154,7 @@ export class ChannelMultiplexer { resolve(channel); } this.underlyingChannel.getWriteBuffer().writeByte(MessageTypes.AckOpen).writeString(id).commit(); - this.onOpenChannelEmitter.fire(channel); + this.onOpenChannelEmitter.fire({ id, channel }); } break; @@ -158,7 +170,7 @@ export class ChannelMultiplexer { case MessageTypes.Data: { const channel = this.openChannels.get(id); if (channel) { - channel.onMessageEmitter.fire(buffer); + channel.onMessageEmitter.fire(() => buffer.copy()); } break; } @@ -167,7 +179,7 @@ export class ChannelMultiplexer { } protected createChannel(id: string): ForwardingChannel { - return new ForwardingChannel(() => this.closeChannel(id), () => { + return new ForwardingChannel(id, () => this.closeChannel(id), () => { const underlying = this.underlyingChannel.getWriteBuffer(); underlying.writeByte(MessageTypes.Data); underlying.writeString(id); @@ -177,8 +189,11 @@ export class ChannelMultiplexer { protected closeChannel(id: string): void { this.underlyingChannel.getWriteBuffer().writeByte(MessageTypes.Close).writeString(id).commit(); - this.openChannels.get(id)!.onCloseEmitter.fire(); - this.openChannels.delete(id); + if (this.openChannels.delete(id)) { + this.openChannels.get(id)!.onCloseEmitter.fire(); + } else { + console.error('The channel does not exist: ', id); + } } open(id: string): Promise { @@ -198,17 +213,17 @@ export class ChannelMultiplexer { * A pipe with two channels at each end for testing. */ export class ChannelPipe { - readonly left: ForwardingChannel = new ForwardingChannel(() => this.right.onCloseEmitter.fire(), () => { - const leftWrite = new ArrrayBufferWriteBuffer(); + readonly left: ForwardingChannel = new ForwardingChannel('left', () => this.right.onCloseEmitter.fire(), () => { + const leftWrite = new ArrayBufferWriteBuffer(); leftWrite.onCommit(buffer => { - this.right.onMessageEmitter.fire(new ArrayBufferReadBuffer(buffer)); + this.right.onMessageEmitter.fire(() => new ArrayBufferReadBuffer(buffer)); }); return leftWrite; }); - readonly right: ForwardingChannel = new ForwardingChannel(() => this.left.onCloseEmitter.fire(), () => { - const rightWrite = new ArrrayBufferWriteBuffer(); + readonly right: ForwardingChannel = new ForwardingChannel('right', () => this.left.onCloseEmitter.fire(), () => { + const rightWrite = new ArrayBufferWriteBuffer(); rightWrite.onCommit(buffer => { - this.left.onMessageEmitter.fire(new ArrayBufferReadBuffer(buffer)); + this.left.onMessageEmitter.fire(() => new ArrayBufferReadBuffer(buffer)); }); return rightWrite; }); diff --git a/packages/core/src/common/message-rpc/connection-handler.ts b/packages/core/src/common/message-rpc/connection-handler.ts index d5fbfa277224a..794bc480aaccf 100644 --- a/packages/core/src/common/message-rpc/connection-handler.ts +++ b/packages/core/src/common/message-rpc/connection-handler.ts @@ -1,18 +1,18 @@ -/******************************************************************************** - * Copyright (C) 2021 Red Hat, Inc. 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 - ********************************************************************************/ +// ***************************************************************************** +// Copyright (C) 2021 Red Hat, Inc. 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 { Channel } from './channel'; import { RpcHandler, RpcProxyHandler } from './rpc-proxy'; diff --git a/packages/core/src/common/message-rpc/experiments.ts b/packages/core/src/common/message-rpc/experiments.ts index 60285ea3d7907..9b8164451cd0a 100644 --- a/packages/core/src/common/message-rpc/experiments.ts +++ b/packages/core/src/common/message-rpc/experiments.ts @@ -1,56 +1,97 @@ -/******************************************************************************** - * Copyright (C) 2021 Red Hat, Inc. 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 { ChannelPipe } from './channel'; -import { RpcHandler, RpcProxyHandler } from './rpc-proxy'; -import * as fs from 'fs'; - -/** - * This file is for fiddling around and testing. Not production code. - */ - -const pipe = new ChannelPipe(); - -interface ReadFile { - read(path: string): Promise; +// ***************************************************************************** +// Copyright (C) 2021 Red Hat, Inc. 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 { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from './array-buffer-message-buffer'; +import { MessageDecoder, MessageEncoder } from './message-encoder'; + +const test1 = { + 'curve': 'yes', + 'successful': false, + 'does': [ + [ + 'tool', + 'strange', + 'declared', + false, + 'if', + false, + false, + true, + true, + 196639994 + ], + -1697924638.043861, + 1921422646, + 'hide', + false, + true, + true, + -400170969, + 550424783, + -2118374202.4598904 + ], + 'fish': 664495385.6069336, + 'eat': -1205575089, + 'boat': 1495629676, + 'arm': 'nation', + 'height': false, + 'underline': 'have', + 'satellites': -20686813.87966633 +}; + +const test2: unknown[] = []; +for (let index = 0; index < 100; index++) { + test2.push(test1); } -class Server implements ReadFile { - read(path: string): Promise { - const bytes = fs.readFileSync(path); - const result = new ArrayBuffer(bytes.byteLength); - bytes.copy(new Uint8Array(result)); - return Promise.resolve(result); - } +const test3: string[] = []; +for (let index = 0; index < 1000; index++) { + test3.push(`${index}`); } -const handler = new RpcHandler(new Server()); -handler.onChannelOpen(pipe.right); +test(test1); +test(test2); +test(test3); -const proxyHandler = new RpcProxyHandler(); -// eslint-disable-next-line no-null/no-null -const proxy: ReadFile = new Proxy(Object.create(null), proxyHandler); -proxyHandler.onChannelOpen(pipe.left); +function test(object: unknown): void { + console.log('Start test'); + const encoder = new MessageEncoder(); + const decoder = new MessageDecoder(); + // const string = fs.readFileSync(process.argv[2], 'utf8'); + // const object = JSON.parse(string); -const t0 = new Date().getTime(); + const start1 = Date.now(); + const result = Buffer.from(JSON.stringify(object)); + const end1 = Date.now(); + console.log(`Stringify encoding of file ${process.argv[2]} took ${end1 - start1} ms. Final byte length: ${result.byteLength}`); -proxy.read(process.argv[2]).then(value => { - const t1 = new Date().getTime(); - console.log(`read file of length: ${value.byteLength} in ${t1 - t0}ms`); - console.log(value.slice(0, 20)); -}).catch(e => { - console.log(e); -}); + const writer = new ArrayBufferWriteBuffer(); + const start2 = Date.now(); + encoder.writeTypedValue(writer, object); + const result2 = writer.getCurrentContents(); + const end2 = Date.now(); + console.log(`New encoding of file ${process.argv[2]} took ${end2 - start2} ms. Final byte length: ${result2.byteLength}`); + const start3 = Date.now(); + const end3 = Date.now(); + console.log(`Stringify Reading took ${end3 - start3} ms for`); + + const reader = new ArrayBufferReadBuffer(result2); + const start4 = Date.now(); + decoder.readTypedValue(reader); + const end4 = Date.now(); + console.log(`New Reading took ${end4 - start4} ms for`); + console.log(); +} diff --git a/packages/core/src/common/message-rpc/message-buffer.ts b/packages/core/src/common/message-rpc/message-buffer.ts index 79466424512b5..82ea175b40fea 100644 --- a/packages/core/src/common/message-rpc/message-buffer.ts +++ b/packages/core/src/common/message-rpc/message-buffer.ts @@ -1,18 +1,18 @@ -/******************************************************************************** - * Copyright (C) 2021 Red Hat, Inc. 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 - ********************************************************************************/ +// ***************************************************************************** +// Copyright (C) 2021 Red Hat, Inc. 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 +// ***************************************************************************** /** * A buffer maintaining a write position capable of writing primitive values @@ -67,4 +67,7 @@ export interface ReadBuffer { readInt(): number; readString(): string; readBytes(): ArrayBuffer; + /* Create a new copy of this read buffer and its current read position. Can be used to read (sub) messages + multiple times.*/ + copy(): ReadBuffer } diff --git a/packages/core/src/common/message-rpc/message-encoder.spec.ts b/packages/core/src/common/message-rpc/message-encoder.spec.ts index 0f6108052c0a5..99761534064a7 100644 --- a/packages/core/src/common/message-rpc/message-encoder.spec.ts +++ b/packages/core/src/common/message-rpc/message-encoder.spec.ts @@ -1,26 +1,26 @@ -/******************************************************************************** - * Copyright (C) 2021 Red Hat, Inc. 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 - ********************************************************************************/ +// ***************************************************************************** +// Copyright (C) 2021 Red Hat, Inc. 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 { expect } from 'chai'; -import { ArrayBufferReadBuffer, ArrrayBufferWriteBuffer } from './array-buffer-message-buffer'; +import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from './array-buffer-message-buffer'; import { MessageDecoder, MessageEncoder } from './message-encoder'; describe('message buffer test', () => { it('encode object', () => { const buffer = new ArrayBuffer(1024); - const writer = new ArrrayBufferWriteBuffer(buffer); + const writer = new ArrayBufferWriteBuffer(buffer); const encoder = new MessageEncoder(); const jsonMangled = JSON.parse(JSON.stringify(encoder)); diff --git a/packages/core/src/common/message-rpc/message-encoder.ts b/packages/core/src/common/message-rpc/message-encoder.ts index 16e3a55593a30..a9bc2a1810e4e 100644 --- a/packages/core/src/common/message-rpc/message-encoder.ts +++ b/packages/core/src/common/message-rpc/message-encoder.ts @@ -1,18 +1,19 @@ -/******************************************************************************** - * Copyright (C) 2021 Red Hat, Inc. 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 - ********************************************************************************/ +// ***************************************************************************** +// Copyright (C) 2021 Red Hat, Inc. 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 { toArrayBuffer } from './array-buffer-message-buffer'; import { ReadBuffer, WriteBuffer } from './message-buffer'; /** @@ -30,7 +31,7 @@ export interface SerializedError { readonly stack: string; } -export const enum MessageType { +export const enum RPCMessageType { Request = 1, Notification = 2, Reply = 3, @@ -39,12 +40,12 @@ export const enum MessageType { } export interface CancelMessage { - type: MessageType.Cancel; + type: RPCMessageType.Cancel; id: number; } export interface RequestMessage { - type: MessageType.Request; + type: RPCMessageType.Request; id: number; method: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -52,7 +53,7 @@ export interface RequestMessage { } export interface NotificationMessage { - type: MessageType.Notification; + type: RPCMessageType.Notification; id: number; method: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -60,14 +61,14 @@ export interface NotificationMessage { } export interface ReplyMessage { - type: MessageType.Reply; + type: RPCMessageType.Reply; id: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any res: any; } export interface ReplyErrMessage { - type: MessageType.ReplyErr; + type: RPCMessageType.ReplyErr; id: number; err: SerializedError; } @@ -116,7 +117,7 @@ export interface ValueDecoder { * @param buf The read buffer to read from * @param recursiveDecode A function that will use the decoders registered on the {@link MessageEncoder} * to read values from the underlying read buffer. This is used mostly to decode structures like an array - * without having to know how to decode the values in the aray. + * without having to know how to decode the values in the array. */ read(buf: ReadBuffer, recursiveDecode: (buf: ReadBuffer) => unknown): unknown; } @@ -172,15 +173,15 @@ export class MessageDecoder { const msgType = buf.readByte(); switch (msgType) { - case MessageType.Request: + case RPCMessageType.Request: return this.parseRequest(buf); - case MessageType.Notification: + case RPCMessageType.Notification: return this.parseNotification(buf); - case MessageType.Reply: + case RPCMessageType.Reply: return this.parseReply(buf); - case MessageType.ReplyErr: + case RPCMessageType.ReplyErr: return this.parseReplyErr(buf); - case MessageType.Cancel: + case RPCMessageType.Cancel: return this.parseCancel(buf); } throw new Error(`Unknown message type: ${msgType}`); @@ -194,7 +195,7 @@ export class MessageDecoder { protected parseCancel(msg: ReadBuffer): CancelMessage { const callId = msg.readInt(); return { - type: MessageType.Cancel, + type: RPCMessageType.Cancel, id: callId }; } @@ -207,7 +208,7 @@ export class MessageDecoder { args = args.map(arg => arg === null ? undefined : arg); // eslint-disable-line no-null/no-null return { - type: MessageType.Request, + type: RPCMessageType.Request, id: callId, method: method, args: args @@ -222,7 +223,7 @@ export class MessageDecoder { args = args.map(arg => arg === null ? undefined : arg); // eslint-disable-line no-null/no-null return { - type: MessageType.Notification, + type: RPCMessageType.Notification, id: callId, method: method, args: args @@ -233,7 +234,7 @@ export class MessageDecoder { const callId = msg.readInt(); const value = this.readTypedValue(msg); return { - type: MessageType.Reply, + type: RPCMessageType.Reply, id: callId, res: value }; @@ -251,7 +252,7 @@ export class MessageDecoder { err.stack = err.stack; } return { - type: MessageType.ReplyErr, + type: RPCMessageType.ReplyErr, id: callId, err: err }; @@ -277,6 +278,7 @@ export class MessageDecoder { return decoder.read(buf, innerBuffer => this.readTypedValue(innerBuffer)); } } + /** * A MessageEncoder writes RCPMessage objects to a WriteBuffer. Note that it is * up to clients to commit the message. This allows for multiple messages being @@ -314,7 +316,8 @@ export class MessageEncoder { } }); this.registerEncoder(ObjectType.Undefined, { - is: value => (typeof value === 'undefined'), + // eslint-disable-next-line no-null/no-null + is: value => (value === undefined || value === null), write: () => { } }); @@ -326,9 +329,10 @@ export class MessageEncoder { }); this.registerEncoder(ObjectType.ByteArray, { - is: value => value instanceof ArrayBuffer, + is: value => value instanceof ArrayBuffer || Buffer.isBuffer(value), write: (buf, value) => { - buf.writeBytes(value); + const arrayBuffer = value instanceof ArrayBuffer ? value : toArrayBuffer(value); + buf.writeBytes(arrayBuffer); } }); } @@ -342,13 +346,13 @@ export class MessageEncoder { } cancel(buf: WriteBuffer, requestId: number): void { - buf.writeByte(MessageType.Cancel); + buf.writeByte(RPCMessageType.Cancel); buf.writeInt(requestId); } // eslint-disable-next-line @typescript-eslint/no-explicit-any notification(buf: WriteBuffer, requestId: number, method: string, args: any[]): void { - buf.writeByte(MessageType.Notification); + buf.writeByte(RPCMessageType.Notification); buf.writeInt(requestId); buf.writeString(method); this.writeArray(buf, args); @@ -356,7 +360,7 @@ export class MessageEncoder { // eslint-disable-next-line @typescript-eslint/no-explicit-any request(buf: WriteBuffer, requestId: number, method: string, args: any[]): void { - buf.writeByte(MessageType.Request); + buf.writeByte(RPCMessageType.Request); buf.writeInt(requestId); buf.writeString(method); this.writeArray(buf, args); @@ -364,14 +368,14 @@ export class MessageEncoder { // eslint-disable-next-line @typescript-eslint/no-explicit-any replyOK(buf: WriteBuffer, requestId: number, res: any): void { - buf.writeByte(MessageType.Reply); + buf.writeByte(RPCMessageType.Reply); buf.writeInt(requestId); this.writeTypedValue(buf, res); } // eslint-disable-next-line @typescript-eslint/no-explicit-any replyErr(buf: WriteBuffer, requestId: number, err: any): void { - buf.writeByte(MessageType.ReplyErr); + buf.writeByte(RPCMessageType.ReplyErr); buf.writeInt(requestId); this.writeTypedValue(buf, err); } diff --git a/packages/core/src/common/message-rpc/rpc-protocol.ts b/packages/core/src/common/message-rpc/rpc-protocol.ts index f9ea1c93cc044..8d805d7bb60a8 100644 --- a/packages/core/src/common/message-rpc/rpc-protocol.ts +++ b/packages/core/src/common/message-rpc/rpc-protocol.ts @@ -1,25 +1,139 @@ -/******************************************************************************** - * Copyright (C) 2021 Red Hat, Inc. 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 - ********************************************************************************/ +// ***************************************************************************** +// Copyright (C) 2021 Red Hat, Inc. 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 +// ***************************************************************************** /* eslint-disable @typescript-eslint/no-explicit-any */ import { Emitter, Event } from '../event'; import { Deferred } from '../promise-util'; -import { Channel } from './channel'; +import { Channel, ReadBufferConstructor } from './channel'; import { ReadBuffer } from './message-buffer'; -import { MessageDecoder, MessageEncoder, MessageType } from './message-encoder'; +import { MessageDecoder, MessageEncoder, RPCMessageType } from './message-encoder'; + +export class RCPConnection { + protected readonly pendingRequests: Map> = new Map(); + protected nextMessageId: number = 0; + + protected readonly encoder: MessageEncoder = new MessageEncoder(); + protected readonly decoder: MessageDecoder = new MessageDecoder(); + protected onNotificationEmitter: Emitter<{ method: string; args: any[]; }> = new Emitter(); + readFileRequestId: number = -1; + + get onNotification(): Event<{ method: string; args: any[]; }> { + return this.onNotificationEmitter.event; + } + + constructor(readonly channel: Channel, public readonly requestHandler: (method: string, args: any[]) => Promise) { + const registration = channel.onMessage((msg: ReadBufferConstructor) => this.handleMessage(msg())); + channel.onClose(() => registration.dispose()); + } + + handleMessage(data: ReadBuffer): void { + const message = this.decoder.parse(data); + switch (message.type) { + case RPCMessageType.Cancel: { + this.handleCancel(message.id); + break; + } + case RPCMessageType.Request: { + this.handleRequest(message.id, message.method, message.args); + break; + } + case RPCMessageType.Notification: { + this.handleNotify(message.id, message.method, message.args); + break; + } + case RPCMessageType.Reply: { + this.handleReply(message.id, message.res); + break; + } + case RPCMessageType.ReplyErr: { + this.handleReplyErr(message.id, message.err); + break; + } + } + } + + protected handleCancel(id: number): void { + // implement cancellation + /* const token = this.cancellationTokens.get(id); + if (token) { + this.cancellationTokens.delete(id); + token.cancel(); + } else { + console.warn(`cancel: no token for message: ${id}`); + }*/ + } + + protected async handleRequest(id: number, method: string, args: any[]): Promise { + + const output = this.channel.getWriteBuffer(); + try { + + const result = await this.requestHandler(method, args); + this.encoder.replyOK(output, id, result); + } catch (err) { + this.encoder.replyErr(output, id, err); + console.log(`error on request ${method} with id ${id}`); + } + output.commit(); + } + + protected async handleNotify(id: number, method: string, args: any[]): Promise { + this.onNotificationEmitter.fire({ method, args }); + } + + protected handleReply(id: number, value: any): void { + const replyHandler = this.pendingRequests.get(id); + if (replyHandler) { + this.pendingRequests.delete(id); + replyHandler.resolve(value); + } else { + console.warn(`reply: no handler for message: ${id}`); + } + } + + protected handleReplyErr(id: number, error: any): void { + const replyHandler = this.pendingRequests.get(id); + if (replyHandler) { + this.pendingRequests.delete(id); + // console.log(`received error id ${id}`); + replyHandler.reject(error); + } else { + console.warn(`error: no handler for message: ${id}`); + } + } + + sendRequest(method: string, args: any[]): Promise { + const id = this.nextMessageId++; + const reply = new Deferred(); + + this.pendingRequests.set(id, reply); + const output = this.channel.getWriteBuffer(); + this.encoder.request(output, id, method, args); + output.commit(); + return reply.promise; + } + + sendNotification(method: string, args: any[]): void { + const output = this.channel.getWriteBuffer(); + this.encoder.notification(output, this.nextMessageId++, method, args); + output.commit(); + } + +} + /** * A RCPServer reads rcp request and notification messages and sends the reply values or * errors from the request to the channel. @@ -34,22 +148,22 @@ export class RPCServer { } constructor(protected channel: Channel, public readonly requestHandler: (method: string, args: any[]) => Promise) { - const registration = channel.onMessage((data: ReadBuffer) => this.handleMessage(data)); + const registration = channel.onMessage((msg: ReadBufferConstructor) => this.handleMessage(msg())); channel.onClose(() => registration.dispose()); } handleMessage(data: ReadBuffer): void { const message = this.decoder.parse(data); switch (message.type) { - case MessageType.Cancel: { + case RPCMessageType.Cancel: { this.handleCancel(message.id); break; } - case MessageType.Request: { + case RPCMessageType.Request: { this.handleRequest(message.id, message.method, message.args); break; } - case MessageType.Notification: { + case RPCMessageType.Notification: { this.handleNotify(message.id, message.method, message.args); break; } @@ -101,19 +215,19 @@ export class RpcClient { protected readonly encoder: MessageEncoder = new MessageEncoder(); protected readonly decoder: MessageDecoder = new MessageDecoder(); - constructor(protected channel: Channel) { - const registration = channel.onMessage((data: ReadBuffer) => this.handleMessage(data)); + constructor(public readonly channel: Channel) { + const registration = channel.onMessage(data => this.handleMessage(data())); channel.onClose(() => registration.dispose()); } handleMessage(data: ReadBuffer): void { const message = this.decoder.parse(data); switch (message.type) { - case MessageType.Reply: { + case RPCMessageType.Reply: { this.handleReply(message.id, message.res); break; } - case MessageType.ReplyErr: { + case RPCMessageType.ReplyErr: { this.handleReplyErr(message.id, message.err); break; } diff --git a/packages/core/src/common/message-rpc/rpc-proxy.ts b/packages/core/src/common/message-rpc/rpc-proxy.ts index 3578f64560942..de47cd3d22651 100644 --- a/packages/core/src/common/message-rpc/rpc-proxy.ts +++ b/packages/core/src/common/message-rpc/rpc-proxy.ts @@ -1,18 +1,18 @@ -/******************************************************************************** - * Copyright (C) 2021 Red Hat, Inc. 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 - ********************************************************************************/ +// ***************************************************************************** +// Copyright (C) 2021 Red Hat, Inc. 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 +// ***************************************************************************** /* eslint-disable @typescript-eslint/no-explicit-any */ import { Deferred } from '../promise-util'; import { Channel } from './channel'; diff --git a/packages/core/src/common/message-rpc/websocket-client-channel.ts b/packages/core/src/common/message-rpc/websocket-client-channel.ts index bf07088f18448..6c5be8c87ecf6 100644 --- a/packages/core/src/common/message-rpc/websocket-client-channel.ts +++ b/packages/core/src/common/message-rpc/websocket-client-channel.ts @@ -1,28 +1,28 @@ -/******************************************************************************** - * Copyright (C) 2018 TypeFox 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 - ********************************************************************************/ +// ***************************************************************************** +// Copyright (C) 2021 Red Hat, Inc. 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 +// ***************************************************************************** /* eslint-disable @typescript-eslint/no-explicit-any */ import ReconnectingWebSocket from 'reconnecting-websocket'; +import { Endpoint } from 'src/browser'; import { v4 as uuid } from 'uuid'; -import { Channel } from './channel'; -import { ReadBuffer, WriteBuffer } from './message-buffer'; -import { ArrayBufferReadBuffer, ArrrayBufferWriteBuffer } from './array-buffer-message-buffer'; -import { Deferred } from '../promise-util'; import { Emitter, Event } from '../event'; -import { Endpoint } from 'src/browser'; +import { Deferred } from '../promise-util'; +import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from './array-buffer-message-buffer'; +import { Channel, ReadBufferConstructor } from './channel'; +import { WriteBuffer } from './message-buffer'; /** * An attempt at a channel implementation over a websocket with fallback to http. @@ -67,8 +67,8 @@ export class WebSocketClientChannel implements Channel { return this.onCloseEmitter.event; } - protected readonly onMessageEmitter: Emitter = new Emitter(); - get onMessage(): Event { + protected readonly onMessageEmitter: Emitter = new Emitter(); + get onMessage(): Event { return this.onMessageEmitter.event; } @@ -83,7 +83,7 @@ export class WebSocketClientChannel implements Channel { protected httpFallbackId = uuid(); protected httpFallbackDisconnected = true; - constructor(protected readonly httpFallbackOptions: HttpFallbackOptions | undefined) { + constructor(readonly id: string, protected readonly httpFallbackOptions: HttpFallbackOptions | undefined) { const url = this.createWebSocketUrl('/services'); const socket = this.createWebSocket(url); socket.onerror = event => this.handleSocketError(event); @@ -94,7 +94,7 @@ export class WebSocketClientChannel implements Channel { this.onCloseEmitter.fire(); }; socket.onmessage = ({ data }) => { - this.onMessageEmitter.fire(new ArrayBufferReadBuffer(data)); + this.onMessageEmitter.fire(() => new ArrayBufferReadBuffer(data)); }; this.socket = socket; window.addEventListener('offline', () => this.tryReconnect()); @@ -102,7 +102,7 @@ export class WebSocketClientChannel implements Channel { } getWriteBuffer(): WriteBuffer { - const result = new ArrrayBufferWriteBuffer(); + const result = new ArrayBufferWriteBuffer(); const httpUrl = this.createHttpWebSocketUrl('/services'); if (this.useHttpFallback) { result.writeString(this.httpFallbackId); @@ -170,7 +170,7 @@ export class WebSocketClientChannel implements Channel { this.fireSocketDidOpen(); } const bytes = await response.arrayBuffer(); - this.onMessageEmitter.fire(new ArrayBufferReadBuffer(bytes)); + this.onMessageEmitter.fire(() => new ArrayBufferReadBuffer(bytes)); } else { timeoutDuration = this.httpFallbackOptions?.errorTimeout || 0; this.httpFallbackDisconnected = true; diff --git a/packages/core/src/common/messaging/abstract-connection-provider.ts b/packages/core/src/common/messaging/abstract-connection-provider.ts index d4c5c3bd3aaf3..b79f374310514 100644 --- a/packages/core/src/common/messaging/abstract-connection-provider.ts +++ b/packages/core/src/common/messaging/abstract-connection-provider.ts @@ -15,8 +15,9 @@ // ***************************************************************************** import { injectable, interfaces } from 'inversify'; -import { ConsoleLogger, createWebSocketConnection, Logger } from 'vscode-ws-jsonrpc'; +import { ConsoleLogger, Logger } from 'vscode-ws-jsonrpc'; import { Emitter, Event } from '../event'; +import { Channel, ChannelMultiplexer } from '../message-rpc/channel'; import { ConnectionHandler } from './handler'; import { JsonRpcProxy, JsonRpcProxyFactory } from './proxy-factory'; import { WebSocketChannel } from './web-socket-channel'; @@ -75,48 +76,35 @@ export abstract class AbstractConnectionProvider return factory.createProxy(); } + protected channelMultiPlexer: ChannelMultiplexer; + + constructor() { + this.channelMultiPlexer = new ChannelMultiplexer(this.createMainChannel()); + } + /** * Install a connection handler for the given path. */ listen(handler: ConnectionHandler, options?: AbstractOptions): void { this.openChannel(handler.path, channel => { - const connection = createWebSocketConnection(channel, this.createLogger()); - connection.onDispose(() => channel.close()); - handler.onConnection(connection); + handler.onConnection(channel); }, options); } - openChannel(path: string, handler: (channel: WebSocketChannel) => void, options?: AbstractOptions): void { - const id = this.channelIdSeq++; - const channel = this.createChannel(id); - this.channels.set(id, channel); - channel.onClose(() => { - if (this.channels.delete(channel.id)) { - const { reconnecting } = { reconnecting: true, ...options }; - if (reconnecting) { - this.openChannel(path, handler, options); - } - } else { - console.error('The ws channel does not exist', channel.id); + async openChannel(path: string, handler: (channel: Channel) => void, options?: AbstractOptions): Promise { + const newChannel = await this.channelMultiPlexer.open(path); + newChannel.onClose(() => { + const { reconnecting } = { reconnecting: true, ...options }; + if (reconnecting) { + this.openChannel(path, handler, options); } }); - channel.onOpen(() => handler(channel)); - channel.open(path); + handler(newChannel); } - protected abstract createChannel(id: number): WebSocketChannel; - - protected handleIncomingRawMessage(data: string): void { - 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.onIncomingMessageActivityEmitter.fire(undefined); - } + protected abstract createMainChannel(): Channel; + // TODO Logger for RPC protected createLogger(): Logger { return new ConsoleLogger(); } diff --git a/packages/core/src/common/messaging/handler.ts b/packages/core/src/common/messaging/handler.ts index ed03d9d331206..1e790d38aeec3 100644 --- a/packages/core/src/common/messaging/handler.ts +++ b/packages/core/src/common/messaging/handler.ts @@ -14,11 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { MessageConnection } from 'vscode-ws-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; } diff --git a/packages/core/src/common/messaging/proxy-factory.spec.ts b/packages/core/src/common/messaging/proxy-factory.spec.ts index 2fd0700a41034..f2eacf5edbf76 100644 --- a/packages/core/src/common/messaging/proxy-factory.spec.ts +++ b/packages/core/src/common/messaging/proxy-factory.spec.ts @@ -14,108 +14,108 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import * as chai from 'chai'; -import { ConsoleLogger } from '../../node/messaging/logger'; -import { JsonRpcProxyFactory, JsonRpcProxy } from './proxy-factory'; -import { createMessageConnection } from 'vscode-jsonrpc/lib/main'; -import * as stream from 'stream'; +// import * as chai from 'chai'; +// import { ConsoleLogger } from '../../node/messaging/logger'; +// import { JsonRpcProxyFactory, JsonRpcProxy } from './proxy-factory'; +// import { createMessageConnection } from 'vscode-jsonrpc/lib/main'; +// import * as stream from 'stream'; -const expect = chai.expect; +// const expect = chai.expect; -class NoTransform extends stream.Transform { +// class NoTransform extends stream.Transform { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - override _transform(chunk: any, encoding: string, callback: Function): void { - callback(undefined, chunk); - } -} +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// override _transform(chunk: any, encoding: string, callback: Function): void { +// callback(undefined, chunk); +// } +// } -class TestServer { - requests: string[] = []; - doStuff(arg: string): Promise { - this.requests.push(arg); - return Promise.resolve(`done: ${arg}`); - } +// class TestServer { +// requests: string[] = []; +// doStuff(arg: string): Promise { +// this.requests.push(arg); +// return Promise.resolve(`done: ${arg}`); +// } - fails(arg: string, otherArg: string): Promise { - throw new Error('fails failed'); - } +// fails(arg: string, otherArg: string): Promise { +// throw new Error('fails failed'); +// } - fails2(arg: string, otherArg: string): Promise { - return Promise.reject(new Error('fails2 failed')); - } -} +// fails2(arg: string, otherArg: string): Promise { +// return Promise.reject(new Error('fails2 failed')); +// } +// } -class TestClient { - notifications: string[] = []; - notifyThat(arg: string): void { - this.notifications.push(arg); - } -} +// class TestClient { +// notifications: string[] = []; +// notifyThat(arg: string): void { +// this.notifications.push(arg); +// } +// } -describe('Proxy-Factory', () => { +// describe('Proxy-Factory', () => { - it('Should correctly send notifications and requests.', done => { - const it = getSetup(); - it.clientProxy.notifyThat('hello'); - function check(): void { - if (it.client.notifications.length === 0) { - console.log('waiting another 50 ms'); - setTimeout(check, 50); - } else { - expect(it.client.notifications[0]).eq('hello'); - it.serverProxy.doStuff('foo').then(result => { - expect(result).to.be.eq('done: foo'); - done(); - }); - } - } - check(); - }); - it('Rejected Promise should result in rejected Promise.', done => { - const it = getSetup(); - const handle = setTimeout(() => done('timeout'), 500); - it.serverProxy.fails('a', 'b').catch(err => { - expect(err.message).to.contain('fails failed'); - clearTimeout(handle); - done(); - }); - }); - it('Remote Exceptions should result in rejected Promise.', done => { - const { serverProxy } = getSetup(); - const handle = setTimeout(() => done('timeout'), 500); - serverProxy.fails2('a', 'b').catch(err => { - expect(err.message).to.contain('fails2 failed'); - clearTimeout(handle); - done(); - }); - }); -}); +// it('Should correctly send notifications and requests.', done => { +// const it = getSetup(); +// it.clientProxy.notifyThat('hello'); +// function check(): void { +// if (it.client.notifications.length === 0) { +// console.log('waiting another 50 ms'); +// setTimeout(check, 50); +// } else { +// expect(it.client.notifications[0]).eq('hello'); +// it.serverProxy.doStuff('foo').then(result => { +// expect(result).to.be.eq('done: foo'); +// done(); +// }); +// } +// } +// check(); +// }); +// it('Rejected Promise should result in rejected Promise.', done => { +// const it = getSetup(); +// const handle = setTimeout(() => done('timeout'), 500); +// it.serverProxy.fails('a', 'b').catch(err => { +// expect(err.message).to.contain('fails failed'); +// clearTimeout(handle); +// done(); +// }); +// }); +// it('Remote Exceptions should result in rejected Promise.', done => { +// const { serverProxy } = getSetup(); +// const handle = setTimeout(() => done('timeout'), 500); +// serverProxy.fails2('a', 'b').catch(err => { +// expect(err.message).to.contain('fails2 failed'); +// clearTimeout(handle); +// done(); +// }); +// }); +// }); -function getSetup(): { - client: TestClient; - clientProxy: JsonRpcProxy; - server: TestServer; - serverProxy: JsonRpcProxy; -} { - const client = new TestClient(); - const server = new TestServer(); +// function getSetup(): { +// client: TestClient; +// clientProxy: JsonRpcProxy; +// server: TestServer; +// serverProxy: JsonRpcProxy; +// } { +// const client = new TestClient(); +// const server = new TestServer(); - const serverProxyFactory = new JsonRpcProxyFactory(client); - const client2server = new NoTransform(); - const server2client = new NoTransform(); - const serverConnection = createMessageConnection(server2client, client2server, new ConsoleLogger()); - serverProxyFactory.listen(serverConnection); - const serverProxy = serverProxyFactory.createProxy(); +// const serverProxyFactory = new JsonRpcProxyFactory(client); +// const client2server = new NoTransform(); +// const server2client = new NoTransform(); +// const serverConnection = createMessageConnection(server2client, client2server, new ConsoleLogger()); +// serverProxyFactory.listen(serverConnection); +// const serverProxy = serverProxyFactory.createProxy(); - const clientProxyFactory = new JsonRpcProxyFactory(server); - const clientConnection = createMessageConnection(client2server, server2client, new ConsoleLogger()); - clientProxyFactory.listen(clientConnection); - const clientProxy = clientProxyFactory.createProxy(); - return { - client, - clientProxy, - server, - serverProxy - }; -} +// const clientProxyFactory = new JsonRpcProxyFactory(server); +// const clientConnection = createMessageConnection(client2server, server2client, new ConsoleLogger()); +// clientProxyFactory.listen(clientConnection); +// const clientProxy = clientProxyFactory.createProxy(); +// return { +// client, +// clientProxy, +// server, +// serverProxy +// }; +// } diff --git a/packages/core/src/common/messaging/proxy-factory.ts b/packages/core/src/common/messaging/proxy-factory.ts index f8869449eae94..99b0deabb149e 100644 --- a/packages/core/src/common/messaging/proxy-factory.ts +++ b/packages/core/src/common/messaging/proxy-factory.ts @@ -16,10 +16,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { MessageConnection, ResponseError } from 'vscode-ws-jsonrpc'; +import { ResponseError } from 'vscode-ws-jsonrpc'; import { ApplicationError } from '../application-error'; -import { Event, Emitter } from '../event'; import { Disposable } from '../disposable'; +import { Emitter, Event } from '../event'; +import { Channel } from '../message-rpc/channel'; +import { RCPConnection } from '../message-rpc/rpc-protocol'; import { ConnectionHandler } from './handler'; export type JsonRpcServer = Disposable & { @@ -45,7 +47,7 @@ export class JsonRpcConnectionHandler implements ConnectionHan readonly factoryConstructor: new () => JsonRpcProxyFactory = JsonRpcProxyFactory ) { } - onConnection(connection: MessageConnection): void { + onConnection(connection: Channel): void { const factory = new this.factoryConstructor(); const proxy = factory.createProxy(); factory.target = this.targetFactory(proxy); @@ -95,13 +97,14 @@ export class JsonRpcConnectionHandler implements ConnectionHan * * @param - The type of the object to expose to JSON-RPC. */ + export class JsonRpcProxyFactory implements ProxyHandler { protected readonly onDidOpenConnectionEmitter = new Emitter(); protected readonly onDidCloseConnectionEmitter = new Emitter(); - protected connectionPromiseResolve: (connection: MessageConnection) => void; - protected connectionPromise: Promise; + protected connectionPromiseResolve: (connection: RCPConnection) => void; + protected connectionPromise: Promise; /** * Build a new JsonRpcProxyFactory. @@ -118,7 +121,7 @@ export class JsonRpcProxyFactory implements ProxyHandler { this.connectionPromiseResolve = resolve ); this.connectionPromise.then(connection => { - connection.onClose(() => + connection.channel.onClose(() => this.onDidCloseConnectionEmitter.fire(undefined) ); this.onDidOpenConnectionEmitter.fire(undefined); @@ -131,11 +134,10 @@ export class JsonRpcProxyFactory implements ProxyHandler { * This connection will be used to send/receive JSON-RPC requests and * response. */ - listen(connection: MessageConnection): void { - connection.onRequest((prop, ...args) => this.onRequest(prop, ...args)); - connection.onNotification((prop, ...args) => this.onNotification(prop, ...args)); - connection.onDispose(() => this.waitForConnection()); - connection.listen(); + listen(channel: Channel): void { + const connection = new RCPConnection(channel, (method, args) => this.onRequest(method, ...args)); + connection.onNotification(event => this.onNotification(event.method, ...event.args)); + this.connectionPromiseResolve(connection); } @@ -239,10 +241,10 @@ export class JsonRpcProxyFactory implements ProxyHandler { new Promise((resolve, reject) => { try { if (isNotify) { - connection.sendNotification(method, ...args); + connection.sendNotification(method, args); resolve(undefined); } else { - const resultPromise = connection.sendRequest(method, ...args) as Promise; + const resultPromise = connection.sendRequest(method, args) as Promise; resultPromise .catch((err: any) => reject(this.deserializeError(capturedError, err))) .then((result: any) => resolve(result)); diff --git a/packages/core/src/common/messaging/web-socket-channel.ts b/packages/core/src/common/messaging/web-socket-channel.ts index 28dff9400068a..202647b5af5cd 100644 --- a/packages/core/src/common/messaging/web-socket-channel.ts +++ b/packages/core/src/common/messaging/web-socket-channel.ts @@ -16,157 +16,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { IWebSocket } from 'vscode-ws-jsonrpc/lib/socket/socket'; -import { Disposable, DisposableCollection } from '../disposable'; -import { Emitter } from '../event'; +import { ForwardingChannel } from '../message-rpc/channel'; -export class WebSocketChannel implements IWebSocket { +export type WebSocketChannel = ForwardingChannel; - static wsPath = '/services'; - - protected readonly closeEmitter = new Emitter<[number, string]>(); - protected readonly toDispose = new DisposableCollection(this.closeEmitter); - - constructor( - readonly id: number, - protected readonly doSend: (content: string) => void - ) { } - - dispose(): void { - this.toDispose.dispose(); - } - - protected checkNotDisposed(): void { - if (this.toDispose.disposed) { - throw new Error('The channel has been disposed.'); - } - } - - handleMessage(message: WebSocketChannel.Message): void { - if (message.kind === 'ready') { - this.fireOpen(); - } else if (message.kind === 'data') { - this.fireMessage(message.content); - } else if (message.kind === 'close') { - this.fireClose(message.code, message.reason); - } - } - - 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(code: number = 1000, reason: string = ''): void { - if (this.closing) { - // Do not try to close the channel if it is already closing. - return; - } - this.checkNotDisposed(); - this.doSend(JSON.stringify({ - kind: 'close', - id: this.id, - code, - reason - })); - this.fireClose(code, reason); - } - - tryClose(code: number = 1000, reason: string = ''): void { - if (this.closing || this.toDispose.disposed) { - // Do not try to close the channel if it is already closing or disposed. - return; - } - this.doSend(JSON.stringify({ - kind: 'close', - id: this.id, - code, - reason - })); - this.fireClose(code, reason); - } - - 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 = () => { })); - } - - protected closing = false; - protected fireClose(code: number, reason: string): void { - if (this.closing) { - return; - } - this.closing = true; - try { - this.closeEmitter.fire([code, reason]); - } finally { - this.closing = false; - } - this.dispose(); - } - onClose(cb: (code: number, reason: string) => void): Disposable { - this.checkNotDisposed(); - return this.closeEmitter.event(([code, reason]) => cb(code, reason)); - } - -} 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 - code: number - reason: string - } - export type Message = OpenMessage | ReadyMessage | DataMessage | CloseMessage; + export const wsPath = '/services'; } diff --git a/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts b/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts index b3d8ce8ab9415..b2ed43b08394e 100644 --- a/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts +++ b/packages/core/src/electron-browser/messaging/electron-ipc-connection-provider.ts @@ -14,12 +14,10 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { Event as ElectronEvent, ipcRenderer } from '@theia/electron/shared/electron'; import { injectable, interfaces } from 'inversify'; +import { Channel } from '../../common/message-rpc/channel'; import { JsonRpcProxy } from '../../common/messaging'; -import { WebSocketChannel } from '../../common/messaging/web-socket-channel'; import { AbstractConnectionProvider } from '../../common/messaging/abstract-connection-provider'; -import { THEIA_ELECTRON_IPC_CHANNEL_NAME } from '../../electron-common/messaging/electron-connection-handler'; export interface ElectronIpcOptions { } @@ -36,15 +34,22 @@ export class ElectronIpcConnectionProvider extends AbstractConnectionProvider { - this.handleIncomingRawMessage(data); - }); + // ipcRenderer. + // ipcRenderer.on(THEIA_ELECTRON_IPC_CHANNEL_NAME, (event: ElectronEvent, data: string) => { + // this.handleIncomingRawMessage(data); + // }); } - protected createChannel(id: number): WebSocketChannel { - return new WebSocketChannel(id, content => { - ipcRenderer.send(THEIA_ELECTRON_IPC_CHANNEL_NAME, content); - }); + // protected createChannel(id: number): WebSocketChannel { + // return new WebSocketChannel(id, content => { + // ipcRenderer.send(THEIA_ELECTRON_IPC_CHANNEL_NAME, content); + // }); + // } + + // FIXME: Properly handle Electron connection case + protected createMainChannel(): Channel { + throw new Error('Not yet implemented'); + } } diff --git a/packages/core/src/electron-browser/messaging/electron-ws-connection-provider.ts b/packages/core/src/electron-browser/messaging/electron-ws-connection-provider.ts index 6f75ea31d0dae..591f24655f271 100644 --- a/packages/core/src/electron-browser/messaging/electron-ws-connection-provider.ts +++ b/packages/core/src/electron-browser/messaging/electron-ws-connection-provider.ts @@ -15,9 +15,8 @@ // ***************************************************************************** import { injectable } from 'inversify'; -import { WebSocketChannel } from '../../common/messaging/web-socket-channel'; -import { WebSocketConnectionProvider, WebSocketOptions } from '../../browser/messaging/ws-connection-provider'; import { FrontendApplicationContribution } from '../../browser/frontend-application'; +import { WebSocketConnectionProvider } from '../../browser/messaging/ws-connection-provider'; /** * Customized connection provider between the frontend and the backend in electron environment. @@ -39,13 +38,10 @@ export class ElectronWebSocketConnectionProvider extends WebSocketConnectionProv for (const channel of [...this.channels.values()]) { // `1001` indicates that an endpoint is "going away", such as a server going down or a browser having navigated away from a page. // But we cannot use `1001`: https://github.com/TypeFox/vscode-ws-jsonrpc/issues/15 - channel.close(1000, 'The frontend is "going away"...'); - } - } + // TODO: Add propery error code close handling for Channel + // channel.close(1000, 'The frontend is "going away"...'); + channel.close(); - override openChannel(path: string, handler: (channel: WebSocketChannel) => void, options?: WebSocketOptions): void { - if (!this.stopping) { - super.openChannel(path, handler, options); } } diff --git a/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts b/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts index 071796c5cf0ca..c345a03f8984c 100644 --- a/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts +++ b/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts @@ -14,15 +14,13 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { IpcMainEvent, ipcMain, WebContents } from '@theia/electron/shared/electron'; +import { ipcMain, IpcMainEvent } from '@theia/electron/shared/electron'; import { inject, injectable, named, postConstruct } from 'inversify'; -import { MessageConnection } from 'vscode-ws-jsonrpc'; -import { createWebSocketConnection } from 'vscode-ws-jsonrpc/lib/socket/connection'; import { ContributionProvider } from '../../common/contribution-provider'; +import { Channel } from '../../common/message-rpc/channel'; import { WebSocketChannel } from '../../common/messaging/web-socket-channel'; -import { MessagingContribution } from '../../node/messaging/messaging-contribution'; -import { ConsoleLogger } from '../../node/messaging/logger'; import { ElectronConnectionHandler, THEIA_ELECTRON_IPC_CHANNEL_NAME } from '../../electron-common/messaging/electron-connection-handler'; +import { MessagingContribution } from '../../node/messaging/messaging-contribution'; import { ElectronMainApplicationContribution } from '../electron-main-application'; import { ElectronMessagingService } from './electron-messaging-service'; @@ -34,6 +32,9 @@ import { ElectronMessagingService } from './electron-messaging-service'; * * This component allows communication between renderer process (frontend) and electron main process. */ + + +// FIXME: Electron implementation @injectable() export class ElectronMessagingContribution implements ElectronMainApplicationContribution, ElectronMessagingService { @@ -59,16 +60,14 @@ export class ElectronMessagingContribution implements ElectronMainApplicationCon } for (const connectionHandler of this.connectionHandlers.getContributions()) { this.channelHandlers.push(connectionHandler.path, (params, channel) => { - const connection = createWebSocketConnection(channel, new ConsoleLogger()); - connectionHandler.onConnection(connection); + connectionHandler.onConnection(channel); }); } } - listen(spec: string, callback: (params: ElectronMessagingService.PathParams, connection: MessageConnection) => void): void { + listen(spec: string, callback: (params: ElectronMessagingService.PathParams, connection: Channel) => void): void { this.ipcChannel(spec, (params, channel) => { - const connection = createWebSocketConnection(channel, new ConsoleLogger()); - callback(params, connection); + callback(params, channel); }); } @@ -78,54 +77,54 @@ export class ElectronMessagingContribution implements ElectronMainApplicationCon } protected handleIpcMessage(event: IpcMainEvent, data: string): void { - const sender = event.sender; - try { - // Get the channel map for a given window id - let channels = this.windowChannels.get(sender.id)!; - if (!channels) { - this.windowChannels.set(sender.id, channels = new Map()); - } - // Start parsing the message to extract the channel id and route - const message: WebSocketChannel.Message = JSON.parse(data.toString()); - // Someone wants to open a logical channel - if (message.kind === 'open') { - const { id, path } = message; - const channel = this.createChannel(id, sender); - if (this.channelHandlers.route(path, channel)) { - channel.ready(); - channels.set(id, channel); - channel.onClose(() => channels.delete(id)); - } else { - console.error('Cannot find a service for the path: ' + path); - } - } else { - const { id } = message; - const channel = channels.get(id); - if (channel) { - channel.handleMessage(message); - } else { - console.error('The ipc channel does not exist', id); - } - } - const close = () => { - for (const channel of Array.from(channels.values())) { - channel.close(undefined, 'webContent destroyed'); - } - channels.clear(); - }; - sender.once('did-navigate', close); // When refreshing the browser window. - sender.once('destroyed', close); // When closing the browser window. - } catch (error) { - console.error('IPC: Failed to handle message', { error, data }); - } + // const sender = event.sender; + // try { + // // Get the channel map for a given window id + // let channels = this.windowChannels.get(sender.id)!; + // if (!channels) { + // this.windowChannels.set(sender.id, channels = new Map()); + // } + // // Start parsing the message to extract the channel id and route + // const message: WebSocketChannel.Message = JSON.parse(data.toString()); + // // Someone wants to open a logical channel + // if (message.kind === 'open') { + // const { id, path } = message; + // const channel = this.createChannel(id, sender); + // if (this.channelHandlers.route(path, channel)) { + // channel.ready(); + // channels.set(id, channel); + // channel.onClose(() => channels.delete(id)); + // } else { + // console.error('Cannot find a service for the path: ' + path); + // } + // } else { + // const { id } = message; + // const channel = channels.get(id); + // if (channel) { + // channel.handleMessage(message); + // } else { + // console.error('The ipc channel does not exist', id); + // } + // } + // const close = () => { + // for (const channel of Array.from(channels.values())) { + // channel.close(undefined, 'webContent destroyed'); + // } + // channels.clear(); + // }; + // sender.once('did-navigate', close); // When refreshing the browser window. + // sender.once('destroyed', close); // When closing the browser window. + // } catch (error) { + // console.error('IPC: Failed to handle message', { error, data }); + // } } - protected createChannel(id: number, sender: WebContents): WebSocketChannel { - return new WebSocketChannel(id, content => { - if (!sender.isDestroyed()) { - sender.send(THEIA_ELECTRON_IPC_CHANNEL_NAME, content); - } - }); - } + // protected createChannel(id: number, sender: WebContents): WebSocketChannel { + // // return new WebSocketChannel(id, content => { + // // if (!sender.isDestroyed()) { + // // sender.send(THEIA_ELECTRON_IPC_CHANNEL_NAME, content); + // // } + // // }); + // } } diff --git a/packages/core/src/electron-main/messaging/electron-messaging-service.ts b/packages/core/src/electron-main/messaging/electron-messaging-service.ts index dde3fdde1d181..ccf2a70aa1925 100644 --- a/packages/core/src/electron-main/messaging/electron-messaging-service.ts +++ b/packages/core/src/electron-main/messaging/electron-messaging-service.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import type { MessageConnection } from 'vscode-jsonrpc'; +import { Channel } from '../../common/message-rpc/channel'; import type { WebSocketChannel } from '../../common/messaging/web-socket-channel'; export interface ElectronMessagingService { @@ -22,7 +22,7 @@ export interface ElectronMessagingService { * 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: ElectronMessagingService.PathParams, connection: MessageConnection) => void): void; + listen(path: string, callback: (params: ElectronMessagingService.PathParams, connection: Channel) => void): void; /** * Accept an ipc channel on the given path. * A path supports the route syntax: https://github.com/rcs/route-parser#what-can-i-use-in-my-routes. diff --git a/packages/core/src/node/messaging/ipc-bootstrap.ts b/packages/core/src/node/messaging/ipc-bootstrap.ts index 0bac13bb163b8..42699dbcc16b0 100644 --- a/packages/core/src/node/messaging/ipc-bootstrap.ts +++ b/packages/core/src/node/messaging/ipc-bootstrap.ts @@ -14,22 +14,47 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** +import { Socket } from 'net'; import 'reflect-metadata'; +import { Emitter } from '../../common'; +import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from '../../common/message-rpc/array-buffer-message-buffer'; +import { Channel, ReadBufferConstructor } from '../../common/message-rpc/channel'; import { dynamicRequire } from '../dynamic-require'; -import { ConsoleLogger } from 'vscode-ws-jsonrpc/lib/logger'; -import { createMessageConnection, IPCMessageReader, IPCMessageWriter, Trace } from 'vscode-ws-jsonrpc'; import { checkParentAlive, IPCEntryPoint } from './ipc-protocol'; checkParentAlive(); const entryPoint = IPCEntryPoint.getScriptFromEnv(); -const reader = new IPCMessageReader(process); -const writer = new IPCMessageWriter(process); -const logger = new ConsoleLogger(); -const connection = createMessageConnection(reader, writer, logger); -connection.trace(Trace.Off, { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - log: (message: any, data?: string) => console.log(message, data) -}); - -dynamicRequire<{ default: IPCEntryPoint }>(entryPoint).default(connection); + +dynamicRequire<{ default: IPCEntryPoint }>(entryPoint).default(createChannel()); + +function createChannel(): Channel { + const pipe = new Socket({ + fd: 4 + }); + + const onCloseEmitter = new Emitter(); + const onMessageEmitter = new Emitter(); + const onErrorEmitter = new Emitter(); + pipe.on('data', (data: Uint8Array) => { + onMessageEmitter.fire(() => new ArrayBufferReadBuffer(data.buffer)); + }); + process.on('exit', () => onCloseEmitter.fire()); + + // FIXME: Add error handling + return { + id: process.pid.toString(), + close: () => { }, + onClose: onCloseEmitter.event, + onError: onErrorEmitter.event, + onMessage: onMessageEmitter.event, + getWriteBuffer: () => { + const result = new ArrayBufferWriteBuffer(); + result.onCommit(buffer => { + pipe.write(new Uint8Array(buffer)); + }); + + return result; + } + }; +} diff --git a/packages/core/src/node/messaging/ipc-connection-provider.ts b/packages/core/src/node/messaging/ipc-connection-provider.ts index 84c256997257a..d37b63593d86f 100644 --- a/packages/core/src/node/messaging/ipc-connection-provider.ts +++ b/packages/core/src/node/messaging/ipc-connection-provider.ts @@ -15,10 +15,13 @@ // ***************************************************************************** import * as cp from 'child_process'; +import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { injectable, inject } from 'inversify'; -import { Trace, Tracer, IPCMessageReader, IPCMessageWriter, createMessageConnection, MessageConnection, Message } from 'vscode-ws-jsonrpc'; -import { ILogger, ConnectionErrorHandler, DisposableCollection, Disposable } from '../../common'; +import { Writable } from 'stream'; +import { Message } from 'vscode-ws-jsonrpc'; +import { ConnectionErrorHandler, Disposable, DisposableCollection, Emitter, ILogger } from '../../common'; +import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from '../../common/message-rpc/array-buffer-message-buffer'; +import { Channel, ReadBufferConstructor } from '../../common/message-rpc/channel'; import { createIpcEnv } from './ipc-protocol'; export interface ResolvedIPCConnectionOptions { @@ -40,7 +43,7 @@ export class IPCConnectionProvider { @inject(ILogger) protected readonly logger: ILogger; - listen(options: IPCConnectionOptions, acceptor: (connection: MessageConnection) => void): Disposable { + listen(options: IPCConnectionOptions, acceptor: (connection: Channel) => void): Disposable { return this.doListen({ logger: this.logger, args: [], @@ -48,7 +51,7 @@ export class IPCConnectionProvider { }, acceptor); } - protected doListen(options: ResolvedIPCConnectionOptions, acceptor: (connection: MessageConnection) => void): Disposable { + protected doListen(options: ResolvedIPCConnectionOptions, acceptor: (connection: Channel) => void): Disposable { const childProcess = this.fork(options); const connection = this.createConnection(childProcess, options); const toStop = new DisposableCollection(); @@ -74,32 +77,42 @@ export class IPCConnectionProvider { return toStop; } - protected createConnection(childProcess: cp.ChildProcess, options: ResolvedIPCConnectionOptions): MessageConnection { - const reader = new IPCMessageReader(childProcess); - const writer = new IPCMessageWriter(childProcess); - const connection = createMessageConnection(reader, writer, { - error: (message: string) => this.logger.error(`[${options.serverName}: ${childProcess.pid}] ${message}`), - warn: (message: string) => this.logger.warn(`[${options.serverName}: ${childProcess.pid}] ${message}`), - info: (message: string) => this.logger.info(`[${options.serverName}: ${childProcess.pid}] ${message}`), - log: (message: string) => this.logger.info(`[${options.serverName}: ${childProcess.pid}] ${message}`) + protected createConnection(childProcess: cp.ChildProcess, options?: ResolvedIPCConnectionOptions): Channel { + + const onCloseEmitter = new Emitter(); + const onMessageEmitter = new Emitter(); + const onErrorEmitter = new Emitter(); + const pipe = childProcess.stdio[4] as Writable; + + pipe.on('data', (data: Uint8Array) => { + onMessageEmitter.fire(() => new ArrayBufferReadBuffer(data.buffer)); }); - const tracer: Tracer = { - log: (message: unknown, data?: string) => this.logger.debug(`[${options.serverName}: ${childProcess.pid}] ${message}` + (typeof data === 'string' ? ' ' + data : '')) - }; - connection.trace(Trace.Verbose, tracer); - this.logger.isDebug().then(isDebug => { - if (!isDebug) { - connection.trace(Trace.Off, tracer); + + childProcess.on('error', err => onErrorEmitter.fire(err)); + childProcess.on('exit', () => onCloseEmitter.fire()); + + return { + id: childProcess.pid.toString(), + close: () => { }, + onClose: onCloseEmitter.event, + onError: onErrorEmitter.event, + onMessage: onMessageEmitter.event, + getWriteBuffer: () => { + const result = new ArrayBufferWriteBuffer(); + result.onCommit(buffer => { + pipe.write(new Uint8Array(buffer)); + }); + + return result; } - }); - return connection; + }; } protected fork(options: ResolvedIPCConnectionOptions): cp.ChildProcess { const forkOptions: cp.ForkOptions = { - silent: true, env: createIpcEnv(options), - execArgv: [] + execArgv: [], + stdio: ['pipe', 'pipe', 'pipe', 'ipc', 'pipe'] }; const inspectArgPrefix = `--${options.serverName}-inspect`; const inspectArg = process.argv.find(v => v.startsWith(inspectArgPrefix)); @@ -108,7 +121,9 @@ export class IPCConnectionProvider { } const childProcess = cp.fork(path.join(__dirname, 'ipc-bootstrap'), options.args, forkOptions); - childProcess.stdout!.on('data', data => this.logger.info(`[${options.serverName}: ${childProcess.pid}] ${data.toString().trim()}`)); + childProcess.stdout!.on('data', data => { + this.logger.info(`[${options.serverName}: ${childProcess.pid}] ${data.toString().trim()}`); + }); childProcess.stderr!.on('data', data => this.logger.error(`[${options.serverName}: ${childProcess.pid}] ${data.toString().trim()}`)); this.logger.debug(`[${options.serverName}: ${childProcess.pid}] IPC started`); diff --git a/packages/core/src/node/messaging/ipc-protocol.ts b/packages/core/src/node/messaging/ipc-protocol.ts index de9a77394b03e..03aa3944521c3 100644 --- a/packages/core/src/node/messaging/ipc-protocol.ts +++ b/packages/core/src/node/messaging/ipc-protocol.ts @@ -15,14 +15,14 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { MessageConnection } from 'vscode-ws-jsonrpc'; +import { Channel } from '../../common/message-rpc/channel'; const THEIA_PARENT_PID = 'THEIA_PARENT_PID'; const THEIA_ENTRY_POINT = 'THEIA_ENTRY_POINT'; export const ipcEntryPoint: string | undefined = process.env[THEIA_ENTRY_POINT]; -export type IPCEntryPoint = (connection: MessageConnection) => void; +export type IPCEntryPoint = (connection: Channel) => void; export namespace IPCEntryPoint { /** * Throws if `THEIA_ENTRY_POINT` is undefined or empty. diff --git a/packages/core/src/node/messaging/messaging-contribution.ts b/packages/core/src/node/messaging/messaging-contribution.ts index 2ee8764854780..e6b5b1b3c50f4 100644 --- a/packages/core/src/node/messaging/messaging-contribution.ts +++ b/packages/core/src/node/messaging/messaging-contribution.ts @@ -16,21 +16,19 @@ import * as http from 'http'; import * as https from 'https'; +import { Container, inject, injectable, interfaces, named, postConstruct } from 'inversify'; import { Server, Socket } from 'socket.io'; -import { injectable, inject, named, postConstruct, interfaces, Container } from 'inversify'; -import { MessageConnection } from 'vscode-ws-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 { ContributionProvider, ConnectionHandler, bindContributionProvider } from '../../common'; +import { bindContributionProvider, ConnectionHandler, ContributionProvider, Emitter, Event } from '../../common/'; +import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer, toArrayBuffer } from '../../common/message-rpc/array-buffer-message-buffer'; +import { Channel, ChannelMultiplexer, ReadBufferConstructor } from '../../common/message-rpc/channel'; +import { WriteBuffer } from '../../common/message-rpc/message-buffer'; import { WebSocketChannel } from '../../common/messaging/web-socket-channel'; import { BackendApplicationContribution } from '../backend-application'; -import { MessagingService, WebSocketChannelConnection } from './messaging-service'; -import { ConsoleLogger } from './logger'; -import { ConnectionContainerModule } from './connection-container-module'; -import Route = require('route-parser'); import { WsRequestValidator } from '../ws-request-validators'; +import { ConnectionContainerModule } from './connection-container-module'; import { MessagingListener } from './messaging-listeners'; +import { MessagingService } from './messaging-service'; +import Route = require('route-parser'); export const MessagingContainer = Symbol('MessagingContainer'); @@ -63,17 +61,15 @@ export class MessagingContribution implements BackendApplicationContribution, Me } } - listen(spec: string, callback: (params: MessagingService.PathParams, connection: MessageConnection) => void): void { + listen(spec: string, callback: (params: MessagingService.PathParams, connection: Channel) => void): void { this.wsChannel(spec, (params, channel) => { - const connection = createWebSocketConnection(channel, new ConsoleLogger()); - callback(params, connection); + callback(params, channel); }); } - forward(spec: string, callback: (params: MessagingService.PathParams, connection: IConnection) => void): void { + forward(spec: string, callback: (params: MessagingService.PathParams, connection: Channel) => void): void { this.wsChannel(spec, (params, channel) => { - const connection = launch.createWebSocketConnection(channel); - callback(params, WebSocketChannelConnection.create(connection, channel)); + callback(params, channel); }); } @@ -125,49 +121,15 @@ export class MessagingContribution implements BackendApplicationContribution, Me } protected handleChannels(socket: Socket): void { + const socketChannel = new SocketIOChannel(socket); + const mulitplexer = new ChannelMultiplexer(socketChannel); const channelHandlers = this.getConnectionChannelHandlers(socket); - const channels = new Map(); - socket.on('message', data => { - try { - const message: WebSocketChannel.Message = JSON.parse(data.toString()); - if (message.kind === 'open') { - const { id, path } = message; - const channel = this.createChannel(id, socket); - if (channelHandlers.route(path, channel)) { - channel.ready(); - console.debug(`Opening channel for service path '${path}'. [ID: ${id}]`); - channels.set(id, channel); - channel.onClose(() => { - console.debug(`Closing channel on service path '${path}'. [ID: ${id}]`); - channels.delete(id); - }); - } else { - console.error('Cannot find a service for the path: ' + path); - } - } else { - const { id } = message; - const channel = channels.get(id); - if (channel) { - channel.handleMessage(message); - } else { - console.error('The ws channel does not exist', id); - } - } - } catch (error) { - console.error('Failed to handle message', { error, data }); + mulitplexer.onDidOpenChannel(event => { + if (channelHandlers.route(event.id, event.channel)) { + console.debug(`Opening channel for service path '${event.id}'.`); + event.channel.onClose(() => console.debug(`Closing channel on service path '${event.id}'.`)); } }); - socket.on('error', err => { - for (const channel of channels.values()) { - channel.fireError(err); - } - }); - socket.on('disconnect', reason => { - for (const channel of channels.values()) { - channel.close(undefined, reason); - } - channels.clear(); - }); } protected createSocketContainer(socket: Socket): Container { @@ -176,7 +138,7 @@ export class MessagingContribution implements BackendApplicationContribution, Me return connectionContainer; } - protected getConnectionChannelHandlers(socket: Socket): MessagingContribution.ConnectionHandlers { + protected getConnectionChannelHandlers(socket: Socket): MessagingContribution.ConnectionHandlers { const connectionContainer = this.createSocketContainer(socket); bindContributionProvider(connectionContainer, ConnectionHandler); connectionContainer.load(...this.connectionModules.getContributions()); @@ -184,21 +146,51 @@ export class MessagingContribution implements BackendApplicationContribution, Me const connectionHandlers = connectionContainer.getNamed>(ContributionProvider, ConnectionHandler); for (const connectionHandler of connectionHandlers.getContributions(true)) { connectionChannelHandlers.push(connectionHandler.path, (_, channel) => { - const connection = createWebSocketConnection(channel, new ConsoleLogger()); - connectionHandler.onConnection(connection); + connectionHandler.onConnection(channel); }); } return connectionChannelHandlers; } - protected createChannel(id: number, socket: Socket): WebSocketChannel { - return new WebSocketChannel(id, content => { - if (socket.connected) { - socket.send(content); - } - }); +} + +export class SocketIOChannel implements Channel { + protected readonly onCloseEmitter: Emitter = new Emitter(); + get onClose(): Event { + return this.onCloseEmitter.event; + } + + protected readonly onMessageEmitter: Emitter = new Emitter(); + get onMessage(): Event { + return this.onMessageEmitter.event; } + protected readonly onErrorEmitter: Emitter = new Emitter(); + get onError(): Event { + return this.onErrorEmitter.event; + } + + readonly id: string; + + constructor(protected readonly socket: Socket) { + socket.on('error', error => this.onErrorEmitter.fire(error)); + socket.on('disconnect', reason => this.onCloseEmitter.fire()); + socket.on('message', (buffer: Buffer) => this.onMessageEmitter.fire(() => new ArrayBufferReadBuffer(toArrayBuffer(buffer)))); + this.id = socket.id; + } + + getWriteBuffer(): WriteBuffer { + const result = new ArrayBufferWriteBuffer(); + if (this.socket.connected) { + result.onCommit(buffer => { + this.socket.emit('message', buffer); + }); + } + return result; + } + close(): void { + // TODO: Implement me + } } export namespace MessagingContribution { diff --git a/packages/core/src/node/messaging/messaging-service.ts b/packages/core/src/node/messaging/messaging-service.ts index 087f6d5850def..648ac1d2fbe2c 100644 --- a/packages/core/src/node/messaging/messaging-service.ts +++ b/packages/core/src/node/messaging/messaging-service.ts @@ -15,8 +15,7 @@ // ***************************************************************************** import { Socket } from 'socket.io'; -import { MessageConnection } from 'vscode-ws-jsonrpc'; -import { IConnection } from 'vscode-ws-jsonrpc/lib/server/connection'; +import { Channel } from '../../common/message-rpc/channel'; import { WebSocketChannel } from '../../common/messaging/web-socket-channel'; export interface MessagingService { @@ -24,12 +23,12 @@ export interface MessagingService { * 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; + listen(path: string, callback: (params: MessagingService.PathParams, connection: Channel) => 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; + forward(path: string, callback: (params: MessagingService.PathParams, connection: Channel) => void): void; /** * Accept a web socket channel on the given path. * A path supports the route syntax: https://github.com/rcs/route-parser#what-can-i-use-in-my-routes. @@ -56,18 +55,3 @@ export namespace MessagingService { configure(service: MessagingService): void; } } - -export interface WebSocketChannelConnection extends IConnection { - channel: WebSocketChannel; -} -export namespace WebSocketChannelConnection { - export function is(connection: IConnection): connection is WebSocketChannelConnection { - return (connection as WebSocketChannelConnection).channel instanceof WebSocketChannel; - } - - export function create(connection: IConnection, channel: WebSocketChannel): WebSocketChannelConnection { - const result = connection as WebSocketChannelConnection; - result.channel = channel; - return result; - } -} diff --git a/packages/core/src/node/messaging/test/test-web-socket-channel.ts b/packages/core/src/node/messaging/test/test-web-socket-channel.ts index 2fbb17c9aa8ec..8e385ace9ff94 100644 --- a/packages/core/src/node/messaging/test/test-web-socket-channel.ts +++ b/packages/core/src/node/messaging/test/test-web-socket-channel.ts @@ -14,34 +14,34 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import * as http from 'http'; -import * as https from 'https'; -import { WebSocketChannel } from '../../../common/messaging/web-socket-channel'; -import { Disposable } from '../../../common/disposable'; -import { AddressInfo } from 'net'; -import { io } from 'socket.io-client'; +// import * as http from 'http'; +// import * as https from 'https'; +// import { WebSocketChannel } from '../../../common/messaging/web-socket-channel'; +// import { Disposable } from '../../../common/disposable'; +// import { AddressInfo } from 'net'; +// import { io } from 'socket.io-client'; -export class TestWebSocketChannel extends WebSocketChannel { +// export class TestWebSocketChannel extends WebSocketChannel { - constructor({ server, path }: { - server: http.Server | https.Server, - path: string - }) { - super(0, content => socket.send(content)); - const socket = io(`ws://localhost:${(server.address() as AddressInfo).port}${WebSocketChannel.wsPath}`); - socket.on('error', error => - this.fireError(error) - ); - socket.on('disconnect', reason => - this.fireClose(0, reason) - ); - socket.on('message', data => { - this.handleMessage(JSON.parse(data.toString())); - }); - socket.on('connect', () => - this.open(path) - ); - this.toDispose.push(Disposable.create(() => socket.close())); - } +// constructor({ server, path }: { +// server: http.Server | https.Server, +// path: string +// }) { +// super(0, content => socket.send(content)); +// const socket = io(`ws://localhost:${(server.address() as AddressInfo).port}${WebSocketChannel.wsPath}`); +// socket.on('error', error => +// this.fireError(error) +// ); +// socket.on('disconnect', reason => +// this.fireClose(0, reason) +// ); +// socket.on('message', data => { +// this.handleMessage(JSON.parse(data.toString())); +// }); +// socket.on('connect', () => +// this.open(path) +// ); +// this.toDispose.push(Disposable.create(() => socket.close())); +// } -} +// } diff --git a/packages/debug/src/browser/debug-session-connection.ts b/packages/debug/src/browser/debug-session-connection.ts index 4ef9db7818a74..f58f7fb8d152d 100644 --- a/packages/debug/src/browser/debug-session-connection.ts +++ b/packages/debug/src/browser/debug-session-connection.ts @@ -16,13 +16,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { DebugProtocol } from 'vscode-debugprotocol'; +import { Disposable, DisposableCollection, Emitter, Event, MaybePromise } from '@theia/core'; +import { Channel } from '@theia/core/lib/common/message-rpc/channel'; import { Deferred } from '@theia/core/lib/common/promise-util'; -import { Event, Emitter, DisposableCollection, Disposable, MaybePromise } from '@theia/core'; import { OutputChannel } from '@theia/output/lib/browser/output-channel'; - -import { Channel } from '../common/debug-service'; - +import { DebugProtocol } from 'vscode-debugprotocol'; export type DebugRequestHandler = (request: DebugProtocol.Request) => MaybePromise; export interface DebugRequestTypes { @@ -168,7 +166,7 @@ export class DebugSessionConnection implements Disposable { this.cancelPendingRequests(); this.onDidCloseEmitter.fire(); }); - connection.onMessage(data => this.handleMessage(data)); + connection.onMessage(data => this.handleMessage(data().readString())); return connection; } @@ -247,7 +245,7 @@ export class DebugSessionConnection implements Disposable { const dateStr = `${now.toLocaleString(undefined, { hour12: false })}.${now.getMilliseconds()}`; this.traceOutputChannel.appendLine(`${this.sessionId.substring(0, 8)} ${dateStr} theia -> adapter: ${JSON.stringify(message, undefined, 4)}`); } - connection.send(messageStr); + connection.getWriteBuffer().writeString(messageStr); } protected handleMessage(data: string): void { diff --git a/packages/debug/src/browser/debug-session-contribution.ts b/packages/debug/src/browser/debug-session-contribution.ts index 3bcee60f38d9a..2a3c0dbe2abf5 100644 --- a/packages/debug/src/browser/debug-session-contribution.ts +++ b/packages/debug/src/browser/debug-session-contribution.ts @@ -26,10 +26,11 @@ import { DebugSessionOptions } from './debug-session-options'; import { OutputChannelManager, OutputChannel } from '@theia/output/lib/browser/output-channel'; import { DebugPreferences } from './debug-preferences'; import { DebugSessionConnection } from './debug-session-connection'; -import { Channel, DebugAdapterPath } from '../common/debug-service'; +import { DebugAdapterPath } from '../common/debug-service'; import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { DebugContribution } from './debug-contribution'; +import { Channel } from '@theia/core/src/common/message-rpc/channel'; /** * DebugSessionContribution symbol for DI. diff --git a/packages/debug/src/browser/debug-session.tsx b/packages/debug/src/browser/debug-session.tsx index 88b3a40435769..9245d06cc4785 100644 --- a/packages/debug/src/browser/debug-session.tsx +++ b/packages/debug/src/browser/debug-session.tsx @@ -16,31 +16,31 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import * as React from '@theia/core/shared/react'; import { LabelProvider } from '@theia/core/lib/browser'; -import { DebugProtocol } from 'vscode-debugprotocol'; -import { Emitter, Event, DisposableCollection, Disposable, MessageClient, MessageType, Mutable, ContributionProvider } from '@theia/core/lib/common'; -import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; -import { EditorManager } from '@theia/editor/lib/browser'; import { CompositeTreeElement } from '@theia/core/lib/browser/source-tree'; -import { DebugSessionConnection, DebugRequestTypes, DebugEventTypes } from './debug-session-connection'; -import { DebugThread, StoppedDetails, DebugThreadData } from './model/debug-thread'; -import { DebugScope } from './console/debug-console-items'; -import { DebugStackFrame } from './model/debug-stack-frame'; -import { DebugSource } from './model/debug-source'; -import { DebugBreakpoint, DebugBreakpointOptions } from './model/debug-breakpoint'; -import { DebugSourceBreakpoint } from './model/debug-source-breakpoint'; -import debounce = require('p-debounce'); +import { ContributionProvider, Disposable, DisposableCollection, Emitter, Event, MessageClient, MessageType, Mutable } from '@theia/core/lib/common'; +import { waitForEvent } from '@theia/core/lib/common/promise-util'; import URI from '@theia/core/lib/common/uri'; +import * as React from '@theia/core/shared/react'; +import { EditorManager } from '@theia/editor/lib/browser'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; +import { TerminalWidget, TerminalWidgetOptions } from '@theia/terminal/lib/browser/base/terminal-widget'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { DebugConfiguration, DebugConsoleMode } from '../common/debug-common'; import { BreakpointManager } from './breakpoint/breakpoint-manager'; +import { ExceptionBreakpoint, SourceBreakpoint } from './breakpoint/breakpoint-marker'; +import { DebugScope } from './console/debug-console-items'; +import { DebugContribution } from './debug-contribution'; +import { DebugEventTypes, DebugRequestTypes, DebugSessionConnection } from './debug-session-connection'; import { DebugSessionOptions, InternalDebugSessionOptions } from './debug-session-options'; -import { DebugConfiguration, DebugConsoleMode } from '../common/debug-common'; -import { SourceBreakpoint, ExceptionBreakpoint } from './breakpoint/breakpoint-marker'; -import { TerminalWidgetOptions, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; +import { DebugBreakpoint, DebugBreakpointOptions } from './model/debug-breakpoint'; import { DebugFunctionBreakpoint } from './model/debug-function-breakpoint'; -import { FileService } from '@theia/filesystem/lib/browser/file-service'; -import { DebugContribution } from './debug-contribution'; -import { waitForEvent } from '@theia/core/lib/common/promise-util'; +import { DebugSource } from './model/debug-source'; +import { DebugSourceBreakpoint } from './model/debug-source-breakpoint'; +import { DebugStackFrame } from './model/debug-stack-frame'; +import { DebugThread, DebugThreadData, StoppedDetails } from './model/debug-thread'; +import debounce = require('p-debounce'); export enum DebugState { Inactive, diff --git a/packages/debug/src/node/debug-adapter-session.ts b/packages/debug/src/node/debug-adapter-session.ts index 03ff950d38a90..e1dabd57d98a7 100644 --- a/packages/debug/src/node/debug-adapter-session.ts +++ b/packages/debug/src/node/debug-adapter-session.ts @@ -26,7 +26,7 @@ import { DebugAdapterSession } from './debug-model'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { Channel } from '../common/debug-service'; +import { Channel } from '@theia/core/lib/common/message-rpc/channel'; /** * [DebugAdapterSession](#DebugAdapterSession) implementation. @@ -53,7 +53,7 @@ export class DebugAdapterSessionImpl implements DebugAdapterSession { throw new Error('The session has already been started, id: ' + this.id); } this.channel = channel; - this.channel.onMessage((message: string) => this.write(message)); + this.channel.onMessage(message => this.write(message().readString())); this.channel.onClose(() => this.channel = undefined); } @@ -80,7 +80,7 @@ export class DebugAdapterSessionImpl implements DebugAdapterSession { protected send(message: string): void { if (this.channel) { - this.channel.send(message); + this.channel.getWriteBuffer().writeString(message); } } diff --git a/packages/debug/src/node/debug-model.ts b/packages/debug/src/node/debug-model.ts index a39352fabbddf..dd73d1d1a6880 100644 --- a/packages/debug/src/node/debug-model.ts +++ b/packages/debug/src/node/debug-model.ts @@ -26,7 +26,7 @@ import { DebugConfiguration } from '../common/debug-configuration'; import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; import { MaybePromise } from '@theia/core/lib/common/types'; import { Event } from '@theia/core/lib/common/event'; -import { Channel } from '../common/debug-service'; +import { Channel } from '@theia/core/lib/common/message-rpc/channel'; // FIXME: break down this file to debug adapter and debug adapter contribution (see Theia file naming conventions) diff --git a/packages/messages/src/browser/notifications-manager.ts b/packages/messages/src/browser/notifications-manager.ts index 0daf43bbe500d..b4207dd65c8ef 100644 --- a/packages/messages/src/browser/notifications-manager.ts +++ b/packages/messages/src/browser/notifications-manager.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; -import { MessageClient, MessageType, Message as PlainMessage, ProgressMessage, ProgressUpdate, CancellationToken } from '@theia/core/lib/common'; +import { MessageClient, Message as PlainMessage, ProgressMessage, ProgressUpdate, CancellationToken, MessageType } from '@theia/core/lib/common'; import { deepClone } from '@theia/core/lib/common/objects'; import { Emitter } from '@theia/core'; import { Deferred } from '@theia/core/lib/common/promise-util'; diff --git a/packages/plugin-ext/src/common/connection.ts b/packages/plugin-ext/src/common/connection.ts index 48ae3adb36363..d334691f4381d 100644 --- a/packages/plugin-ext/src/common/connection.ts +++ b/packages/plugin-ext/src/common/connection.ts @@ -13,27 +13,38 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { Channel } from '@theia/debug/lib/common/debug-service'; +import { Emitter, Event } from '@theia/core/lib/common/event'; +import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from '@theia/core/lib/common/message-rpc/array-buffer-message-buffer'; +import { Channel, ReadBufferConstructor } from '@theia/core/lib/common/message-rpc/channel'; +import { WriteBuffer } from '@theia/core/lib/common/message-rpc/message-buffer'; import { ConnectionExt, ConnectionMain } from './plugin-api-rpc'; -import { Emitter } from '@theia/core/lib/common/event'; /** * A channel communicating with a counterpart in a plugin host. */ export class PluginChannel implements Channel { - private messageEmitter: Emitter = new Emitter(); + private messageEmitter: Emitter = new Emitter(); private errorEmitter: Emitter = new Emitter(); private closedEmitter: Emitter = new Emitter(); constructor( - protected readonly id: string, + readonly id: string, protected readonly connection: ConnectionExt | ConnectionMain) { } + getWriteBuffer(): WriteBuffer { + const result = new ArrayBufferWriteBuffer(); + result.onCommit(buffer => { + this.connection.$sendMessage(this.id, new ArrayBufferReadBuffer(buffer).readString()); + }); + + return result; + } + send(content: string): void { this.connection.$sendMessage(this.id, content); } - fireMessageReceived(msg: string): void { + fireMessageReceived(msg: ReadBufferConstructor): void { this.messageEmitter.fire(msg); } @@ -45,18 +56,16 @@ export class PluginChannel implements Channel { this.closedEmitter.fire(); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onMessage(cb: (data: any) => void): void { - this.messageEmitter.event(cb); + get onMessage(): Event { + return this.messageEmitter.event; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onError(cb: (reason: any) => void): void { - this.errorEmitter.event(cb); + get onError(): Event { + return this.errorEmitter.event; } - onClose(cb: (code: number, reason: string) => void): void { - this.closedEmitter.event(() => cb(-1, 'closed')); + get onClose(): Event { + return this.closedEmitter.event; } close(): void { @@ -80,7 +89,10 @@ export class ConnectionImpl implements ConnectionMain, ConnectionExt { */ async $sendMessage(id: string, message: string): Promise { if (this.connections.has(id)) { - this.connections.get(id)!.fireMessageReceived(message); + const writer = new ArrayBufferWriteBuffer(); + writer.writeString(message); + const reader = new ArrayBufferReadBuffer(writer.getCurrentContents()); + this.connections.get(id)!.fireMessageReceived(() => reader); } else { console.warn(`Received message for unknown connection: ${id}`); } diff --git a/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts b/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts index cd3615827c36c..e3b1ab50ee83c 100644 --- a/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts +++ b/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts @@ -30,7 +30,7 @@ import { TerminalOptionsExt } from '../../../common/plugin-api-rpc'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { DebugContribution } from '@theia/debug/lib/browser/debug-contribution'; import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; -import { Channel } from '@theia/debug/lib/common/debug-service'; +import { Channel } from '@theia/core/lib/common/message-rpc/channel'; export class PluginDebugSession extends DebugSession { constructor( diff --git a/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-session.ts b/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-session.ts index 890f46be08036..c5c307d363b4c 100644 --- a/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-session.ts +++ b/packages/plugin-ext/src/plugin/node/debug/plugin-debug-adapter-session.ts @@ -17,7 +17,7 @@ import { DebugAdapterSessionImpl } from '@theia/debug/lib/node/debug-adapter-session'; import * as theia from '@theia/plugin'; import { DebugAdapter } from '@theia/debug/lib/node/debug-model'; -import { Channel } from '@theia/debug/lib/common/debug-service'; +import { Channel } from '@theia/core/lib/common/message-rpc/channel'; /* eslint-disable @typescript-eslint/no-explicit-any */ diff --git a/packages/task/src/node/task-server.slow-spec.ts b/packages/task/src/node/task-server.slow-spec.ts index fbe968348d9d2..76b0b9f4339ab 100644 --- a/packages/task/src/node/task-server.slow-spec.ts +++ b/packages/task/src/node/task-server.slow-spec.ts @@ -17,20 +17,18 @@ /* eslint-disable no-unused-expressions */ // tslint:disable-next-line:no-implicit-dependencies -import 'reflect-metadata'; -import { createTaskTestContainer } from './test/task-test-container'; +import { isOSX, isWindows } from '@theia/core/lib/common/os'; +import { expectThrowsAsync } from '@theia/core/lib/common/test/expect'; +import URI from '@theia/core/lib/common/uri'; +import { FileUri } from '@theia/core/lib/node'; import { BackendApplication } from '@theia/core/lib/node/backend-application'; -import { TaskExitedEvent, TaskInfo, TaskServer, TaskWatcher, TaskConfiguration } from '../common'; -import { ProcessType, ProcessTaskConfiguration } from '../common/process/task-protocol'; +import { expect } from 'chai'; import * as http from 'http'; import * as https from 'https'; -import { isWindows, isOSX } from '@theia/core/lib/common/os'; -import { FileUri } from '@theia/core/lib/node'; -import { terminalsPath } from '@theia/terminal/lib/common/terminal-protocol'; -import { expectThrowsAsync } from '@theia/core/lib/common/test/expect'; -import { TestWebSocketChannel } from '@theia/core/lib/node/messaging/test/test-web-socket-channel'; -import { expect } from 'chai'; -import URI from '@theia/core/lib/common/uri'; +import 'reflect-metadata'; +import { TaskConfiguration, TaskExitedEvent, TaskInfo, TaskServer, TaskWatcher } from '../common'; +import { ProcessTaskConfiguration, ProcessType } from '../common/process/task-protocol'; +import { createTaskTestContainer } from './test/task-test-container'; // test scripts that we bundle with tasks const commandShortRunning = './task'; @@ -87,47 +85,48 @@ describe('Task server / back-end', function (): void { _server.close(); }); - it('task running in terminal - expected data is received from the terminal ws server', async function (): Promise { - const someString = 'someSingleWordString'; - - // This test is flaky on Windows and fails intermittently. Disable it for now - if (isWindows) { - this.skip(); - return; - } - - // create task using terminal process - const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning); - const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('shell', `${command} ${someString}`), wsRoot); - const terminalId = taskInfo.terminalId; - - const messagesToWaitFor = 10; - const messages: string[] = []; - - // hook-up to terminal's ws and confirm that it outputs expected tasks' output - await new Promise((resolve, reject) => { - const channel = new TestWebSocketChannel({ server, path: `${terminalsPath}/${terminalId}` }); - channel.onError(reject); - channel.onClose((code, reason) => reject(new Error(`channel is closed with '${code}' code and '${reason}' reason`))); - channel.onMessage(msg => { - // check output of task on terminal is what we expect - const expected = `${isOSX ? 'tasking osx' : 'tasking'}... ${someString}`; - // Instead of waiting for one message from the terminal, we wait for several ones as the very first message can be something unexpected. - // For instance: `nvm is not compatible with the \"PREFIX\" environment variable: currently set to \"/usr/local\"\r\n` - const currentMessage = msg.toString(); - messages.unshift(currentMessage); - if (currentMessage.indexOf(expected) !== -1) { - resolve(); - channel.close(); - return; - } - if (messages.length >= messagesToWaitFor) { - reject(new Error(`expected sub-string not found in terminal output. Expected: "${expected}" vs Actual messages: ${JSON.stringify(messages)}`)); - channel.close(); - } - }); - }); - }); + // FIXME + // it('task running in terminal - expected data is received from the terminal ws server', async function (): Promise { + // const someString = 'someSingleWordString'; + + // // This test is flaky on Windows and fails intermittently. Disable it for now + // if (isWindows) { + // this.skip(); + // return; + // } + + // // create task using terminal process + // const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning); + // const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('shell', `${command} ${someString}`), wsRoot); + // const terminalId = taskInfo.terminalId; + + // const messagesToWaitFor = 10; + // const messages: string[] = []; + + // // hook-up to terminal's ws and confirm that it outputs expected tasks' output + // await new Promise((resolve, reject) => { + // const channel = new TestWebSocketChannel({ server, path: `${terminalsPath}/${terminalId}` }); + // channel.onError(reject); + // channel.onClose((code, reason) => reject(new Error(`channel is closed with '${code}' code and '${reason}' reason`))); + // channel.onMessage(msg => { + // // check output of task on terminal is what we expect + // const expected = `${isOSX ? 'tasking osx' : 'tasking'}... ${someString}`; + // // Instead of waiting for one message from the terminal, we wait for several ones as the very first message can be something unexpected. + // // For instance: `nvm is not compatible with the \"PREFIX\" environment variable: currently set to \"/usr/local\"\r\n` + // const currentMessage = msg.toString(); + // messages.unshift(currentMessage); + // if (currentMessage.indexOf(expected) !== -1) { + // resolve(); + // channel.close(); + // return; + // } + // if (messages.length >= messagesToWaitFor) { + // reject(new Error(`expected sub-string not found in terminal output. Expected: "${expected}" vs Actual messages: ${JSON.stringify(messages)}`)); + // channel.close(); + // } + // }); + // }); + // }); it('task using raw process - task server success response shall not contain a terminal id', async function (): Promise { const someString = 'someSingleWordString'; diff --git a/packages/terminal/src/browser/terminal-widget-impl.ts b/packages/terminal/src/browser/terminal-widget-impl.ts index 311b9122e3659..22c093d24e437 100644 --- a/packages/terminal/src/browser/terminal-widget-impl.ts +++ b/packages/terminal/src/browser/terminal-widget-impl.ts @@ -14,30 +14,30 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { Terminal, RendererType } from 'xterm'; -import { FitAddon } from 'xterm-addon-fit'; -import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify'; -import { ContributionProvider, Disposable, Event, Emitter, ILogger, DisposableCollection } from '@theia/core'; -import { Widget, Message, WebSocketConnectionProvider, StatefulWidget, isFirefox, MessageLoop, KeyCode, codicon } from '@theia/core/lib/browser'; +import { ContributionProvider, Disposable, DisposableCollection, Emitter, Event, ILogger } from '@theia/core'; +import { codicon, isFirefox, KeyCode, Message, MessageLoop, StatefulWidget, WebSocketConnectionProvider, Widget } from '@theia/core/lib/browser'; +import { Key } from '@theia/core/lib/browser/keys'; import { isOSX } from '@theia/core/lib/common'; +import { nls } from '@theia/core/lib/common/nls'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import URI from '@theia/core/lib/common/uri'; +import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify'; +import { RCPConnection } from '@theia/core/lib/common/message-rpc/rpc-protocol'; +import { CommandLineOptions, ShellCommandBuilder } from '@theia/process/lib/common/shell-command-builder'; import { WorkspaceService } from '@theia/workspace/lib/browser'; -import { ShellTerminalServerProxy, IShellTerminalPreferences } from '../common/shell-terminal-protocol'; -import { terminalsPath } from '../common/terminal-protocol'; +import { RendererType, Terminal } from 'xterm'; +import { FitAddon } from 'xterm-addon-fit'; import { IBaseTerminalServer, TerminalProcessInfo } from '../common/base-terminal-protocol'; +import { IShellTerminalPreferences, ShellTerminalServerProxy } from '../common/shell-terminal-protocol'; +import { terminalsPath } from '../common/terminal-protocol'; import { TerminalWatcher } from '../common/terminal-watcher'; -import { TerminalWidgetOptions, TerminalWidget, TerminalDimensions } from './base/terminal-widget'; -import { MessageConnection } from '@theia/core/shared/vscode-ws-jsonrpc'; -import { Deferred } from '@theia/core/lib/common/promise-util'; -import { TerminalPreferences, TerminalRendererType, isTerminalRendererType, DEFAULT_TERMINAL_RENDERER_TYPE, CursorStyle } from './terminal-preferences'; -import { TerminalContribution } from './terminal-contribution'; -import URI from '@theia/core/lib/common/uri'; import { TerminalService } from './base/terminal-service'; -import { TerminalSearchWidgetFactory, TerminalSearchWidget } from './search/terminal-search-widget'; +import { TerminalDimensions, TerminalWidget, TerminalWidgetOptions } from './base/terminal-widget'; +import { TerminalSearchWidget, TerminalSearchWidgetFactory } from './search/terminal-search-widget'; +import { TerminalContribution } from './terminal-contribution'; import { TerminalCopyOnSelectionHandler } from './terminal-copy-on-selection-handler'; +import { CursorStyle, DEFAULT_TERMINAL_RENDERER_TYPE, isTerminalRendererType, TerminalPreferences, TerminalRendererType } from './terminal-preferences'; import { TerminalThemeService } from './terminal-theme-service'; -import { CommandLineOptions, ShellCommandBuilder } from '@theia/process/lib/common/shell-command-builder'; -import { Key } from '@theia/core/lib/browser/keys'; -import { nls } from '@theia/core/lib/common/nls'; export const TERMINAL_WIDGET_FACTORY_ID = 'terminal'; @@ -58,7 +58,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget protected searchBox: TerminalSearchWidget; protected restored = false; protected closeOnDispose = true; - protected waitForConnection: Deferred | undefined; + protected waitForConnection: Deferred | undefined; protected hoverMessage: HTMLDivElement; protected lastTouchEnd: TouchEvent | undefined; protected isAttachedCloseListener: boolean = false; @@ -507,16 +507,23 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget } this.toDisposeOnConnect.dispose(); this.toDispose.push(this.toDisposeOnConnect); - const waitForConnection = this.waitForConnection = new Deferred(); + const waitForConnection = this.waitForConnection = new Deferred(); this.webSocketConnectionProvider.listen({ path: `${terminalsPath}/${this.terminalId}`, onConnection: connection => { - connection.onNotification('onData', (data: string) => this.write(data)); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const requestHandler = (method: string, args: any[]) => Promise.resolve(); + const rpc = new RCPConnection(connection, requestHandler); + rpc.onNotification(event => { + if (event.method === 'onData') { + this.write(event.args[0]); + } + }); // Excludes the device status code emitted by Xterm.js const sendData = (data?: string) => { if (data && !this.deviceStatusCodes.has(data) && !this.disableEnterWhenAttachCloseListener()) { - return connection.sendRequest('write', data); + return rpc.sendRequest('write', [data]); } }; @@ -524,12 +531,10 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget disposable.push(this.term.onData(sendData)); disposable.push(this.term.onBinary(sendData)); - connection.onDispose(() => disposable.dispose()); + connection.onClose(() => disposable.dispose()); - this.toDisposeOnConnect.push(connection); - connection.listen(); if (waitForConnection) { - waitForConnection.resolve(connection); + waitForConnection.resolve(rpc); } } }, { reconnecting: false }); @@ -579,7 +584,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget sendText(text: string): void { if (this.waitForConnection) { this.waitForConnection.promise.then(connection => - connection.sendRequest('write', text) + connection.sendRequest('write', [text]) ); } } 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 6d39ddd973f20..4070c448d705d 100644 --- a/packages/terminal/src/node/terminal-backend-contribution.slow-spec.ts +++ b/packages/terminal/src/node/terminal-backend-contribution.slow-spec.ts @@ -14,44 +14,42 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { createTerminalTestContainer } from './test/terminal-test-container'; -import { BackendApplication } from '@theia/core/lib/node/backend-application'; -import { IShellTerminalServer } from '../common/shell-terminal-protocol'; -import * as http from 'http'; -import * as https from 'https'; -import { terminalsPath } from '../common/terminal-protocol'; -import { TestWebSocketChannel } from '@theia/core/lib/node/messaging/test/test-web-socket-channel'; +// import { BackendApplication } from '@theia/core/lib/node/backend-application'; +// import * as http from 'http'; +// import * as https from 'https'; +// import { IShellTerminalServer } from '../common/shell-terminal-protocol'; +// import { createTerminalTestContainer } from './test/terminal-test-container'; -describe('Terminal Backend Contribution', function (): void { +// describe('Terminal Backend Contribution', function (): void { - this.timeout(10000); - let server: http.Server | https.Server; - let shellTerminalServer: IShellTerminalServer; + // this.timeout(10000); + // let server: http.Server | https.Server; + // let shellTerminalServer: IShellTerminalServer; - beforeEach(async () => { - const container = createTerminalTestContainer(); - const application = container.get(BackendApplication); - shellTerminalServer = container.get(IShellTerminalServer); - server = await application.start(); - }); + // beforeEach(async () => { + // const container = createTerminalTestContainer(); + // const application = container.get(BackendApplication); + // shellTerminalServer = container.get(IShellTerminalServer); + // server = await application.start(); + // }); - afterEach(() => { - const s = server; - server = undefined!; - shellTerminalServer = undefined!; - s.close(); - }); + // afterEach(() => { + // const s = server; + // server = undefined!; + // shellTerminalServer = undefined!; + // s.close(); + // }); - it('is data received from the terminal ws server', async () => { - const terminalId = await shellTerminalServer.create({}); - await new Promise((resolve, reject) => { - const channel = new TestWebSocketChannel({ server, path: `${terminalsPath}/${terminalId}` }); - channel.onError(reject); - channel.onClose((code, reason) => reject(new Error(`channel is closed with '${code}' code and '${reason}' reason`))); - channel.onOpen(() => { - resolve(); - channel.close(); - }); - }); - }); -}); + // it('is data received from the terminal ws server', async () => { + // const terminalId = await shellTerminalServer.create({}); + // await new Promise((resolve, reject) => { + // const channel = new TestWebSocketChannel({ server, path: `${terminalsPath}/${terminalId}` }); + // channel.onError(reject); + // channel.onClose((code, reason) => reject(new Error(`channel is closed with '${code}' code and '${reason}' reason`))); + // channel.onOpen(() => { + // resolve(); + // channel.close(); + // }); + // }); + // }); +// }); diff --git a/packages/terminal/src/node/terminal-backend-contribution.ts b/packages/terminal/src/node/terminal-backend-contribution.ts index 4675b7a32290c..b671bd4cda6ac 100644 --- a/packages/terminal/src/node/terminal-backend-contribution.ts +++ b/packages/terminal/src/node/terminal-backend-contribution.ts @@ -14,11 +14,12 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable, inject, named } from '@theia/core/shared/inversify'; import { ILogger } from '@theia/core/lib/common'; -import { TerminalProcess, ProcessManager } from '@theia/process/lib/node'; -import { terminalsPath } from '../common/terminal-protocol'; +import { RCPConnection } from '@theia/core/lib/common/message-rpc/rpc-protocol'; import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; +import { ProcessManager, TerminalProcess } from '@theia/process/lib/node'; +import { terminalsPath } from '../common/terminal-protocol'; @injectable() export class TerminalBackendContribution implements MessagingService.Contribution { @@ -35,14 +36,17 @@ export class TerminalBackendContribution implements MessagingService.Contributio const termProcess = this.processManager.get(id); if (termProcess instanceof TerminalProcess) { const output = termProcess.createOutputStream(); - output.on('data', data => connection.sendNotification('onData', data.toString())); - connection.onRequest('write', (data: string) => termProcess.write(data)); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const requestHandler = async (method: string, args: any[]) => { + if (method === 'write' && args[0]) { + termProcess.write(args[0].toString()); + } + }; + const rpc = new RCPConnection(connection, requestHandler); + output.on('data', data => rpc.sendNotification('onData', data)); connection.onClose(() => output.dispose()); - connection.listen(); - } else { - connection.dispose(); } }); } - } + diff --git a/yarn.lock b/yarn.lock index 9f56f314ef37e..4c0a8254157ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2192,6 +2192,13 @@ resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== +"@types/chai-spies@1.0.3": + version "1.0.3" + resolved "http://localhost:4873/@types%2fchai-spies/-/chai-spies-1.0.3.tgz#a52dc61af3853ec9b80965040811d15dfd401542" + integrity sha512-RBZjhVuK7vrg4rWMt04UF5zHYwfHnpk5mIWu3nQvU3AKGDixXzSjZ6v0zke6pBcaJqMv3IBZ5ibLWPMRDL0sLw== + dependencies: + "@types/chai" "*" + "@types/chai-string@^1.4.0": version "1.4.2" resolved "https://registry.yarnpkg.com/@types/chai-string/-/chai-string-1.4.2.tgz#0f116504a666b6c6a3c42becf86634316c9a19ac" @@ -2199,9 +2206,9 @@ dependencies: "@types/chai" "*" -"@types/chai@*", "@types/chai@^4.2.7": +"@types/chai@*", "@types/chai@4.3.0", "@types/chai@^4.2.7": version "4.3.0" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc" + resolved "http://localhost:4873/@types%2fchai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc" integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw== "@types/component-emitter@^1.2.10": @@ -3890,11 +3897,28 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +chai-spies@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chai-spies/-/chai-spies-1.0.0.tgz#d16b39336fb316d03abf8c375feb23c0c8bb163d" + integrity sha512-elF2ZUczBsFoP07qCfMO/zeggs8pqCf3fZGyK5+2X4AndS8jycZYID91ztD9oQ7d/0tnS963dPkd0frQEThDsg== + chai-string@^1.4.0: version "1.5.0" resolved "https://registry.yarnpkg.com/chai-string/-/chai-string-1.5.0.tgz#0bdb2d8a5f1dbe90bc78ec493c1c1c180dd4d3d2" integrity sha512-sydDC3S3pNAQMYwJrs6dQX0oBQ6KfIPuOZ78n7rocW0eJJlsHPh2t3kwW7xfwYA/1Bf6/arGtSUo16rxR2JFlw== +chai@4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49" + integrity sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.2" + deep-eql "^3.0.1" + get-func-name "^2.0.0" + pathval "^1.1.1" + type-detect "^4.0.5" + chai@^4.2.0: version "4.3.6" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.6.tgz#ffe4ba2d9fa9d6680cc0b370adae709ec9011e9c" From 6bcf87230e9c51aacb38deedcbc8a54a9388ce8c Mon Sep 17 00:00:00 2001 From: Tobias Ortmayr Date: Fri, 1 Apr 2022 14:42:17 +0200 Subject: [PATCH 3/3] [WIP] Improve message encoding and cleanup integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactor the message encoders of the new message-rpc protocol to improve overall performance. - Align main websocket creation: Implement `ẀebSocketMainChannel` class which is responsible for establishing the main websocket connection between frontend and backend. Use a `IWebSocket` wrapper so that the class can be reused in the frontend and backend runtimes and is independent of the actual library used for creating the websocket. - Improve rpc protocol error handling. In particular ensure that remote call errors are properly decoded to `ResponseErrors`. - Ensure that the `RemoteFileSystemProvider` API uses Uint8Arrays over plain number arrays. This enables direct serialization as buffers and reduces the overhead of unnecessarily converting from and to Uint8 arrays. - Refactor terminal widget and terminal backend contribution so that the widgets communicates with the underlying terminal process using the new rpc protocol. - Rework the IPC bootstrap protocol so that it uses a binary pipe for message transmission instead of the `ipc` pipe which only supports string encoding. Note: This is a WIP PR that is not ready to be reviewed yet as some concepts are not cleaned up yet or fully implemented (e.g. electron support & debug adapter protocol support) It's intention is to present the current state of #10684 to interested parties. Contributed on behalf of STMicroelectronics. --- .../messaging/ws-connection-provider.ts | 57 +-- packages/core/src/common/index.ts | 1 + .../core/src/common/message-rpc/README.md | 10 - .../array-buffer-message-buffer.spec.ts | 8 +- .../array-buffer-message-buffer.ts | 107 ++++-- .../src/common/message-rpc/channel.spec.ts | 6 +- .../core/src/common/message-rpc/channel.ts | 48 +-- .../common/message-rpc/connection-handler.ts | 38 -- .../src/common/message-rpc/experiments.ts | 97 ----- packages/core/src/common/message-rpc/index.ts | 18 + .../src/common/message-rpc/message-buffer.ts | 75 +++- ...er.spec.ts => rpc-message-encoder.spec.ts} | 6 +- ...sage-encoder.ts => rpc-message-encoder.ts} | 354 ++++++++++-------- .../src/common/message-rpc/rpc-protocol.ts | 201 +++------- .../core/src/common/message-rpc/rpc-proxy.ts | 93 ----- .../src/common/messaging/proxy-factory.ts | 8 +- .../common/messaging/web-socket-channel.ts | 58 ++- .../electron-messaging-contribution.ts | 1 - .../core/src/node/messaging/ipc-bootstrap.ts | 4 +- .../node/messaging/ipc-connection-provider.ts | 4 +- .../node/messaging/messaging-contribution.ts | 63 +--- .../messaging/test/test-web-socket-channel.ts | 55 ++- .../src/browser/debug-session-connection.ts | 4 +- .../src/browser/debug-session-contribution.ts | 2 +- packages/filesystem/src/common/files.ts | 2 +- .../src/common/remote-file-system-provider.ts | 37 +- packages/plugin-ext/src/common/connection.ts | 12 +- .../task/src/node/task-server.slow-spec.ts | 86 ++--- .../src/browser/terminal-widget-impl.ts | 12 +- ...terminal-backend-contribution.slow-spec.ts | 69 ++-- .../src/node/terminal-backend-contribution.ts | 23 +- yarn.lock | 4 +- 32 files changed, 689 insertions(+), 874 deletions(-) delete mode 100644 packages/core/src/common/message-rpc/README.md delete mode 100644 packages/core/src/common/message-rpc/connection-handler.ts delete mode 100644 packages/core/src/common/message-rpc/experiments.ts create mode 100644 packages/core/src/common/message-rpc/index.ts rename packages/core/src/common/message-rpc/{message-encoder.spec.ts => rpc-message-encoder.spec.ts} (89%) rename packages/core/src/common/message-rpc/{message-encoder.ts => rpc-message-encoder.ts} (52%) delete mode 100644 packages/core/src/common/message-rpc/rpc-proxy.ts diff --git a/packages/core/src/browser/messaging/ws-connection-provider.ts b/packages/core/src/browser/messaging/ws-connection-provider.ts index 6b5136af01a52..b325ab6dac295 100644 --- a/packages/core/src/browser/messaging/ws-connection-provider.ts +++ b/packages/core/src/browser/messaging/ws-connection-provider.ts @@ -17,11 +17,9 @@ import { decorate, injectable, interfaces, unmanaged } from 'inversify'; import { io, Socket } from 'socket.io-client'; import { Emitter, Event, JsonRpcProxy, JsonRpcProxyFactory } from '../../common'; -import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from '../../common/message-rpc/array-buffer-message-buffer'; -import { Channel, ReadBufferConstructor } from '../../common/message-rpc/channel'; -import { WriteBuffer } from '../../common/message-rpc/message-buffer'; +import { Channel } from '../../common/message-rpc/channel'; import { AbstractConnectionProvider } from '../../common/messaging/abstract-connection-provider'; -import { WebSocketChannel } from '../../common/messaging/web-socket-channel'; +import { IWebSocket, WebSocketChannel, WebSocketMainChannel } from '../../common/messaging/web-socket-channel'; import { Endpoint } from '../endpoint'; decorate(injectable(), JsonRpcProxyFactory); @@ -56,7 +54,7 @@ export class WebSocketConnectionProvider extends AbstractConnectionProvider { this.fireSocketDidOpen(); }); @@ -133,43 +131,14 @@ export class WebSocketConnectionProvider extends AbstractConnectionProvider = new Emitter(); - get onClose(): Event { - return this.onCloseEmitter.event; - } - - protected readonly onMessageEmitter: Emitter = new Emitter(); - get onMessage(): Event { - return this.onMessageEmitter.event; - } - - protected readonly onErrorEmitter: Emitter = new Emitter(); - get onError(): Event { - return this.onErrorEmitter.event; - } - - readonly id: string; - - constructor(protected readonly socket: Socket) { - socket.on('error', error => this.onErrorEmitter.fire(error)); - socket.on('disconnect', reason => this.onCloseEmitter.fire()); - socket.on('message', buffer => this.onMessageEmitter.fire(() => new ArrayBufferReadBuffer(buffer))); - this.id = socket.id; - } - - getWriteBuffer(): WriteBuffer { - const result = new ArrayBufferWriteBuffer(); - if (this.socket.connected) { - result.onCommit(buffer => { - this.socket.emit('message', buffer); - }); - } - return result; - } - - close(): void { - this.socket.close(); - } - +function toIWebSocket(socket: Socket): IWebSocket { + return { + close: () => socket.close(), + isConnected: () => socket.connected, + onClose: cb => socket.on('disconnect', () => cb()), + onError: cb => socket.on('error', cb), + onMessage: cb => socket.on('message', data => cb(data)), + send: message => socket.emit('message', message) + }; } + diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index 5c944c157087a..e82ecddfa1268 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -29,6 +29,7 @@ export * from './contribution-provider'; export * from './path'; export * from './logger'; export * from './messaging'; +export * from './message-rpc'; export * from './message-service'; export * from './message-service-protocol'; export * from './progress-service'; diff --git a/packages/core/src/common/message-rpc/README.md b/packages/core/src/common/message-rpc/README.md deleted file mode 100644 index d94e3170c0906..0000000000000 --- a/packages/core/src/common/message-rpc/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# message-rpc - -An attempt to rewrite the theia RPC infrastructure with a couple of changes: - -1. "Zero-copy" message writing and reading -2. Support for binary buffers without ever encoding them -3. Separate RPC server from RPC client -4. Use a unified "Channel" interface - -A lot of this code is more or less copied from the current Theia code. diff --git a/packages/core/src/common/message-rpc/array-buffer-message-buffer.spec.ts b/packages/core/src/common/message-rpc/array-buffer-message-buffer.spec.ts index 8d72251856176..8b2d43a50755c 100644 --- a/packages/core/src/common/message-rpc/array-buffer-message-buffer.spec.ts +++ b/packages/core/src/common/message-rpc/array-buffer-message-buffer.spec.ts @@ -21,8 +21,8 @@ describe('array message buffer tests', () => { const buffer = new ArrayBuffer(1024); const writer = new ArrayBufferWriteBuffer(buffer); - writer.writeByte(8); - writer.writeInt(10000); + writer.writeUint8(8); + writer.writeUint32(10000); writer.writeBytes(new Uint8Array([1, 2, 3, 4])); writer.writeString('this is a string'); writer.writeString('another string'); @@ -32,8 +32,8 @@ describe('array message buffer tests', () => { const reader = new ArrayBufferReadBuffer(written); - expect(reader.readByte()).equal(8); - expect(reader.readInt()).equal(10000); + expect(reader.readUint8()).equal(8); + expect(reader.readUint32()).equal(10000); expect(reader.readBytes()).deep.equal(new Uint8Array([1, 2, 3, 4]).buffer); expect(reader.readString()).equal('this is a string'); expect(reader.readString()).equal('another string'); diff --git a/packages/core/src/common/message-rpc/array-buffer-message-buffer.ts b/packages/core/src/common/message-rpc/array-buffer-message-buffer.ts index 2b2c1a9e0eeae..06f390de95852 100644 --- a/packages/core/src/common/message-rpc/array-buffer-message-buffer.ts +++ b/packages/core/src/common/message-rpc/array-buffer-message-buffer.ts @@ -14,24 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** import { Emitter, Event } from '../event'; -import { ReadBuffer, WriteBuffer } from './message-buffer'; - -/** - * Converts the given node {@link Buffer} to an {@link ArrayBuffer}. The node buffer implementation is backed by an `Uint8Array` - * so the conversion can be efficiently achieved by slicing the section that is represented by the `Buffer` from the underlying - * array buffer. - * @param buffer The buffer that should be converted. - * @returns an `ArrayBuffer`representation of the given buffer. - */ -export function toArrayBuffer(buffer: Buffer | ArrayBuffer): ArrayBuffer { - if (buffer instanceof ArrayBuffer) { - return buffer; - } - if (buffer.byteOffset === 0 && buffer.byteLength === buffer.buffer.byteLength) { - return buffer.buffer; - } - return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); -} +import { getUintType, UintType, ReadBuffer, WriteBuffer } from './message-buffer'; export class ArrayBufferWriteBuffer implements WriteBuffer { constructor(private buffer: ArrayBuffer = new ArrayBuffer(1024), private offset: number = 0) { @@ -54,19 +37,42 @@ export class ArrayBufferWriteBuffer implements WriteBuffer { return this; } - writeByte(value: number): WriteBuffer { + writeUint8(value: number): WriteBuffer { this.ensureCapacity(1); this.msg.setUint8(this.offset++, value); return this; } - writeInt(value: number): WriteBuffer { + writeUint16(value: number): WriteBuffer { + this.ensureCapacity(2); + this.msg.setUint16(this.offset, value); + this.offset += 2; + return this; + } + + writeUint32(value: number): WriteBuffer { this.ensureCapacity(4); this.msg.setUint32(this.offset, value); this.offset += 4; return this; } + writeInteger(value: number): WriteBuffer { + const type = getUintType(value); + this.writeUint8(type); + switch (type) { + case UintType.Uint8: + this.writeUint8(value); + break; + case UintType.Uint16: + this.writeUint16(value); + break; + default: + this.writeUint32(value); + } + return this; + } + writeString(value: string): WriteBuffer { const encoded = this.encodeString(value); this.writeBytes(encoded); @@ -78,8 +84,8 @@ export class ArrayBufferWriteBuffer implements WriteBuffer { } writeBytes(value: ArrayBuffer): WriteBuffer { - this.ensureCapacity(value.byteLength + 4); - this.writeInt(value.byteLength); + this.writeInteger(value.byteLength); + this.ensureCapacity(value.byteLength); new Uint8Array(this.buffer).set(new Uint8Array(value), this.offset); this.offset += value.byteLength; return this; @@ -96,6 +102,7 @@ export class ArrayBufferWriteBuffer implements WriteBuffer { getCurrentContents(): ArrayBuffer { return this.buffer.slice(0, this.offset); + } } @@ -110,23 +117,36 @@ export class ArrayBufferReadBuffer implements ReadBuffer { return new DataView(this.buffer); } - readByte(): number { + readUint8(): number { return this.msg.getUint8(this.offset++); } - readInt(): number { - try { - const result = this.msg.getInt32(this.offset); - this.offset += 4; - return result; - } catch (err) { - throw err; + readUint16(): number { + const result = this.msg.getUint16(this.offset); + this.offset += 2; + return result; + } + + readUint32(): number { + const result = this.msg.getInt32(this.offset); + this.offset += 4; + return result; + } + + readInteger(): number { + const type = this.readUint8(); + switch (type) { + case UintType.Uint8: + return this.readUint8(); + case UintType.Uint16: + return this.readUint16(); + default: + return this.readUint32(); } } readString(): string { - const len = this.msg.getUint32(this.offset); - this.offset += 4; + const len = this.readInteger(); const result = this.decodeString(this.buffer.slice(this.offset, this.offset + len)); this.offset += len; return result; @@ -137,14 +157,31 @@ export class ArrayBufferReadBuffer implements ReadBuffer { } readBytes(): ArrayBuffer { - const length = this.msg.getUint32(this.offset); - this.offset += 4; + const length = this.readInteger(); const result = this.buffer.slice(this.offset, this.offset + length); this.offset += length; return result; } - copy(): ReadBuffer { + sliceAtCurrentPosition(): ReadBuffer { return new ArrayBufferReadBuffer(this.buffer, this.offset); } } + +/** + * Retrieve an {@link ArrayBuffer} view for the given buffer. Some {@link Uint8Array} buffer implementations e.g node's {@link Buffer} + * are using shared memory array buffers under the hood. Therefore we need to check the buffers `byteOffset` and `length` and slice + * the underlying array buffer if needed. + * @param buffer The Uint8Array or ArrayBuffer that should be converted. + * @returns a trimmed `ArrayBuffer` representation for the given buffer. + */ +export function toArrayBuffer(buffer: Uint8Array | ArrayBuffer): ArrayBuffer { + if (buffer instanceof ArrayBuffer) { + return buffer; + } + if (buffer.byteOffset === 0 && buffer.byteLength === buffer.buffer.byteLength) { + return buffer.buffer; + } + + return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); +} diff --git a/packages/core/src/common/message-rpc/channel.spec.ts b/packages/core/src/common/message-rpc/channel.spec.ts index 1ed41c76e7fa2..7c99fbb22f0a0 100644 --- a/packages/core/src/common/message-rpc/channel.spec.ts +++ b/packages/core/src/common/message-rpc/channel.spec.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { assert, expect, spy, use } from 'chai'; import * as spies from 'chai-spies'; -import { ChannelMultiplexer, ChannelPipe, ReadBufferConstructor } from './channel'; +import { ChannelMultiplexer, ChannelPipe, ReadBufferFactory } from './channel'; use(spies); @@ -40,14 +40,14 @@ describe('multiplexer test', () => { assert.isNotNull(rightFirst); assert.isNotNull(rightSecond); - const leftSecondSpy = spy((buf: ReadBufferConstructor) => { + const leftSecondSpy = spy((buf: ReadBufferFactory) => { const message = buf().readString(); expect(message).equal('message for second'); }); leftSecond.onMessage(leftSecondSpy); - const rightFirstSpy = spy((buf: ReadBufferConstructor) => { + const rightFirstSpy = spy((buf: ReadBufferFactory) => { const message = buf().readString(); expect(message).equal('message for first'); }); diff --git a/packages/core/src/common/message-rpc/channel.ts b/packages/core/src/common/message-rpc/channel.ts index abf835d0c52ee..b2423d3651a0b 100644 --- a/packages/core/src/common/message-rpc/channel.ts +++ b/packages/core/src/common/message-rpc/channel.ts @@ -20,7 +20,7 @@ import { ReadBuffer, WriteBuffer } from './message-buffer'; export type ReadBufferConstructor = () => ReadBuffer; /** - * A channel is a bidirectinal communications channel with lifecycle and + * A channel is a bidirectional communications channel with lifecycle and * error signalling. Note that creation of channels is specific to particular * implementations and thus not part of the protocol. */ @@ -34,11 +34,9 @@ export interface Channel { */ onError: Event; /** - * A message has arrived and can be read using a {@link ReadBuffer}. Since one `ReadBuffer` cannot be reused - * by multiple listener to read the same message again, each lister has to construct its - * own buffer using the given {@link ReadBufferConstructor} + * A message has arrived and can be read by listeners using a {@link ReadBufferFactory}. */ - onMessage: Event; + onMessage: Event; /** * Obtain a {@link WriteBuffer} to write a message to the channel. */ @@ -51,6 +49,8 @@ export interface Channel { readonly id: string; } +export type ReadBufferFactory = () => ReadBuffer; + export enum MessageTypes { Open = 1, Close = 2, @@ -59,10 +59,11 @@ export enum MessageTypes { } /** - * Helper class to implement the single channels on a {@link ChannelMultiplexer} + * Helper class to implement the single channels on a {@link ChannelMultiplexer}. */ export class ForwardingChannel implements Channel { - constructor(readonly id: string, private readonly closeHander: () => void, private readonly writeBufferSource: () => WriteBuffer) { + + constructor(readonly id: string, protected readonly closeHandler: () => void, protected readonly writeBufferSource: () => WriteBuffer) { } onCloseEmitter: Emitter = new Emitter(); @@ -73,8 +74,8 @@ export class ForwardingChannel implements Channel { get onError(): Event { return this.onErrorEmitter.event; }; - onMessageEmitter: Emitter = new Emitter(); - get onMessage(): Event { + onMessageEmitter: Emitter = new Emitter(); + get onMessage(): Event { return this.onMessageEmitter.event; }; @@ -89,12 +90,11 @@ export class ForwardingChannel implements Channel { } close(): void { - this.closeHander(); + this.closeHandler(); } } /** - * A class to encode/decode multiple channels over a single underlying {@link Channel} * The write buffers in this implementation immediately write to the underlying * channel, so we rely on writers to the multiplexed channels to always commit their * messages and always in one go. @@ -122,14 +122,14 @@ export class ChannelMultiplexer { protected handleClose(): void { this.pendingOpen.clear(); - this.openChannels.forEach(channel => { - channel.close(); + this.openChannels.forEach((channel, id) => { + this.closeChannel(id, true); }); this.openChannels.clear(); } protected handleMessage(buffer: ReadBuffer): void { - const type = buffer.readByte(); + const type = buffer.readUint8(); const id = buffer.readString(); switch (type) { case MessageTypes.AckOpen: { @@ -153,7 +153,7 @@ export class ChannelMultiplexer { // edge case: both side try to open a channel at the same time. resolve(channel); } - this.underlyingChannel.getWriteBuffer().writeByte(MessageTypes.AckOpen).writeString(id).commit(); + this.underlyingChannel.getWriteBuffer().writeUint8(MessageTypes.AckOpen).writeString(id).commit(); this.onOpenChannelEmitter.fire({ id, channel }); } @@ -170,7 +170,7 @@ export class ChannelMultiplexer { case MessageTypes.Data: { const channel = this.openChannels.get(id); if (channel) { - channel.onMessageEmitter.fire(() => buffer.copy()); + channel.onMessageEmitter.fire(() => buffer.sliceAtCurrentPosition()); } break; } @@ -181,26 +181,26 @@ export class ChannelMultiplexer { protected createChannel(id: string): ForwardingChannel { return new ForwardingChannel(id, () => this.closeChannel(id), () => { const underlying = this.underlyingChannel.getWriteBuffer(); - underlying.writeByte(MessageTypes.Data); + underlying.writeUint8(MessageTypes.Data); underlying.writeString(id); return underlying; }); } - protected closeChannel(id: string): void { - this.underlyingChannel.getWriteBuffer().writeByte(MessageTypes.Close).writeString(id).commit(); - if (this.openChannels.delete(id)) { - this.openChannels.get(id)!.onCloseEmitter.fire(); - } else { - console.error('The channel does not exist: ', id); + protected closeChannel(id: string, remoteClose = false): void { + this.underlyingChannel.getWriteBuffer().writeUint8(MessageTypes.Close).writeString(id).commit(); + if (remoteClose) { + // The main channel was closed from the remote site => also trigger `onClose` event of the forwarding channel + this.openChannels.get(id)?.onCloseEmitter.fire(); } + this.openChannels.delete(id); } open(id: string): Promise { const result = new Promise((resolve, reject) => { this.pendingOpen.set(id, resolve); }); - this.underlyingChannel.getWriteBuffer().writeByte(MessageTypes.Open).writeString(id).commit(); + this.underlyingChannel.getWriteBuffer().writeUint8(MessageTypes.Open).writeString(id).commit(); return result; } diff --git a/packages/core/src/common/message-rpc/connection-handler.ts b/packages/core/src/common/message-rpc/connection-handler.ts deleted file mode 100644 index 794bc480aaccf..0000000000000 --- a/packages/core/src/common/message-rpc/connection-handler.ts +++ /dev/null @@ -1,38 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2021 Red Hat, Inc. 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 { Channel } from './channel'; -import { RpcHandler, RpcProxyHandler } from './rpc-proxy'; - -interface ConnectionHandler { - onConnection(connection: Channel): void; -} - -export class JsonRpcConnectionHandler implements ConnectionHandler { - constructor( - readonly path: string, - readonly targetFactory: (proxy: T) => unknown, - ) { } - - onConnection(connection: Channel): void { - const proxyHandler = new RpcProxyHandler(); - // eslint-disable-next-line no-null/no-null - const proxy = new Proxy(Object.create(null), proxyHandler); - const target = this.targetFactory(proxy); - - new RpcHandler(target).onChannelOpen(connection); - proxyHandler.onChannelOpen(connection); - } -} diff --git a/packages/core/src/common/message-rpc/experiments.ts b/packages/core/src/common/message-rpc/experiments.ts deleted file mode 100644 index 9b8164451cd0a..0000000000000 --- a/packages/core/src/common/message-rpc/experiments.ts +++ /dev/null @@ -1,97 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2021 Red Hat, Inc. 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 { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from './array-buffer-message-buffer'; -import { MessageDecoder, MessageEncoder } from './message-encoder'; - -const test1 = { - 'curve': 'yes', - 'successful': false, - 'does': [ - [ - 'tool', - 'strange', - 'declared', - false, - 'if', - false, - false, - true, - true, - 196639994 - ], - -1697924638.043861, - 1921422646, - 'hide', - false, - true, - true, - -400170969, - 550424783, - -2118374202.4598904 - ], - 'fish': 664495385.6069336, - 'eat': -1205575089, - 'boat': 1495629676, - 'arm': 'nation', - 'height': false, - 'underline': 'have', - 'satellites': -20686813.87966633 -}; - -const test2: unknown[] = []; -for (let index = 0; index < 100; index++) { - test2.push(test1); -} - -const test3: string[] = []; -for (let index = 0; index < 1000; index++) { - test3.push(`${index}`); -} - -test(test1); -test(test2); -test(test3); - -function test(object: unknown): void { - console.log('Start test'); - const encoder = new MessageEncoder(); - const decoder = new MessageDecoder(); - // const string = fs.readFileSync(process.argv[2], 'utf8'); - // const object = JSON.parse(string); - - const start1 = Date.now(); - const result = Buffer.from(JSON.stringify(object)); - const end1 = Date.now(); - console.log(`Stringify encoding of file ${process.argv[2]} took ${end1 - start1} ms. Final byte length: ${result.byteLength}`); - - const writer = new ArrayBufferWriteBuffer(); - const start2 = Date.now(); - encoder.writeTypedValue(writer, object); - const result2 = writer.getCurrentContents(); - const end2 = Date.now(); - console.log(`New encoding of file ${process.argv[2]} took ${end2 - start2} ms. Final byte length: ${result2.byteLength}`); - - const start3 = Date.now(); - const end3 = Date.now(); - console.log(`Stringify Reading took ${end3 - start3} ms for`); - - const reader = new ArrayBufferReadBuffer(result2); - const start4 = Date.now(); - decoder.readTypedValue(reader); - const end4 = Date.now(); - console.log(`New Reading took ${end4 - start4} ms for`); - console.log(); -} diff --git a/packages/core/src/common/message-rpc/index.ts b/packages/core/src/common/message-rpc/index.ts new file mode 100644 index 0000000000000..8cada9981de3e --- /dev/null +++ b/packages/core/src/common/message-rpc/index.ts @@ -0,0 +1,18 @@ +// ***************************************************************************** +// Copyright (C) 2022 STMicroelectronics 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 './rpc-protocol'; +export * from './channel'; +export * from './message-buffer'; diff --git a/packages/core/src/common/message-rpc/message-buffer.ts b/packages/core/src/common/message-rpc/message-buffer.ts index 82ea175b40fea..56276e94dfdf0 100644 --- a/packages/core/src/common/message-rpc/message-buffer.ts +++ b/packages/core/src/common/message-rpc/message-buffer.ts @@ -18,11 +18,17 @@ * A buffer maintaining a write position capable of writing primitive values */ export interface WriteBuffer { - writeByte(byte: number): WriteBuffer - writeInt(value: number): WriteBuffer; + writeUint8(byte: number): WriteBuffer + writeUint16(value: number): WriteBuffer + writeUint32(value: number): WriteBuffer; writeString(value: string): WriteBuffer; writeBytes(value: ArrayBuffer): WriteBuffer; - + /** + * Writes a number as integer value.The best suited encoding format(Uint8 Uint16 or Uint32) is + * computed automatically and encoded as the first byte. Mainly used to persist length values of + * strings and arrays. + */ + writeInteger(value: number): WriteBuffer /** * Makes any writes to the buffer permanent, for example by sending the writes over a channel. * You must obtain a new write buffer after committing @@ -33,13 +39,24 @@ export interface WriteBuffer { export class ForwardingWriteBuffer implements WriteBuffer { constructor(protected readonly underlying: WriteBuffer) { } - writeByte(byte: number): WriteBuffer { - this.underlying.writeByte(byte); + + writeUint8(byte: number): WriteBuffer { + this.underlying.writeUint8(byte); + return this; + } + + writeUint16(value: number): WriteBuffer { + this.underlying.writeUint16(value); + return this; + } + + writeUint32(value: number): WriteBuffer { + this.underlying.writeUint32(value); return this; } - writeInt(value: number): WriteBuffer { - this.underlying.writeInt(value); + writeInteger(value: number): WriteBuffer { + this.underlying.writeInteger(value); return this; } @@ -58,16 +75,50 @@ export class ForwardingWriteBuffer implements WriteBuffer { } } +export enum UintType { + Uint8 = 1, + Uint16 = 2, + Uint32 = 3 +} + +/** + * Checks wether the given number is an unsigned integer and returns the {@link UintType} + * that is needed to store it in binary format. + * @param value The number for which the UintType should be retrieved. + * @returns the corresponding UInt type. + * @throws An error if the given number is not an unsigned integer. + */ +export function getUintType(value: number): UintType { + if (value < 0 || (value % 1) !== 0) { + throw new Error(`Could not determine IntType. ${value} is not an unsigned integer`); + } + if (value <= 255) { + return UintType.Uint8; + } else if (value <= 65535) { + return UintType.Uint16; + } + return UintType.Uint32; +} + /** * A buffer maintaining a read position in a buffer containing a received message capable of * reading primitive values. */ export interface ReadBuffer { - readByte(): number; - readInt(): number; + readUint8(): number; + readUint16(): number; + readUint32(): number; readString(): string; readBytes(): ArrayBuffer; - /* Create a new copy of this read buffer and its current read position. Can be used to read (sub) messages - multiple times.*/ - copy(): ReadBuffer + + /** + * Reads a number as int value. The encoding format(Uint8, Uint16, or Uint32) is expected to be + * encoded in the first byte. + */ + readInteger(): number + /** + * Returns a new read buffer whose starting read position is the current read position of this buffer. + * Can be used to read (sub) messages multiple times. + */ + sliceAtCurrentPosition(): ReadBuffer } diff --git a/packages/core/src/common/message-rpc/message-encoder.spec.ts b/packages/core/src/common/message-rpc/rpc-message-encoder.spec.ts similarity index 89% rename from packages/core/src/common/message-rpc/message-encoder.spec.ts rename to packages/core/src/common/message-rpc/rpc-message-encoder.spec.ts index 99761534064a7..32060af0c487e 100644 --- a/packages/core/src/common/message-rpc/message-encoder.spec.ts +++ b/packages/core/src/common/message-rpc/rpc-message-encoder.spec.ts @@ -15,14 +15,14 @@ // ***************************************************************************** import { expect } from 'chai'; import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from './array-buffer-message-buffer'; -import { MessageDecoder, MessageEncoder } from './message-encoder'; +import { RpcMessageDecoder, RpcMessageEncoder } from './rpc-message-encoder'; describe('message buffer test', () => { it('encode object', () => { const buffer = new ArrayBuffer(1024); const writer = new ArrayBufferWriteBuffer(buffer); - const encoder = new MessageEncoder(); + const encoder = new RpcMessageEncoder(); const jsonMangled = JSON.parse(JSON.stringify(encoder)); encoder.writeTypedValue(writer, encoder); @@ -31,7 +31,7 @@ describe('message buffer test', () => { const reader = new ArrayBufferReadBuffer(written); - const decoder = new MessageDecoder(); + const decoder = new RpcMessageDecoder(); const decoded = decoder.readTypedValue(reader); expect(decoded).deep.equal(jsonMangled); diff --git a/packages/core/src/common/message-rpc/message-encoder.ts b/packages/core/src/common/message-rpc/rpc-message-encoder.ts similarity index 52% rename from packages/core/src/common/message-rpc/message-encoder.ts rename to packages/core/src/common/message-rpc/rpc-message-encoder.ts index a9bc2a1810e4e..aaedcb4c493a6 100644 --- a/packages/core/src/common/message-rpc/message-encoder.ts +++ b/packages/core/src/common/message-rpc/rpc-message-encoder.ts @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright (C) 2021 Red Hat, Inc. and others. +// Copyright (C) 2022 Red Hat, Inc. 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 @@ -13,8 +13,12 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** +// partly based on https://github.com/microsoft/vscode/blob/435f8a4cae52fc9850766af92d5df3c492f59341/src/vs/workbench/services/extensions/common/rpcProtocol. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { ResponseError } from 'vscode-languageserver-protocol'; import { toArrayBuffer } from './array-buffer-message-buffer'; -import { ReadBuffer, WriteBuffer } from './message-buffer'; +import { getUintType, UintType, ReadBuffer, WriteBuffer } from './message-buffer'; /** * This code lets you encode rpc protocol messages (request/reply/notification/error/cancel) @@ -24,14 +28,9 @@ import { ReadBuffer, WriteBuffer } from './message-buffer'; * is distinct and the same at both ends of a channel. */ -export interface SerializedError { - readonly $isError: true; - readonly name: string; - readonly message: string; - readonly stack: string; -} +export type RpcMessage = RequestMessage | ReplyMessage | ReplyErrMessage | CancelMessage | NotificationMessage; -export const enum RPCMessageType { +export const enum RpcMessageType { Request = 1, Notification = 2, Reply = 3, @@ -40,60 +39,59 @@ export const enum RPCMessageType { } export interface CancelMessage { - type: RPCMessageType.Cancel; + type: RpcMessageType.Cancel; id: number; } export interface RequestMessage { - type: RPCMessageType.Request; + type: RpcMessageType.Request; id: number; method: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any args: any[]; } export interface NotificationMessage { - type: RPCMessageType.Notification; + type: RpcMessageType.Notification; id: number; method: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any args: any[]; } export interface ReplyMessage { - type: RPCMessageType.Reply; + type: RpcMessageType.Reply; id: number; - // eslint-disable-next-line @typescript-eslint/no-explicit-any res: any; } export interface ReplyErrMessage { - type: RPCMessageType.ReplyErr; + type: RpcMessageType.ReplyErr; id: number; - err: SerializedError; + err: any; } -export type RPCMessage = RequestMessage | ReplyMessage | ReplyErrMessage | CancelMessage | NotificationMessage; +/** + * The tag values for the default {@link ValueEncoder}s & {@link ValueDecoder}s + */ -enum ObjectType { - JSON = 0, - ByteArray = 1, - ObjectArray = 2, - Undefined = 3, - Object = 4 +export enum ObjectType { + JSON = 1, + ArrayBuffer = 2, + ByteArray = 3, + UNDEFINED = 4, + ObjectArray = 5 } + /** * A value encoder writes javascript values to a write buffer. Encoders will be asked * in turn (ordered by their tag value, descending) whether they can encode a given value * This means encoders with higher tag values have priority. Since the default encoders - * have tag values from 0-4, they can be easily overridden. + * have tag values from 1-3, they can be easily overridden. */ export interface ValueEncoder { /** * Returns true if this encoder wants to encode this value. * @param value the value to be encoded */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any is(value: any): boolean; /** * Write the given value to the buffer. Will only be called if {@link is(value)} returns true. @@ -103,8 +101,7 @@ export interface ValueEncoder { * to write a value to the underlying buffer. This is used mostly to write structures like an array * without having to know how to encode the values in the array */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - write(buf: WriteBuffer, value: any, recursiveEncode: (buf: WriteBuffer, value: any) => void): void; + write(buf: WriteBuffer, value: any, recursiveEncode?: (buf: WriteBuffer, value: any) => void): void; } /** @@ -115,7 +112,7 @@ export interface ValueDecoder { * Reads a value from a read buffer. This method will be called for the decoder that is * registered for the tag associated with the value encoder that encoded this value. * @param buf The read buffer to read from - * @param recursiveDecode A function that will use the decoders registered on the {@link MessageEncoder} + * @param recursiveDecode A function that will use the decoders registered on the {@link RpcMessageDecoder} * to read values from the underlying read buffer. This is used mostly to decode structures like an array * without having to know how to decode the values in the array. */ @@ -123,11 +120,16 @@ export interface ValueDecoder { } /** - * A MessageDecoder parses a ReadBuffer into a RCPMessage + * A `RpcMessageDecoder` parses a a binary message received via {@link ReadBuffer} into a {@link RpcMessage} */ +export class RpcMessageDecoder { -export class MessageDecoder { protected decoders: Map = new Map(); + /** + * Declares the Uint8 type (i.e. the amount of bytes) necessary to store a decoder tag + * value in the buffer. + */ + protected tagIntType: UintType; constructor() { this.registerDecoder(ObjectType.JSON, { @@ -136,52 +138,75 @@ export class MessageDecoder { return JSON.parse(json); } }); - this.registerDecoder(ObjectType.ByteArray, { - read: buf => buf.readBytes() + + this.registerDecoder(ObjectType.UNDEFINED, { + read: () => undefined }); - this.registerDecoder(ObjectType.ObjectArray, { - read: buf => this.readArray(buf) + + this.registerDecoder(ObjectType.ByteArray, { + read: buf => new Uint8Array(buf.readBytes()) }); - this.registerDecoder(ObjectType.Undefined, { - read: () => undefined + this.registerDecoder(ObjectType.ArrayBuffer, { + read: buf => buf.readBytes() }); - this.registerDecoder(ObjectType.Object, { - read: (buf, recursiveRead) => { - const propertyCount = buf.readInt(); - const result = Object.create({}); - for (let i = 0; i < propertyCount; i++) { - const key = buf.readString(); - const value = recursiveRead(buf); - result[key] = value; + this.registerDecoder(ObjectType.ObjectArray, { + read: buf => { + const encodedSeparately = buf.readUint8() === 1; + + if (!encodedSeparately) { + return this.readTypedValue(buf); + } + const length = buf.readInteger(); + const result = new Array(length); + for (let i = 0; i < length; i++) { + result[i] = this.readTypedValue(buf); } return result; } }); } + /** + * Registers a new {@link ValueDecoder} for the given tag. + * After the successful registration the {@link tagIntType} is recomputed + * by retrieving the highest tag value and calculating the required Uint size to store it. + * @param tag the tag for which the decoder should be registered. + * @param decoder the decoder that should be registered. + */ registerDecoder(tag: number, decoder: ValueDecoder): void { if (this.decoders.has(tag)) { throw new Error(`Decoder already registered: ${tag}`); } this.decoders.set(tag, decoder); + const maxTagId = Array.from(this.decoders.keys()).sort().reverse()[0]; + this.tagIntType = getUintType(maxTagId); + } + + readTypedValue(buf: ReadBuffer): any { + const type = buf.readUint8(); + const decoder = this.decoders.get(type); + if (!decoder) { + throw new Error(`No decoder registered for tag ${type}`); + } + return decoder.read(buf, innerBuffer => this.readTypedValue(innerBuffer)); } - parse(buf: ReadBuffer): RPCMessage { + parse(buf: ReadBuffer): RpcMessage { try { - const msgType = buf.readByte(); + const msgType = buf.readUint8(); switch (msgType) { - case RPCMessageType.Request: + case RpcMessageType.Request: return this.parseRequest(buf); - case RPCMessageType.Notification: + case RpcMessageType.Notification: return this.parseNotification(buf); - case RPCMessageType.Reply: + case RpcMessageType.Reply: return this.parseReply(buf); - case RPCMessageType.ReplyErr: + case RpcMessageType.ReplyErr: return this.parseReplyErr(buf); - case RPCMessageType.Cancel: + case RpcMessageType.Cancel: return this.parseCancel(buf); } throw new Error(`Unknown message type: ${msgType}`); @@ -193,22 +218,22 @@ export class MessageDecoder { } protected parseCancel(msg: ReadBuffer): CancelMessage { - const callId = msg.readInt(); + const callId = msg.readUint32(); return { - type: RPCMessageType.Cancel, + type: RpcMessageType.Cancel, id: callId }; } protected parseRequest(msg: ReadBuffer): RequestMessage { - const callId = msg.readInt(); + const callId = msg.readUint32(); const method = msg.readString(); - let args = this.readArray(msg); + let args = this.readTypedValue(msg) as any[]; // convert `null` to `undefined`, since we don't use `null` in internal plugin APIs args = args.map(arg => arg === null ? undefined : arg); // eslint-disable-line no-null/no-null return { - type: RPCMessageType.Request, + type: RpcMessageType.Request, id: callId, method: method, args: args @@ -216,14 +241,14 @@ export class MessageDecoder { } protected parseNotification(msg: ReadBuffer): NotificationMessage { - const callId = msg.readInt(); + const callId = msg.readUint32(); const method = msg.readString(); - let args = this.readArray(msg); + let args = this.readTypedValue(msg) as any[]; // convert `null` to `undefined`, since we don't use `null` in internal plugin APIs args = args.map(arg => arg === null ? undefined : arg); // eslint-disable-line no-null/no-null return { - type: RPCMessageType.Notification, + type: RpcMessageType.Notification, id: callId, method: method, args: args @@ -231,64 +256,45 @@ export class MessageDecoder { } parseReply(msg: ReadBuffer): ReplyMessage { - const callId = msg.readInt(); + const callId = msg.readUint32(); const value = this.readTypedValue(msg); return { - type: RPCMessageType.Reply, + type: RpcMessageType.Reply, id: callId, res: value }; } parseReplyErr(msg: ReadBuffer): ReplyErrMessage { - const callId = msg.readInt(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let err: any = this.readTypedValue(msg); - if (err && err.$isError) { - err = new Error(); - err.name = err.name; - err.message = err.message; - err.stack = err.stack; - } + const callId = msg.readUint32(); + + const err = this.readTypedValue(msg); + const responseError = new ResponseError(err.code, err.message, err.data); + return { - type: RPCMessageType.ReplyErr, + type: RpcMessageType.ReplyErr, id: callId, - err: err + err: responseError }; } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readArray(buf: ReadBuffer): any[] { - const length = buf.readInt(); - const result = new Array(length); - for (let i = 0; i < length; i++) { - result[i] = this.readTypedValue(buf); - } - return result; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readTypedValue(buf: ReadBuffer): any { - const type = buf.readInt(); - const decoder = this.decoders.get(type); - if (!decoder) { - throw new Error(`No decoder for tag ${type}`); - } - return decoder.read(buf, innerBuffer => this.readTypedValue(innerBuffer)); - } } /** - * A MessageEncoder writes RCPMessage objects to a WriteBuffer. Note that it is + * A `RpcMessageEncoder` writes {@link RpcMessage} objects to a {@link WriteBuffer}. Note that it is * up to clients to commit the message. This allows for multiple messages being * encoded before sending. */ -export class MessageEncoder { +export class RpcMessageEncoder { + protected readonly encoders: [number, ValueEncoder][] = []; protected readonly registeredTags: Set = new Set(); + protected tagIntType: UintType; constructor() { + this.registerEncoders(); + } + + protected registerEncoders(): void { // encoders will be consulted in reverse order of registration, so the JSON fallback needs to be last this.registerEncoder(ObjectType.JSON, { is: () => true, @@ -296,109 +302,129 @@ export class MessageEncoder { buf.writeString(JSON.stringify(value)); } }); - this.registerEncoder(ObjectType.Object, { - is: value => typeof value === 'object', - write: (buf, object, recursiveEncode) => { - const properties = Object.keys(object); - const relevant = []; - for (const property of properties) { - const value = object[property]; - if (typeof value !== 'function') { - relevant.push([property, value]); - } - } - buf.writeInt(relevant.length); - for (const [property, value] of relevant) { - buf.writeString(property); - recursiveEncode(buf, value); - } - } - }); - this.registerEncoder(ObjectType.Undefined, { + this.registerEncoder(ObjectType.UNDEFINED, { // eslint-disable-next-line no-null/no-null - is: value => (value === undefined || value === null), + is: value => value == null, write: () => { } }); - this.registerEncoder(ObjectType.ObjectArray, { - is: value => Array.isArray(value), - write: (buf, value) => { - this.writeArray(buf, value); + this.registerEncoder(ObjectType.ByteArray, { + is: value => value instanceof Uint8Array, + write: (buf, value: Uint8Array) => { + /* When running in a nodejs context the received Uint8Array might be + a nodejs Buffer allocated from node's Buffer pool, which is not transferrable. + Therefore we use the `toArrayBuffer` utility method to retrieve the correct ArrayBuffer */ + const arrayBuffer = toArrayBuffer(value); + buf.writeBytes(arrayBuffer); } }); - this.registerEncoder(ObjectType.ByteArray, { - is: value => value instanceof ArrayBuffer || Buffer.isBuffer(value), - write: (buf, value) => { - const arrayBuffer = value instanceof ArrayBuffer ? value : toArrayBuffer(value); - buf.writeBytes(arrayBuffer); + this.registerEncoder(ObjectType.ArrayBuffer, { + is: value => value instanceof ArrayBuffer, + write: (buf, value: ArrayBuffer) => buf.writeBytes(value) + }); + + this.registerEncoder(ObjectType.ObjectArray, { + is: value => Array.isArray(value), + write: (buf, args: any[]) => { + const encodeSeparately = this.requiresSeparateEncoding(args); + buf.writeUint8(encodeSeparately ? 1 : 0); + if (!encodeSeparately) { + this.writeTypedValue(buf, args, ObjectType.ObjectArray); + } else { + buf.writeInteger(args.length); + for (let i = 0; i < args.length; i++) { + this.writeTypedValue(buf, args[i], ObjectType.ObjectArray); + } + } } }); } + /** + * Registers a new {@link ValueEncoder} for the given tag. + * After the successful registration the {@link tagIntType} is recomputed + * by retrieving the highest tag value and calculating the required Uint size to store it. + * @param tag the tag for which the encoder should be registered. + * @param decoder the encoder that should be registered. + */ registerEncoder(tag: number, encoder: ValueEncoder): void { if (this.registeredTags.has(tag)) { throw new Error(`Tag already registered: ${tag}`); } this.registeredTags.add(tag); this.encoders.push([tag, encoder]); + const maxTagId = this.encoders.map(value => value[0]).sort().reverse()[0]; + this.tagIntType = getUintType(maxTagId); + } + + /** + * Processes the given array of request arguments to determine whether it contains + * arguments that require separate encoding (e.g. buffers) i.e. each argument needs to be encoded individually. + * If there are no arguments that require separate encoding the entire array can be encoded in one go with + * the fallback JSON encoder. + * @param args The request args. + * @returns `true` if the arguments require separate encoding, `false` otherwise. + */ + protected requiresSeparateEncoding(args: any[]): boolean { + return args.find(arg => arg instanceof Uint8Array || arg instanceof ArrayBuffer) !== undefined; + } + + writeString(buf: WriteBuffer, value: string): void { + buf.writeString(value); + } + + /** + * Writes the given value into the given {@link WriteBuffer}. Is potentially + * reused by some of the registered {@link ValueEncoder}s. Value encoders can pass + * their tag value as `excludeTag` to avoid encoding with the same parent encoder in case of + * recursive encoding. + * @param buf The buffer to write to. + * @param value The value that should be encoded. + * @param excludeTag Tag of an encode that should not be considered. + */ + writeTypedValue(buf: WriteBuffer, value: any, excludeTag: number = -1): void { + for (let i: number = this.encoders.length - 1; i >= 0; i--) { + const encoder = this.encoders[i]; + if (encoder[0] !== excludeTag && encoder[1].is(value)) { + buf.writeUint8(this.encoders[i][0]); + this.encoders[i][1].write(buf, value, (innerBuffer, innerValue) => { + this.writeTypedValue(innerBuffer, innerValue); + }); + return; + } + } } cancel(buf: WriteBuffer, requestId: number): void { - buf.writeByte(RPCMessageType.Cancel); - buf.writeInt(requestId); + buf.writeUint8(RpcMessageType.Cancel); + buf.writeUint32(requestId); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any notification(buf: WriteBuffer, requestId: number, method: string, args: any[]): void { - buf.writeByte(RPCMessageType.Notification); - buf.writeInt(requestId); + buf.writeUint8(RpcMessageType.Notification); + buf.writeUint32(requestId); buf.writeString(method); - this.writeArray(buf, args); + this.writeTypedValue(buf, args); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any request(buf: WriteBuffer, requestId: number, method: string, args: any[]): void { - buf.writeByte(RPCMessageType.Request); - buf.writeInt(requestId); + buf.writeUint8(RpcMessageType.Request); + buf.writeUint32(requestId); buf.writeString(method); - this.writeArray(buf, args); + this.writeTypedValue(buf, args); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any replyOK(buf: WriteBuffer, requestId: number, res: any): void { - buf.writeByte(RPCMessageType.Reply); - buf.writeInt(requestId); + buf.writeUint8(RpcMessageType.Reply); + buf.writeUint32(requestId); this.writeTypedValue(buf, res); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any replyErr(buf: WriteBuffer, requestId: number, err: any): void { - buf.writeByte(RPCMessageType.ReplyErr); - buf.writeInt(requestId); + buf.writeUint8(RpcMessageType.ReplyErr); + buf.writeUint32(requestId); this.writeTypedValue(buf, err); } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - writeTypedValue(buf: WriteBuffer, value: any): void { - for (let i: number = this.encoders.length - 1; i >= 0; i--) { - if (this.encoders[i][1].is(value)) { - buf.writeInt(this.encoders[i][0]); - this.encoders[i][1].write(buf, value, (innerBuffer, innerValue) => { - this.writeTypedValue(innerBuffer, innerValue); - }); - return; - } - } - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - writeArray(buf: WriteBuffer, value: any[]): void { - buf.writeInt(value.length); - for (let i = 0; i < value.length; i++) { - this.writeTypedValue(buf, value[i]); - } - } - } diff --git a/packages/core/src/common/message-rpc/rpc-protocol.ts b/packages/core/src/common/message-rpc/rpc-protocol.ts index 8d805d7bb60a8..f2c3046654bf8 100644 --- a/packages/core/src/common/message-rpc/rpc-protocol.ts +++ b/packages/core/src/common/message-rpc/rpc-protocol.ts @@ -17,153 +17,45 @@ import { Emitter, Event } from '../event'; import { Deferred } from '../promise-util'; -import { Channel, ReadBufferConstructor } from './channel'; +import { Channel, ReadBufferFactory } from './channel'; import { ReadBuffer } from './message-buffer'; -import { MessageDecoder, MessageEncoder, RPCMessageType } from './message-encoder'; +import { RpcMessageDecoder, RpcMessageEncoder, RpcMessageType } from './rpc-message-encoder'; -export class RCPConnection { - protected readonly pendingRequests: Map> = new Map(); - protected nextMessageId: number = 0; - - protected readonly encoder: MessageEncoder = new MessageEncoder(); - protected readonly decoder: MessageDecoder = new MessageDecoder(); - protected onNotificationEmitter: Emitter<{ method: string; args: any[]; }> = new Emitter(); - readFileRequestId: number = -1; - - get onNotification(): Event<{ method: string; args: any[]; }> { - return this.onNotificationEmitter.event; - } - - constructor(readonly channel: Channel, public readonly requestHandler: (method: string, args: any[]) => Promise) { - const registration = channel.onMessage((msg: ReadBufferConstructor) => this.handleMessage(msg())); - channel.onClose(() => registration.dispose()); - } - - handleMessage(data: ReadBuffer): void { - const message = this.decoder.parse(data); - switch (message.type) { - case RPCMessageType.Cancel: { - this.handleCancel(message.id); - break; - } - case RPCMessageType.Request: { - this.handleRequest(message.id, message.method, message.args); - break; - } - case RPCMessageType.Notification: { - this.handleNotify(message.id, message.method, message.args); - break; - } - case RPCMessageType.Reply: { - this.handleReply(message.id, message.res); - break; - } - case RPCMessageType.ReplyErr: { - this.handleReplyErr(message.id, message.err); - break; - } - } - } - - protected handleCancel(id: number): void { - // implement cancellation - /* const token = this.cancellationTokens.get(id); - if (token) { - this.cancellationTokens.delete(id); - token.cancel(); - } else { - console.warn(`cancel: no token for message: ${id}`); - }*/ - } - - protected async handleRequest(id: number, method: string, args: any[]): Promise { - - const output = this.channel.getWriteBuffer(); - try { - - const result = await this.requestHandler(method, args); - this.encoder.replyOK(output, id, result); - } catch (err) { - this.encoder.replyErr(output, id, err); - console.log(`error on request ${method} with id ${id}`); - } - output.commit(); - } - - protected async handleNotify(id: number, method: string, args: any[]): Promise { - this.onNotificationEmitter.fire({ method, args }); - } - - protected handleReply(id: number, value: any): void { - const replyHandler = this.pendingRequests.get(id); - if (replyHandler) { - this.pendingRequests.delete(id); - replyHandler.resolve(value); - } else { - console.warn(`reply: no handler for message: ${id}`); - } - } - - protected handleReplyErr(id: number, error: any): void { - const replyHandler = this.pendingRequests.get(id); - if (replyHandler) { - this.pendingRequests.delete(id); - // console.log(`received error id ${id}`); - replyHandler.reject(error); - } else { - console.warn(`error: no handler for message: ${id}`); - } - } - - sendRequest(method: string, args: any[]): Promise { - const id = this.nextMessageId++; - const reply = new Deferred(); - - this.pendingRequests.set(id, reply); - const output = this.channel.getWriteBuffer(); - this.encoder.request(output, id, method, args); - output.commit(); - return reply.promise; - } - - sendNotification(method: string, args: any[]): void { - const output = this.channel.getWriteBuffer(); - this.encoder.notification(output, this.nextMessageId++, method, args); - output.commit(); - } - -} +/** + * Handles request messages received by the {@link RpcServer}. + */ +export type RequestHandler = (method: string, args: any[]) => Promise; /** - * A RCPServer reads rcp request and notification messages and sends the reply values or + * A RpcServer reads rcp request and notification messages and sends the reply values or * errors from the request to the channel. */ -export class RPCServer { - protected readonly encoder: MessageEncoder = new MessageEncoder(); - protected readonly decoder: MessageDecoder = new MessageDecoder(); +export class RpcServer { + protected readonly encoder = new RpcMessageEncoder(); + protected readonly decoder = new RpcMessageDecoder(); protected onNotificationEmitter: Emitter<{ method: string; args: any[]; }> = new Emitter(); get onNotification(): Event<{ method: string; args: any[]; }> { return this.onNotificationEmitter.event; } - constructor(protected channel: Channel, public readonly requestHandler: (method: string, args: any[]) => Promise) { - const registration = channel.onMessage((msg: ReadBufferConstructor) => this.handleMessage(msg())); + constructor(protected channel: Channel, public readonly requestHandler: RequestHandler) { + const registration = channel.onMessage((msg: ReadBufferFactory) => this.handleMessage(msg())); channel.onClose(() => registration.dispose()); } handleMessage(data: ReadBuffer): void { const message = this.decoder.parse(data); switch (message.type) { - case RPCMessageType.Cancel: { + case RpcMessageType.Cancel: { this.handleCancel(message.id); break; } - case RPCMessageType.Request: { + case RpcMessageType.Request: { this.handleRequest(message.id, message.method, message.args); break; } - case RPCMessageType.Notification: { + case RpcMessageType.Notification: { this.handleNotify(message.id, message.method, message.args); break; } @@ -182,27 +74,25 @@ export class RPCServer { } protected async handleRequest(id: number, method: string, args: any[]): Promise { + const output = this.channel.getWriteBuffer(); try { - // console.log(`handling request ${method} with id ${id}`); + const result = await this.requestHandler(method, args); this.encoder.replyOK(output, id, result); - // console.log(`handled request ${method} with id ${id}`); } catch (err) { this.encoder.replyErr(output, id, err); - console.log(`error on request ${method} with id ${id}`); } output.commit(); } protected async handleNotify(id: number, method: string, args: any[]): Promise { - // console.log(`handling notification ${method} with id ${id}`); this.onNotificationEmitter.fire({ method, args }); } } /** - * An RpcClient sends requests and notifications to a remote server. + * An RpClient sends requests and notifications to a remote server. * Clients can get a promise for the request result that will be either resolved or * rejected depending on the success of the request. * The RpcClient keeps track of outstanding requests and matches replies to the appropriate request @@ -210,10 +100,11 @@ export class RPCServer { */ export class RpcClient { protected readonly pendingRequests: Map> = new Map(); + protected nextMessageId: number = 0; - protected readonly encoder: MessageEncoder = new MessageEncoder(); - protected readonly decoder: MessageDecoder = new MessageDecoder(); + protected readonly encoder = new RpcMessageEncoder(); + protected readonly decoder = new RpcMessageDecoder(); constructor(public readonly channel: Channel) { const registration = channel.onMessage(data => this.handleMessage(data())); @@ -223,11 +114,11 @@ export class RpcClient { handleMessage(data: ReadBuffer): void { const message = this.decoder.parse(data); switch (message.type) { - case RPCMessageType.Reply: { + case RpcMessageType.Reply: { this.handleReply(message.id, message.res); break; } - case RPCMessageType.ReplyErr: { + case RpcMessageType.ReplyErr: { this.handleReplyErr(message.id, message.err); break; } @@ -236,7 +127,6 @@ export class RpcClient { protected handleReply(id: number, value: any): void { const replyHandler = this.pendingRequests.get(id); - // console.log(`received reply with id ${id}`); if (replyHandler) { this.pendingRequests.delete(id); replyHandler.resolve(value); @@ -246,20 +136,22 @@ export class RpcClient { } protected handleReplyErr(id: number, error: any): void { - const replyHandler = this.pendingRequests.get(id); - if (replyHandler) { - this.pendingRequests.delete(id); - // console.log(`received error id ${id}`); - replyHandler.reject(error); - } else { - console.warn(`error: no handler for message: ${id}`); + try { + const replyHandler = this.pendingRequests.get(id); + if (replyHandler) { + this.pendingRequests.delete(id); + replyHandler.reject(error); + } else { + console.warn(`error: no handler for message: ${id}`); + } + } catch (err) { + throw err; } } sendRequest(method: string, args: any[]): Promise { const id = this.nextMessageId++; const reply = new Deferred(); - // console.log(`sending request ${method} with id ${id}`); this.pendingRequests.set(id, reply); const output = this.channel.getWriteBuffer(); @@ -269,9 +161,34 @@ export class RpcClient { } sendNotification(method: string, args: any[]): void { - // console.log(`sending notification ${method} with id ${this.nextMessageId + 1}`); const output = this.channel.getWriteBuffer(); this.encoder.notification(output, this.nextMessageId++, method, args); output.commit(); } } +/** + * A RpcConnection can be used to to establish a bi-directional RPC connection. It is capable of + * both sending & receiving requests and notifications to/from the channel. It acts a + * both a {@link RpcServer} and a {@link RpcClient} + */ +export class RpcConnection { + protected rpcClient: RpcClient; + protected rpcServer: RpcServer; + + get onNotification(): Event<{ method: string; args: any[]; }> { + return this.rpcServer.onNotification; + } + + constructor(readonly channel: Channel, public readonly requestHandler: (method: string, args: any[]) => Promise) { + this.rpcClient = new RpcClient(channel); + this.rpcServer = new RpcServer(channel, requestHandler); + } + sendRequest(method: string, args: any[]): Promise { + return this.rpcClient.sendRequest(method, args); + } + + sendNotification(method: string, args: any[]): void { + this.rpcClient.sendNotification(method, args); + } +} + diff --git a/packages/core/src/common/message-rpc/rpc-proxy.ts b/packages/core/src/common/message-rpc/rpc-proxy.ts deleted file mode 100644 index de47cd3d22651..0000000000000 --- a/packages/core/src/common/message-rpc/rpc-proxy.ts +++ /dev/null @@ -1,93 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2021 Red Hat, Inc. 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 -// ***************************************************************************** -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Deferred } from '../promise-util'; -import { Channel } from './channel'; -import { RpcClient, RPCServer } from './rpc-protocol'; - -/** - * A proxy handler that will send any method invocation on the proxied object - * as a rcp protocol message over a channel. - */ -export class RpcProxyHandler implements ProxyHandler { - private channelDeferred: Deferred = new Deferred(); - - onChannelOpen(channel: Channel): void { - const client = new RpcClient(channel); - this.channelDeferred.resolve(client); - } - - get?(target: T, p: string | symbol, receiver: any): any { - const isNotify = this.isNotification(p); - return (...args: any[]) => { - const method = p.toString(); - return this.channelDeferred.promise.then((connection: RpcClient) => - new Promise((resolve, reject) => { - try { - if (isNotify) { - // console.info(`Send notification ${method}`); - connection.sendNotification(method, args); - resolve(undefined); - } else { - // console.info(`Send request ${method}`); - const resultPromise = connection.sendRequest(method, args) as Promise; - resultPromise.then((result: any) => { - // console.info(`request succeeded: ${method}`); - resolve(result); - }).catch(e => { - reject(e); - }); - } - } catch (err) { - reject(err); - } - }) - ); - }; - } - - /** - * Return whether the given property represents a notification. If true, - * the promise returned from the invocation will resolve immediatey to `undefined` - * - * A property leads to a notification rather than a method call if its name - * begins with `notify` or `on`. - * - * @param p - The property being called on the proxy. - * @return Whether `p` represents a notification. - */ - protected isNotification(p: PropertyKey): boolean { - return p.toString().startsWith('notify') || p.toString().startsWith('on'); - } -} - -export class RpcHandler { - constructor(readonly target: any) { - } - - onChannelOpen(channel: Channel): void { - const server = new RPCServer(channel, (method: string, args: any[]) => this.handleRequest(method, args)); - server.onNotification((e: { method: string, args: any }) => this.onNotification(e.method, e.args)); - } - - protected async handleRequest(method: string, args: any[]): Promise { - return this.target[method](...args); - } - - protected onNotification(method: string, args: any[]): void { - this.target[method](args); - } -} diff --git a/packages/core/src/common/messaging/proxy-factory.ts b/packages/core/src/common/messaging/proxy-factory.ts index 99b0deabb149e..2a060c08fc7ef 100644 --- a/packages/core/src/common/messaging/proxy-factory.ts +++ b/packages/core/src/common/messaging/proxy-factory.ts @@ -21,7 +21,7 @@ import { ApplicationError } from '../application-error'; import { Disposable } from '../disposable'; import { Emitter, Event } from '../event'; import { Channel } from '../message-rpc/channel'; -import { RCPConnection } from '../message-rpc/rpc-protocol'; +import { RpcConnection } from '../message-rpc/rpc-protocol'; import { ConnectionHandler } from './handler'; export type JsonRpcServer = Disposable & { @@ -103,8 +103,8 @@ export class JsonRpcProxyFactory implements ProxyHandler { protected readonly onDidOpenConnectionEmitter = new Emitter(); protected readonly onDidCloseConnectionEmitter = new Emitter(); - protected connectionPromiseResolve: (connection: RCPConnection) => void; - protected connectionPromise: Promise; + protected connectionPromiseResolve: (connection: RpcConnection) => void; + protected connectionPromise: Promise; /** * Build a new JsonRpcProxyFactory. @@ -135,7 +135,7 @@ export class JsonRpcProxyFactory implements ProxyHandler { * response. */ listen(channel: Channel): void { - const connection = new RCPConnection(channel, (method, args) => this.onRequest(method, ...args)); + const connection = new RpcConnection(channel, (method, args) => this.onRequest(method, ...args)); connection.onNotification(event => this.onNotification(event.method, ...event.args)); this.connectionPromiseResolve(connection); diff --git a/packages/core/src/common/messaging/web-socket-channel.ts b/packages/core/src/common/messaging/web-socket-channel.ts index 202647b5af5cd..a26e79b02d844 100644 --- a/packages/core/src/common/messaging/web-socket-channel.ts +++ b/packages/core/src/common/messaging/web-socket-channel.ts @@ -16,10 +16,66 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { ForwardingChannel } from '../message-rpc/channel'; +import { Emitter, Event } from '../event'; +import { WriteBuffer } from '../message-rpc'; +import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from '../message-rpc/array-buffer-message-buffer'; +import { Channel, ForwardingChannel, ReadBufferFactory } from '../message-rpc/channel'; +/** + * The messaging connection between a choesive frontend and backend service. + */ export type WebSocketChannel = ForwardingChannel; export namespace WebSocketChannel { export const wsPath = '/services'; } +export interface IWebSocket { + send(message: ArrayBuffer): void; + close(): void; + isConnected(): boolean; + onMessage(cb: (message: ArrayBuffer) => void): void; + onError(cb: (reason: any) => void): void; + onClose(cb: () => void): void; +} + +export class WebSocketMainChannel implements Channel { + protected readonly onCloseEmitter: Emitter = new Emitter(); + get onClose(): Event { + return this.onCloseEmitter.event; + } + + protected readonly onMessageEmitter: Emitter = new Emitter(); + get onMessage(): Event { + return this.onMessageEmitter.event; + } + + protected readonly onErrorEmitter: Emitter = new Emitter(); + get onError(): Event { + return this.onErrorEmitter.event; + } + + readonly id: string; + + constructor(protected readonly socket: IWebSocket) { + socket.onClose(() => this.onCloseEmitter.fire()); + socket.onError(error => this.onErrorEmitter.fire(error)); + socket.onMessage(buffer => this.onMessageEmitter.fire(() => new ArrayBufferReadBuffer(buffer))); + + this.id = 'main_channel'; + } + + getWriteBuffer(): WriteBuffer { + const result = new ArrayBufferWriteBuffer(); + if (this.socket.isConnected()) { + result.onCommit(buffer => { + this.socket.send(buffer); + }); + } + return result; + } + + close(): void { + this.socket.close(); + } + +} diff --git a/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts b/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts index c345a03f8984c..7dc626a43bbab 100644 --- a/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts +++ b/packages/core/src/electron-main/messaging/electron-messaging-contribution.ts @@ -33,7 +33,6 @@ import { ElectronMessagingService } from './electron-messaging-service'; * This component allows communication between renderer process (frontend) and electron main process. */ - // FIXME: Electron implementation @injectable() export class ElectronMessagingContribution implements ElectronMainApplicationContribution, ElectronMessagingService { diff --git a/packages/core/src/node/messaging/ipc-bootstrap.ts b/packages/core/src/node/messaging/ipc-bootstrap.ts index 42699dbcc16b0..abddcab3164f9 100644 --- a/packages/core/src/node/messaging/ipc-bootstrap.ts +++ b/packages/core/src/node/messaging/ipc-bootstrap.ts @@ -18,7 +18,7 @@ import { Socket } from 'net'; import 'reflect-metadata'; import { Emitter } from '../../common'; import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from '../../common/message-rpc/array-buffer-message-buffer'; -import { Channel, ReadBufferConstructor } from '../../common/message-rpc/channel'; +import { Channel, ReadBufferFactory } from '../../common/message-rpc/channel'; import { dynamicRequire } from '../dynamic-require'; import { checkParentAlive, IPCEntryPoint } from './ipc-protocol'; @@ -34,7 +34,7 @@ function createChannel(): Channel { }); const onCloseEmitter = new Emitter(); - const onMessageEmitter = new Emitter(); + const onMessageEmitter = new Emitter(); const onErrorEmitter = new Emitter(); pipe.on('data', (data: Uint8Array) => { onMessageEmitter.fire(() => new ArrayBufferReadBuffer(data.buffer)); diff --git a/packages/core/src/node/messaging/ipc-connection-provider.ts b/packages/core/src/node/messaging/ipc-connection-provider.ts index d37b63593d86f..e708662f739d1 100644 --- a/packages/core/src/node/messaging/ipc-connection-provider.ts +++ b/packages/core/src/node/messaging/ipc-connection-provider.ts @@ -21,7 +21,7 @@ import { Writable } from 'stream'; import { Message } from 'vscode-ws-jsonrpc'; import { ConnectionErrorHandler, Disposable, DisposableCollection, Emitter, ILogger } from '../../common'; import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from '../../common/message-rpc/array-buffer-message-buffer'; -import { Channel, ReadBufferConstructor } from '../../common/message-rpc/channel'; +import { Channel, ReadBufferFactory } from '../../common/message-rpc/channel'; import { createIpcEnv } from './ipc-protocol'; export interface ResolvedIPCConnectionOptions { @@ -80,7 +80,7 @@ export class IPCConnectionProvider { protected createConnection(childProcess: cp.ChildProcess, options?: ResolvedIPCConnectionOptions): Channel { const onCloseEmitter = new Emitter(); - const onMessageEmitter = new Emitter(); + const onMessageEmitter = new Emitter(); const onErrorEmitter = new Emitter(); const pipe = childProcess.stdio[4] as Writable; diff --git a/packages/core/src/node/messaging/messaging-contribution.ts b/packages/core/src/node/messaging/messaging-contribution.ts index e6b5b1b3c50f4..4b58b988f55f0 100644 --- a/packages/core/src/node/messaging/messaging-contribution.ts +++ b/packages/core/src/node/messaging/messaging-contribution.ts @@ -18,17 +18,16 @@ import * as http from 'http'; import * as https from 'https'; import { Container, inject, injectable, interfaces, named, postConstruct } from 'inversify'; import { Server, Socket } from 'socket.io'; -import { bindContributionProvider, ConnectionHandler, ContributionProvider, Emitter, Event } from '../../common/'; -import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer, toArrayBuffer } from '../../common/message-rpc/array-buffer-message-buffer'; -import { Channel, ChannelMultiplexer, ReadBufferConstructor } from '../../common/message-rpc/channel'; -import { WriteBuffer } from '../../common/message-rpc/message-buffer'; -import { WebSocketChannel } from '../../common/messaging/web-socket-channel'; +import { ContributionProvider, ConnectionHandler, bindContributionProvider } from '../../common'; +import { IWebSocket, WebSocketChannel, WebSocketMainChannel } from '../../common/messaging/web-socket-channel'; import { BackendApplicationContribution } from '../backend-application'; -import { WsRequestValidator } from '../ws-request-validators'; -import { ConnectionContainerModule } from './connection-container-module'; -import { MessagingListener } from './messaging-listeners'; import { MessagingService } from './messaging-service'; +import { ConnectionContainerModule } from './connection-container-module'; import Route = require('route-parser'); +import { WsRequestValidator } from '../ws-request-validators'; +import { MessagingListener } from './messaging-listeners'; +import { toArrayBuffer } from '../../common/message-rpc/array-buffer-message-buffer'; +import { Channel, ChannelMultiplexer } from '../../common/message-rpc'; export const MessagingContainer = Symbol('MessagingContainer'); @@ -121,7 +120,7 @@ export class MessagingContribution implements BackendApplicationContribution, Me } protected handleChannels(socket: Socket): void { - const socketChannel = new SocketIOChannel(socket); + const socketChannel = new WebSocketMainChannel(toIWebSocket(socket)); const mulitplexer = new ChannelMultiplexer(socketChannel); const channelHandlers = this.getConnectionChannelHandlers(socket); mulitplexer.onDidOpenChannel(event => { @@ -154,43 +153,15 @@ export class MessagingContribution implements BackendApplicationContribution, Me } -export class SocketIOChannel implements Channel { - protected readonly onCloseEmitter: Emitter = new Emitter(); - get onClose(): Event { - return this.onCloseEmitter.event; - } - - protected readonly onMessageEmitter: Emitter = new Emitter(); - get onMessage(): Event { - return this.onMessageEmitter.event; - } - - protected readonly onErrorEmitter: Emitter = new Emitter(); - get onError(): Event { - return this.onErrorEmitter.event; - } - - readonly id: string; - - constructor(protected readonly socket: Socket) { - socket.on('error', error => this.onErrorEmitter.fire(error)); - socket.on('disconnect', reason => this.onCloseEmitter.fire()); - socket.on('message', (buffer: Buffer) => this.onMessageEmitter.fire(() => new ArrayBufferReadBuffer(toArrayBuffer(buffer)))); - this.id = socket.id; - } - - getWriteBuffer(): WriteBuffer { - const result = new ArrayBufferWriteBuffer(); - if (this.socket.connected) { - result.onCommit(buffer => { - this.socket.emit('message', buffer); - }); - } - return result; - } - close(): void { - // TODO: Implement me - } +function toIWebSocket(socket: Socket): IWebSocket { + return { + close: () => socket.disconnect(), + isConnected: () => socket.connected, + onClose: cb => socket.on('disconnect', () => cb()), + onError: cb => socket.on('error', cb), + onMessage: cb => socket.on('message', data => cb(toArrayBuffer(data))), + send: message => socket.emit('message', message) + }; } export namespace MessagingContribution { diff --git a/packages/core/src/node/messaging/test/test-web-socket-channel.ts b/packages/core/src/node/messaging/test/test-web-socket-channel.ts index 8e385ace9ff94..2b2fd52f794e0 100644 --- a/packages/core/src/node/messaging/test/test-web-socket-channel.ts +++ b/packages/core/src/node/messaging/test/test-web-socket-channel.ts @@ -14,34 +14,33 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -// import * as http from 'http'; -// import * as https from 'https'; -// import { WebSocketChannel } from '../../../common/messaging/web-socket-channel'; -// import { Disposable } from '../../../common/disposable'; -// import { AddressInfo } from 'net'; -// import { io } from 'socket.io-client'; +import * as http from 'http'; +import * as https from 'https'; +import { AddressInfo } from 'net'; +import { io } from 'socket.io-client'; +import { toArrayBuffer } from '../../../common/message-rpc/array-buffer-message-buffer'; +import { IWebSocket, WebSocketChannel, WebSocketMainChannel } from '../../../common/messaging/web-socket-channel'; -// export class TestWebSocketChannel extends WebSocketChannel { +export class TestWebSocketChannel extends WebSocketMainChannel { -// constructor({ server, path }: { -// server: http.Server | https.Server, -// path: string -// }) { -// super(0, content => socket.send(content)); -// const socket = io(`ws://localhost:${(server.address() as AddressInfo).port}${WebSocketChannel.wsPath}`); -// socket.on('error', error => -// this.fireError(error) -// ); -// socket.on('disconnect', reason => -// this.fireClose(0, reason) -// ); -// socket.on('message', data => { -// this.handleMessage(JSON.parse(data.toString())); -// }); -// socket.on('connect', () => -// this.open(path) -// ); -// this.toDispose.push(Disposable.create(() => socket.close())); -// } + constructor({ server, path }: { + server: http.Server | https.Server, + path: string + }) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + super(createSocket(server)); -// } + } +} + +function createSocket(server: http.Server): IWebSocket { + const socket = io(`ws://localhost:${(server.address() as AddressInfo).port}${WebSocketChannel.wsPath}`); + return { + close: () => socket.disconnect(), + isConnected: () => socket.connected, + onClose: cb => socket.on('disconnect', () => cb()), + onError: cb => socket.on('error', cb), + onMessage: cb => socket.on('message', data => cb(toArrayBuffer(data))), + send: message => socket.emit('message', message) + }; +} diff --git a/packages/debug/src/browser/debug-session-connection.ts b/packages/debug/src/browser/debug-session-connection.ts index f58f7fb8d152d..897543c8c39bb 100644 --- a/packages/debug/src/browser/debug-session-connection.ts +++ b/packages/debug/src/browser/debug-session-connection.ts @@ -16,9 +16,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Disposable, DisposableCollection, Emitter, Event, MaybePromise } from '@theia/core'; -import { Channel } from '@theia/core/lib/common/message-rpc/channel'; import { Deferred } from '@theia/core/lib/common/promise-util'; +import { Event, Emitter, DisposableCollection, Disposable, MaybePromise, Channel } from '@theia/core'; import { OutputChannel } from '@theia/output/lib/browser/output-channel'; import { DebugProtocol } from 'vscode-debugprotocol'; export type DebugRequestHandler = (request: DebugProtocol.Request) => MaybePromise; @@ -114,6 +113,7 @@ const standardDebugEvents = new Set([ 'thread' ]); +// TODO: Proper message RPC for debug session protocol export class DebugSessionConnection implements Disposable { private sequence = 1; diff --git a/packages/debug/src/browser/debug-session-contribution.ts b/packages/debug/src/browser/debug-session-contribution.ts index 2a3c0dbe2abf5..2550c1c747673 100644 --- a/packages/debug/src/browser/debug-session-contribution.ts +++ b/packages/debug/src/browser/debug-session-contribution.ts @@ -30,7 +30,7 @@ import { DebugAdapterPath } from '../common/debug-service'; import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { DebugContribution } from './debug-contribution'; -import { Channel } from '@theia/core/src/common/message-rpc/channel'; +import { Channel } from '@theia/core/lib/common/message-rpc/channel'; /** * DebugSessionContribution symbol for DI. diff --git a/packages/filesystem/src/common/files.ts b/packages/filesystem/src/common/files.ts index 2ea5fff79f518..d66ff21e4dfdb 100644 --- a/packages/filesystem/src/common/files.ts +++ b/packages/filesystem/src/common/files.ts @@ -837,7 +837,7 @@ export function hasOpenReadWriteCloseCapability(provider: FileSystemProvider): p */ export interface FileSystemProviderWithFileReadStreamCapability extends FileSystemProvider { /** - * Read the contents of the given file as stream. + * Read the contents of the given file as stream. * @param resource The `URI` of the file. * * @return The `ReadableStreamEvents` for the readable stream of the given file. diff --git a/packages/filesystem/src/common/remote-file-system-provider.ts b/packages/filesystem/src/common/remote-file-system-provider.ts index 5edb5dbbad9e7..f67e198db75f7 100644 --- a/packages/filesystem/src/common/remote-file-system-provider.ts +++ b/packages/filesystem/src/common/remote-file-system-provider.ts @@ -42,11 +42,11 @@ export interface RemoteFileSystemServer extends JsonRpcServer; open(resource: string, opts: FileOpenOptions): Promise; close(fd: number): Promise; - read(fd: number, pos: number, length: number): Promise<{ bytes: number[]; bytesRead: number; }>; + read(fd: number, pos: number, length: number): Promise<{ bytes: Uint8Array; bytesRead: number; }>; readFileStream(resource: string, opts: FileReadStreamOptions, token: CancellationToken): Promise; - readFile(resource: string): Promise; - write(fd: number, pos: number, data: number[], offset: number, length: number): Promise; - writeFile(resource: string, content: number[], opts: FileWriteOptions): Promise; + readFile(resource: string): Promise; + write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise; + writeFile(resource: string, content: Uint8Array, opts: FileWriteOptions): Promise; delete(resource: string, opts: FileDeleteOptions): Promise; mkdir(resource: string): Promise; readdir(resource: string): Promise<[string, FileType][]>; @@ -70,7 +70,7 @@ export interface RemoteFileSystemClient { notifyDidChangeFile(event: { changes: RemoteFileChange[] }): void; notifyFileWatchError(): void; notifyDidChangeCapabilities(capabilities: FileSystemProviderCapabilities): void; - onFileStreamData(handle: number, data: number[]): void; + onFileStreamData(handle: number, data: Uint8Array): void; onFileStreamEnd(handle: number, error: RemoteFileStreamError | undefined): void; } @@ -169,7 +169,7 @@ export class RemoteFileSystemProvider implements Required, D this.onFileWatchErrorEmitter.fire(); }, notifyDidChangeCapabilities: capabilities => this.setCapabilities(capabilities), - onFileStreamData: (handle, data) => this.onFileStreamDataEmitter.fire([handle, Uint8Array.from(data)]), + onFileStreamData: (handle, data) => this.onFileStreamDataEmitter.fire([handle, data]), onFileStreamEnd: (handle, error) => this.onFileStreamEndEmitter.fire([handle, error]) }); const onInitialized = this.server.onDidOpenConnection(() => { @@ -224,7 +224,7 @@ export class RemoteFileSystemProvider implements Required, D async readFile(resource: URI): Promise { const bytes = await this.server.readFile(resource.toString()); - return Uint8Array.from(bytes); + return bytes; } readFileStream(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { @@ -264,11 +264,11 @@ export class RemoteFileSystemProvider implements Required, D } write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { - return this.server.write(fd, pos, [...data.values()], offset, length); + return this.server.write(fd, pos, data, offset, length); } writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { - return this.server.writeFile(resource.toString(), [...content.values()], opts); + return this.server.writeFile(resource.toString(), content, opts); } delete(resource: URI, opts: FileDeleteOptions): Promise { @@ -412,34 +412,33 @@ export class FileSystemProviderServer implements RemoteFileSystemServer { throw new Error('not supported'); } - async read(fd: number, pos: number, length: number): Promise<{ bytes: number[]; bytesRead: number; }> { + async read(fd: number, pos: number, length: number): Promise<{ bytes: Uint8Array; bytesRead: number; }> { if (hasOpenReadWriteCloseCapability(this.provider)) { const buffer = BinaryBuffer.alloc(this.BUFFER_SIZE); const bytes = buffer.buffer; const bytesRead = await this.provider.read(fd, pos, bytes, 0, length); - return { bytes: [...bytes.values()], bytesRead }; + return { bytes, bytesRead }; } throw new Error('not supported'); } - write(fd: number, pos: number, data: number[], offset: number, length: number): Promise { + write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { if (hasOpenReadWriteCloseCapability(this.provider)) { - return this.provider.write(fd, pos, Uint8Array.from(data), offset, length); + return this.provider.write(fd, pos, data, offset, length); } throw new Error('not supported'); } - async readFile(resource: string): Promise { + async readFile(resource: string): Promise { if (hasReadWriteCapability(this.provider)) { - const buffer = await this.provider.readFile(new URI(resource)); - return [...buffer.values()]; + return this.provider.readFile(new URI(resource)); } throw new Error('not supported'); } - writeFile(resource: string, content: number[], opts: FileWriteOptions): Promise { + writeFile(resource: string, content: Uint8Array, opts: FileWriteOptions): Promise { if (hasReadWriteCapability(this.provider)) { - return this.provider.writeFile(new URI(resource), Uint8Array.from(content), opts); + return this.provider.writeFile(new URI(resource), content, opts); } throw new Error('not supported'); } @@ -497,7 +496,7 @@ export class FileSystemProviderServer implements RemoteFileSystemServer { if (hasFileReadStreamCapability(this.provider)) { const handle = this.readFileStreamSeq++; const stream = this.provider.readFileStream(new URI(resource), opts, token); - stream.on('data', data => this.client?.onFileStreamData(handle, [...data.values()])); + stream.on('data', data => this.client?.onFileStreamData(handle, data)); stream.on('error', error => { const code = error instanceof FileSystemProviderError ? error.code : undefined; const { name, message, stack } = error; diff --git a/packages/plugin-ext/src/common/connection.ts b/packages/plugin-ext/src/common/connection.ts index d334691f4381d..6888494814fe1 100644 --- a/packages/plugin-ext/src/common/connection.ts +++ b/packages/plugin-ext/src/common/connection.ts @@ -13,17 +13,17 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** +import { ConnectionExt, ConnectionMain } from './plugin-api-rpc'; import { Emitter, Event } from '@theia/core/lib/common/event'; +import { ReadBufferFactory } from '@theia/core/lib/common/message-rpc/channel'; +import { WriteBuffer, Channel } from '@theia/core'; import { ArrayBufferReadBuffer, ArrayBufferWriteBuffer } from '@theia/core/lib/common/message-rpc/array-buffer-message-buffer'; -import { Channel, ReadBufferConstructor } from '@theia/core/lib/common/message-rpc/channel'; -import { WriteBuffer } from '@theia/core/lib/common/message-rpc/message-buffer'; -import { ConnectionExt, ConnectionMain } from './plugin-api-rpc'; /** * A channel communicating with a counterpart in a plugin host. */ export class PluginChannel implements Channel { - private messageEmitter: Emitter = new Emitter(); + private messageEmitter: Emitter = new Emitter(); private errorEmitter: Emitter = new Emitter(); private closedEmitter: Emitter = new Emitter(); @@ -44,7 +44,7 @@ export class PluginChannel implements Channel { this.connection.$sendMessage(this.id, content); } - fireMessageReceived(msg: ReadBufferConstructor): void { + fireMessageReceived(msg: ReadBufferFactory): void { this.messageEmitter.fire(msg); } @@ -56,7 +56,7 @@ export class PluginChannel implements Channel { this.closedEmitter.fire(); } - get onMessage(): Event { + get onMessage(): Event { return this.messageEmitter.event; } diff --git a/packages/task/src/node/task-server.slow-spec.ts b/packages/task/src/node/task-server.slow-spec.ts index 76b0b9f4339ab..c07bde36899bb 100644 --- a/packages/task/src/node/task-server.slow-spec.ts +++ b/packages/task/src/node/task-server.slow-spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/quotes */ // ***************************************************************************** // Copyright (C) 2017-2019 Ericsson and others. // @@ -29,6 +30,8 @@ import 'reflect-metadata'; import { TaskConfiguration, TaskExitedEvent, TaskInfo, TaskServer, TaskWatcher } from '../common'; import { ProcessTaskConfiguration, ProcessType } from '../common/process/task-protocol'; import { createTaskTestContainer } from './test/task-test-container'; +import { TestWebSocketChannel } from "@theia/core/lib/node/messaging/test/test-web-socket-channel"; +import { terminalsPath } from '@theia/terminal/lib/common/terminal-protocol'; // test scripts that we bundle with tasks const commandShortRunning = './task'; @@ -85,48 +88,47 @@ describe('Task server / back-end', function (): void { _server.close(); }); - // FIXME - // it('task running in terminal - expected data is received from the terminal ws server', async function (): Promise { - // const someString = 'someSingleWordString'; - - // // This test is flaky on Windows and fails intermittently. Disable it for now - // if (isWindows) { - // this.skip(); - // return; - // } - - // // create task using terminal process - // const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning); - // const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('shell', `${command} ${someString}`), wsRoot); - // const terminalId = taskInfo.terminalId; - - // const messagesToWaitFor = 10; - // const messages: string[] = []; - - // // hook-up to terminal's ws and confirm that it outputs expected tasks' output - // await new Promise((resolve, reject) => { - // const channel = new TestWebSocketChannel({ server, path: `${terminalsPath}/${terminalId}` }); - // channel.onError(reject); - // channel.onClose((code, reason) => reject(new Error(`channel is closed with '${code}' code and '${reason}' reason`))); - // channel.onMessage(msg => { - // // check output of task on terminal is what we expect - // const expected = `${isOSX ? 'tasking osx' : 'tasking'}... ${someString}`; - // // Instead of waiting for one message from the terminal, we wait for several ones as the very first message can be something unexpected. - // // For instance: `nvm is not compatible with the \"PREFIX\" environment variable: currently set to \"/usr/local\"\r\n` - // const currentMessage = msg.toString(); - // messages.unshift(currentMessage); - // if (currentMessage.indexOf(expected) !== -1) { - // resolve(); - // channel.close(); - // return; - // } - // if (messages.length >= messagesToWaitFor) { - // reject(new Error(`expected sub-string not found in terminal output. Expected: "${expected}" vs Actual messages: ${JSON.stringify(messages)}`)); - // channel.close(); - // } - // }); - // }); - // }); + it('task running in terminal - expected data is received from the terminal ws server', async function (): Promise { + const someString = 'someSingleWordString'; + + // This test is flaky on Windows and fails intermittently. Disable it for now + if (isWindows) { + this.skip(); + return; + } + + // create task using terminal process + const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning); + const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('shell', `${command} ${someString}`), wsRoot); + const terminalId = taskInfo.terminalId; + + const messagesToWaitFor = 10; + const messages: string[] = []; + + // hook-up to terminal's ws and confirm that it outputs expected tasks' output + await new Promise((resolve, reject) => { + const channel = new TestWebSocketChannel({ server, path: `${terminalsPath}/${terminalId}` }); + channel.onError(reject); + channel.onClose(() => reject(new Error('Channel has been closed'))); + channel.onMessage(msg => { + // check output of task on terminal is what we expect + const expected = `${isOSX ? 'tasking osx' : 'tasking'}... ${someString}`; + // Instead of waiting for one message from the terminal, we wait for several ones as the very first message can be something unexpected. + // For instance: `nvm is not compatible with the \"PREFIX\" environment variable: currently set to \"/usr/local\"\r\n` + const currentMessage = msg.toString(); + messages.unshift(currentMessage); + if (currentMessage.indexOf(expected) !== -1) { + resolve(); + channel.close(); + return; + } + if (messages.length >= messagesToWaitFor) { + reject(new Error(`expected sub-string not found in terminal output. Expected: "${expected}" vs Actual messages: ${JSON.stringify(messages)}`)); + channel.close(); + } + }); + }); + }); it('task using raw process - task server success response shall not contain a terminal id', async function (): Promise { const someString = 'someSingleWordString'; diff --git a/packages/terminal/src/browser/terminal-widget-impl.ts b/packages/terminal/src/browser/terminal-widget-impl.ts index 22c093d24e437..afb30a605f1a6 100644 --- a/packages/terminal/src/browser/terminal-widget-impl.ts +++ b/packages/terminal/src/browser/terminal-widget-impl.ts @@ -22,7 +22,7 @@ import { nls } from '@theia/core/lib/common/nls'; import { Deferred } from '@theia/core/lib/common/promise-util'; import URI from '@theia/core/lib/common/uri'; import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify'; -import { RCPConnection } from '@theia/core/lib/common/message-rpc/rpc-protocol'; +import { RequestHandler, RpcConnection } from '@theia/core/lib/common/message-rpc/rpc-protocol'; import { CommandLineOptions, ShellCommandBuilder } from '@theia/process/lib/common/shell-command-builder'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { RendererType, Terminal } from 'xterm'; @@ -58,7 +58,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget protected searchBox: TerminalSearchWidget; protected restored = false; protected closeOnDispose = true; - protected waitForConnection: Deferred | undefined; + protected waitForConnection: Deferred | undefined; protected hoverMessage: HTMLDivElement; protected lastTouchEnd: TouchEvent | undefined; protected isAttachedCloseListener: boolean = false; @@ -507,13 +507,15 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget } this.toDisposeOnConnect.dispose(); this.toDispose.push(this.toDisposeOnConnect); - const waitForConnection = this.waitForConnection = new Deferred(); + const waitForConnection = this.waitForConnection = new Deferred(); this.webSocketConnectionProvider.listen({ path: `${terminalsPath}/${this.terminalId}`, onConnection: connection => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const requestHandler = (method: string, args: any[]) => Promise.resolve(); - const rpc = new RCPConnection(connection, requestHandler); + + const requestHandler: RequestHandler = _method => this.logger.warn('Received an unhandled RPC request from the terminal process'); + + const rpc = new RpcConnection(connection, requestHandler); rpc.onNotification(event => { if (event.method === 'onData') { this.write(event.args[0]); 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 4070c448d705d..29c720c0a0dd7 100644 --- a/packages/terminal/src/node/terminal-backend-contribution.slow-spec.ts +++ b/packages/terminal/src/node/terminal-backend-contribution.slow-spec.ts @@ -14,42 +14,43 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -// import { BackendApplication } from '@theia/core/lib/node/backend-application'; -// import * as http from 'http'; -// import * as https from 'https'; -// import { IShellTerminalServer } from '../common/shell-terminal-protocol'; -// import { createTerminalTestContainer } from './test/terminal-test-container'; +import { BackendApplication } from '@theia/core/lib/node/backend-application'; +import * as http from 'http'; +import * as https from 'https'; +import { IShellTerminalServer } from '../common/shell-terminal-protocol'; +import { createTerminalTestContainer } from './test/terminal-test-container'; +import { TestWebSocketChannel } from '@theia/core/lib/node/messaging/test/test-web-socket-channel'; +import { terminalsPath } from '../common/terminal-protocol'; -// describe('Terminal Backend Contribution', function (): void { +describe('Terminal Backend Contribution', function (): void { - // this.timeout(10000); - // let server: http.Server | https.Server; - // let shellTerminalServer: IShellTerminalServer; + this.timeout(10000); + let server: http.Server | https.Server; + let shellTerminalServer: IShellTerminalServer; - // beforeEach(async () => { - // const container = createTerminalTestContainer(); - // const application = container.get(BackendApplication); - // shellTerminalServer = container.get(IShellTerminalServer); - // server = await application.start(); - // }); + beforeEach(async () => { + const container = createTerminalTestContainer(); + const application = container.get(BackendApplication); + shellTerminalServer = container.get(IShellTerminalServer); + server = await application.start(); + }); - // afterEach(() => { - // const s = server; - // server = undefined!; - // shellTerminalServer = undefined!; - // s.close(); - // }); + afterEach(() => { + const s = server; + server = undefined!; + shellTerminalServer = undefined!; + s.close(); + }); - // it('is data received from the terminal ws server', async () => { - // const terminalId = await shellTerminalServer.create({}); - // await new Promise((resolve, reject) => { - // const channel = new TestWebSocketChannel({ server, path: `${terminalsPath}/${terminalId}` }); - // channel.onError(reject); - // channel.onClose((code, reason) => reject(new Error(`channel is closed with '${code}' code and '${reason}' reason`))); - // channel.onOpen(() => { - // resolve(); - // channel.close(); - // }); - // }); - // }); -// }); + it('is data received from the terminal ws server', async () => { + const terminalId = await shellTerminalServer.create({}); + await new Promise((resolve, reject) => { + const channel = new TestWebSocketChannel({ server, path: `${terminalsPath}/${terminalId}` }); + channel.onError(reject); + channel.onClose(() => reject(new Error('channel is closed'))); + resolve(); + channel.close(); + }); + }); + +}); diff --git a/packages/terminal/src/node/terminal-backend-contribution.ts b/packages/terminal/src/node/terminal-backend-contribution.ts index b671bd4cda6ac..60786a1c72ab1 100644 --- a/packages/terminal/src/node/terminal-backend-contribution.ts +++ b/packages/terminal/src/node/terminal-backend-contribution.ts @@ -14,12 +14,12 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { ILogger } from '@theia/core/lib/common'; -import { RCPConnection } from '@theia/core/lib/common/message-rpc/rpc-protocol'; -import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service'; -import { inject, injectable, named } from '@theia/core/shared/inversify'; -import { ProcessManager, TerminalProcess } from '@theia/process/lib/node'; +import { injectable, inject, named } from '@theia/core/shared/inversify'; +import { ILogger, RequestHandler } from '@theia/core/lib/common'; +import { TerminalProcess, ProcessManager } from '@theia/process/lib/node'; import { terminalsPath } from '../common/terminal-protocol'; +import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service'; +import { RpcConnection } from '@theia/core/'; @injectable() export class TerminalBackendContribution implements MessagingService.Contribution { @@ -36,14 +36,19 @@ export class TerminalBackendContribution implements MessagingService.Contributio const termProcess = this.processManager.get(id); if (termProcess instanceof TerminalProcess) { const output = termProcess.createOutputStream(); + // Create a RPC connection to the terminal process // eslint-disable-next-line @typescript-eslint/no-explicit-any - const requestHandler = async (method: string, args: any[]) => { + const requestHandler: RequestHandler = async (method: string, args: any[]) => { if (method === 'write' && args[0]) { - termProcess.write(args[0].toString()); + termProcess.write(args[0]); } + this.logger.warn('Terminal process received a request with unsupported method or argument', { method, args }); }; - const rpc = new RCPConnection(connection, requestHandler); - output.on('data', data => rpc.sendNotification('onData', data)); + + const rpc = new RpcConnection(connection, requestHandler); + output.on('data', data => { + rpc.sendNotification('onData', [data]); + }); connection.onClose(() => output.dispose()); } }); diff --git a/yarn.lock b/yarn.lock index 4c0a8254157ba..489dd97a539a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2194,7 +2194,7 @@ "@types/chai-spies@1.0.3": version "1.0.3" - resolved "http://localhost:4873/@types%2fchai-spies/-/chai-spies-1.0.3.tgz#a52dc61af3853ec9b80965040811d15dfd401542" + resolved "https://registry.yarnpkg.com/@types%2fchai-spies/-/chai-spies-1.0.3.tgz#a52dc61af3853ec9b80965040811d15dfd401542" integrity sha512-RBZjhVuK7vrg4rWMt04UF5zHYwfHnpk5mIWu3nQvU3AKGDixXzSjZ6v0zke6pBcaJqMv3IBZ5ibLWPMRDL0sLw== dependencies: "@types/chai" "*" @@ -2208,7 +2208,7 @@ "@types/chai@*", "@types/chai@4.3.0", "@types/chai@^4.2.7": version "4.3.0" - resolved "http://localhost:4873/@types%2fchai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc" + resolved "https://registry.yarnpkg.com/@types%2fchai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc" integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw== "@types/component-emitter@^1.2.10":