From fe015e61c0183594d47ed6eacd0a5787a577f9a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 9 Jul 2019 17:32:00 +0200 Subject: [PATCH 01/19] flow control and discard watermark --- demo/client.ts | 5 +- demo/server.js | 93 +++++++++++++++++++------ src/InputHandler.ts | 11 +++ src/Terminal.ts | 98 +++++++++++---------------- src/common/services/OptionsService.ts | 3 +- 5 files changed, 126 insertions(+), 84 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index e2259841f7..cc6c75b8fd 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -224,7 +224,7 @@ function initOptions(term: TerminalType): void { 'handler', 'screenKeys', 'termName', - 'useFlowControl', + // 'useFlowControl', // Complex option 'theme' ]; @@ -236,7 +236,8 @@ function initOptions(term: TerminalType): void { fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], rendererType: ['dom', 'canvas'], - wordSeparator: null + wordSeparator: null, + answerbackString: null }; const options = Object.keys((term)._core.options); const booleanOptions = []; diff --git a/demo/server.js b/demo/server.js index 8955431b1a..2fae61421c 100644 --- a/demo/server.js +++ b/demo/server.js @@ -9,13 +9,31 @@ var pty = require('node-pty'); */ const USE_BINARY_UTF8 = false; +/** + * Whether to use flow control. + * This must be in sync with frontend option useFlowControl! + */ +const USE_FLOW_CONTROL = true; + +// send ENQ as ACK request (hardcoded in xterm.js) +const FLOW_CONTROL_ACK_REQUEST = '\x05'; +// ACK response (answerbackString in xterm.js) +const FLOW_CONTROL_ACK_RESPONSE = '\x06\x06\x06\x06'; +// send ACK request every n-th bytes +const ACK_WATERMARK = 131072; +// max allowed pending ACK requests before pausing pty +const MAX_PENDING_ACK = 7; + +// settings for prebuffering +const MAX_SEND_INTERVAL = 5; +const MAX_CHUNK_SIZE = 16384; + function startServer() { var app = express(); expressWs(app); - var terminals = {}, - logs = {}; + var terminals = {}; app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css')); app.get('/logo.png', (req, res) => res.sendFile(__dirname + '/logo.png')); @@ -52,10 +70,6 @@ function startServer() { console.log('Created terminal with PID: ' + term.pid); terminals[term.pid] = term; - logs[term.pid] = ''; - term.on('data', function(data) { - logs[term.pid] += data; - }); res.send(term.pid.toString()); res.end(); }); @@ -74,34 +88,55 @@ function startServer() { app.ws('/terminals/:pid', function (ws, req) { var term = terminals[parseInt(req.params.pid)]; console.log('Connected to terminal ' + term.pid); - ws.send(logs[term.pid]); - // string message buffering - function buffer(socket, timeout) { + const _send = data => { + // handle only 'open' websocket state + if (ws.readyState === 1) { + // setTimeout(() => ws.send(data), 200); + ws.send(data); + } + } + + /** + * message buffering - limits are MAX_SEND_INTERVAL and MAX_CHUNK_SIZE + */ + // string message + function buffer(timeout, limit) { let s = ''; let sender = null; return (data) => { s += data; - if (!sender) { + if (s.length > limit) { + clearTimeout(sender); + _send(s); + s = ''; + sender = null; + } else if (!sender) { sender = setTimeout(() => { - socket.send(s); + _send(s); s = ''; sender = null; }, timeout); } }; } - // binary message buffering - function bufferUtf8(socket, timeout) { + // binary message + function bufferUtf8(timeout, limit) { let buffer = []; let sender = null; let length = 0; return (data) => { buffer.push(data); length += data.length; - if (!sender) { + if (length > limit) { + clearTimeout(sender); + _send(Buffer.concat(buffer, length)); + buffer = []; + sender = null; + length = 0; + } else if (!sender) { sender = setTimeout(() => { - socket.send(Buffer.concat(buffer, length)); + _send(Buffer.concat(buffer, length)); buffer = []; sender = null; length = 0; @@ -109,16 +144,33 @@ function startServer() { } }; } - const send = USE_BINARY_UTF8 ? bufferUtf8(ws, 5) : buffer(ws, 5); + const send = (USE_BINARY_UTF8 ? bufferUtf8 : buffer)(MAX_SEND_INTERVAL, MAX_CHUNK_SIZE); + + let ackPending = 0; + let bytesSent = 0; term.on('data', function(data) { - try { - send(data); - } catch (ex) { - // The WebSocket is not open, ignore + send(data); + if (USE_FLOW_CONTROL) { + bytesSent += data.length; + if (bytesSent > ACK_WATERMARK) { + send(FLOW_CONTROL_ACK_REQUEST); + ackPending++; + bytesSent = 0; + if (ackPending > MAX_PENDING_ACK) { + term.pause(); + } + } } }); ws.on('message', function(msg) { + if (USE_FLOW_CONTROL && msg === FLOW_CONTROL_ACK_RESPONSE) { + ackPending--; + if (ackPending <= MAX_PENDING_ACK) { + term.resume(); + } + return; + } term.write(msg); }); ws.on('close', function () { @@ -126,7 +178,6 @@ function startServer() { console.log('Closed terminal ' + term.pid); // Clean things up delete terminals[term.pid]; - delete logs[term.pid]; }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 577029f84c..7a0ca0a8d8 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -211,6 +211,9 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.setExecuteHandler(C1.NEL, () => this.nextLine()); this._parser.setExecuteHandler(C1.HTS, () => this.tabSet()); + // install ENQ handler - disabled by default, set useFlowControl to enable + // this._parser.setExecuteHandler(C0.ENQ, () => this.enquiry()); + /** * OSC handler */ @@ -495,6 +498,14 @@ export class InputHandler extends Disposable implements IInputHandler { return this._parser.addOscHandler(ident, callback); } + /** + * ENQ + * Enquiry (Ctrl-E). + */ + public enquiry(): void { + this._coreService.triggerDataEvent(this._terminal.options.answerbackString); + } + /** * BEL * Bell (Ctrl-G). diff --git a/src/Terminal.ts b/src/Terminal.ts index 1e51f43998..e05e818ced 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -63,11 +63,15 @@ import { CoreService } from 'common/services/CoreService'; const document = (typeof window !== 'undefined') ? window.document : null; /** - * The amount of write requests to queue before sending an XOFF signal to the - * pty process. This number must be small in order for ^C and similar sequences - * to be responsive. + * Safety watermark to avoid memory exhaustion and browser engine crash on fast data input. + * Once hit the terminal will stop working. Enable flow control to avoid this limit + * and make sure that your backend correctly propagates this to the underlying pty. + * (see docs for further instructions) + * Since this limit is meant as a safety parachute to prevent browser crashs, + * it is set to a very high number. Typically xterm.js gets unresponsive with + * a much lower number (>500 kB). */ -const WRITE_BUFFER_PAUSE_THRESHOLD = 5; +const DISCARD_WATERMARK = 50000000; // ~50 MB /** * The max number of ms to spend on writes before allowing the renderer to @@ -160,14 +164,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public writeBuffer: string[]; public writeBufferUtf8: Uint8Array[]; private _writeInProgress: boolean; - /** - * Whether _xterm.js_ sent XOFF in order to catch up with the pty process. - * This is a distinct state from writeStopped so that if the user requested - * XOFF via ^S that it will not automatically resume when the writeBuffer goes - * below threshold. + * Sum of length of pending chunks in all write buffers. + * Note: For the string chunks the actual memory usage is + * doubled (JSString char takes 2 bytes). */ - private _xoffSentToCatchUp: boolean; + private _writeBuffersPendingSize: number = 0; /** Whether writing has been stopped as a result of XOFF */ // private _writeStopped: boolean; @@ -292,8 +294,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.writeBufferUtf8 = []; this._writeInProgress = false; - this._xoffSentToCatchUp = false; - // this._writeStopped = false; this._userScrolling = false; // Register input handler and refire/handle events @@ -421,6 +421,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } } break; + case 'useFlowControl': + if (this.optionsService.options.useFlowControl) { + (this._inputHandler as any)._parser.setExecuteHandler(C0.ENQ, () => (this._inputHandler as any).enquiry()); + } else { + (this._inputHandler as any)._parser.clearExecuteHandler(C0.ENQ); + } } }); } @@ -1211,17 +1217,16 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return; } - this.writeBufferUtf8.push(data); - - // Send XOFF to pause the pty process if the write buffer becomes too large so - // xterm.js can catch up before more data is sent. This is necessary in order - // to keep signals such as ^C responsive. - if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBufferUtf8.length >= WRITE_BUFFER_PAUSE_THRESHOLD) { - // XOFF - stop pty pipe - // XON will be triggered by emulator before processing data chunk - this._coreService.triggerDataEvent(C0.DC3); - this._xoffSentToCatchUp = true; + // safety measure: dont allow the backend to crash + // the terminal by writing to much data to fast. + // If we hit this, the terminal cant keep up with data written + // and will start to degenerate. + if (this._writeBuffersPendingSize > DISCARD_WATERMARK) { + throw new Error('write data discarded, use flow control to avoid losing data'); } + this._writeBuffersPendingSize += data.length; + + this.writeBufferUtf8.push(data); if (!this._writeInProgress && this.writeBufferUtf8.length > 0) { // Kick off a write which will write all data in sequence recursively @@ -1244,23 +1249,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp const data = this.writeBufferUtf8[bufferOffset]; bufferOffset++; - // If XOFF was sent in order to catch up with the pty process, resume it if - // we reached the end of the writeBuffer to allow more data to come in. - if (this._xoffSentToCatchUp && this.writeBufferUtf8.length === bufferOffset) { - this._coreService.triggerDataEvent(C0.DC1); - this._xoffSentToCatchUp = false; - } - this._refreshStart = this.buffer.y; this._refreshEnd = this.buffer.y; - // HACK: Set the parser state based on it's state at the time of return. - // This works around the bug #662 which saw the parser state reset in the - // middle of parsing escape sequence in two chunks. For some reason the - // state of the parser resets to 0 after exiting parser.parse. This change - // just sets the state back based on the correct return statement. - this._inputHandler.parseUtf8(data); + this._writeBuffersPendingSize -= data.length; this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); @@ -1298,17 +1291,16 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return; } - this.writeBuffer.push(data); - - // Send XOFF to pause the pty process if the write buffer becomes too large so - // xterm.js can catch up before more data is sent. This is necessary in order - // to keep signals such as ^C responsive. - if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) { - // XOFF - stop pty pipe - // XON will be triggered by emulator before processing data chunk - this._coreService.triggerDataEvent(C0.DC3); - this._xoffSentToCatchUp = true; + // safety measure: dont allow the backend to crash + // the terminal by writing to much data to fast. + // If we hit this, the terminal cant keep up with data written + // and will start to degenerate. + if (this._writeBuffersPendingSize > DISCARD_WATERMARK) { + throw new Error('write data discarded, use flow control to avoid losing data'); } + this._writeBuffersPendingSize += data.length; + + this.writeBuffer.push(data); if (!this._writeInProgress && this.writeBuffer.length > 0) { // Kick off a write which will write all data in sequence recursively @@ -1331,23 +1323,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp const data = this.writeBuffer[bufferOffset]; bufferOffset++; - // If XOFF was sent in order to catch up with the pty process, resume it if - // we reached the end of the writeBuffer to allow more data to come in. - if (this._xoffSentToCatchUp && this.writeBuffer.length === bufferOffset) { - this._coreService.triggerDataEvent(C0.DC1); - this._xoffSentToCatchUp = false; - } - this._refreshStart = this.buffer.y; this._refreshEnd = this.buffer.y; - // HACK: Set the parser state based on it's state at the time of return. - // This works around the bug #662 which saw the parser state reset in the - // middle of parsing escape sequence in two chunks. For some reason the - // state of the parser resets to 0 after exiting parser.parse. This change - // just sets the state back based on the correct return statement. - this._inputHandler.parse(data); + this._writeBuffersPendingSize -= data.length; this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); @@ -1864,7 +1844,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp const writeBuffer = this.writeBuffer; const writeBufferUtf8 = this.writeBufferUtf8; const writeInProgress = this._writeInProgress; - const xoffSentToCatchUp = this._xoffSentToCatchUp; const userScrolling = this._userScrolling; this._setup(); @@ -1881,7 +1860,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.writeBuffer = writeBuffer; this.writeBufferUtf8 = writeBufferUtf8; this._writeInProgress = writeInProgress; - this._xoffSentToCatchUp = xoffSentToCatchUp; this._userScrolling = userScrolling; // do a full screen refresh diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index ab04148822..edc2e9dc17 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -47,7 +47,8 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ debug: false, cancelEvents: false, useFlowControl: false, - wordSeparator: ' ()[]{}\'"' + wordSeparator: ' ()[]{}\'"', + answerbackString: '\x06\x06\x06\x06' }); /** From ecb241e61089529e2f89c48a49d27f59c1677628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 9 Jul 2019 17:51:49 +0200 Subject: [PATCH 02/19] disable flow control in server.js by default --- demo/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/server.js b/demo/server.js index 2fae61421c..846fcf9e50 100644 --- a/demo/server.js +++ b/demo/server.js @@ -13,7 +13,7 @@ const USE_BINARY_UTF8 = false; * Whether to use flow control. * This must be in sync with frontend option useFlowControl! */ -const USE_FLOW_CONTROL = true; +const USE_FLOW_CONTROL = false; // send ENQ as ACK request (hardcoded in xterm.js) const FLOW_CONTROL_ACK_REQUEST = '\x05'; From fdedbb2efff68236eb1ba4abe7a4ea442b8de56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 9 Jul 2019 18:41:50 +0200 Subject: [PATCH 03/19] fine tune ACK settings --- demo/server.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/demo/server.js b/demo/server.js index 846fcf9e50..4d19fd374f 100644 --- a/demo/server.js +++ b/demo/server.js @@ -22,11 +22,11 @@ const FLOW_CONTROL_ACK_RESPONSE = '\x06\x06\x06\x06'; // send ACK request every n-th bytes const ACK_WATERMARK = 131072; // max allowed pending ACK requests before pausing pty -const MAX_PENDING_ACK = 7; +const MAX_PENDING_ACK = 4; // settings for prebuffering const MAX_SEND_INTERVAL = 5; -const MAX_CHUNK_SIZE = 16384; +const MAX_CHUNK_SIZE = 65536; function startServer() { @@ -92,7 +92,8 @@ function startServer() { const _send = data => { // handle only 'open' websocket state if (ws.readyState === 1) { - // setTimeout(() => ws.send(data), 200); + // test high latency + // setTimeout(() => ws.send(data), 250); ws.send(data); } } From 660d5363d21586fd4b3ec4ebdb9cf3d3e20c5455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 11 Jul 2019 01:08:37 +0200 Subject: [PATCH 04/19] remove useFlowControl, set answer to empty string --- demo/server.js | 8 ++++---- src/InputHandler.ts | 8 +++++--- src/Terminal.ts | 7 +------ src/common/services/OptionsService.ts | 3 +-- src/common/services/Services.d.ts | 2 +- src/public/Terminal.ts | 8 ++++---- typings/xterm.d.ts | 15 +++++++++++---- 7 files changed, 27 insertions(+), 24 deletions(-) diff --git a/demo/server.js b/demo/server.js index 4d19fd374f..a3dd3e10a3 100644 --- a/demo/server.js +++ b/demo/server.js @@ -11,14 +11,14 @@ const USE_BINARY_UTF8 = false; /** * Whether to use flow control. - * This must be in sync with frontend option useFlowControl! + * This must be in sync with answerbackString in xterm.js. */ const USE_FLOW_CONTROL = false; // send ENQ as ACK request (hardcoded in xterm.js) const FLOW_CONTROL_ACK_REQUEST = '\x05'; -// ACK response (answerbackString in xterm.js) -const FLOW_CONTROL_ACK_RESPONSE = '\x06\x06\x06\x06'; +// ACK response +const FLOW_CONTROL_ACK_RESPONSE = '\x06\x06\x06\x06'; // must be in line with answerbackString in xterm.js // send ACK request every n-th bytes const ACK_WATERMARK = 131072; // max allowed pending ACK requests before pausing pty @@ -166,7 +166,7 @@ function startServer() { }); ws.on('message', function(msg) { if (USE_FLOW_CONTROL && msg === FLOW_CONTROL_ACK_RESPONSE) { - ackPending--; + ackPending = Math.max(--ackPending, 0); if (ackPending <= MAX_PENDING_ACK) { term.resume(); } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7a0ca0a8d8..1721c8a1d3 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -211,8 +211,8 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.setExecuteHandler(C1.NEL, () => this.nextLine()); this._parser.setExecuteHandler(C1.HTS, () => this.tabSet()); - // install ENQ handler - disabled by default, set useFlowControl to enable - // this._parser.setExecuteHandler(C0.ENQ, () => this.enquiry()); + // install ENQ handler + this._parser.setExecuteHandler(C0.ENQ, () => this.enquiry()); /** * OSC handler @@ -503,7 +503,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Enquiry (Ctrl-E). */ public enquiry(): void { - this._coreService.triggerDataEvent(this._terminal.options.answerbackString); + if (this._terminal.options.answerbackString) { + this._coreService.triggerDataEvent(this._terminal.options.answerbackString); + } } /** diff --git a/src/Terminal.ts b/src/Terminal.ts index e05e818ced..edc300b536 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -409,6 +409,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.refresh(0, this.rows - 1); } } + break; case 'windowsMode': if (this.optionsService.options.windowsMode) { if (!this._windowsMode) { @@ -421,12 +422,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } } break; - case 'useFlowControl': - if (this.optionsService.options.useFlowControl) { - (this._inputHandler as any)._parser.setExecuteHandler(C0.ENQ, () => (this._inputHandler as any).enquiry()); - } else { - (this._inputHandler as any)._parser.clearExecuteHandler(C0.ENQ); - } } }); } diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index edc2e9dc17..e95860c578 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -46,9 +46,8 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ screenKeys: false, debug: false, cancelEvents: false, - useFlowControl: false, wordSeparator: ' ()[]{}\'"', - answerbackString: '\x06\x06\x06\x06' + answerbackString: '' }); /** diff --git a/src/common/services/Services.d.ts b/src/common/services/Services.d.ts index f46f3d76f3..3a7ebe1937 100644 --- a/src/common/services/Services.d.ts +++ b/src/common/services/Services.d.ts @@ -112,7 +112,7 @@ export interface ITerminalOptions { debug: boolean; screenKeys: boolean; termName: string; - useFlowControl: boolean; + answerbackString: string; } export interface ITheme { diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index f06d003aa3..f3dd47bf43 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -133,8 +133,8 @@ export class Terminal implements ITerminalApi { public writeUtf8(data: Uint8Array): void { this._core.writeUtf8(data); } - public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'rendererType' | 'termName' | 'wordSeparator'): string; - public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean; + public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'rendererType' | 'termName' | 'wordSeparator' | 'answerbackString'): string; + public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'visualBell'): boolean; public getOption(key: 'colors'): string[]; public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; public getOption(key: 'handler'): (data: string) => void; @@ -142,11 +142,11 @@ export class Terminal implements ITerminalApi { public getOption(key: any): any { return this._core.optionsService.getOption(key); } - public setOption(key: 'bellSound' | 'fontFamily' | 'termName' | 'wordSeparator', value: string): void; + public setOption(key: 'bellSound' | 'fontFamily' | 'termName' | 'wordSeparator' | 'answerbackString', value: string): void; public setOption(key: 'fontWeight' | 'fontWeightBold', value: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'): void; public setOption(key: 'bellStyle', value: 'none' | 'visual' | 'sound' | 'both'): void; public setOption(key: 'cursorStyle', value: 'block' | 'underline' | 'bar'): void; - public setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void; + public setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'visualBell', value: boolean): void; public setOption(key: 'colors', value: string[]): void; public setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void; public setOption(key: 'handler', value: (data: string) => void): void; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index c915a4ba71..1b5a70508e 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -183,6 +183,13 @@ declare module 'xterm' { * double click to select work logic. */ wordSeparator?: string; + + /** + * String response to be set on an ENQ request. + * This can be used as a flow control mechanism as the response is generated + * by the terminal parser, thus all previous data is guaranteed to be processed. + */ + answerbackString?: string; } /** @@ -676,12 +683,12 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold'| 'rendererType' | 'termName' | 'wordSeparator'): string; + getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold'| 'rendererType' | 'termName' | 'wordSeparator' | 'answerbackString'): string; /** * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell' | 'windowsMode'): boolean; + getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'visualBell' | 'windowsMode'): boolean; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -708,7 +715,7 @@ declare module 'xterm' { * @param key The option key. * @param value The option value. */ - setOption(key: 'fontFamily' | 'termName' | 'bellSound' | 'wordSeparator', value: string): void; + setOption(key: 'fontFamily' | 'termName' | 'bellSound' | 'wordSeparator' | 'answerbackString', value: string): void; /** * Sets an option on the terminal. * @param key The option key. @@ -732,7 +739,7 @@ declare module 'xterm' { * @param key The option key. * @param value The option value. */ - setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'screenKeys' | 'useFlowControl' | 'visualBell' | 'windowsMode', value: boolean): void; + setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'screenKeys' | 'visualBell' | 'windowsMode', value: boolean): void; /** * Sets an option on the terminal. * @param key The option key. From fa9cba178c517a2d89a313c0330cd546f6974c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 11 Jul 2019 01:55:20 +0200 Subject: [PATCH 05/19] cleanup useFlowControl --- demo/client.ts | 1 - src/Types.d.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index cc6c75b8fd..7efb9ae369 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -224,7 +224,6 @@ function initOptions(term: TerminalType): void { 'handler', 'screenKeys', 'termName', - // 'useFlowControl', // Complex option 'theme' ]; diff --git a/src/Types.d.ts b/src/Types.d.ts index ee6815dae6..8585e6d106 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -288,7 +288,6 @@ export interface ITerminalOptions extends IPublicTerminalOptions { handler?: (data: string) => void; screenKeys?: boolean; termName?: string; - useFlowControl?: boolean; } export interface ILinkifier { From afac52415d66ead8039c012b6a2e1558d3247884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 11 Jul 2019 03:03:57 +0200 Subject: [PATCH 06/19] make switching flow control in demo easier --- demo/client.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 7efb9ae369..9b7eff2e08 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -27,6 +27,20 @@ import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon'; // little weird here as we're importing "this" module import { Terminal as TerminalType, ITerminalOptions } from 'xterm'; + +/** + * Whether to use flow control in the demo. + * This must be in sync with the settings in server.js. + */ +const USE_FLOW_CONTROL = false; + +/** + * Whether to use UTF8 binary transport in the demo. + * (Must also be switched in server.js) + */ +const USE_BINARY_UTF8 = false; + + export interface IWindowWithTerminal extends Window { term: TerminalType; Terminal?: typeof TerminalType; @@ -102,6 +116,7 @@ function createTerminal(): void { const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; term = new Terminal({ + answerbackString: USE_FLOW_CONTROL ? '\x06\x06\x06\x06' : '', windowsMode: isWindows } as ITerminalOptions); @@ -170,14 +185,7 @@ function createTerminal(): void { } function runRealTerminal(): void { - /** - * The demo defaults to string transport by default. - * To run it with UTF8 binary transport, swap comment on - * the lines below. (Must also be switched in server.js) - */ - term.loadAddon(new AttachAddon(socket)); - // term.loadAddon(new AttachAddon(socket, {inputUtf8: true})); - + term.loadAddon(new AttachAddon(socket, {inputUtf8: USE_BINARY_UTF8})); term._initialized = true; } From 8000e336ace46abb4d6f466559edf7e4af59969e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 11 Jul 2019 15:35:45 +0200 Subject: [PATCH 07/19] state tests for ENQ --- src/InputHandler.test.ts | 95 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index b8f94d5bff..98a7ce674f 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -14,6 +14,7 @@ import { Attributes } from 'common/buffer/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; import { Params } from 'common/parser/Params'; import { MockCoreService } from 'common/TestUtils.test'; +import { ParserState } from 'common/parser/Constants'; describe('InputHandler', () => { describe('save and restore cursor', () => { @@ -697,4 +698,98 @@ describe('InputHandler', () => { }); }); }); + describe('ENQ - answerbackString', () => { + function getParserState(term: Terminal): ParserState { + return (term as any)._inputHandler._parser.currentState; + } + let term: TestTerminal; + let response: string[]; + const answer = 'xterm. js is still alive!'; + beforeEach(() => { + term = new TestTerminal(); + term.optionsService.options.answerbackString = answer; + response = []; + term.onData(e => response.push(e)); + }); + it('should respond with answer string', () => { + term.writeSync('some data with \x05in it'); + assert.deepEqual(response, [answer]); + }); + it('should not respond with empty string', () => { + term.optionsService.options.answerbackString = ''; + assert.deepEqual(response, []); + }); + describe('in-depth state testing', () => { + it('GROUND', () => { + term.writeSync('ab\x05'); + assert.deepEqual(response, [answer]); + assert.equal(getParserState(term), ParserState.GROUND); + }); + it('ESCAPE', () => { + term.writeSync('ab\x1b\x05'); + assert.deepEqual(response, [answer]); + assert.equal(getParserState(term), ParserState.ESCAPE); + }); + it('ESCAPE_INTERMEDIATE', () => { + term.writeSync('ab\x1b%\x05'); + assert.deepEqual(response, [answer]); + assert.equal(getParserState(term), ParserState.ESCAPE_INTERMEDIATE); + }); + it('CSI_ENTRY', () => { + term.writeSync('ab\x1b[\x05'); + assert.deepEqual(response, [answer]); + assert.equal(getParserState(term), ParserState.CSI_ENTRY); + }); + it('CSI_PARAM', () => { + term.writeSync('ab\x1b[;\x05'); + assert.deepEqual(response, [answer]); + assert.equal(getParserState(term), ParserState.CSI_PARAM); + }); + it('CSI_INTERMEDIATE', () => { + term.writeSync('ab\x1b[;%\x05'); + assert.deepEqual(response, [answer]); + assert.equal(getParserState(term), ParserState.CSI_INTERMEDIATE); + }); + it('CSI_IGNORE', () => { + term.writeSync('ab\x1b[;%1\x05'); + assert.deepEqual(response, [answer]); + assert.equal(getParserState(term), ParserState.CSI_IGNORE); + }); + it('SOS_PM_APC_STRING', () => { + term.writeSync('ab\x1bX\x05'); + assert.deepEqual(response, []); // C0 ignored here! + assert.equal(getParserState(term), ParserState.SOS_PM_APC_STRING); + }); + it('OSC_STRING', () => { + term.writeSync('ab\x1b]\x05'); + assert.deepEqual(response, []); // C0 ignored here! + assert.equal(getParserState(term), ParserState.OSC_STRING); + }); + it('DCS_ENTRY', () => { + term.writeSync('ab\x1bP\x05'); + assert.deepEqual(response, []); // C0 ignored here! + assert.equal(getParserState(term), ParserState.DCS_ENTRY); + }); + it('DCS_PARAM', () => { + term.writeSync('ab\x1bP;\x05'); + assert.deepEqual(response, []); // C0 ignored here! + assert.equal(getParserState(term), ParserState.DCS_PARAM); + }); + it('DCS_IGNORE', () => { + term.writeSync('ab\x1bP%;\x05'); + assert.deepEqual(response, []); // C0 is ignored here! + assert.equal(getParserState(term), ParserState.DCS_IGNORE); + }); + it('DCS_INTERMEDIATE', () => { + term.writeSync('ab\x1bP%\x05'); + assert.deepEqual(response, []); // C0 is ignored here! + assert.equal(getParserState(term), ParserState.DCS_INTERMEDIATE); + }); + it('DCS_PASSTHROUGH', () => { + term.writeSync('ab\x1bP;p\x05'); + assert.deepEqual(response, []); // C0 is valid payload data here! + assert.equal(getParserState(term), ParserState.DCS_PASSTHROUGH); + }); + }); + }); }); From 8cb010446a5fd1dc8f05cb425bbc20864a3161ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 11 Jul 2019 23:21:14 +0200 Subject: [PATCH 08/19] write callback and promise --- src/Terminal.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index edc300b536..8783f117ff 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -162,6 +162,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // user input states public writeBuffer: string[]; + public writeBufferCallback: (() => void)[] = []; public writeBufferUtf8: Uint8Array[]; private _writeInProgress: boolean; /** @@ -1275,7 +1276,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Writes text to the terminal. * @param data The text to write to the terminal. */ - public write(data: string): void { + public write(data: string, cb?: () => void): void { // Ensure the terminal isn't disposed if (this._isDisposed) { return; @@ -1296,6 +1297,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._writeBuffersPendingSize += data.length; this.writeBuffer.push(data); + this.writeBufferCallback.push(cb); if (!this._writeInProgress && this.writeBuffer.length > 0) { // Kick off a write which will write all data in sequence recursively @@ -1307,6 +1309,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } } + private _stringExecutor = (data: string) => (resolve: () => void) => this.write(data, resolve); + + public writePromise(data: string): Promise { + return new Promise(this._stringExecutor(data)); + } + protected _innerWrite(bufferOffset: number = 0): void { // Ensure the terminal isn't disposed if (this._isDisposed) { @@ -1316,6 +1324,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp const startTime = Date.now(); while (this.writeBuffer.length > bufferOffset) { const data = this.writeBuffer[bufferOffset]; + const cb = this.writeBufferCallback[bufferOffset]; bufferOffset++; this._refreshStart = this.buffer.y; @@ -1323,6 +1332,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._inputHandler.parse(data); this._writeBuffersPendingSize -= data.length; + if (cb) { + cb(); + } this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); @@ -1336,12 +1348,14 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // trim already processed chunks if we are above threshold if (bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) { this.writeBuffer = this.writeBuffer.slice(bufferOffset); + this.writeBufferCallback = this.writeBufferCallback.slice(bufferOffset); bufferOffset = 0; } setTimeout(() => this._innerWrite(bufferOffset), 0); } else { this._writeInProgress = false; this.writeBuffer = []; + this.writeBufferCallback = []; } } From 2a66e4df3c33157a23f9532033bed3ccb10b2eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 11 Jul 2019 23:33:57 +0200 Subject: [PATCH 09/19] simple is better --- src/Terminal.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 8783f117ff..4e7b7b4fff 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1309,10 +1309,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } } - private _stringExecutor = (data: string) => (resolve: () => void) => this.write(data, resolve); - public writePromise(data: string): Promise { - return new Promise(this._stringExecutor(data)); + return new Promise(resolve => this.write(data, resolve)); } protected _innerWrite(bufferOffset: number = 0): void { From 8e48c7946be71fc8d8e26058461e41c069dadae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 12 Jul 2019 23:42:27 +0200 Subject: [PATCH 10/19] ThinProtocol added --- addons/xterm-addon-attach/src/AttachAddon.ts | 9 ++- addons/xterm-addon-attach/src/ThinProtocol.ts | 80 +++++++++++++++++++ demo/server.js | 42 +++------- 3 files changed, 99 insertions(+), 32 deletions(-) create mode 100644 addons/xterm-addon-attach/src/ThinProtocol.ts diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts index 9dc45ecbd3..7cd75b9d2d 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.ts @@ -6,6 +6,7 @@ */ import { Terminal, IDisposable, ITerminalAddon } from 'xterm'; +import { ThinProtocol, MessageType } from './ThinProtocol'; interface IAttachOptions { bidirectional?: boolean; @@ -17,6 +18,7 @@ export class AttachAddon implements ITerminalAddon { private _bidirectional: boolean; private _utf8: boolean; private _disposables: IDisposable[] = []; + private _tp: ThinProtocol = new ThinProtocol(); constructor(socket: WebSocket, options?: IAttachOptions) { this._socket = socket; @@ -41,10 +43,15 @@ export class AttachAddon implements ITerminalAddon { this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose())); this._disposables.push(addSocketListener(this._socket, 'error', () => this.dispose())); + + // test the ACK (heartbeat like) + setInterval(() => this._socket.send(this._tp.ack()), 1000); } public dispose(): void { this._disposables.forEach(d => d.dispose()); + this._tp.clearIncomingHandler(MessageType.DATA); + this._tp.clearIncomingHandler(MessageType.ACK); } private _sendData(data: string): void { @@ -53,7 +60,7 @@ export class AttachAddon implements ITerminalAddon { if (this._socket.readyState !== 1) { return; } - this._socket.send(data); + this._socket.send(this._tp.data(data)); } } diff --git a/addons/xterm-addon-attach/src/ThinProtocol.ts b/addons/xterm-addon-attach/src/ThinProtocol.ts new file mode 100644 index 0000000000..68f5ff8d0d --- /dev/null +++ b/addons/xterm-addon-attach/src/ThinProtocol.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * Message types for ThinProtocol. + * Currently we only support 2 messages types. + * Future versions might extend this by other types + * that dont fit into DATA (like resize or mouse reports). + */ +export enum MessageType { + /** Plain data, no further handling. */ + DATA = 0, + /** ACK, sent for every n-th byte finally processed. */ + ACK +} + +/** + * ThinProtocol + * + * Usage: + * ```typescript + * const tp = new ThinProtocol(); + * // attach incoming data handler + * tp.setIncomingHandler(MessageType.DATA, ); + * // attach incoming ACK handler + * tp.setIncomingHandler(MessageType.ACK, ); + * ... + * // Reading: + * // To strip and route raw messages feed the chunk into unwrap, + * // which will call the registered incoming handler. + * tp.unwrap(); + * ... + * // Writing: + * // Either call wrap with message type and payload, + * // or use the convenient functions: + * chunk = tp.wrap(type, payload); + * chunk = tp.data(payload); // create a DATA chunk + * chunk = tp.ack(); // create an ACK chunk + * ``` + */ +export class ThinProtocol { + private _handlers: (((data:string) => void) | null)[] = new Array(Object.keys(MessageType).length); + + /** Register a handler for `type`. */ + public setIncomingHandler(type: MessageType, cb: (data:string) => void): void { + this._handlers[type] = cb; + } + /** Remove handler for `type`. */ + public clearIncomingHandler(type: MessageType): void { + this._handlers[type] = null; + } + + /** Process incoming message and call associated handler. */ + public unwrap(msg: string): void { + let handler: ((data:string) => void) | null; + if (msg && (handler = this._handlers[msg.charCodeAt(0)])) { + handler(msg.slice(1)); + } + } + + /** Create new message of `type`. */ + public wrap(type: MessageType, payload?: string): string { + if (payload) { + return String.fromCharCode(type) + payload; + } + return String.fromCharCode(type); + } + + /** Create a plain ACK message (no payload). */ + public ack(): string { + return this.wrap(MessageType.ACK); + } + + /** Create a DATA message. */ + public data(data: string): string { + return this.wrap(MessageType.DATA, data); + } +} diff --git a/demo/server.js b/demo/server.js index a3dd3e10a3..1150cca618 100644 --- a/demo/server.js +++ b/demo/server.js @@ -2,6 +2,8 @@ var express = require('express'); var expressWs = require('express-ws'); var os = require('os'); var pty = require('node-pty'); +var ThinProtocol = require('../addons/xterm-addon-attach/out/ThinProtocol').ThinProtocol; +var MessageType = require('../addons/xterm-addon-attach/out/ThinProtocol').MessageType; /** * Whether to use UTF8 binary transport. @@ -15,10 +17,6 @@ const USE_BINARY_UTF8 = false; */ const USE_FLOW_CONTROL = false; -// send ENQ as ACK request (hardcoded in xterm.js) -const FLOW_CONTROL_ACK_REQUEST = '\x05'; -// ACK response -const FLOW_CONTROL_ACK_RESPONSE = '\x06\x06\x06\x06'; // must be in line with answerbackString in xterm.js // send ACK request every n-th bytes const ACK_WATERMARK = 131072; // max allowed pending ACK requests before pausing pty @@ -147,33 +145,15 @@ function startServer() { } const send = (USE_BINARY_UTF8 ? bufferUtf8 : buffer)(MAX_SEND_INTERVAL, MAX_CHUNK_SIZE); - let ackPending = 0; - let bytesSent = 0; - - term.on('data', function(data) { - send(data); - if (USE_FLOW_CONTROL) { - bytesSent += data.length; - if (bytesSent > ACK_WATERMARK) { - send(FLOW_CONTROL_ACK_REQUEST); - ackPending++; - bytesSent = 0; - if (ackPending > MAX_PENDING_ACK) { - term.pause(); - } - } - } - }); - ws.on('message', function(msg) { - if (USE_FLOW_CONTROL && msg === FLOW_CONTROL_ACK_RESPONSE) { - ackPending = Math.max(--ackPending, 0); - if (ackPending <= MAX_PENDING_ACK) { - term.resume(); - } - return; - } - term.write(msg); - }); + // set up the thin protocol to receive ACKs + const tp = new ThinProtocol(); + // we do a one sided protocol usage and only read in server part + tp.setIncomingHandler(MessageType.DATA, msg => term.write(msg)); + tp.setIncomingHandler(MessageType.ACK, msg => console.log('ACK', msg)); + + term.on('data', data => send(data)); + ws.on('message', msg => tp.unwrap(msg)); + ws.on('close', function () { term.kill(); console.log('Closed terminal ' + term.pid); From b7f311b8269b6991ab3d238f2bd4fdf791d4a139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 13 Jul 2019 00:42:23 +0200 Subject: [PATCH 11/19] flow control with ACK reports --- addons/xterm-addon-attach/src/AttachAddon.ts | 28 ++++++++++-- demo/server.js | 48 +++++++++++++++----- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts index 7cd75b9d2d..37f3a8bbbe 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.ts @@ -19,6 +19,8 @@ export class AttachAddon implements ITerminalAddon { private _utf8: boolean; private _disposables: IDisposable[] = []; private _tp: ThinProtocol = new ThinProtocol(); + private _ack_watermark = 131072; + private _bytes_seen = 0; constructor(socket: WebSocket, options?: IAttachOptions) { this._socket = socket; @@ -30,11 +32,27 @@ export class AttachAddon implements ITerminalAddon { public activate(terminal: Terminal): void { if (this._utf8) { - this._disposables.push(addSocketListener(this._socket, 'message', - (ev: MessageEvent | Event | CloseEvent) => terminal.writeUtf8(new Uint8Array((ev as any).data as ArrayBuffer)))); + this._disposables.push( + addSocketListener(this._socket, 'message', + (ev: MessageEvent | Event | CloseEvent) => { + terminal.writeUtf8(new Uint8Array((ev as MessageEvent).data as ArrayBuffer)); + } + ) + ); } else { - this._disposables.push(addSocketListener(this._socket, 'message', - (ev: MessageEvent | Event | CloseEvent) => terminal.write((ev as any).data as string))); + this._disposables.push( + addSocketListener(this._socket, 'message', + (ev: MessageEvent | Event | CloseEvent) => { + this._bytes_seen += (ev as MessageEvent).data.length; + if (this._bytes_seen > this._ack_watermark) { + (terminal as any)._core.write((ev as MessageEvent).data as string, () => this._socket.send(this._tp.ack())); + this._bytes_seen = 0; + } else { + terminal.write((ev as MessageEvent).data as string); + } + } + ) + ); } if (this._bidirectional) { @@ -45,7 +63,7 @@ export class AttachAddon implements ITerminalAddon { this._disposables.push(addSocketListener(this._socket, 'error', () => this.dispose())); // test the ACK (heartbeat like) - setInterval(() => this._socket.send(this._tp.ack()), 1000); + // setInterval(() => this._socket.send(this._tp.ack()), 1000); } public dispose(): void { diff --git a/demo/server.js b/demo/server.js index 1150cca618..aa80fe65e8 100644 --- a/demo/server.js +++ b/demo/server.js @@ -13,14 +13,15 @@ const USE_BINARY_UTF8 = false; /** * Whether to use flow control. - * This must be in sync with answerbackString in xterm.js. */ -const USE_FLOW_CONTROL = false; +const USE_FLOW_CONTROL = true; -// send ACK request every n-th bytes -const ACK_WATERMARK = 131072; +// expect ACK every n-th bytes +const ACK_WATERMARK = 131072; // must be in line with attach addon setting! // max allowed pending ACK requests before pausing pty -const MAX_PENDING_ACK = 4; +const MAX_PENDING_ACK = 5; +// min pending ACK before resming pty +const MIN_PENDING_ACK = 3; // settings for prebuffering const MAX_SEND_INTERVAL = 5; @@ -87,12 +88,42 @@ function startServer() { var term = terminals[parseInt(req.params.pid)]; console.log('Connected to terminal ' + term.pid); + // set up the thin protocol to receive ACKs + const tp = new ThinProtocol(); + // we do a one sided protocol usage and only read in server part + tp.setIncomingHandler(MessageType.DATA, msg => term.write(msg)); + tp.setIncomingHandler(MessageType.ACK, () => { + if (USE_FLOW_CONTROL) { + if (pending_acks === MIN_PENDING_ACK + 1) { + term.resume(); + } + pending_acks = Math.max(--pending_acks, 0); + } + }); + + // incomming chunks are routed through thin protocol to separate DATA from ACK + ws.on('message', msg => tp.unwrap(msg)); + + let pending_acks = 0; + let bytes_sent = 0; + + // final ws send call, also does the flow control const _send = data => { // handle only 'open' websocket state if (ws.readyState === 1) { // test high latency // setTimeout(() => ws.send(data), 250); ws.send(data); + if (USE_FLOW_CONTROL) { + bytes_sent += data .length; + if (bytes_sent > ACK_WATERMARK) { + pending_acks++; + bytes_sent = 0; + if (pending_acks > MAX_PENDING_ACK) { + term.pause(); + } + } + } } } @@ -145,14 +176,7 @@ function startServer() { } const send = (USE_BINARY_UTF8 ? bufferUtf8 : buffer)(MAX_SEND_INTERVAL, MAX_CHUNK_SIZE); - // set up the thin protocol to receive ACKs - const tp = new ThinProtocol(); - // we do a one sided protocol usage and only read in server part - tp.setIncomingHandler(MessageType.DATA, msg => term.write(msg)); - tp.setIncomingHandler(MessageType.ACK, msg => console.log('ACK', msg)); - term.on('data', data => send(data)); - ws.on('message', msg => tp.unwrap(msg)); ws.on('close', function () { term.kill(); From 9bef06bd6c9b098adae2ada52d76e415b1289fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 13 Jul 2019 14:22:34 +0200 Subject: [PATCH 12/19] harmonize server.js & client.ts --- addons/xterm-addon-attach/src/AttachAddon.ts | 18 ++++++---- .../typings/xterm-addon-attach.d.ts | 7 ++++ demo/client.ts | 11 +++--- demo/server.js | 35 +++++++++++++------ 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts index 37f3a8bbbe..9300307c5c 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.ts @@ -11,6 +11,7 @@ import { ThinProtocol, MessageType } from './ThinProtocol'; interface IAttachOptions { bidirectional?: boolean; inputUtf8?: boolean; + flowControl?: number; } export class AttachAddon implements ITerminalAddon { @@ -19,7 +20,7 @@ export class AttachAddon implements ITerminalAddon { private _utf8: boolean; private _disposables: IDisposable[] = []; private _tp: ThinProtocol = new ThinProtocol(); - private _ack_watermark = 131072; + private _flowControl = 0; private _bytes_seen = 0; constructor(socket: WebSocket, options?: IAttachOptions) { @@ -28,6 +29,7 @@ export class AttachAddon implements ITerminalAddon { this._socket.binaryType = 'arraybuffer'; this._bidirectional = (options && options.bidirectional === false) ? false : true; this._utf8 = !!(options && options.inputUtf8); + this._flowControl = (options && options.flowControl) ? Math.max(options.flowControl, 0) : 0; } public activate(terminal: Terminal): void { @@ -40,11 +42,11 @@ export class AttachAddon implements ITerminalAddon { ) ); } else { - this._disposables.push( - addSocketListener(this._socket, 'message', + this._disposables.push(this._flowControl + ? addSocketListener(this._socket, 'message', (ev: MessageEvent | Event | CloseEvent) => { this._bytes_seen += (ev as MessageEvent).data.length; - if (this._bytes_seen > this._ack_watermark) { + if (this._bytes_seen > this._flowControl) { (terminal as any)._core.write((ev as MessageEvent).data as string, () => this._socket.send(this._tp.ack())); this._bytes_seen = 0; } else { @@ -52,6 +54,11 @@ export class AttachAddon implements ITerminalAddon { } } ) + : addSocketListener(this._socket, 'message', + (ev: MessageEvent | Event | CloseEvent) => { + terminal.write((ev as MessageEvent).data as string); + } + ) ); } @@ -61,9 +68,6 @@ export class AttachAddon implements ITerminalAddon { this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose())); this._disposables.push(addSocketListener(this._socket, 'error', () => this.dispose())); - - // test the ACK (heartbeat like) - // setInterval(() => this._socket.send(this._tp.ack()), 1000); } public dispose(): void { diff --git a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts index 8c66714089..df37d47eec 100644 --- a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts +++ b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts @@ -19,6 +19,13 @@ declare module 'xterm-addon-attach' { * otherwise always binary UTF8 data. */ inputUtf8?: boolean; + + /** + * Whether to use flow control. + * Set this to a positive number to send an ACK reply every n-th processed byte. + * Default is 0 (flow control disabled). + */ + flowControl?: number; } export class AttachAddon implements ITerminalAddon { diff --git a/demo/client.ts b/demo/client.ts index 9b7eff2e08..0d39f09609 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -30,9 +30,13 @@ import { Terminal as TerminalType, ITerminalOptions } from 'xterm'; /** * Whether to use flow control in the demo. - * This must be in sync with the settings in server.js. + * Setting this to a positive number will send an ACK reply + * to the backend for every n-th processed byte. The backend + * keeps tracks of this to decide whether xterm.js is to far behind + * and will pause/resume the pty accordingly. + * Caveat: This number must be in line with the setting in server.js! */ -const USE_FLOW_CONTROL = false; +const FLOW_CONTROL = 131072; /** * Whether to use UTF8 binary transport in the demo. @@ -116,7 +120,6 @@ function createTerminal(): void { const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; term = new Terminal({ - answerbackString: USE_FLOW_CONTROL ? '\x06\x06\x06\x06' : '', windowsMode: isWindows } as ITerminalOptions); @@ -185,7 +188,7 @@ function createTerminal(): void { } function runRealTerminal(): void { - term.loadAddon(new AttachAddon(socket, {inputUtf8: USE_BINARY_UTF8})); + term.loadAddon(new AttachAddon(socket, {inputUtf8: USE_BINARY_UTF8, flowControl: FLOW_CONTROL})); term._initialized = true; } diff --git a/demo/server.js b/demo/server.js index aa80fe65e8..4ca43f100d 100644 --- a/demo/server.js +++ b/demo/server.js @@ -13,11 +13,15 @@ const USE_BINARY_UTF8 = false; /** * Whether to use flow control. + * Setting this to a positive number will install some bookkeeping + * about sent bytes and wait for ACK responses from the frontend. + * If the pending ACK counter hits MAX_PENDING_ACK the pty will be paused + * (indicating that the frontend is to far behind) and resumed once + * the pending ACKs drop below MIN_PENDING_ACK. + * Caveat: This number must be in line with the setting in client.ts! */ -const USE_FLOW_CONTROL = true; +const FLOW_CONTROL = 131072; -// expect ACK every n-th bytes -const ACK_WATERMARK = 131072; // must be in line with attach addon setting! // max allowed pending ACK requests before pausing pty const MAX_PENDING_ACK = 5; // min pending ACK before resming pty @@ -88,13 +92,20 @@ function startServer() { var term = terminals[parseInt(req.params.pid)]; console.log('Connected to terminal ' + term.pid); - // set up the thin protocol to receive ACKs + /** + * ThinProtocol + * The procotol allows to send different message types in-band. + * We use it here to separate incoming normal DATA messages from ACK replies. + * In the demo the protocol is only used for incoming data + * (one sided, outgoing data is kept as plain data stream). + */ const tp = new ThinProtocol(); - // we do a one sided protocol usage and only read in server part + // route DATA messages to pty tp.setIncomingHandler(MessageType.DATA, msg => term.write(msg)); + // do flow control with ACK replies tp.setIncomingHandler(MessageType.ACK, () => { - if (USE_FLOW_CONTROL) { - if (pending_acks === MIN_PENDING_ACK + 1) { + if (FLOW_CONTROL) { + if (pending_acks === MIN_PENDING_ACK) { term.resume(); } pending_acks = Math.max(--pending_acks, 0); @@ -111,12 +122,12 @@ function startServer() { const _send = data => { // handle only 'open' websocket state if (ws.readyState === 1) { - // test high latency + // swap comments to test high latency // setTimeout(() => ws.send(data), 250); ws.send(data); - if (USE_FLOW_CONTROL) { + if (FLOW_CONTROL) { bytes_sent += data .length; - if (bytes_sent > ACK_WATERMARK) { + if (bytes_sent > FLOW_CONTROL) { pending_acks++; bytes_sent = 0; if (pending_acks > MAX_PENDING_ACK) { @@ -128,7 +139,9 @@ function startServer() { } /** - * message buffering - limits are MAX_SEND_INTERVAL and MAX_CHUNK_SIZE + * message prebuffering - limits are MAX_SEND_INTERVAL and MAX_CHUNK_SIZE + * This is needed to reduce pressure on the websocket by chaining very small + * chunks into bigger ones. */ // string message function buffer(timeout, limit) { From 8e9ab0042c9043a119a8a7aaa7244751f099dbab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 13 Jul 2019 14:31:26 +0200 Subject: [PATCH 13/19] add protocol to d.ts --- .../typings/xterm-addon-attach.d.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts index df37d47eec..77994416aa 100644 --- a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts +++ b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts @@ -33,4 +33,38 @@ declare module 'xterm-addon-attach' { public activate(terminal: Terminal): void; public dispose(): void; } + + /** + * Message types for ThinProtocol. + */ + export enum MessageType { + /** Plain data, no further handling. */ + DATA = 0, + /** ACK, sent for every n-th byte finally processed. */ + ACK + } + + /** + * ThinProtocol + * Thin protocol to send different message types in-band. + */ + export class ThinProtocol { + /** Register a handler for `type`. */ + public setIncomingHandler(type: MessageType, cb: (data:string) => void): void; + + /** Remove handler for `type`. */ + public clearIncomingHandler(type: MessageType): void; + + /** Process incoming message and call associated handler. */ + public unwrap(msg: string): void; + + /** Create new message of `type`. */ + public wrap(type: MessageType, payload?: string): string; + + /** Convenient method to create a plain ACK message (no payload). */ + public ack(): string; + + /** Convenient method to create a DATA message. */ + public data(data: string): string; + } } From 0d58b6cad266c4e8cb4a518b3f518d4594b3e2b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 13 Jul 2019 14:41:59 +0200 Subject: [PATCH 14/19] apply cb to utf8 write --- addons/xterm-addon-attach/src/AttachAddon.ts | 30 +++++++++++-------- addons/xterm-addon-attach/src/ThinProtocol.ts | 8 ++--- .../typings/xterm-addon-attach.d.ts | 10 +++---- src/Terminal.ts | 14 +++++---- src/TestUtils.test.ts | 4 +-- src/Types.d.ts | 4 +-- src/public/Terminal.ts | 8 ++--- typings/xterm.d.ts | 6 ++-- 8 files changed, 48 insertions(+), 36 deletions(-) diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts index 9300307c5c..a436e5457c 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.ts @@ -21,7 +21,7 @@ export class AttachAddon implements ITerminalAddon { private _disposables: IDisposable[] = []; private _tp: ThinProtocol = new ThinProtocol(); private _flowControl = 0; - private _bytes_seen = 0; + private _bytesSeen = 0; constructor(socket: WebSocket, options?: IAttachOptions) { this._socket = socket; @@ -34,31 +34,37 @@ export class AttachAddon implements ITerminalAddon { public activate(terminal: Terminal): void { if (this._utf8) { - this._disposables.push( - addSocketListener(this._socket, 'message', + this._disposables.push(this._flowControl + ? addSocketListener(this._socket, 'message', (ev: MessageEvent | Event | CloseEvent) => { - terminal.writeUtf8(new Uint8Array((ev as MessageEvent).data as ArrayBuffer)); + const bytes = new Uint8Array((ev as MessageEvent).data as ArrayBuffer); + this._bytesSeen += bytes.length; + if (this._bytesSeen > this._flowControl) { + terminal.writeUtf8(bytes, () => this._socket.send(this._tp.ack())); + this._bytesSeen = 0; + } else { + terminal.writeUtf8(bytes); + } } ) + : addSocketListener(this._socket, 'message', + (ev: MessageEvent | Event | CloseEvent) => terminal.writeUtf8(new Uint8Array((ev as MessageEvent).data as ArrayBuffer))) ); } else { this._disposables.push(this._flowControl ? addSocketListener(this._socket, 'message', (ev: MessageEvent | Event | CloseEvent) => { - this._bytes_seen += (ev as MessageEvent).data.length; - if (this._bytes_seen > this._flowControl) { - (terminal as any)._core.write((ev as MessageEvent).data as string, () => this._socket.send(this._tp.ack())); - this._bytes_seen = 0; + this._bytesSeen += (ev as MessageEvent).data.length; + if (this._bytesSeen > this._flowControl) { + terminal.write((ev as MessageEvent).data as string, () => this._socket.send(this._tp.ack())); + this._bytesSeen = 0; } else { terminal.write((ev as MessageEvent).data as string); } } ) : addSocketListener(this._socket, 'message', - (ev: MessageEvent | Event | CloseEvent) => { - terminal.write((ev as MessageEvent).data as string); - } - ) + (ev: MessageEvent | Event | CloseEvent) => terminal.write((ev as MessageEvent).data as string)) ); } diff --git a/addons/xterm-addon-attach/src/ThinProtocol.ts b/addons/xterm-addon-attach/src/ThinProtocol.ts index 68f5ff8d0d..33b5845ea4 100644 --- a/addons/xterm-addon-attach/src/ThinProtocol.ts +++ b/addons/xterm-addon-attach/src/ThinProtocol.ts @@ -18,7 +18,7 @@ export enum MessageType { /** * ThinProtocol - * + * * Usage: * ```typescript * const tp = new ThinProtocol(); @@ -41,10 +41,10 @@ export enum MessageType { * ``` */ export class ThinProtocol { - private _handlers: (((data:string) => void) | null)[] = new Array(Object.keys(MessageType).length); + private _handlers: (((data: string) => void) | null)[] = new Array(Object.keys(MessageType).length); /** Register a handler for `type`. */ - public setIncomingHandler(type: MessageType, cb: (data:string) => void): void { + public setIncomingHandler(type: MessageType, cb: (data: string) => void): void { this._handlers[type] = cb; } /** Remove handler for `type`. */ @@ -54,7 +54,7 @@ export class ThinProtocol { /** Process incoming message and call associated handler. */ public unwrap(msg: string): void { - let handler: ((data:string) => void) | null; + let handler: ((data: string) => void) | null; if (msg && (handler = this._handlers[msg.charCodeAt(0)])) { handler(msg.slice(1)); } diff --git a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts index 77994416aa..f2266a4a41 100644 --- a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts +++ b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts @@ -50,20 +50,20 @@ declare module 'xterm-addon-attach' { */ export class ThinProtocol { /** Register a handler for `type`. */ - public setIncomingHandler(type: MessageType, cb: (data:string) => void): void; + public setIncomingHandler(type: MessageType, cb: (data: string) => void): void; /** Remove handler for `type`. */ public clearIncomingHandler(type: MessageType): void; - + /** Process incoming message and call associated handler. */ public unwrap(msg: string): void; - + /** Create new message of `type`. */ public wrap(type: MessageType, payload?: string): string; - + /** Convenient method to create a plain ACK message (no payload). */ public ack(): string; - + /** Convenient method to create a DATA message. */ public data(data: string): string; } diff --git a/src/Terminal.ts b/src/Terminal.ts index 4e7b7b4fff..465831e49a 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -164,6 +164,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public writeBuffer: string[]; public writeBufferCallback: (() => void)[] = []; public writeBufferUtf8: Uint8Array[]; + public writeBufferUtf8Callback: (() => void)[] = []; private _writeInProgress: boolean; /** * Sum of length of pending chunks in all write buffers. @@ -1202,7 +1203,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Writes raw utf8 bytes to the terminal. * @param data UintArray with UTF8 bytes to write to the terminal. */ - public writeUtf8(data: Uint8Array): void { + public writeUtf8(data: Uint8Array, cb?: () => void): void { // Ensure the terminal isn't disposed if (this._isDisposed) { return; @@ -1223,6 +1224,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._writeBuffersPendingSize += data.length; this.writeBufferUtf8.push(data); + this.writeBufferUtf8Callback.push(cb); if (!this._writeInProgress && this.writeBufferUtf8.length > 0) { // Kick off a write which will write all data in sequence recursively @@ -1243,6 +1245,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp const startTime = Date.now(); while (this.writeBufferUtf8.length > bufferOffset) { const data = this.writeBufferUtf8[bufferOffset]; + const cb = this.writeBufferUtf8Callback[bufferOffset]; bufferOffset++; this._refreshStart = this.buffer.y; @@ -1250,6 +1253,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._inputHandler.parseUtf8(data); this._writeBuffersPendingSize -= data.length; + if (cb) { + cb(); + } this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); @@ -1263,12 +1269,14 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // trim already processed chunks if we are above threshold if (bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) { this.writeBufferUtf8 = this.writeBufferUtf8.slice(bufferOffset); + this.writeBufferUtf8Callback = this.writeBufferUtf8Callback.slice(bufferOffset); bufferOffset = 0; } setTimeout(() => this._innerWriteUtf8(bufferOffset), 0); } else { this._writeInProgress = false; this.writeBufferUtf8 = []; + this.writeBufferUtf8Callback = []; } } @@ -1309,10 +1317,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } } - public writePromise(data: string): Promise { - return new Promise(resolve => this.write(data, resolve)); - } - protected _innerWrite(bufferOffset: number = 0): void { // Ensure the terminal isn't disposed if (this._isDisposed) { diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 62ae609e96..bc50cc26b8 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -117,10 +117,10 @@ export class MockTerminal implements ITerminal { clear(): void { throw new Error('Method not implemented.'); } - write(data: string): void { + write(data: string, cb?: () => void): void { throw new Error('Method not implemented.'); } - writeUtf8(data: Uint8Array): void { + writeUtf8(data: Uint8Array, cb?: () => void): void { throw new Error('Method not implemented.'); } bracketedPasteMode: boolean; diff --git a/src/Types.d.ts b/src/Types.d.ts index 8585e6d106..f8fc4dead6 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -261,8 +261,8 @@ export interface IPublicTerminal extends IDisposable { scrollToBottom(): void; scrollToLine(line: number): void; clear(): void; - write(data: string): void; - writeUtf8(data: Uint8Array): void; + write(data: string, cb?: () => void): void; + writeUtf8(data: Uint8Array, cb?: () => void): void; refresh(start: number, end: number): void; reset(): void; } diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index f3dd47bf43..79ddb4b1ba 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -127,11 +127,11 @@ export class Terminal implements ITerminalApi { public clear(): void { this._core.clear(); } - public write(data: string): void { - this._core.write(data); + public write(data: string, cb?: () => void): void { + this._core.write(data, cb); } - public writeUtf8(data: Uint8Array): void { - this._core.writeUtf8(data); + public writeUtf8(data: Uint8Array, cb?: () => void): void { + this._core.writeUtf8(data, cb); } public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'rendererType' | 'termName' | 'wordSeparator' | 'answerbackString'): string; public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'visualBell'): boolean; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 1b5a70508e..41b2ffe00d 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -662,8 +662,9 @@ declare module 'xterm' { /** * Writes text to the terminal. * @param data The text to write to the terminal. + * @param cb Optional callback, called once the input was parsed. */ - write(data: string): void; + write(data: string, cb?: () => void): void; /** * Writes text to the terminal, followed by a break line character (\n). @@ -676,8 +677,9 @@ declare module 'xterm' { * over the string based write method due to lesser data conversions needed * on the way from the pty to xterm.js. * @param data The data to write to the terminal. + * @param cb Optional callback, called once the input was parsed. */ - writeUtf8(data: Uint8Array): void; + writeUtf8(data: Uint8Array, cb?: () => void): void; /** * Retrieves an option's value from the terminal. From 833563c09544ae09062d82c53547455ba99a1673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 13 Jul 2019 15:29:31 +0200 Subject: [PATCH 15/19] add cb to writeln --- src/Terminal.ts | 4 ++-- src/TestUtils.test.ts | 2 +- src/Types.d.ts | 2 +- src/public/Terminal.ts | 4 ++-- typings/xterm.d.ts | 6 ++++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 465831e49a..99dcd50638 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1365,8 +1365,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Writes text to the terminal, followed by a break line character (\n). * @param data The text to write to the terminal. */ - public writeln(data: string): void { - this.write(data + '\r\n'); + public writeln(data: string, cb?: () => void): void { + this.write(data + '\r\n', cb); } /** diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index bc50cc26b8..02e6316668 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -63,7 +63,7 @@ export class MockTerminal implements ITerminal { resize(columns: number, rows: number): void { throw new Error('Method not implemented.'); } - writeln(data: string): void { + writeln(data: string, cb?: () => void): void { throw new Error('Method not implemented.'); } open(parent: HTMLElement): void { diff --git a/src/Types.d.ts b/src/Types.d.ts index f8fc4dead6..89b2f73e89 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -237,7 +237,7 @@ export interface IPublicTerminal extends IDisposable { blur(): void; focus(): void; resize(columns: number, rows: number): void; - writeln(data: string): void; + writeln(data: string, cb?: () => void): void; open(parent: HTMLElement): void; attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 79ddb4b1ba..1ef79e3720 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -48,8 +48,8 @@ export class Terminal implements ITerminalApi { this._verifyIntegers(columns, rows); this._core.resize(columns, rows); } - public writeln(data: string): void { - this._core.writeln(data); + public writeln(data: string, cb?: () => void): void { + this._core.writeln(data, cb); } public open(parent: HTMLElement): void { this._core.open(parent); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 41b2ffe00d..343d17e2a0 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -667,10 +667,12 @@ declare module 'xterm' { write(data: string, cb?: () => void): void; /** - * Writes text to the terminal, followed by a break line character (\n). + * Writes data to the terminal appending a carriage return and line feed (\r\n), + * thus marking the end of data as as line break. * @param data The text to write to the terminal. + * @param cb Optional callback, called once the input was parsed. */ - writeln(data: string): void; + writeln(data: string, cb?: () => void): void; /** * Writes UTF8 data to the terminal. This has a slight performance advantage From 92578183c17c66f12381feea4fe1b5e185941e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 21 Jul 2019 00:14:58 +0200 Subject: [PATCH 16/19] apply option changes --- src/common/services/Services.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 1af55e9ad9..6ba8ea58b4 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -196,13 +196,13 @@ export interface ITerminalOptions { theme: ITheme; windowsMode: boolean; wordSeparator: string; + answerbackString: string; [key: string]: any; cancelEvents: boolean; convertEol: boolean; screenKeys: boolean; termName: string; - useFlowControl: boolean; } export interface ITheme { From eed28063af51b5128e952d2b466f5a37cfa00ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 21 Jul 2019 01:29:41 +0200 Subject: [PATCH 17/19] thin protocol with binary message option --- addons/xterm-addon-attach/src/AttachAddon.ts | 7 +++ addons/xterm-addon-attach/src/ThinProtocol.ts | 48 +++++++++++++++++-- .../typings/xterm-addon-attach.d.ts | 4 +- demo/server.js | 5 ++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts index a436e5457c..81cb7d5770 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.ts @@ -74,6 +74,13 @@ export class AttachAddon implements ITerminalAddon { this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose())); this._disposables.push(addSocketListener(this._socket, 'error', () => this.dispose())); + + // test binary + let counter = 0; + setInterval(() => { + counter = (counter + 1) & 255; + this._socket.send(this._tp.binary(String.fromCharCode(counter))); + }, 100); } public dispose(): void { diff --git a/addons/xterm-addon-attach/src/ThinProtocol.ts b/addons/xterm-addon-attach/src/ThinProtocol.ts index 33b5845ea4..91df1bf06e 100644 --- a/addons/xterm-addon-attach/src/ThinProtocol.ts +++ b/addons/xterm-addon-attach/src/ThinProtocol.ts @@ -13,9 +13,32 @@ export enum MessageType { /** Plain data, no further handling. */ DATA = 0, /** ACK, sent for every n-th byte finally processed. */ - ACK + ACK, + /** + * By default, DATA will be treated as UTF8 data, To be able to send + * raw non UTF-8 conform byte data, use the BINARY type. + * Currently only the string version of the protocol is implemented + * which uses BASE64 transport encoding for binary data. + * A later binary version of the protocol might skip the base64 step. + */ + BINARY } + +/** + * Get base64 encoder / decoder for binary messages. + */ +const _global = Function('return this')(); + +const base64Encode = (_global.btoa !== undefined) + ? btoa : (_global.Buffer !== undefined) + ? (data: string) => Buffer.from(data, 'binary').toString('base64') : null; + +const base64Decode = (_global.atob !== undefined) + ? atob : (_global.Buffer !== undefined) + ? (data: string) => Buffer.from(data, 'base64').toString('binary') : null; + + /** * ThinProtocol * @@ -55,14 +78,28 @@ export class ThinProtocol { /** Process incoming message and call associated handler. */ public unwrap(msg: string): void { let handler: ((data: string) => void) | null; - if (msg && (handler = this._handlers[msg.charCodeAt(0)])) { - handler(msg.slice(1)); + let type: MessageType = msg.charCodeAt(0); + if (msg && (handler = this._handlers[type])) { + if (type === MessageType.BINARY) { + if (!base64Encode || !base64Decode) { + throw new Error('binary messages not working - missing base64 support'); + } + handler(base64Decode(msg.slice(1))); + } else { + handler(msg.slice(1)); + } } } /** Create new message of `type`. */ public wrap(type: MessageType, payload?: string): string { if (payload) { + if (type === MessageType.BINARY) { + if (!base64Encode || !base64Decode) { + throw new Error('binary messages not working - missing base64 support'); + } + payload = base64Encode(payload); + } return String.fromCharCode(type) + payload; } return String.fromCharCode(type); @@ -77,4 +114,9 @@ export class ThinProtocol { public data(data: string): string { return this.wrap(MessageType.DATA, data); } + + /** Create a BINARY message. */ + public binary(data: string): string { + return this.wrap(MessageType.BINARY, data); + } } diff --git a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts index f2266a4a41..e63d0f3905 100644 --- a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts +++ b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts @@ -41,7 +41,9 @@ declare module 'xterm-addon-attach' { /** Plain data, no further handling. */ DATA = 0, /** ACK, sent for every n-th byte finally processed. */ - ACK + ACK, + /** Raw binary data (uses base64 for transmission) */ + BINARY } /** diff --git a/demo/server.js b/demo/server.js index 4ca43f100d..c670edfaac 100644 --- a/demo/server.js +++ b/demo/server.js @@ -111,6 +111,11 @@ function startServer() { pending_acks = Math.max(--pending_acks, 0); } }); + // digest binary data + tp.setIncomingHandler(MessageType.BINARY, msg => { + // term.write(Buffer.from(msg, 'binary')); + console.log(Buffer.from(msg, 'binary')); + }); // incomming chunks are routed through thin protocol to separate DATA from ACK ws.on('message', msg => tp.unwrap(msg)); From 5a85eefc35b330e022e4da111efb99770efff30c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 21 Jul 2019 01:36:48 +0200 Subject: [PATCH 18/19] make linter happy --- addons/xterm-addon-attach/src/ThinProtocol.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-attach/src/ThinProtocol.ts b/addons/xterm-addon-attach/src/ThinProtocol.ts index 91df1bf06e..0846e6acd1 100644 --- a/addons/xterm-addon-attach/src/ThinProtocol.ts +++ b/addons/xterm-addon-attach/src/ThinProtocol.ts @@ -28,14 +28,14 @@ export enum MessageType { /** * Get base64 encoder / decoder for binary messages. */ -const _global = Function('return this')(); +const globalObject = Function('return this')(); -const base64Encode = (_global.btoa !== undefined) - ? btoa : (_global.Buffer !== undefined) +const base64Encode = (globalObject.btoa !== undefined) + ? btoa : (globalObject.Buffer !== undefined) ? (data: string) => Buffer.from(data, 'binary').toString('base64') : null; -const base64Decode = (_global.atob !== undefined) - ? atob : (_global.Buffer !== undefined) +const base64Decode = (globalObject.atob !== undefined) + ? atob : (globalObject.Buffer !== undefined) ? (data: string) => Buffer.from(data, 'base64').toString('binary') : null; @@ -78,7 +78,7 @@ export class ThinProtocol { /** Process incoming message and call associated handler. */ public unwrap(msg: string): void { let handler: ((data: string) => void) | null; - let type: MessageType = msg.charCodeAt(0); + const type: MessageType = msg.charCodeAt(0); if (msg && (handler = this._handlers[type])) { if (type === MessageType.BINARY) { if (!base64Encode || !base64Decode) { From cec77695bcd200144a98a0039139de065189bf67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 21 Jul 2019 04:07:35 +0200 Subject: [PATCH 19/19] binary version of the protocol --- addons/xterm-addon-attach/src/AttachAddon.ts | 22 ++++--- addons/xterm-addon-attach/src/ThinProtocol.ts | 65 ++++++++++++++++--- .../typings/xterm-addon-attach.d.ts | 32 ++++++++- demo/client.ts | 18 +++-- demo/server.js | 23 ++++--- 5 files changed, 127 insertions(+), 33 deletions(-) diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts index 81cb7d5770..b905b1c80b 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.ts @@ -6,12 +6,13 @@ */ import { Terminal, IDisposable, ITerminalAddon } from 'xterm'; -import { ThinProtocol, MessageType } from './ThinProtocol'; +import { ThinProtocolString, MessageType, ThinProtocolBinary } from './ThinProtocol'; interface IAttachOptions { bidirectional?: boolean; inputUtf8?: boolean; flowControl?: number; + outputBinary?: boolean; } export class AttachAddon implements ITerminalAddon { @@ -19,9 +20,11 @@ export class AttachAddon implements ITerminalAddon { private _bidirectional: boolean; private _utf8: boolean; private _disposables: IDisposable[] = []; - private _tp: ThinProtocol = new ThinProtocol(); + private _tp: ThinProtocolBinary | ThinProtocolString; private _flowControl = 0; private _bytesSeen = 0; + private _outputBinary: boolean; + private _textencoder: TextEncoder = new TextEncoder(); constructor(socket: WebSocket, options?: IAttachOptions) { this._socket = socket; @@ -30,6 +33,8 @@ export class AttachAddon implements ITerminalAddon { this._bidirectional = (options && options.bidirectional === false) ? false : true; this._utf8 = !!(options && options.inputUtf8); this._flowControl = (options && options.flowControl) ? Math.max(options.flowControl, 0) : 0; + this._outputBinary = !!(options && options.outputBinary); + this._tp = (this._outputBinary) ? new ThinProtocolBinary() : new ThinProtocolString(); } public activate(terminal: Terminal): void { @@ -74,13 +79,6 @@ export class AttachAddon implements ITerminalAddon { this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose())); this._disposables.push(addSocketListener(this._socket, 'error', () => this.dispose())); - - // test binary - let counter = 0; - setInterval(() => { - counter = (counter + 1) & 255; - this._socket.send(this._tp.binary(String.fromCharCode(counter))); - }, 100); } public dispose(): void { @@ -95,7 +93,11 @@ export class AttachAddon implements ITerminalAddon { if (this._socket.readyState !== 1) { return; } - this._socket.send(this._tp.data(data)); + if (this._outputBinary) { + this._socket.send((this._tp as ThinProtocolBinary).data(this._textencoder.encode(data))); + } else { + this._socket.send((this._tp as ThinProtocolString).data(data)); + } } } diff --git a/addons/xterm-addon-attach/src/ThinProtocol.ts b/addons/xterm-addon-attach/src/ThinProtocol.ts index 0846e6acd1..43406a3f5a 100644 --- a/addons/xterm-addon-attach/src/ThinProtocol.ts +++ b/addons/xterm-addon-attach/src/ThinProtocol.ts @@ -5,9 +5,6 @@ /** * Message types for ThinProtocol. - * Currently we only support 2 messages types. - * Future versions might extend this by other types - * that dont fit into DATA (like resize or mouse reports). */ export enum MessageType { /** Plain data, no further handling. */ @@ -15,11 +12,12 @@ export enum MessageType { /** ACK, sent for every n-th byte finally processed. */ ACK, /** + * Binary message type. * By default, DATA will be treated as UTF8 data, To be able to send - * raw non UTF-8 conform byte data, use the BINARY type. - * Currently only the string version of the protocol is implemented - * which uses BASE64 transport encoding for binary data. - * A later binary version of the protocol might skip the base64 step. + * raw non UTF-8 conform data, use the BINARY type. + * With the string protocol the data gets base64 encoded, + * with the binary protocol this message type gets no special + * treatment. */ BINARY } @@ -63,7 +61,7 @@ const base64Decode = (globalObject.atob !== undefined) * chunk = tp.ack(); // create an ACK chunk * ``` */ -export class ThinProtocol { +export class ThinProtocolString { private _handlers: (((data: string) => void) | null)[] = new Array(Object.keys(MessageType).length); /** Register a handler for `type`. */ @@ -106,8 +104,8 @@ export class ThinProtocol { } /** Create a plain ACK message (no payload). */ - public ack(): string { - return this.wrap(MessageType.ACK); + public ack(data?: string): string { + return this.wrap(MessageType.ACK, data); } /** Create a DATA message. */ @@ -120,3 +118,50 @@ export class ThinProtocol { return this.wrap(MessageType.BINARY, data); } } + +export class ThinProtocolBinary { + private _handlers: (((data: Uint8Array) => void) | null)[] = new Array(Object.keys(MessageType).length); + + /** Register a handler for `type`. */ + public setIncomingHandler(type: MessageType, cb: (data: Uint8Array) => void): void { + this._handlers[type] = cb; + } + /** Remove handler for `type`. */ + public clearIncomingHandler(type: MessageType): void { + this._handlers[type] = null; + } + + /** Process incoming message and call associated handler. */ + public unwrap(msg: Uint8Array): void { + let handler: ((data: Uint8Array) => void) | null; + const type: MessageType = msg[0]; + if (msg && (handler = this._handlers[type])) { + handler(msg.subarray(1)); + } + } + + /** Create new message of `type`. */ + public wrap(type: MessageType, payload?: Uint8Array): Uint8Array { + const msg = new Uint8Array(payload ? payload.length + 1 : 1); + msg[0] = type; + if (payload) { + msg.set(payload, 1); + } + return msg; + } + + /** Create a plain ACK message (no payload). */ + public ack(data?: Uint8Array): Uint8Array { + return this.wrap(MessageType.ACK, data); + } + + /** Create a DATA message. */ + public data(data: Uint8Array): Uint8Array { + return this.wrap(MessageType.DATA, data); + } + + /** Create a BINARY message. */ + public binary(data: Uint8Array): Uint8Array { + return this.wrap(MessageType.BINARY, data); + } +} diff --git a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts index e63d0f3905..a322cf371e 100644 --- a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts +++ b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts @@ -26,6 +26,11 @@ declare module 'xterm-addon-attach' { * Default is 0 (flow control disabled). */ flowControl?: number; + + /** + * Whether to use binary transport for outgoing messages (recommended). + */ + outputBinary?: boolean; } export class AttachAddon implements ITerminalAddon { @@ -50,7 +55,7 @@ declare module 'xterm-addon-attach' { * ThinProtocol * Thin protocol to send different message types in-band. */ - export class ThinProtocol { + export class ThinProtocolString { /** Register a handler for `type`. */ public setIncomingHandler(type: MessageType, cb: (data: string) => void): void; @@ -68,5 +73,30 @@ declare module 'xterm-addon-attach' { /** Convenient method to create a DATA message. */ public data(data: string): string; + + /** Create a BINARY message. */ + public binary(data: string): string; } } + +export class ThinProtocolBinary { + /** Register a handler for `type`. */ + public setIncomingHandler(type: MessageType, cb: (data: Uint8Array) => void): void; + /** Remove handler for `type`. */ + public clearIncomingHandler(type: MessageType): void; + + /** Process incoming message and call associated handler. */ + public unwrap(msg: Uint8Array): void; + + /** Create new message of `type`. */ + public wrap(type: MessageType, payload?: Uint8Array): Uint8Array; + + /** Create a plain ACK message (no payload). */ + public ack(data?: Uint8Array): Uint8Array; + + /** Create a DATA message. */ + public data(data: Uint8Array): Uint8Array; + + /** Create a BINARY message. */ + public binary(data: Uint8Array): Uint8Array; +} diff --git a/demo/client.ts b/demo/client.ts index 87971b383c..8e98fbb064 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -39,11 +39,17 @@ import { Terminal as TerminalType, ITerminalOptions } from 'xterm'; const FLOW_CONTROL = 131072; /** - * Whether to use UTF8 binary transport in the demo. - * (Must also be switched in server.js) + * Whether to use UTF8 binary transport for incoming data. + * (Must be in line with USE_OUTGOING_BINARY in server.js) */ -const USE_BINARY_UTF8 = false; +const USE_INCOMING_BINARY = false; +/** + * Whether to use binary websocket transport for outgoing messages. + * This is recommended to avoid additional encoding cost for raw binary data. + * (Must be in line with USE_INCOMING_BINARY in server.js) + */ +const USE_OUTGOING_BINARY = true; export interface IWindowWithTerminal extends Window { term: TerminalType; @@ -188,7 +194,11 @@ function createTerminal(): void { } function runRealTerminal(): void { - term.loadAddon(new AttachAddon(socket, {inputUtf8: USE_BINARY_UTF8, flowControl: FLOW_CONTROL})); + term.loadAddon(new AttachAddon(socket, { + inputUtf8: USE_INCOMING_BINARY, + flowControl: FLOW_CONTROL, + outputBinary: USE_OUTGOING_BINARY + })); term._initialized = true; } diff --git a/demo/server.js b/demo/server.js index c670edfaac..5d42904231 100644 --- a/demo/server.js +++ b/demo/server.js @@ -2,14 +2,20 @@ var express = require('express'); var expressWs = require('express-ws'); var os = require('os'); var pty = require('node-pty'); -var ThinProtocol = require('../addons/xterm-addon-attach/out/ThinProtocol').ThinProtocol; +var ThinProtocolString = require('../addons/xterm-addon-attach/out/ThinProtocol').ThinProtocolString; +var ThinProtocolBinary = require('../addons/xterm-addon-attach/out/ThinProtocol').ThinProtocolBinary; var MessageType = require('../addons/xterm-addon-attach/out/ThinProtocol').MessageType; /** - * Whether to use UTF8 binary transport. + * Whether to use outgoing UTF8 binary transport. * (Must also be switched in client.ts) */ -const USE_BINARY_UTF8 = false; +const USE_OUTGOING_BINARY = false; + +/** + * Whether to use binary for incoming data. + */ +const USE_INCOMING_BINARY = true; /** * Whether to use flow control. @@ -68,7 +74,7 @@ function startServer() { rows: rows || 24, cwd: env.PWD, env: env, - encoding: USE_BINARY_UTF8 ? null : 'utf8' + encoding: USE_OUTGOING_BINARY ? null : 'utf8' }); console.log('Created terminal with PID: ' + term.pid); @@ -99,7 +105,7 @@ function startServer() { * In the demo the protocol is only used for incoming data * (one sided, outgoing data is kept as plain data stream). */ - const tp = new ThinProtocol(); + const tp = USE_INCOMING_BINARY ? new ThinProtocolBinary() : new ThinProtocolString(); // route DATA messages to pty tp.setIncomingHandler(MessageType.DATA, msg => term.write(msg)); // do flow control with ACK replies @@ -113,8 +119,9 @@ function startServer() { }); // digest binary data tp.setIncomingHandler(MessageType.BINARY, msg => { - // term.write(Buffer.from(msg, 'binary')); - console.log(Buffer.from(msg, 'binary')); + // if we receive data as string we have to decode + // it first to bytes to avoid node-pty applying UTF8 to it + term.write(USE_INCOMING_BINARY ? msg : Buffer.from(msg, 'binary')); }); // incomming chunks are routed through thin protocol to separate DATA from ACK @@ -192,7 +199,7 @@ function startServer() { } }; } - const send = (USE_BINARY_UTF8 ? bufferUtf8 : buffer)(MAX_SEND_INTERVAL, MAX_CHUNK_SIZE); + const send = (USE_OUTGOING_BINARY ? bufferUtf8 : buffer)(MAX_SEND_INTERVAL, MAX_CHUNK_SIZE); term.on('data', data => send(data));