diff --git a/doc/api/readline.md b/doc/api/readline.md index 4a8d8f20ac03c6..68cce286fc6ae0 100644 --- a/doc/api/readline.md +++ b/doc/api/readline.md @@ -601,6 +601,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 cd1bfb8b6aaa51..41a4a958b1137d 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; } @@ -320,6 +327,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' + }); +}