From f7dade1023178bcae5a3eccee728daec46afedfd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 9 Sep 2019 09:28:33 -0700 Subject: [PATCH 01/38] Add/polish the readme for all addons Fixes #2193 --- addons/xterm-addon-attach/README.md | 22 ++++++++++++++++++++++ addons/xterm-addon-fit/README.md | 24 ++++++++++++++++++++++++ addons/xterm-addon-search/README.md | 23 +++++++++++++++++++++++ addons/xterm-addon-web-links/README.md | 6 ++---- addons/xterm-addon-webgl/README.md | 23 +++++++++++++++++++++++ 5 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 addons/xterm-addon-attach/README.md create mode 100644 addons/xterm-addon-fit/README.md create mode 100644 addons/xterm-addon-search/README.md create mode 100644 addons/xterm-addon-webgl/README.md diff --git a/addons/xterm-addon-attach/README.md b/addons/xterm-addon-attach/README.md new file mode 100644 index 00000000..2040455c --- /dev/null +++ b/addons/xterm-addon-attach/README.md @@ -0,0 +1,22 @@ +## xterm-addon-attach + +An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables attaching to a web socket. This addon requires xterm.js v4+. + +### Install + +```bash +npm install --save xterm-addon-attach +``` + +### Usage + +```ts +import { Terminal } from 'xterm'; +import { FitAddon } from 'xterm-addon-attach'; + +const terminal = new Terminal(); +const attachAddon = new AttachAddon(webSocket); +terminal.loadAddon(attachAddon); +``` + +See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts) for more advanced usage. diff --git a/addons/xterm-addon-fit/README.md b/addons/xterm-addon-fit/README.md new file mode 100644 index 00000000..321b2cf7 --- /dev/null +++ b/addons/xterm-addon-fit/README.md @@ -0,0 +1,24 @@ +## xterm-addon-fit + +An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables fitting the terminal's dimensions to a containing element. This addon requires xterm.js v4+. + +### Install + +```bash +npm install --save xterm-addon-fit +``` + +### Usage + +```ts +import { Terminal } from 'xterm'; +import { FitAddon } from 'xterm-addon-fit'; + +const terminal = new Terminal(); +const fitAddon = new FitAddon(); +terminal.loadAddon(fitAddon); +terminal.open(containerElement); +fitAddon.fit(); +``` + +See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-fit/typings/xterm-addon-fit.d.ts) for more advanced usage. diff --git a/addons/xterm-addon-search/README.md b/addons/xterm-addon-search/README.md new file mode 100644 index 00000000..91bcc34b --- /dev/null +++ b/addons/xterm-addon-search/README.md @@ -0,0 +1,23 @@ +## xterm-addon-search + +An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables searching the buffer. This addon requires xterm.js v4+. + +### Install + +```bash +npm install --save xterm-addon-search +``` + +### Usage + +```ts +import { Terminal } from 'xterm'; +import { SearchAddon } from 'xterm-addon-search'; + +const terminal = new Terminal(); +const searchAddon = new SearchAddon(); +terminal.loadAddon(searchAddon); +searchAddon.findNext('foo'); +``` + +See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-search/typings/xterm-addon-search.d.ts) for more advanced usage. diff --git a/addons/xterm-addon-web-links/README.md b/addons/xterm-addon-web-links/README.md index dc9e1c32..e53f4edf 100644 --- a/addons/xterm-addon-web-links/README.md +++ b/addons/xterm-addon-web-links/README.md @@ -1,8 +1,6 @@ ## xterm-addon-web-links -[![Build Status](https://dev.azure.com/xtermjs/xterm-addon-web-links/_apis/build/status/xtermjs.xterm-addon-web-links?branchName=master)](https://dev.azure.com/xtermjs/xterm-addon-web-links/_build/latest?definitionId=5&branchName=master) - -An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enabled web links. This addon requires xterm.js 3.14+. +An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables web links. This addon requires xterm.js v4+. ### Install @@ -20,4 +18,4 @@ const terminal = new Terminal(); terminal.loadAddon(new WebLinksAddon()); ``` -You can also specify a custom handler and options, see the [API](https://github.com/xtermjs/xterm-addon-web-links/blob/master/typings/web-links.d.ts) for more details. +See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts) for more advanced usage. diff --git a/addons/xterm-addon-webgl/README.md b/addons/xterm-addon-webgl/README.md new file mode 100644 index 00000000..026a738e --- /dev/null +++ b/addons/xterm-addon-webgl/README.md @@ -0,0 +1,23 @@ +## xterm-addon-webgl + +An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL-based renderer. This addon requires xterm.js v4+. + +⚠️ This is an experimental addon that is [missing some features and may be unstable](https://github.com/xtermjs/xterm.js/issues?q=is%3Aopen+is%3Aissue+label%3Aarea%2Faddon%2Fwebgl) ⚠️ + +### Install + +```bash +npm install --save xterm-addon-webgl +``` + +### Usage + +```ts +import { Terminal } from 'xterm'; +import { WebglAddon } from 'xterm-addon-webgl'; + +const terminal = new Terminal(); +terminal.loadAddon(new WebglAddon()); +``` + +See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts) for more advanced usage. From 87c167a212d7ef0544ef4a42c0859ea66e334c5b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 9 Sep 2019 09:32:32 -0700 Subject: [PATCH 02/38] Improve logging for addon publish --- bin/publish.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/publish.js b/bin/publish.js index 0610d116..d95ecc57 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -30,9 +30,11 @@ const addonPackageDirs = [ path.resolve(__dirname, '../addons/xterm-addon-web-links'), path.resolve(__dirname, '../addons/xterm-addon-webgl') ]; +console.log(`Checking if addons need to be published`); addonPackageDirs.forEach(p => { const addon = path.basename(p); if (changedFiles.some(e => e.indexOf(addon) !== -1)) { + console.log(`Try publish ${addon}`); checkAndPublishPackage(p); } }); From 1f12d365c8b5d34e552a7e41257dc3682dba6136 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 9 Sep 2019 10:21:37 -0700 Subject: [PATCH 03/38] Expose source maps on demo server Fixes #2258 --- demo/server.js | 5 ++--- demo/tsconfig.json | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/demo/server.js b/demo/server.js index 8955431b..f1fae572 100644 --- a/demo/server.js +++ b/demo/server.js @@ -32,9 +32,8 @@ function startServer() { res.sendFile(__dirname + '/style.css'); }); - app.get('/dist/client-bundle.js', function(req, res){ - res.sendFile(__dirname + '/dist/client-bundle.js'); - }); + app.use('/dist', express.static(__dirname + '/dist')); + app.use('/src', express.static(__dirname + '/src')); app.post('/terminals', function (req, res) { const env = Object.assign({}, process.env); diff --git a/demo/tsconfig.json b/demo/tsconfig.json index 2bf76f67..7a53302d 100644 --- a/demo/tsconfig.json +++ b/demo/tsconfig.json @@ -9,7 +9,8 @@ "xterm-addon-attach": ["../addons/xterm-addon-attach"], "xterm-addon-fit": ["../addons/xterm-addon-fit"], "xterm-addon-search": ["../addons/xterm-addon-search"], - "xterm-addon-web-links": ["../addons/xterm-addon-web-links"] + "xterm-addon-web-links": ["../addons/xterm-addon-web-links"], + "xterm-addon-webgl": ["../addons/xterm-addon-webgl"] } }, "include": [ From b990eb42c6b57efce595005194c624e83f2408b9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 9 Sep 2019 10:25:07 -0700 Subject: [PATCH 04/38] Fix paths in example for v4 Fixes #2146 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2853696f..498278b8 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t - - + +
From 230d26afc737b48a45ebd5691d4702c94bf99304 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 10 Sep 2019 15:54:01 -0700 Subject: [PATCH 05/38] Bump addon versions for v4 release --- addons/xterm-addon-attach/package.json | 2 +- addons/xterm-addon-fit/package.json | 2 +- addons/xterm-addon-search/package.json | 2 +- addons/xterm-addon-web-links/package.json | 2 +- addons/xterm-addon-webgl/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-attach/package.json b/addons/xterm-addon-attach/package.json index 79b350e1..6a4fa464 100644 --- a/addons/xterm-addon-attach/package.json +++ b/addons/xterm-addon-attach/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-attach", - "version": "0.1.0", + "version": "0.2.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-fit/package.json b/addons/xterm-addon-fit/package.json index 77471c26..03a2c1e6 100644 --- a/addons/xterm-addon-fit/package.json +++ b/addons/xterm-addon-fit/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-fit", - "version": "0.1.0", + "version": "0.2.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-search/package.json b/addons/xterm-addon-search/package.json index 6871c566..4438f418 100644 --- a/addons/xterm-addon-search/package.json +++ b/addons/xterm-addon-search/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-search", - "version": "0.1.0", + "version": "0.2.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-web-links/package.json b/addons/xterm-addon-web-links/package.json index 2e390690..a6240abd 100644 --- a/addons/xterm-addon-web-links/package.json +++ b/addons/xterm-addon-web-links/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-web-links", - "version": "0.1.0", + "version": "0.2.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-webgl/package.json b/addons/xterm-addon-webgl/package.json index 0db5b204..378516d5 100644 --- a/addons/xterm-addon-webgl/package.json +++ b/addons/xterm-addon-webgl/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-webgl", - "version": "0.1.0", + "version": "0.2.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" From e34b557f67650127b3dfbb214c6e5c3ba09d521a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 10 Sep 2019 16:29:25 -0700 Subject: [PATCH 06/38] Remove experimental flag from ITerminalAddon.activate --- typings/xterm.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index cae54dc1..d0a33cba 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -804,7 +804,7 @@ declare module 'xterm' { */ export interface ITerminalAddon extends IDisposable { /** - * (EXPERIMENTAL) This is called when the addon is activated. + * This is called when the addon is activated. */ activate(terminal: Terminal): void; } From 9a1347969241372b25ca23349930f4ce7694c1d6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 10 Sep 2019 17:00:18 -0700 Subject: [PATCH 07/38] v4.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 68467a89..f52f7b79 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.14.0", + "version": "4.0.0", "main": "lib/xterm.js", "style": "css/xterm.css", "types": "typings/xterm.d.ts", From 7f9f75704201d44cf731287b40e6cf284652c72e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 11 Sep 2019 14:35:36 -0700 Subject: [PATCH 08/38] Fix web links addon support in Edge regex.flags isn't supported on Edge Fixes #2417 --- src/browser/Linkifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/Linkifier.ts b/src/browser/Linkifier.ts index a5454ac5..53a32aa7 100644 --- a/src/browser/Linkifier.ts +++ b/src/browser/Linkifier.ts @@ -211,7 +211,7 @@ export class Linkifier implements ILinkifier { */ private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher): void { // clone regex to do a global search on text - const rex = new RegExp(matcher.regex.source, matcher.regex.flags + 'g'); + const rex = new RegExp(matcher.regex.source, (matcher.regex.flags || '') + 'g'); let match; let stringIndex = -1; while ((match = rex.exec(text)) !== null) { From e042f25b8d45ee250d1fcf9a9800053d67271420 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 11 Sep 2019 08:51:01 -0700 Subject: [PATCH 09/38] Expose ctor at window.Terminal Fixes #2415 --- webpack.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 3f55dda3..a9a60241 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -35,7 +35,6 @@ module.exports = { output: { filename: 'xterm.js', path: path.resolve('./lib'), - library: 'Terminal', libraryTarget: 'umd' }, mode: 'production' From 75ba717bd7519056156df26dbdbe2be25490cf1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 12 Sep 2019 13:36:58 +0200 Subject: [PATCH 10/38] merge write methods, optional callback --- src/InputHandler.ts | 26 +--- src/Terminal.test.ts | 2 +- src/Terminal.ts | 205 +++++++-------------------- src/TestUtils.test.ts | 2 +- src/Types.d.ts | 9 +- src/public/Terminal.ts | 14 +- test/benchmark/Terminal.benchmark.ts | 6 +- typings/xterm.d.ts | 24 ++-- 8 files changed, 85 insertions(+), 203 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 56b2d8be..f3ed0454 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -324,7 +324,7 @@ export class InputHandler extends Disposable implements IInputHandler { super.dispose(); } - public parse(data: string): void { + public parse(data: string | Uint8Array): void { let buffer = this._bufferService.buffer; const cursorStartX = buffer.x; const cursorStartY = buffer.y; @@ -334,25 +334,11 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._parseBuffer.length < data.length) { this._parseBuffer = new Uint32Array(data.length); } - this._parser.parse(this._parseBuffer, this._stringDecoder.decode(data, this._parseBuffer)); - - buffer = this._bufferService.buffer; - if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { - this._onCursorMove.fire(); - } - } - - public parseUtf8(data: Uint8Array): void { - let buffer = this._bufferService.buffer; - const cursorStartX = buffer.x; - const cursorStartY = buffer.y; - - this._logService.debug('parsing data', data); - - if (this._parseBuffer.length < data.length) { - this._parseBuffer = new Uint32Array(data.length); - } - this._parser.parse(this._parseBuffer, this._utf8Decoder.decode(data, this._parseBuffer)); + this._parser.parse(this._parseBuffer, + (typeof data === 'string') + ? this._stringDecoder.decode(data, this._parseBuffer) + : this._utf8Decoder.decode(data, this._parseBuffer) + ); buffer = this._bufferService.buffer; if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 59864f0a..9cf10018 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -31,7 +31,7 @@ describe('Terminal', () => { (term)._compositionHelper = new MockCompositionHelper(); // Force synchronous writes term.write = (data) => { - term.writeBuffer.push(data); + (term)._writeBuffer.push(data); (term)._innerWrite(); }; (term).element = { diff --git a/src/Terminal.ts b/src/Terminal.ts index d793abfc..8b26069c 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -67,11 +67,14 @@ import { CoreMouseService } from 'common/services/CoreMouseService'; 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. + * 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 100 times 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 @@ -80,8 +83,15 @@ const WRITE_BUFFER_PAUSE_THRESHOLD = 5; * depends on the time it takes for the renderer to draw the frame. */ const WRITE_TIMEOUT_MS = 12; + +/** + * Threshold of max held chunks in the write buffer, that were already processed. + * This is a tradeoff between extensive write buffer shifts (bad runtime) and high + * memory consumption by data thats not used anymore. + */ const WRITE_BUFFER_LENGTH_THRESHOLD = 50; + export class Terminal extends Disposable implements ITerminal, IDisposable, IInputHandlingTerminal { public textarea: HTMLTextAreaElement; public element: HTMLElement; @@ -153,21 +163,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public params: (string | number)[]; public currentParam: string | number; - // user input states - 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. - */ - private _xoffSentToCatchUp: boolean; - - /** Whether writing has been stopped as a result of XOFF */ - // private _writeStopped: boolean; + // write data related containers + protected _writeBuffer: (Uint8Array | string)[] = []; + private _pendingWriteDataSize: number = 0; + private _writeChunkCallbacks: ((() => void) | undefined)[] = []; + private _writeInProgress = false; // Store if user went browsing history in scrollback private _userScrolling: boolean; @@ -306,13 +306,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.params = []; this.currentParam = 0; - // user input states - this.writeBuffer = []; - this.writeBufferUtf8 = []; - this._writeInProgress = false; - - this._xoffSentToCatchUp = false; - // this._writeStopped = false; this._userScrolling = false; // Register input handler and refire/handle events @@ -1144,139 +1137,37 @@ 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 write(data: string | Uint8Array, callback?: () => void): void { // Ensure the terminal isn't disposed - if (this._isDisposed) { + // NOOP on empty data + if (this._isDisposed || !data.length) { return; } - // Ignore falsy data values - if (!data) { - return; + if (this._pendingWriteDataSize > DISCARD_WATERMARK) { + throw new Error('write data discarded, use flow control to avoid losing data'); } - this.writeBufferUtf8.push(data); + this._pendingWriteDataSize += data.length; + this._writeBuffer.push(data); + this._writeChunkCallbacks.push(callback); - // 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; - } - - if (!this._writeInProgress && this.writeBufferUtf8.length > 0) { - // Kick off a write which will write all data in sequence recursively + if (!this._writeInProgress) { this._writeInProgress = true; - // Kick off an async innerWrite so more writes can come in while processing data - setTimeout(() => { - this._innerWriteUtf8(); - }); - } - } - - protected _innerWriteUtf8(bufferOffset: number = 0): void { - // Ensure the terminal isn't disposed - if (this._isDisposed) { - this.writeBufferUtf8 = []; - } - - const startTime = Date.now(); - while (this.writeBufferUtf8.length > bufferOffset) { - 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._inputHandler.parseUtf8(data); - - this.refresh(this._dirtyRowService.start, this._dirtyRowService.end); - - if (Date.now() - startTime >= WRITE_TIMEOUT_MS) { - break; - } - } - if (this.writeBufferUtf8.length > bufferOffset) { - // Allow renderer to catch up before processing the next batch - // trim already processed chunks if we are above threshold - if (bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) { - this.writeBufferUtf8 = this.writeBufferUtf8.slice(bufferOffset); - bufferOffset = 0; - } - setTimeout(() => this._innerWriteUtf8(bufferOffset), 0); - } else { - this._writeInProgress = false; - this.writeBufferUtf8 = []; - } - } - - /** - * Writes text to the terminal. - * @param data The text to write to the terminal. - */ - public write(data: string): void { - // Ensure the terminal isn't disposed - if (this._isDisposed) { - return; - } - - // Ignore falsy data values (including the empty string) - if (!data) { - 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; - } - - if (!this._writeInProgress && this.writeBuffer.length > 0) { - // Kick off a write which will write all data in sequence recursively - this._writeInProgress = true; - // Kick off an async innerWrite so more writes can come in while processing data - setTimeout(() => { - this._innerWrite(); - }); + setTimeout(() => this._innerWrite()); } } protected _innerWrite(bufferOffset: number = 0): void { - // Ensure the terminal isn't disposed - if (this._isDisposed) { - this.writeBuffer = []; - } - const startTime = Date.now(); - while (this.writeBuffer.length > bufferOffset) { - const data = this.writeBuffer[bufferOffset]; + while (this._writeBuffer.length > bufferOffset) { + const data = this._writeBuffer[bufferOffset]; + const cb = this._writeChunkCallbacks[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._inputHandler.parse(data); + this._pendingWriteDataSize -= data.length; + if (cb) cb(); this.refresh(this._dirtyRowService.start, this._dirtyRowService.end); @@ -1284,26 +1175,36 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp break; } } - if (this.writeBuffer.length > bufferOffset) { + if (this._writeBuffer.length > bufferOffset) { // Allow renderer to catch up before processing the next batch // trim already processed chunks if we are above threshold if (bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) { - this.writeBuffer = this.writeBuffer.slice(bufferOffset); + this._writeBuffer = this._writeBuffer.slice(bufferOffset); + this._writeChunkCallbacks = this._writeChunkCallbacks.slice(bufferOffset); bufferOffset = 0; } setTimeout(() => this._innerWrite(bufferOffset), 0); } else { this._writeInProgress = false; - this.writeBuffer = []; + this._writeBuffer = []; + this._writeChunkCallbacks = []; } } + /** + * @deprecated use write instead + */ + public writeUtf8(data: Uint8Array, callback?: () => void): void { + this.write(data, callback); + } + /** * 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 | Uint8Array, callback?: () => void): void { + this.write(data); + this.write('\r\n', callback); } public paste(data: string): void { @@ -1743,10 +1644,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp const customKeyEventHandler = this._customKeyEventHandler; const inputHandler = this._inputHandler; const cursorState = this.cursorState; - const writeBuffer = this.writeBuffer; - const writeBufferUtf8 = this.writeBufferUtf8; + const writeBuffer = this._writeBuffer; const writeInProgress = this._writeInProgress; - const xoffSentToCatchUp = this._xoffSentToCatchUp; const userScrolling = this._userScrolling; this._setup(); @@ -1761,10 +1660,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._customKeyEventHandler = customKeyEventHandler; this._inputHandler = inputHandler; this.cursorState = cursorState; - this.writeBuffer = writeBuffer; - this.writeBufferUtf8 = writeBufferUtf8; + this._writeBuffer = writeBuffer; this._writeInProgress = writeInProgress; - this._xoffSentToCatchUp = xoffSentToCatchUp; this._userScrolling = userScrolling; // do a full screen refresh diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index ad19f721..a30f60ce 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -20,7 +20,7 @@ import { ISelectionService } from 'browser/services/Services'; export class TestTerminal extends Terminal { writeSync(data: string): void { - this.writeBuffer.push(data); + this._writeBuffer.push(data); this._innerWrite(); } keyDown(ev: any): boolean { return this._keyDown(ev); } diff --git a/src/Types.d.ts b/src/Types.d.ts index 13c65a4d..026efe52 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -73,8 +73,7 @@ export interface ICompositionHelper { * Calls the parser and handles actions generated by the parser. */ export interface IInputHandler { - parse(data: string): void; - parseUtf8(data: Uint8Array): void; + parse(data: string | Uint8Array): void; print(data: Uint32Array, start: number, end: number): void; /** C0 BEL */ bell(): void; @@ -151,7 +150,6 @@ export interface IInputHandler { export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor { screenElement: HTMLElement; browser: IBrowser; - writeBuffer: string[]; cursorHidden: boolean; cursorState: number; buffer: IBuffer; @@ -192,7 +190,6 @@ export interface IPublicTerminal extends IDisposable { blur(): void; focus(): void; resize(columns: number, rows: number): void; - writeln(data: string): void; open(parent: HTMLElement): void; attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; @@ -218,8 +215,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 | Uint8Array, callback?: () => void): void; + writeln(data: string | Uint8Array, callback?: () => void): void; paste(data: string): void; refresh(start: number, end: number): void; reset(): void; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 6d6cedca..eb45623d 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -55,9 +55,6 @@ export class Terminal implements ITerminalApi { this._verifyIntegers(columns, rows); this._core.resize(columns, rows); } - public writeln(data: string): void { - this._core.writeln(data); - } public open(parent: HTMLElement): void { this._core.open(parent); } @@ -128,11 +125,14 @@ export class Terminal implements ITerminalApi { public clear(): void { this._core.clear(); } - public write(data: string): void { - this._core.write(data); + public write(data: string | Uint8Array, callback?: () => void): void { + this._core.write(data, callback); } - public writeUtf8(data: Uint8Array): void { - this._core.writeUtf8(data); + public writeUtf8(data: Uint8Array, callback?: () => void): void { + this._core.write(data, callback); + } + public writeln(data: string | Uint8Array, callback?: () => void): void { + this._core.writeln(data, callback); } public paste(data: string): void { this._core.paste(data); diff --git a/test/benchmark/Terminal.benchmark.ts b/test/benchmark/Terminal.benchmark.ts index a0b8fd29..676aa891 100644 --- a/test/benchmark/Terminal.benchmark.ts +++ b/test/benchmark/Terminal.benchmark.ts @@ -11,12 +11,12 @@ import { Terminal } from 'Terminal'; class TestTerminal extends Terminal { writeSync(data: string): void { - this.writeBuffer.push(data); + this._writeBuffer.push(data); this._innerWrite(); } writeSyncUtf8(data: Uint8Array): void { - this.writeBufferUtf8.push(data); - this._innerWriteUtf8(); + this._writeBuffer.push(data); + this._innerWrite(); } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d0a33cba..8e279f8c 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -650,24 +650,26 @@ declare module 'xterm' { clear(): void; /** - * Writes text to the terminal. - * @param data The text to write to the terminal. + * Write data to the terminal. + * `data` can either be raw bytes given as Uint8Array from the pty or a string. + * Raw bytes will always be treated as UTF-8 encoded, string data as UTF-16. + * `callback` is an optional callback that gets called once the data + * chunk was processed by the parser. */ - write(data: string): void; + write(data: string | Uint8Array, callback?: () => void): void; /** - * Writes text to the terminal, followed by a break line character (\n). - * @param data The text to write to the terminal. + * Writes data to the terminal, followed by a break line character (\n). + * `callback` is an optional callback that gets called once the data + * chunk was processed by the parser. */ - writeln(data: string): void; + writeln(data: string | Uint8Array, callback?: () => void): void; /** - * Writes UTF8 data to the terminal. This has a slight performance advantage - * 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. + * Write UTF8 data to the terminal. Deprecated, use `.write` instead. + * @deprecated */ - writeUtf8(data: Uint8Array): void; + writeUtf8(data: Uint8Array, callback?: () => void): void; /** * Writes text to the terminal, performing the necessary transformations for pasted text. From 05fad38240366186103b5196b3170a365b217132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 12 Sep 2019 13:58:52 +0200 Subject: [PATCH 11/38] fix private access in integration tests --- addons/xterm-addon-search/src/SearchAddon.api.ts | 2 +- addons/xterm-addon-webgl/src/WebglRenderer.api.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.api.ts b/addons/xterm-addon-search/src/SearchAddon.api.ts index f970fcc8..186cccca 100644 --- a/addons/xterm-addon-search/src/SearchAddon.api.ts +++ b/addons/xterm-addon-search/src/SearchAddon.api.ts @@ -113,7 +113,7 @@ async function openTerminal(options: ITerminalOptions = {}): Promise { async function writeSync(data: string): Promise { await page.evaluate(`window.term.write('${data}');`); while (true) { - if (await page.evaluate(`window.term._core.writeBuffer.length === 0`)) { + if (await page.evaluate(`window.term._core._writeBuffer.length === 0`)) { break; } } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index b26790c4..82ac0adf 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -147,7 +147,7 @@ async function openTerminal(options: ITerminalOptions = {}): Promise { async function writeSync(data: string): Promise { await page.evaluate(`window.term.write('${data}');`); while (true) { - if (await page.evaluate(`window.term._core.writeBuffer.length === 0`)) { + if (await page.evaluate(`window.term._core._writeBuffer.length === 0`)) { break; } } From 293f54e8dd9c8d46232a762b807e723eb64ace7a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 12 Sep 2019 11:04:41 -0700 Subject: [PATCH 12/38] Improve debug logging by printing character codes --- src/InputHandler.ts | 2 +- src/common/services/CoreService.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 56b2d8be..7a0aa8c8 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -329,7 +329,7 @@ export class InputHandler extends Disposable implements IInputHandler { const cursorStartX = buffer.x; const cursorStartY = buffer.y; - this._logService.debug('parsing data', data); + this._logService.debug(`parsing data "${data}"`, data.split('').map(e => e.charCodeAt(0))); if (this._parseBuffer.length < data.length) { this._parseBuffer = new Uint32Array(data.length); diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index d17b0c93..71f45e47 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -54,7 +54,7 @@ export class CoreService implements ICoreService { } // Fire onData API - this._logService.debug('sending data', data); + this._logService.debug(`sending data "${data}"`, data.split('').map(e => e.charCodeAt(0))); this._onData.fire(data); } } From 0a4883b2b3de69efdf762c378b0947589ec152d3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 12 Sep 2019 11:13:53 -0700 Subject: [PATCH 13/38] Use more C0 constants in Keyboard --- src/common/input/Keyboard.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/common/input/Keyboard.ts b/src/common/input/Keyboard.ts index 3cc0eb76..2f54add6 100644 --- a/src/common/input/Keyboard.ts +++ b/src/common/input/Keyboard.ts @@ -316,23 +316,18 @@ export function evaluateKeyboardEvent( if (ev.keyCode >= 65 && ev.keyCode <= 90) { result.key = String.fromCharCode(ev.keyCode - 64); } else if (ev.keyCode === 32) { - // NUL - result.key = String.fromCharCode(0); + result.key = C0.NUL; } else if (ev.keyCode >= 51 && ev.keyCode <= 55) { // escape, file sep, group sep, record sep, unit sep result.key = String.fromCharCode(ev.keyCode - 51 + 27); } else if (ev.keyCode === 56) { - // delete - result.key = String.fromCharCode(127); + result.key = C0.DEL; } else if (ev.keyCode === 219) { - // ^[ - Control Sequence Introducer (CSI) - result.key = String.fromCharCode(27); + result.key = C0.ESC; } else if (ev.keyCode === 220) { - // ^\ - String Terminator (ST) - result.key = String.fromCharCode(28); + result.key = C0.FS; } else if (ev.keyCode === 221) { - // ^] - Operating System Command (OSC) - result.key = String.fromCharCode(29); + result.key = C0.GS; } } else if ((!isMac || macOptionIsMeta) && ev.altKey && !ev.metaKey) { // On macOS this is a third level shift when !macOptionIsMeta. Use instead. From fc4f34544663f6a0bf0762a4574c3148f6274146 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 12 Sep 2019 11:24:04 -0700 Subject: [PATCH 14/38] Don't transform incoming data This would run for all data --- src/InputHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7a0aa8c8..56b2d8be 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -329,7 +329,7 @@ export class InputHandler extends Disposable implements IInputHandler { const cursorStartX = buffer.x; const cursorStartY = buffer.y; - this._logService.debug(`parsing data "${data}"`, data.split('').map(e => e.charCodeAt(0))); + this._logService.debug('parsing data', data); if (this._parseBuffer.length < data.length) { this._parseBuffer = new Uint32Array(data.length); From 761e437c69da456b41c007e9196426903676ad43 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 12 Sep 2019 13:38:30 -0700 Subject: [PATCH 15/38] Allow lazy evaluated log params --- src/common/services/CoreService.ts | 2 +- src/common/services/LogService.ts | 33 ++++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 71f45e47..0e0ba609 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -54,7 +54,7 @@ export class CoreService implements ICoreService { } // Fire onData API - this._logService.debug(`sending data "${data}"`, data.split('').map(e => e.charCodeAt(0))); + this._logService.debug(`sending data "${data}"`, () => data.split('').map(e => e.charCodeAt(0))); this._onData.fire(data); } } diff --git a/src/common/services/LogService.ts b/src/common/services/LogService.ts index 6740ad4a..4f48d8fe 100644 --- a/src/common/services/LogService.ts +++ b/src/common/services/LogService.ts @@ -5,12 +5,14 @@ import { ILogService, IOptionsService } from 'common/services/Services'; +type LogType = (message?: any, ...optionalParams: any[]) => void; + interface IConsole { - log(message?: any, ...optionalParams: any[]): void; - error(message?: any, ...optionalParams: any[]): void; - info(message?: any, ...optionalParams: any[]): void; - trace(message?: any, ...optionalParams: any[]): void; - warn(message?: any, ...optionalParams: any[]): void; + log: LogType; + error: LogType; + info: LogType; + trace: LogType; + warn: LogType; } // console is available on both node.js and browser contexts but the common @@ -56,27 +58,40 @@ export class LogService implements ILogService { this._logLevel = optionsKeyToLogLevel[this._optionsService.options.logLevel]; } + private _evalLazyOptionalParams(optionalParams: any[]): void { + for (let i = 0; i < optionalParams.length; i++) { + if (typeof optionalParams[i] === 'function') { + optionalParams[i] = optionalParams[i](); + } + } + } + + private _log(type: LogType, message: string, optionalParams: any[]): void { + this._evalLazyOptionalParams(optionalParams); + type.call(console, LOG_PREFIX + message, ...optionalParams); + } + debug(message: string, ...optionalParams: any[]): void { if (this._logLevel <= LogLevel.DEBUG) { - console.log.call(console, LOG_PREFIX + message, ...optionalParams); + this._log(console.log, message, optionalParams); } } info(message: string, ...optionalParams: any[]): void { if (this._logLevel <= LogLevel.INFO) { - console.info.call(console, LOG_PREFIX + message, ...optionalParams); + this._log(console.info, message, optionalParams); } } warn(message: string, ...optionalParams: any[]): void { if (this._logLevel <= LogLevel.WARN) { - console.warn.call(console, LOG_PREFIX + message, ...optionalParams); + this._log(console.warn, message, optionalParams); } } error(message: string, ...optionalParams: any[]): void { if (this._logLevel <= LogLevel.ERROR) { - console.error.call(console, LOG_PREFIX + message, ...optionalParams); + this._log(console.error, message, optionalParams); } } } From 9d291d1d12312d8efebac1a0edcd3cb1ff99815b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 12 Sep 2019 15:16:02 -0700 Subject: [PATCH 16/38] Remove extra build in publish Release job We no longer publish a dist dir --- azure-pipelines.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c784a611..26f0ca73 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -121,8 +121,7 @@ jobs: displayName: 'Install Yarn' - script: | yarn - BUILD_DIR=dist npm run build displayName: 'Install dependencies and build' - script: | NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js - displayName: 'Publish to npm' + displayName: 'Package and publish to npm' From 597757d0ee6fdeaea61fefb62150aa1f78f30cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 13 Sep 2019 12:44:13 +0200 Subject: [PATCH 17/38] use write callbacks in addon integration tests --- addons/xterm-addon-search/src/SearchAddon.api.ts | 7 +------ addons/xterm-addon-webgl/src/WebglRenderer.api.ts | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.api.ts b/addons/xterm-addon-search/src/SearchAddon.api.ts index 186cccca..14e12a8c 100644 --- a/addons/xterm-addon-search/src/SearchAddon.api.ts +++ b/addons/xterm-addon-search/src/SearchAddon.api.ts @@ -111,12 +111,7 @@ async function openTerminal(options: ITerminalOptions = {}): Promise { } async function writeSync(data: string): Promise { - await page.evaluate(`window.term.write('${data}');`); - while (true) { - if (await page.evaluate(`window.term._core._writeBuffer.length === 0`)) { - break; - } - } + return page.evaluate(`new Promise(resolve => window.term.write('${data}', resolve))`); } function makeData(length: number): string { diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 82ac0adf..66be22d9 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -145,12 +145,7 @@ async function openTerminal(options: ITerminalOptions = {}): Promise { } async function writeSync(data: string): Promise { - await page.evaluate(`window.term.write('${data}');`); - while (true) { - if (await page.evaluate(`window.term._core._writeBuffer.length === 0`)) { - break; - } - } + return page.evaluate(`new Promise(resolve => window.term.write('${data}', resolve))`); } async function getCellColor(col: number, row: number): Promise { From 50e552262a20edff860a8c96b2aaffdf99901a18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 13 Sep 2019 13:00:47 +0200 Subject: [PATCH 18/38] remove writeUtf8 from core --- src/Terminal.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 8b26069c..ad6dab11 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1191,13 +1191,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } } - /** - * @deprecated use write instead - */ - public writeUtf8(data: Uint8Array, callback?: () => void): void { - this.write(data, callback); - } - /** * Writes text to the terminal, followed by a break line character (\n). * @param data The text to write to the terminal. From dcbc7e1014f71399e47ac1002c054adade6f19a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 13 Sep 2019 13:06:54 +0200 Subject: [PATCH 19/38] remove writeln from core --- src/Terminal.test.ts | 12 ++++++------ src/Terminal.ts | 9 --------- src/Types.d.ts | 1 - src/public/Terminal.ts | 3 ++- 4 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 9cf10018..2bdfcd01 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -254,7 +254,7 @@ describe('Terminal', () => { let startYDisp: number; beforeEach(() => { for (let i = 0; i < INIT_ROWS * 2; i++) { - term.writeln('test'); + term.write('test\r\n'); } startYDisp = INIT_ROWS + 1; }); @@ -289,7 +289,7 @@ describe('Terminal', () => { let startYDisp: number; beforeEach(() => { for (let i = 0; i < term.rows * 3; i++) { - term.writeln('test'); + term.write('test\r\n'); } startYDisp = (term.rows * 2) + 1; }); @@ -312,7 +312,7 @@ describe('Terminal', () => { describe('scrollToTop', () => { beforeEach(() => { for (let i = 0; i < term.rows * 3; i++) { - term.writeln('test'); + term.write('test\r\n'); } }); it('should scroll to the top', () => { @@ -326,7 +326,7 @@ describe('Terminal', () => { let startYDisp: number; beforeEach(() => { for (let i = 0; i < term.rows * 3; i++) { - term.writeln('test'); + term.write('test\r\n'); } startYDisp = (term.rows * 2) + 1; }); @@ -347,7 +347,7 @@ describe('Terminal', () => { let startYDisp: number; beforeEach(() => { for (let i = 0; i < term.rows * 3; i++) { - term.writeln('test'); + term.write('test\r\n'); } startYDisp = (term.rows * 2) + 1; }); @@ -392,7 +392,7 @@ describe('Terminal', () => { it('should not scroll down, when a custom keydown handler prevents the event', () => { // Add some output to the terminal for (let i = 0; i < term.rows * 3; i++) { - term.writeln('test'); + term.write('test\r\n'); } const startYDisp = (term.rows * 2) + 1; term.attachCustomKeyEventHandler(() => { diff --git a/src/Terminal.ts b/src/Terminal.ts index ad6dab11..1b1d324f 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1191,15 +1191,6 @@ 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 | Uint8Array, callback?: () => void): void { - this.write(data); - this.write('\r\n', callback); - } - public paste(data: string): void { paste(data, this.textarea, this.bracketedPasteMode, this._coreService); } diff --git a/src/Types.d.ts b/src/Types.d.ts index 026efe52..cb45d3b1 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -216,7 +216,6 @@ export interface IPublicTerminal extends IDisposable { scrollToLine(line: number): void; clear(): void; write(data: string | Uint8Array, callback?: () => void): void; - writeln(data: string | Uint8Array, callback?: () => void): void; paste(data: string): void; refresh(start: number, end: number): void; reset(): void; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index eb45623d..b8a70ff7 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -132,7 +132,8 @@ export class Terminal implements ITerminalApi { this._core.write(data, callback); } public writeln(data: string | Uint8Array, callback?: () => void): void { - this._core.writeln(data, callback); + this._core.write(data); + this._core.write('\r\n', callback); } public paste(data: string): void { this._core.paste(data); From fe74b5588d5fea05e00d8f9534f97c7655438053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 13 Sep 2019 13:11:23 +0200 Subject: [PATCH 20/38] use write callback in benchmark test --- test/benchmark/Terminal.benchmark.ts | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/test/benchmark/Terminal.benchmark.ts b/test/benchmark/Terminal.benchmark.ts index 676aa891..0e34ebb3 100644 --- a/test/benchmark/Terminal.benchmark.ts +++ b/test/benchmark/Terminal.benchmark.ts @@ -9,17 +9,6 @@ import { spawn } from 'node-pty'; import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder'; import { Terminal } from 'Terminal'; -class TestTerminal extends Terminal { - writeSync(data: string): void { - this._writeBuffer.push(data); - this._innerWrite(); - } - writeSyncUtf8(data: Uint8Array): void { - this._writeBuffer.push(data); - this._innerWrite(); - } -} - perfContext('Terminal: ls -lR /usr/lib', () => { let content = ''; let contentUtf8: Uint8Array; @@ -56,23 +45,23 @@ perfContext('Terminal: ls -lR /usr/lib', () => { }); perfContext('write', () => { - let terminal: TestTerminal; + let terminal: Terminal; before(() => { - terminal = new TestTerminal({cols: 80, rows: 25, scrollback: 1000}); + terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000}); }); - new ThroughputRuntimeCase('', () => { - terminal.writeSync(content); + new ThroughputRuntimeCase('', async () => { + await new Promise(resolve => terminal.write(content, resolve)); return {payloadSize: contentUtf8.length}; }, {fork: false}).showAverageThroughput(); }); perfContext('writeUtf8', () => { - let terminal: TestTerminal; + let terminal: Terminal; before(() => { - terminal = new TestTerminal({cols: 80, rows: 25, scrollback: 1000}); + terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000}); }); - new ThroughputRuntimeCase('', () => { - terminal.writeSyncUtf8(contentUtf8); + new ThroughputRuntimeCase('', async () => { + await new Promise(resolve => terminal.write(content, resolve)); return {payloadSize: contentUtf8.length}; }, {fork: false}).showAverageThroughput(); }); From a95e451227511704df6e3f90d2ea4d2d0b4b94f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 13 Sep 2019 13:19:33 +0200 Subject: [PATCH 21/38] add params in doc --- typings/xterm.d.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 8e279f8c..faa83d72 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -655,6 +655,8 @@ declare module 'xterm' { * Raw bytes will always be treated as UTF-8 encoded, string data as UTF-16. * `callback` is an optional callback that gets called once the data * chunk was processed by the parser. + * @param data The data to write to the terminal. + * @param callback Optional callback when data was processed. */ write(data: string | Uint8Array, callback?: () => void): void; @@ -662,12 +664,16 @@ declare module 'xterm' { * Writes data to the terminal, followed by a break line character (\n). * `callback` is an optional callback that gets called once the data * chunk was processed by the parser. + * @param data The data to write to the terminal. + * @param callback Optional callback when data was processed. */ writeln(data: string | Uint8Array, callback?: () => void): void; /** - * Write UTF8 data to the terminal. Deprecated, use `.write` instead. - * @deprecated + * Write UTF8 data to the terminal. + * @param data The data to write to the terminal. + * @param callback Optional callback when data was processed. + * @deprecated use `write` instead */ writeUtf8(data: Uint8Array, callback?: () => void): void; From d9ba19ba9ecd3ad4facd6dd224341dbfd5fde3e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 13 Sep 2019 13:23:59 +0200 Subject: [PATCH 22/38] remove write sync hack from Terminal.test.ts --- src/Terminal.test.ts | 79 +++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 2bdfcd01..667bf076 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -29,11 +29,6 @@ describe('Terminal', () => { (term).renderer = new MockRenderer(); term.viewport = new MockViewport(); (term)._compositionHelper = new MockCompositionHelper(); - // Force synchronous writes - term.write = (data) => { - (term)._writeBuffer.push(data); - (term)._innerWrite(); - }; (term).element = { classList: { toggle: () => { }, @@ -59,18 +54,18 @@ describe('Terminal', () => { // }); it('should fire the onCursorMove event', (done) => { term.onCursorMove(() => done()); - term.write('foo'); + term.writeSync('foo'); }); it('should fire the onLineFeed event', (done) => { term.onLineFeed(() => done()); - term.write('\n'); + term.writeSync('\n'); }); it('should fire a scroll event when scrollback is created', (done) => { term.onScroll(() => done()); - term.write('\n'.repeat(INIT_ROWS)); + term.writeSync('\n'.repeat(INIT_ROWS)); }); it('should fire a scroll event when scrollback is cleared', (done) => { - term.write('\n'.repeat(INIT_ROWS)); + term.writeSync('\n'.repeat(INIT_ROWS)); term.onScroll(() => done()); term.clear(); }); @@ -195,7 +190,7 @@ describe('Terminal', () => { it('should clear a buffer larger than rows', () => { // Fill the buffer with dummy rows for (let i = 0; i < term.rows * 2; i++) { - term.write('test\n'); + term.writeSync('test\n'); } const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); @@ -244,7 +239,7 @@ describe('Terminal', () => { assert.equal(e, '\x1b[200~foo\x1b[201~'); done(); }); - term.write('\x1b[?2004h'); + term.writeSync('\x1b[?2004h'); term.paste('foo'); }); }); @@ -254,7 +249,7 @@ describe('Terminal', () => { let startYDisp: number; beforeEach(() => { for (let i = 0; i < INIT_ROWS * 2; i++) { - term.write('test\r\n'); + term.writeSync('test\r\n'); } startYDisp = INIT_ROWS + 1; }); @@ -289,7 +284,7 @@ describe('Terminal', () => { let startYDisp: number; beforeEach(() => { for (let i = 0; i < term.rows * 3; i++) { - term.write('test\r\n'); + term.writeSync('test\r\n'); } startYDisp = (term.rows * 2) + 1; }); @@ -312,7 +307,7 @@ describe('Terminal', () => { describe('scrollToTop', () => { beforeEach(() => { for (let i = 0; i < term.rows * 3; i++) { - term.write('test\r\n'); + term.writeSync('test\r\n'); } }); it('should scroll to the top', () => { @@ -326,7 +321,7 @@ describe('Terminal', () => { let startYDisp: number; beforeEach(() => { for (let i = 0; i < term.rows * 3; i++) { - term.write('test\r\n'); + term.writeSync('test\r\n'); } startYDisp = (term.rows * 2) + 1; }); @@ -347,7 +342,7 @@ describe('Terminal', () => { let startYDisp: number; beforeEach(() => { for (let i = 0; i < term.rows * 3; i++) { - term.write('test\r\n'); + term.writeSync('test\r\n'); } startYDisp = (term.rows * 2) + 1; }); @@ -392,7 +387,7 @@ describe('Terminal', () => { it('should not scroll down, when a custom keydown handler prevents the event', () => { // Add some output to the terminal for (let i = 0; i < term.rows * 3; i++) { - term.write('test\r\n'); + term.writeSync('test\r\n'); } const startYDisp = (term.rows * 2) + 1; term.attachCustomKeyEventHandler(() => { @@ -737,7 +732,7 @@ describe('Terminal', () => { const high = String.fromCharCode(0xD800); const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { - term.write(high + String.fromCharCode(i)); + term.writeSync(high + String.fromCharCode(i)); const tchar = term.buffer.lines.get(0).loadCell(0, cell); expect(tchar.getChars()).eql(high + String.fromCharCode(i)); expect(tchar.getChars().length).eql(2); @@ -751,7 +746,7 @@ describe('Terminal', () => { const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; - term.write(high + String.fromCharCode(i)); + term.writeSync(high + String.fromCharCode(i)); expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars()).eql(high + String.fromCharCode(i)); expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars().length).eql(2); expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(''); @@ -764,7 +759,7 @@ describe('Terminal', () => { for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; term.wraparoundMode = true; - term.write('a' + high + String.fromCharCode(i)); + term.writeSync('a' + high + String.fromCharCode(i)); expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a'); expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i)); expect(term.buffer.lines.get(1).loadCell(0, cell).getChars().length).eql(2); @@ -782,7 +777,7 @@ describe('Terminal', () => { if (width !== 1) { continue; } - term.write('a' + high + String.fromCharCode(i)); + term.writeSync('a' + high + String.fromCharCode(i)); // auto wraparound mode should cut off the rest of the line expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql(high + String.fromCharCode(i)); expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(2); @@ -794,8 +789,8 @@ describe('Terminal', () => { const high = String.fromCharCode(0xD800); const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { - term.write(high); - term.write(String.fromCharCode(i)); + term.writeSync(high); + term.writeSync(String.fromCharCode(i)); const tchar = term.buffer.lines.get(0).loadCell(0, cell); expect(tchar.getChars()).eql(high + String.fromCharCode(i)); expect(tchar.getChars().length).eql(2); @@ -809,7 +804,7 @@ describe('Terminal', () => { describe('unicode - combining characters', () => { const cell = new CellData(); it('café', () => { - term.write('cafe\u0301'); + term.writeSync('cafe\u0301'); term.buffer.lines.get(0).loadCell(3, cell); expect(cell.getChars()).eql('e\u0301'); expect(cell.getChars().length).eql(2); @@ -817,7 +812,7 @@ describe('Terminal', () => { }); it('café - end of line', () => { term.buffer.x = term.cols - 1 - 3; - term.write('cafe\u0301'); + term.writeSync('cafe\u0301'); term.buffer.lines.get(0).loadCell(term.cols - 1, cell); expect(cell.getChars()).eql('e\u0301'); expect(cell.getChars().length).eql(2); @@ -829,7 +824,7 @@ describe('Terminal', () => { }); it('multiple combined é', () => { term.wraparoundMode = true; - term.write(Array(100).join('e\u0301')); + term.writeSync(Array(100).join('e\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); expect(cell.getChars()).eql('e\u0301'); @@ -843,7 +838,7 @@ describe('Terminal', () => { }); it('multiple surrogate with combined', () => { term.wraparoundMode = true; - term.write(Array(100).join('\uD800\uDC00\u0301')); + term.writeSync(Array(100).join('\uD800\uDC00\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); expect(cell.getChars()).eql('\uD800\uDC00\u0301'); @@ -861,18 +856,18 @@ describe('Terminal', () => { const cell = new CellData(); it('cursor movement even', () => { expect(term.buffer.x).eql(0); - term.write('¥'); + term.writeSync('¥'); expect(term.buffer.x).eql(2); }); it('cursor movement odd', () => { term.buffer.x = 1; expect(term.buffer.x).eql(1); - term.write('¥'); + term.writeSync('¥'); expect(term.buffer.x).eql(3); }); it('line of ¥ even', () => { term.wraparoundMode = true; - term.write(Array(50).join('¥')); + term.writeSync(Array(50).join('¥')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); if (i % 2) { @@ -893,7 +888,7 @@ describe('Terminal', () => { it('line of ¥ odd', () => { term.wraparoundMode = true; term.buffer.x = 1; - term.write(Array(50).join('¥')); + term.writeSync(Array(50).join('¥')); for (let i = 1; i < term.cols - 1; ++i) { term.buffer.lines.get(0).loadCell(i, cell); if (!(i % 2)) { @@ -918,7 +913,7 @@ describe('Terminal', () => { it('line of ¥ with combining odd', () => { term.wraparoundMode = true; term.buffer.x = 1; - term.write(Array(50).join('¥\u0301')); + term.writeSync(Array(50).join('¥\u0301')); for (let i = 1; i < term.cols - 1; ++i) { term.buffer.lines.get(0).loadCell(i, cell); if (!(i % 2)) { @@ -942,7 +937,7 @@ describe('Terminal', () => { }); it('line of ¥ with combining even', () => { term.wraparoundMode = true; - term.write(Array(50).join('¥\u0301')); + term.writeSync(Array(50).join('¥\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); if (i % 2) { @@ -963,7 +958,7 @@ describe('Terminal', () => { it('line of surrogate fullwidth with combining odd', () => { term.wraparoundMode = true; term.buffer.x = 1; - term.write(Array(50).join('\ud843\ude6d\u0301')); + term.writeSync(Array(50).join('\ud843\ude6d\u0301')); for (let i = 1; i < term.cols - 1; ++i) { term.buffer.lines.get(0).loadCell(i, cell); if (!(i % 2)) { @@ -987,7 +982,7 @@ describe('Terminal', () => { }); it('line of surrogate fullwidth with combining even', () => { term.wraparoundMode = true; - term.write(Array(50).join('\ud843\ude6d\u0301')); + term.writeSync(Array(50).join('\ud843\ude6d\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); if (i % 2) { @@ -1010,11 +1005,11 @@ describe('Terminal', () => { describe('insert mode', () => { const cell = new CellData(); it('halfwidth - all', () => { - term.write(Array(9).join('0123456789').slice(-80)); + term.writeSync(Array(9).join('0123456789').slice(-80)); term.buffer.x = 10; term.buffer.y = 0; term.insertMode = true; - term.write('abcde'); + term.writeSync('abcde'); expect(term.buffer.lines.get(0).length).eql(term.cols); expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a'); expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('e'); @@ -1022,11 +1017,11 @@ describe('Terminal', () => { expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('4'); }); it('fullwidth - insert', () => { - term.write(Array(9).join('0123456789').slice(-80)); + term.writeSync(Array(9).join('0123456789').slice(-80)); term.buffer.x = 10; term.buffer.y = 0; term.insertMode = true; - term.write('¥¥¥'); + term.writeSync('¥¥¥'); expect(term.buffer.lines.get(0).length).eql(term.cols); expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('¥'); expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql(''); @@ -1035,16 +1030,16 @@ describe('Terminal', () => { expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('3'); }); it('fullwidth - right border', () => { - term.write(Array(41).join('¥')); + term.writeSync(Array(41).join('¥')); term.buffer.x = 10; term.buffer.y = 0; term.insertMode = true; - term.write('a'); + term.writeSync('a'); expect(term.buffer.lines.get(0).length).eql(term.cols); expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a'); expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('¥'); expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // fullwidth char got replaced - term.write('b'); + term.writeSync('b'); expect(term.buffer.lines.get(0).length).eql(term.cols); expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('b'); expect(term.buffer.lines.get(0).loadCell(12, cell).getChars()).eql('¥'); From c2ec16a16c834c4449547e3e1aab2da9eb4a4b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 13 Sep 2019 16:31:28 +0200 Subject: [PATCH 23/38] encapsulate deferred writing in WriteBuffer class --- src/InputHandler.ts | 1 + src/Terminal.ts | 100 +++--------------------- src/TestUtils.test.ts | 4 - src/common/input/WriteBuffer.ts | 110 +++++++++++++++++++++++++++ test/benchmark/Terminal.benchmark.ts | 8 +- 5 files changed, 127 insertions(+), 96 deletions(-) create mode 100644 src/common/input/WriteBuffer.ts diff --git a/src/InputHandler.ts b/src/InputHandler.ts index f3ed0454..74358583 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -344,6 +344,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { this._onCursorMove.fire(); } + this._terminal.refresh(this._dirtyRowService.start, this._dirtyRowService.end); } public print(data: Uint32Array, start: number, end: number): void { diff --git a/src/Terminal.ts b/src/Terminal.ts index 1b1d324f..aa55ebcb 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -62,35 +62,11 @@ import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, import { DirtyRowService } from 'common/services/DirtyRowService'; import { InstantiationService } from 'common/services/InstantiationService'; import { CoreMouseService } from 'common/services/CoreMouseService'; +import { WriteBuffer } from 'common/input/WriteBuffer'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; -/** - * Safety watermark to avoid memory exhaustion and browser engine crash on fast data input. - * 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 100 times lower number (>500 kB). - */ -const DISCARD_WATERMARK = 50000000; // ~50 MB - -/** - * The max number of ms to spend on writes before allowing the renderer to - * catch up with a 0ms setTimeout. A value of < 33 to keep us close to - * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS - * depends on the time it takes for the renderer to draw the frame. - */ -const WRITE_TIMEOUT_MS = 12; - -/** - * Threshold of max held chunks in the write buffer, that were already processed. - * This is a tradeoff between extensive write buffer shifts (bad runtime) and high - * memory consumption by data thats not used anymore. - */ -const WRITE_BUFFER_LENGTH_THRESHOLD = 50; - export class Terminal extends Disposable implements ITerminal, IDisposable, IInputHandlingTerminal { public textarea: HTMLTextAreaElement; @@ -163,11 +139,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public params: (string | number)[]; public currentParam: string | number; - // write data related containers - protected _writeBuffer: (Uint8Array | string)[] = []; - private _pendingWriteDataSize: number = 0; - private _writeChunkCallbacks: ((() => void) | undefined)[] = []; - private _writeInProgress = false; + // write buffer + private _deferredWriteBuffer: WriteBuffer; // Store if user went browsing history in scrollback private _userScrolling: boolean; @@ -258,6 +231,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._setupOptionsListeners(); this._setup(); + + this._deferredWriteBuffer = new WriteBuffer(data => this._inputHandler.parse(data)); } public dispose(): void { @@ -1137,60 +1112,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } } - public write(data: string | Uint8Array, callback?: () => void): void { - // Ensure the terminal isn't disposed - // NOOP on empty data - if (this._isDisposed || !data.length) { - return; - } - - if (this._pendingWriteDataSize > DISCARD_WATERMARK) { - throw new Error('write data discarded, use flow control to avoid losing data'); - } - - this._pendingWriteDataSize += data.length; - this._writeBuffer.push(data); - this._writeChunkCallbacks.push(callback); - - if (!this._writeInProgress) { - this._writeInProgress = true; - setTimeout(() => this._innerWrite()); - } - } - - protected _innerWrite(bufferOffset: number = 0): void { - const startTime = Date.now(); - while (this._writeBuffer.length > bufferOffset) { - const data = this._writeBuffer[bufferOffset]; - const cb = this._writeChunkCallbacks[bufferOffset]; - bufferOffset++; - - this._inputHandler.parse(data); - this._pendingWriteDataSize -= data.length; - if (cb) cb(); - - this.refresh(this._dirtyRowService.start, this._dirtyRowService.end); - - if (Date.now() - startTime >= WRITE_TIMEOUT_MS) { - break; - } - } - if (this._writeBuffer.length > bufferOffset) { - // Allow renderer to catch up before processing the next batch - // trim already processed chunks if we are above threshold - if (bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) { - this._writeBuffer = this._writeBuffer.slice(bufferOffset); - this._writeChunkCallbacks = this._writeChunkCallbacks.slice(bufferOffset); - bufferOffset = 0; - } - setTimeout(() => this._innerWrite(bufferOffset), 0); - } else { - this._writeInProgress = false; - this._writeBuffer = []; - this._writeChunkCallbacks = []; - } - } - public paste(data: string): void { paste(data, this.textarea, this.bracketedPasteMode, this._coreService); } @@ -1628,8 +1549,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp const customKeyEventHandler = this._customKeyEventHandler; const inputHandler = this._inputHandler; const cursorState = this.cursorState; - const writeBuffer = this._writeBuffer; - const writeInProgress = this._writeInProgress; const userScrolling = this._userScrolling; this._setup(); @@ -1644,8 +1563,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._customKeyEventHandler = customKeyEventHandler; this._inputHandler = inputHandler; this.cursorState = cursorState; - this._writeBuffer = writeBuffer; - this._writeInProgress = writeInProgress; this._userScrolling = userScrolling; // do a full screen refresh @@ -1676,6 +1593,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // return this.options.bellStyle === 'sound' || // this.options.bellStyle === 'both'; } + + public write(data: string | Uint8Array, callback?: () => void): void { + this._deferredWriteBuffer.write(data, callback); + } + public writeSync(data: string | Uint8Array): void { + this._deferredWriteBuffer.writeSync(data); + } } /** diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index a30f60ce..a70f751c 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -19,10 +19,6 @@ import { IParams, IFunctionIdentifier } from 'common/parser/Types'; import { ISelectionService } from 'browser/services/Services'; export class TestTerminal extends Terminal { - writeSync(data: string): void { - this._writeBuffer.push(data); - this._innerWrite(); - } keyDown(ev: any): boolean { return this._keyDown(ev); } keyPress(ev: any): boolean { return this._keyPress(ev); } } diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts new file mode 100644 index 00000000..c7d82458 --- /dev/null +++ b/src/common/input/WriteBuffer.ts @@ -0,0 +1,110 @@ + +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +declare const setTimeout: (handler: () => void, timeout?: number) => void; + +/** + * Safety watermark to avoid memory exhaustion and browser engine crash on fast data input. + * 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 100 times lower number (>500 kB). + */ +const DISCARD_WATERMARK = 50000000; // ~50 MB + +/** + * The max number of ms to spend on writes before allowing the renderer to + * catch up with a 0ms setTimeout. A value of < 33 to keep us close to + * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS + * depends on the time it takes for the renderer to draw the frame. + */ +const WRITE_TIMEOUT_MS = 12; + +/** + * Threshold of max held chunks in the write buffer, that were already processed. + * This is a tradeoff between extensive write buffer shifts (bad runtime) and high + * memory consumption by data thats not used anymore. + */ +const WRITE_BUFFER_LENGTH_THRESHOLD = 50; + +export class WriteBuffer { + private _writeBuffer: (string | Uint8Array)[] = []; + private _callbacks: ((() => void) | undefined)[] = []; + private _pendingData = 0; + private _bufferOffset = 0; + + constructor(private _action: (data: string | Uint8Array) => void) { } + + public writeSync(data: string | Uint8Array): void { + // force sync processing on pending data chunks to avoid in-band data scrambling + // does the same as innerWrite but without event loop + if (this._writeBuffer.length) { + for (let i = this._bufferOffset; i < this._writeBuffer.length; ++i) { + const data = this._writeBuffer[i]; + const cb = this._callbacks[i]; + this._action(data); + if (cb) cb(); + } + // reset all to avoid reprocessing of chunks with scheduled innerWrite call + this._writeBuffer = []; + this._callbacks = []; + this._pendingData = 0; + // stop scheduled innerWrite by offset > length condition + this._bufferOffset = 0x7FFFFFFF; + } + // handle current data chunk + this._action(data); + } + + public write(data: string | Uint8Array, callback?: () => void): void { + if (this._pendingData > DISCARD_WATERMARK) { + throw new Error('write data discarded, use flow control to avoid losing data'); + } + + // schedule chunk processing for next event loop run + if (!this._writeBuffer.length) { + this._bufferOffset = 0; + setTimeout(() => this._innerWrite()); + } + + this._pendingData += data.length; + this._writeBuffer.push(data); + this._callbacks.push(callback); + } + + protected _innerWrite(): void { + const startTime = Date.now(); + while (this._writeBuffer.length > this._bufferOffset) { + const data = this._writeBuffer[this._bufferOffset]; + const cb = this._callbacks[this._bufferOffset]; + this._bufferOffset++; + + this._action(data); + this._pendingData -= data.length; + if (cb) cb(); + + if (Date.now() - startTime >= WRITE_TIMEOUT_MS) { + break; + } + } + if (this._writeBuffer.length > this._bufferOffset) { + // Allow renderer to catch up before processing the next batch + // trim already processed chunks if we are above threshold + if (this._bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) { + this._writeBuffer = this._writeBuffer.slice(this._bufferOffset); + this._callbacks = this._callbacks.slice(this._bufferOffset); + this._bufferOffset = 0; + } + setTimeout(() => this._innerWrite(), 0); + } else { + this._writeBuffer = []; + this._callbacks = []; + this._pendingData = 0; + this._bufferOffset = 0; + } + } +} diff --git a/test/benchmark/Terminal.benchmark.ts b/test/benchmark/Terminal.benchmark.ts index 0e34ebb3..71a36cc7 100644 --- a/test/benchmark/Terminal.benchmark.ts +++ b/test/benchmark/Terminal.benchmark.ts @@ -49,8 +49,8 @@ perfContext('Terminal: ls -lR /usr/lib', () => { before(() => { terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000}); }); - new ThroughputRuntimeCase('', async () => { - await new Promise(resolve => terminal.write(content, resolve)); + new ThroughputRuntimeCase('', () => { + terminal.writeSync(content); return {payloadSize: contentUtf8.length}; }, {fork: false}).showAverageThroughput(); }); @@ -60,8 +60,8 @@ perfContext('Terminal: ls -lR /usr/lib', () => { before(() => { terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000}); }); - new ThroughputRuntimeCase('', async () => { - await new Promise(resolve => terminal.write(content, resolve)); + new ThroughputRuntimeCase('', () => { + terminal.writeSync(content); return {payloadSize: contentUtf8.length}; }, {fork: false}).showAverageThroughput(); }); From 1ac3cf8fca117061693fc9bc0cf3b5f93640652f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 13 Sep 2019 16:45:40 +0200 Subject: [PATCH 24/38] use callback in paste integration test --- test/api/Terminal.api.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index b2157b1e..29a361a9 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -83,11 +83,9 @@ describe('API Integration Tests', function(): void { window.term.onData(e => calls.push(e)); window.term.paste('foo'); window.term.paste('\\r\\nfoo\\nbar\\r'); - window.term.write('\\x1b[?2004h'); - // TODO: Use promise/callback for write when we support that - // Force sync write - window.term._core._innerWrite(); - window.term.paste('foo'); + window.term.write('\\x1b[?2004h', () => { + window.term.paste('foo'); + }); `); assert.deepEqual(await page.evaluate(`window.calls`), ['foo', '\rfoo\rbar\r', '\x1b[200~foo\x1b[201~']); }); From 91734f76599578443a72f0fd272814784372f075 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 13 Sep 2019 09:16:22 -0700 Subject: [PATCH 25/38] Set addon peer deps to v4 Fixes #2428 --- addons/xterm-addon-attach/package.json | 2 +- addons/xterm-addon-fit/package.json | 2 +- addons/xterm-addon-search/package.json | 2 +- addons/xterm-addon-web-links/package.json | 2 +- addons/xterm-addon-webgl/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-attach/package.json b/addons/xterm-addon-attach/package.json index 6a4fa464..a8d9b035 100644 --- a/addons/xterm-addon-attach/package.json +++ b/addons/xterm-addon-attach/package.json @@ -16,6 +16,6 @@ "prepublishOnly": "npm run package" }, "peerDependencies": { - "xterm": "^3.14.0" + "xterm": "^4.0.0" } } diff --git a/addons/xterm-addon-fit/package.json b/addons/xterm-addon-fit/package.json index 03a2c1e6..ec7458db 100644 --- a/addons/xterm-addon-fit/package.json +++ b/addons/xterm-addon-fit/package.json @@ -16,6 +16,6 @@ "prepublishOnly": "npm run package" }, "peerDependencies": { - "xterm": "^3.14.0" + "xterm": "^4.0.0" } } diff --git a/addons/xterm-addon-search/package.json b/addons/xterm-addon-search/package.json index 4438f418..9f81728e 100644 --- a/addons/xterm-addon-search/package.json +++ b/addons/xterm-addon-search/package.json @@ -16,6 +16,6 @@ "prepublishOnly": "npm run package" }, "peerDependencies": { - "xterm": "^3.14.0" + "xterm": "^4.0.0" } } diff --git a/addons/xterm-addon-web-links/package.json b/addons/xterm-addon-web-links/package.json index a6240abd..01e79775 100644 --- a/addons/xterm-addon-web-links/package.json +++ b/addons/xterm-addon-web-links/package.json @@ -16,6 +16,6 @@ "prepublishOnly": "npm run package" }, "peerDependencies": { - "xterm": "^3.14.0" + "xterm": "^4.0.0" } } diff --git a/addons/xterm-addon-webgl/package.json b/addons/xterm-addon-webgl/package.json index 378516d5..728ebd66 100644 --- a/addons/xterm-addon-webgl/package.json +++ b/addons/xterm-addon-webgl/package.json @@ -16,6 +16,6 @@ "prepublishOnly": "npm run package" }, "peerDependencies": { - "xterm": "^3.14.0" + "xterm": "^4.0.0" } } From aba4d76ec61451ad8b5b3bdc95db7a83206b899a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 13 Sep 2019 09:16:55 -0700 Subject: [PATCH 26/38] v0.2.1 of all addons --- addons/xterm-addon-attach/package.json | 2 +- addons/xterm-addon-fit/package.json | 2 +- addons/xterm-addon-search/package.json | 2 +- addons/xterm-addon-web-links/package.json | 2 +- addons/xterm-addon-webgl/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-attach/package.json b/addons/xterm-addon-attach/package.json index a8d9b035..5a5c5d75 100644 --- a/addons/xterm-addon-attach/package.json +++ b/addons/xterm-addon-attach/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-attach", - "version": "0.2.0", + "version": "0.2.1", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-fit/package.json b/addons/xterm-addon-fit/package.json index ec7458db..fa1bf852 100644 --- a/addons/xterm-addon-fit/package.json +++ b/addons/xterm-addon-fit/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-fit", - "version": "0.2.0", + "version": "0.2.1", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-search/package.json b/addons/xterm-addon-search/package.json index 9f81728e..c2e14032 100644 --- a/addons/xterm-addon-search/package.json +++ b/addons/xterm-addon-search/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-search", - "version": "0.2.0", + "version": "0.2.1", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-web-links/package.json b/addons/xterm-addon-web-links/package.json index 01e79775..6167de74 100644 --- a/addons/xterm-addon-web-links/package.json +++ b/addons/xterm-addon-web-links/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-web-links", - "version": "0.2.0", + "version": "0.2.1", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-webgl/package.json b/addons/xterm-addon-webgl/package.json index 728ebd66..9a01aea0 100644 --- a/addons/xterm-addon-webgl/package.json +++ b/addons/xterm-addon-webgl/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-webgl", - "version": "0.2.0", + "version": "0.2.1", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" From 3b537c7e9fd94a989fc26fc20cfba8789ff3a71f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 13 Sep 2019 09:32:38 -0700 Subject: [PATCH 27/38] Improve api jsdoc --- typings/xterm.d.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index faa83d72..44ef66bd 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -651,21 +651,21 @@ declare module 'xterm' { /** * Write data to the terminal. - * `data` can either be raw bytes given as Uint8Array from the pty or a string. - * Raw bytes will always be treated as UTF-8 encoded, string data as UTF-16. - * `callback` is an optional callback that gets called once the data - * chunk was processed by the parser. - * @param data The data to write to the terminal. - * @param callback Optional callback when data was processed. + * @param data The data to write to the terminal. This can either be raw + * bytes given as Uint8Array from the pty or a string. Raw bytes will always + * be treated as UTF-8 encoded, string data as UTF-16. + * @param callback Optional callback that fires when the data was processed + * by the parser. */ write(data: string | Uint8Array, callback?: () => void): void; /** * Writes data to the terminal, followed by a break line character (\n). - * `callback` is an optional callback that gets called once the data - * chunk was processed by the parser. - * @param data The data to write to the terminal. - * @param callback Optional callback when data was processed. + * @param data The data to write to the terminal. This can either be raw + * bytes given as Uint8Array from the pty or a string. Raw bytes will always + * be treated as UTF-8 encoded, string data as UTF-16. + * @param callback Optional callback that fires when the data was processed + * by the parser. */ writeln(data: string | Uint8Array, callback?: () => void): void; From bf209fb71ff4888efc5bafb7e9f010b971f07ffd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 14 Sep 2019 11:53:42 -0700 Subject: [PATCH 28/38] Add tests for IL (CSI Ps L) Part of #2117 --- test/api/InputHandler.api.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index 9ca0c943..ae1ecda8 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -196,6 +196,16 @@ describe('InputHandler Integration Tests', function(): void { assert.deepEqual(await getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']); }); + it.only('IL: Insert Ps Line(s) (default = 1) - CSI Ps L', async function(): Promise { + await page.evaluate(` + // Default + window.term.write('foo\x1b[La') + // Explicit + window.term.write('\x1b[2Lb') + `); + assert.deepEqual(await getLinesAsArray(4), ['b', '', 'a', 'foo']); + }); + describe('DSR: Device Status Report', () => { it('Status Report - CSI 5 n', async function(): Promise { await page.evaluate(` From 96f944522e1beb750f8ddd6031e860cf3c0c3d2b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 14 Sep 2019 12:05:00 -0700 Subject: [PATCH 29/38] Add tests for DL (CSI Ps M) Part of #2117 --- test/api/InputHandler.api.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index ae1ecda8..60bc0e9d 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -196,7 +196,7 @@ describe('InputHandler Integration Tests', function(): void { assert.deepEqual(await getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']); }); - it.only('IL: Insert Ps Line(s) (default = 1) - CSI Ps L', async function(): Promise { + it('IL: Insert Ps Line(s) (default = 1) - CSI Ps L', async function(): Promise { await page.evaluate(` // Default window.term.write('foo\x1b[La') @@ -206,6 +206,16 @@ describe('InputHandler Integration Tests', function(): void { assert.deepEqual(await getLinesAsArray(4), ['b', '', 'a', 'foo']); }); + it('DL: Delete Ps Line(s) (default = 1) - CSI Ps M', async function(): Promise { + await page.evaluate(` + // Default + window.term.write('a\\nb\x1b[1F\x1b[M') + // Explicit + window.term.write('\x1b[1Ed\\ne\\nf\x1b[2F\x1b[2M') + `); + assert.deepEqual(await getLinesAsArray(5), [' b', ' f', '', '', '']); + }); + describe('DSR: Device Status Report', () => { it('Status Report - CSI 5 n', async function(): Promise { await page.evaluate(` From 74ba282056c290a93d5cf65a62c61c159edde77c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 14 Sep 2019 12:15:16 -0700 Subject: [PATCH 30/38] Add tests for DCH (CSI Ps P) Part of #2117 --- test/api/InputHandler.api.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index 60bc0e9d..a892967a 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -216,6 +216,16 @@ describe('InputHandler Integration Tests', function(): void { assert.deepEqual(await getLinesAsArray(5), [' b', ' f', '', '', '']); }); + it('DCH: Delete Ps Character(s) (default = 1) - CSI Ps P', async function(): Promise { + await page.evaluate(` + // Default + window.term.write('abc\x1b[1;1H\x1b[P') + // Explicit + window.term.write('\\n\\rdef\x1b[2;1H\x1b[2P') + `); + assert.deepEqual(await getLinesAsArray(5), ['bc', 'f', '', '', '']); + }); + describe('DSR: Device Status Report', () => { it('Status Report - CSI 5 n', async function(): Promise { await page.evaluate(` From bc7ba7bd85f7ed938d46a1164533c88ce4ffe410 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 14 Sep 2019 12:57:41 -0700 Subject: [PATCH 31/38] Simplify test --- test/api/InputHandler.api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index a892967a..05b16010 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -223,7 +223,7 @@ describe('InputHandler Integration Tests', function(): void { // Explicit window.term.write('\\n\\rdef\x1b[2;1H\x1b[2P') `); - assert.deepEqual(await getLinesAsArray(5), ['bc', 'f', '', '', '']); + assert.deepEqual(await getLinesAsArray(2), ['bc', 'f']); }); describe('DSR: Device Status Report', () => { From d1bd60338274d7245cb0f3eb118b9b15bc0abc0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 22 Sep 2019 20:39:09 +0200 Subject: [PATCH 32/38] unit tests --- src/Terminal.ts | 9 +-- src/common/input/WriteBuffer.test.ts | 88 ++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 src/common/input/WriteBuffer.test.ts diff --git a/src/Terminal.ts b/src/Terminal.ts index aa55ebcb..f95873b8 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -140,7 +140,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public currentParam: string | number; // write buffer - private _deferredWriteBuffer: WriteBuffer; + private _writeBuffer: WriteBuffer; // Store if user went browsing history in scrollback private _userScrolling: boolean; @@ -232,7 +232,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._setupOptionsListeners(); this._setup(); - this._deferredWriteBuffer = new WriteBuffer(data => this._inputHandler.parse(data)); + this._writeBuffer = new WriteBuffer(data => this._inputHandler.parse(data)); } public dispose(): void { @@ -1595,10 +1595,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } public write(data: string | Uint8Array, callback?: () => void): void { - this._deferredWriteBuffer.write(data, callback); + this._writeBuffer.write(data, callback); } + public writeSync(data: string | Uint8Array): void { - this._deferredWriteBuffer.writeSync(data); + this._writeBuffer.writeSync(data); } } diff --git a/src/common/input/WriteBuffer.test.ts b/src/common/input/WriteBuffer.test.ts new file mode 100644 index 00000000..ab9433da --- /dev/null +++ b/src/common/input/WriteBuffer.test.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { WriteBuffer } from './WriteBuffer'; + +declare let Buffer: any; + +function toBytes(s: string): Uint8Array { + return Buffer.from(s); +} + +function fromBytes(bytes: Uint8Array): string { + return bytes.toString(); +} + +describe('WriteBuffer', () => { + let wb: WriteBuffer; + let stack: (string | Uint8Array)[] = []; + let cbStack: string[] = []; + beforeEach(() => { + stack = []; + cbStack = []; + wb = new WriteBuffer(data => { stack.push(data); }); + }); + describe('write input', () => { + it('string', done => { + wb.write('a._'); + wb.write('b.x', () => { cbStack.push('b'); }); + wb.write('c._'); + wb.write('d.x', () => { cbStack.push('d'); }); + wb.write('e', () => { + assert.deepEqual(stack, ['a._', 'b.x', 'c._', 'd.x', 'e']); + assert.deepEqual(cbStack, ['b', 'd']); + done(); + }); + }); + it('bytes', done => { + wb.write(toBytes('a._')); + wb.write(toBytes('b.x'), () => { cbStack.push('b'); }); + wb.write(toBytes('c._')); + wb.write(toBytes('d.x'), () => { cbStack.push('d'); }); + wb.write(toBytes('e'), () => { + assert.deepEqual(stack.map(val => typeof val === 'string' ? '' : fromBytes(val)), ['a._', 'b.x', 'c._', 'd.x', 'e']); + assert.deepEqual(cbStack, ['b', 'd']); + done(); + }); + }); + it('string/bytes mixed', done => { + wb.write('a._'); + wb.write('b.x', () => { cbStack.push('b'); }); + wb.write(toBytes('c._')); + wb.write(toBytes('d.x'), () => { cbStack.push('d'); }); + wb.write(toBytes('e'), () => { + assert.deepEqual(stack.map(val => typeof val === 'string' ? val : fromBytes(val)), ['a._', 'b.x', 'c._', 'd.x', 'e']); + assert.deepEqual(cbStack, ['b', 'd']); + done(); + }); + }); + it('write callback works for empty chunks', done => { + wb.write('a', () => { cbStack.push('a'); }); + wb.write('', () => { cbStack.push('b'); }); + wb.write(toBytes('c'), () => { cbStack.push('c'); }); + wb.write(new Uint8Array(0), () => { cbStack.push('d'); }); + wb.write('e', () => { + assert.deepEqual(stack.map(val => typeof val === 'string' ? val : fromBytes(val)), ['a', '', 'c', '', 'e']); + assert.deepEqual(cbStack, ['a', 'b', 'c', 'd']); + done(); + }); + }); + it('writeSync', done => { + wb.write('a', () => { cbStack.push('a'); }); + wb.write('b', () => { cbStack.push('b'); }); + wb.write('c', () => { cbStack.push('c'); }); + wb.writeSync('d'); + assert.deepEqual(stack, ['a', 'b', 'c', 'd']); + assert.deepEqual(cbStack, ['a', 'b', 'c']); + wb.write('x', () => { cbStack.push('x'); }); + wb.write('', () => { + assert.deepEqual(stack, ['a', 'b', 'c', 'd', 'x', '']); + assert.deepEqual(cbStack, ['a', 'b', 'c', 'x']); + done(); + }); + }); + }); +}); From 6aab756ccdd43438814629f1263e14067e71e054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 22 Sep 2019 20:57:00 +0200 Subject: [PATCH 33/38] integration callback tests --- test/api/Terminal.api.ts | 66 +++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index 29a361a9..034b04c4 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -51,6 +51,44 @@ describe('API Integration Tests', function(): void { assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文'); }); + it('write with callback', async function(): Promise { + await openTerminal(); + await page.evaluate(` + window.term.write('foo', () => { window.__x = 'a'; }); + window.term.write('bar', () => { window.__x += 'b'; }); + window.term.write('文', () => { window.__x += 'c'; }); + `); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文'); + assert.equal(await page.evaluate(`window.__x`), 'abc'); + }); + + it('write - bytes (UTF8)', async function(): Promise { + await openTerminal(); + await page.evaluate(` + // foo + window.term.write(new Uint8Array([102, 111, 111])); + // bar + window.term.write(new Uint8Array([98, 97, 114])); + // 文 + window.term.write(new Uint8Array([230, 150, 135])); + `); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文'); + }); + + it('write - bytes (UTF8) with callback', async function(): Promise { + await openTerminal(); + await page.evaluate(` + // foo + window.term.write(new Uint8Array([102, 111, 111]), () => { window.__x = 'A'; }); + // bar + window.term.write(new Uint8Array([98, 97, 114]), () => { window.__x += 'B'; }); + // 文 + window.term.write(new Uint8Array([230, 150, 135]), () => { window.__x += 'C'; }); + `); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文'); + assert.equal(await page.evaluate(`window.__x`), 'ABC'); + }); + it('writeln', async function(): Promise { await openTerminal(); await page.evaluate(` @@ -63,17 +101,29 @@ describe('API Integration Tests', function(): void { assert.equal(await page.evaluate(`window.term.buffer.getLine(2).translateToString(true)`), '文'); }); - it('writeUtf8', async function(): Promise { + it('writeln with callback', async function(): Promise { await openTerminal(); await page.evaluate(` - // foo - window.term.writeUtf8(new Uint8Array([102, 111, 111])); - // bar - window.term.writeUtf8(new Uint8Array([98, 97, 114])); - // 文 - window.term.writeUtf8(new Uint8Array([230, 150, 135])); + window.term.writeln('foo', () => { window.__x = '1'; }); + window.term.writeln('bar', () => { window.__x += '2'; }); + window.term.writeln('文', () => { window.__x += '3'; }); `); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文'); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo'); + assert.equal(await page.evaluate(`window.term.buffer.getLine(1).translateToString(true)`), 'bar'); + assert.equal(await page.evaluate(`window.term.buffer.getLine(2).translateToString(true)`), '文'); + assert.equal(await page.evaluate(`window.__x`), '123'); + }); + + it('writeln - bytes (UTF8)', async function(): Promise { + await openTerminal(); + await page.evaluate(` + window.term.writeln(new Uint8Array([102, 111, 111])); + window.term.writeln(new Uint8Array([98, 97, 114])); + window.term.writeln(new Uint8Array([230, 150, 135])); + `); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo'); + assert.equal(await page.evaluate(`window.term.buffer.getLine(1).translateToString(true)`), 'bar'); + assert.equal(await page.evaluate(`window.term.buffer.getLine(2).translateToString(true)`), '文'); }); it('paste', async function(): Promise { From d34d1f56cb3702a6523d02fb748e73d92c7f6706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 22 Sep 2019 22:33:55 +0200 Subject: [PATCH 34/38] simplify attach addon --- .../xterm-addon-attach/src/AttachAddon.api.ts | 2 +- addons/xterm-addon-attach/src/AttachAddon.ts | 18 ++++++++---------- .../typings/xterm-addon-attach.d.ts | 8 -------- demo/client.ts | 7 ------- demo/server.js | 9 ++++----- src/Terminal.ts | 2 +- 6 files changed, 14 insertions(+), 32 deletions(-) diff --git a/addons/xterm-addon-attach/src/AttachAddon.api.ts b/addons/xterm-addon-attach/src/AttachAddon.api.ts index 945824a2..c5b2d858 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.api.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.api.ts @@ -54,7 +54,7 @@ describe('AttachAddon', () => { const server = new WebSocket.Server({ port }); const data = new Uint8Array([102, 111, 111]); server.on('connection', socket => socket.send(data)); - await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}'), { inputUtf8: true }))`); + await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo'); server.close(); }); diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts index 9dc45ecb..d0bbce16 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.ts @@ -9,13 +9,11 @@ import { Terminal, IDisposable, ITerminalAddon } from 'xterm'; interface IAttachOptions { bidirectional?: boolean; - inputUtf8?: boolean; } export class AttachAddon implements ITerminalAddon { private _socket: WebSocket; private _bidirectional: boolean; - private _utf8: boolean; private _disposables: IDisposable[] = []; constructor(socket: WebSocket, options?: IAttachOptions) { @@ -23,17 +21,17 @@ export class AttachAddon implements ITerminalAddon { // always set binary type to arraybuffer, we do not handle blobs this._socket.binaryType = 'arraybuffer'; this._bidirectional = (options && options.bidirectional === false) ? false : true; - this._utf8 = !!(options && options.inputUtf8); } 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)))); - } 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) => { + const data: ArrayBuffer | string = (ev as any).data; + terminal.write(typeof data === 'string' ? data : new Uint8Array(data)); + } + ) + ); if (this._bidirectional) { this._disposables.push(terminal.onData(data => this._sendData(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 8c667140..1aa21357 100644 --- a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts +++ b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts @@ -11,14 +11,6 @@ declare module 'xterm-addon-attach' { * Whether input should be written to the backend. Defaults to `true`. */ bidirectional?: boolean; - - /** - * Whether to use UTF8 binary transport for incoming messages. Defaults to `false`. - * Note: This must be in line with the server side of the websocket. - * Always send string messages from the backend if this options is false, - * otherwise always binary UTF8 data. - */ - inputUtf8?: boolean; } export class AttachAddon implements ITerminalAddon { diff --git a/demo/client.ts b/demo/client.ts index 040292e4..92bac731 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -167,14 +167,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._initialized = true; } diff --git a/demo/server.js b/demo/server.js index f1fae572..a2987558 100644 --- a/demo/server.js +++ b/demo/server.js @@ -4,10 +4,9 @@ var os = require('os'); var pty = require('node-pty'); /** - * Whether to use UTF8 binary transport. - * (Must also be switched in client.ts) + * Whether to use binary transport. */ -const USE_BINARY_UTF8 = false; +const USE_BINARY = true; function startServer() { @@ -46,7 +45,7 @@ function startServer() { rows: rows || 24, cwd: env.PWD, env: env, - encoding: USE_BINARY_UTF8 ? null : 'utf8' + encoding: USE_BINARY ? null : 'utf8' }); console.log('Created terminal with PID: ' + term.pid); @@ -108,7 +107,7 @@ function startServer() { } }; } - const send = USE_BINARY_UTF8 ? bufferUtf8(ws, 5) : buffer(ws, 5); + const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5); term.on('data', function(data) { try { diff --git a/src/Terminal.ts b/src/Terminal.ts index f95873b8..53d8fe3c 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1597,7 +1597,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public write(data: string | Uint8Array, callback?: () => void): void { this._writeBuffer.write(data, callback); } - + public writeSync(data: string | Uint8Array): void { this._writeBuffer.writeSync(data); } From f3b7866a19bc9cab90044602abb9c789464786eb Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 23 Sep 2019 07:47:02 -0700 Subject: [PATCH 35/38] Fix NPE when open called twice Fixes #2433 --- src/Terminal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index d793abfc..098c383b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -604,8 +604,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); - this._theme = this.options.theme; - this.options.theme = null; + this._theme = this.options.theme || this._theme; + this.options.theme = undefined; this._colorManager = new ColorManager(document, this.options.allowTransparency); this._colorManager.setTheme(this._theme); From 2fd24849b28ebbb52ad228f1354b746bb122c0d0 Mon Sep 17 00:00:00 2001 From: Vitaly Petrov Date: Tue, 24 Sep 2019 17:46:12 +0300 Subject: [PATCH 36/38] Update README.md --- addons/xterm-addon-attach/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-attach/README.md b/addons/xterm-addon-attach/README.md index 2040455c..67ebf17b 100644 --- a/addons/xterm-addon-attach/README.md +++ b/addons/xterm-addon-attach/README.md @@ -12,7 +12,7 @@ npm install --save xterm-addon-attach ```ts import { Terminal } from 'xterm'; -import { FitAddon } from 'xterm-addon-attach'; +import { AttachAddon } from 'xterm-addon-attach'; const terminal = new Terminal(); const attachAddon = new AttachAddon(webSocket); From 0e8883ac993c890c6b45db374a5e4d1b8a5df467 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 25 Sep 2019 17:00:31 -0700 Subject: [PATCH 37/38] Strongly type addSocketListener --- addons/xterm-addon-attach/src/AttachAddon.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts index d0bbce16..117b2b58 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.ts @@ -25,12 +25,10 @@ export class AttachAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._disposables.push( - addSocketListener(this._socket, 'message', - (ev: MessageEvent | Event | CloseEvent) => { - const data: ArrayBuffer | string = (ev as any).data; - terminal.write(typeof data === 'string' ? data : new Uint8Array(data)); - } - ) + addSocketListener(this._socket, 'message', ev => { + const data: ArrayBuffer | string = ev.data; + terminal.write(typeof data === 'string' ? data : new Uint8Array(data)); + }) ); if (this._bidirectional) { @@ -55,7 +53,7 @@ export class AttachAddon implements ITerminalAddon { } } -function addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: MessageEvent | Event | CloseEvent) => any): IDisposable { +function addSocketListener(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable { socket.addEventListener(type, handler); return { dispose: () => { From 9014a27d7d0e423f60d5265cf9bcb81637d2d9d0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 28 Sep 2019 09:25:41 -0700 Subject: [PATCH 38/38] Fix demo on Windows Fixes #2441 --- demo/server.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/demo/server.js b/demo/server.js index a2987558..9a45d06f 100644 --- a/demo/server.js +++ b/demo/server.js @@ -3,11 +3,8 @@ var expressWs = require('express-ws'); var os = require('os'); var pty = require('node-pty'); -/** - * Whether to use binary transport. - */ -const USE_BINARY = true; - +// Whether to use binary transport. +const USE_BINARY = os.platform() !== "win32"; function startServer() { var app = express();