From a1123f0a29471169ede46ef2a582cd7f2f59ddf5 Mon Sep 17 00:00:00 2001 From: Nitzan Uziely <linkgoron@gmail.com> Date: Fri, 26 Mar 2021 19:30:00 +0300 Subject: [PATCH] readline: add AbortSignal support to interface Add abort signal support to Interface PR-URL: https://github.com/nodejs/node/pull/37932 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> --- doc/api/readline.md | 5 +++ lib/readline.js | 19 +++++++- test/parallel/test-readline-interface.js | 57 +++++++++++++++++++++++- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/doc/api/readline.md b/doc/api/readline.md index 4a8d8f20ac03c6..5081dc713fe8a4 100644 --- a/doc/api/readline.md +++ b/doc/api/readline.md @@ -544,6 +544,9 @@ the current position of the cursor down. <!-- YAML added: v0.1.98 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/37932 + description: The `signal` option is supported now. - version: v15.8.0 pr-url: https://github.com/nodejs/node/pull/33662 description: The `history` option is supported now. @@ -601,6 +604,8 @@ changes: **Default:** `500`. * `tabSize` {integer} The number of spaces a tab is equal to (minimum 1). **Default:** `8`. + * `signal` {AbortSignal} Allows closing the interface using an AbortSignal. + Aborting the signal will internally call `close` on the interface. * Returns: {readline.Interface} The `readline.createInterface()` method creates a new `readline.Interface` diff --git a/lib/readline.js b/lib/readline.js index 040434a1e38e5f..1ee3ee2bb8ec76 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -74,6 +74,7 @@ const { ERR_INVALID_CURSOR_POS, } = codes; const { + validateAbortSignal, validateArray, validateCallback, validateString, @@ -150,7 +151,7 @@ function Interface(input, output, completer, terminal) { let removeHistoryDuplicates = false; let crlfDelay; let prompt = '> '; - + let signal; if (input && input.input) { // An options object was given output = input.output; @@ -158,6 +159,7 @@ function Interface(input, output, completer, terminal) { terminal = input.terminal; history = input.history; historySize = input.historySize; + signal = input.signal; if (input.tabSize !== undefined) { validateUint32(input.tabSize, 'tabSize', true); this.tabSize = input.tabSize; @@ -176,6 +178,11 @@ function Interface(input, output, completer, terminal) { ); } } + + if (signal) { + validateAbortSignal(signal, 'options.signal'); + } + crlfDelay = input.crlfDelay; input = input.input; } @@ -312,6 +319,16 @@ function Interface(input, output, completer, terminal) { self.once('close', onSelfCloseWithTerminal); } + if (signal) { + const onAborted = () => self.close(); + if (signal.aborted) { + process.nextTick(onAborted); + } else { + signal.addEventListener('abort', onAborted, { once: true }); + self.once('close', () => signal.removeEventListener('abort', onAborted)); + } + } + // Current line this.line = ''; diff --git a/test/parallel/test-readline-interface.js b/test/parallel/test-readline-interface.js index 4660e5b9f56937..42de0499976a00 100644 --- a/test/parallel/test-readline-interface.js +++ b/test/parallel/test-readline-interface.js @@ -31,7 +31,7 @@ const { getStringWidth, stripVTControlCharacters } = require('internal/util/inspect'); -const EventEmitter = require('events').EventEmitter; +const { EventEmitter, getEventListeners } = require('events'); const { Writable, Readable } = require('stream'); class FakeInput extends EventEmitter { @@ -1132,3 +1132,58 @@ for (let i = 0; i < 12; i++) { rl.line = `a${' '.repeat(1e6)}a`; rl.cursor = rl.line.length; } + +{ + const fi = new FakeInput(); + const signal = AbortSignal.abort(); + + const rl = readline.createInterface({ + input: fi, + output: fi, + signal, + }); + rl.on('close', common.mustCall()); + assert.strictEqual(getEventListeners(signal, 'abort').length, 0); +} + +{ + const fi = new FakeInput(); + const ac = new AbortController(); + const { signal } = ac; + const rl = readline.createInterface({ + input: fi, + output: fi, + signal, + }); + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + rl.on('close', common.mustCall()); + ac.abort(); + assert.strictEqual(getEventListeners(signal, 'abort').length, 0); +} + +{ + const fi = new FakeInput(); + const ac = new AbortController(); + const { signal } = ac; + const rl = readline.createInterface({ + input: fi, + output: fi, + signal, + }); + assert.strictEqual(getEventListeners(signal, 'abort').length, 1); + rl.close(); + assert.strictEqual(getEventListeners(signal, 'abort').length, 0); +} + +{ + // Constructor throws if signal is not an abort signal + assert.throws(() => { + readline.createInterface({ + input: new FakeInput(), + signal: {}, + }); + }, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE' + }); +}