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'
+  });
+}