From 51ad99b49b2695e4d64d11cb414fc432c38ecef2 Mon Sep 17 00:00:00 2001
From: legendecas <legendecas@gmail.com>
Date: Mon, 19 Aug 2019 21:03:08 +0800
Subject: [PATCH] src,lib: make ^C print a JS stack trace

If terminating the process with ctrl-c / SIGINT, prints a JS stacktrace
leading up to the currently executing code.

The feature would be enabled under option `--trace-sigint`.

Conditions of no stacktrace on sigint:

- has (an) active sigint listener(s);
- main thread is idle (i.e. uv polling), a message instead of stacktrace
  would be printed.

PR-URL: https://github.com/nodejs/node/pull/29207
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Christopher Hiller <boneskull@boneskull.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
---
 doc/api/cli.md                                |   8 +
 doc/node.1                                    |   2 +
 lib/internal/bootstrap/pre_execution.js       |  13 ++
 lib/internal/watchdog.js                      |  59 +++++++
 node.gyp                                      |   1 +
 src/async_wrap.h                              |   1 +
 src/memory_tracker-inl.h                      |  14 ++
 src/memory_tracker.h                          |   7 +
 src/node_binding.cc                           |   1 +
 src/node_options.cc                           |   5 +
 src/node_options.h                            |   1 +
 src/node_watchdog.cc                          | 144 +++++++++++++++++-
 src/node_watchdog.h                           |  54 ++++++-
 test/pseudo-tty/test-trace-sigint-disabled.js |  39 +++++
 .../pseudo-tty/test-trace-sigint-disabled.out |   0
 test/pseudo-tty/test-trace-sigint-on-idle.js  |  30 ++++
 test/pseudo-tty/test-trace-sigint-on-idle.out |   1 +
 test/pseudo-tty/test-trace-sigint.js          |  30 ++++
 test/pseudo-tty/test-trace-sigint.out         |   9 ++
 test/sequential/test-async-wrap-getasyncid.js |   1 +
 20 files changed, 405 insertions(+), 15 deletions(-)
 create mode 100644 lib/internal/watchdog.js
 create mode 100644 test/pseudo-tty/test-trace-sigint-disabled.js
 create mode 100644 test/pseudo-tty/test-trace-sigint-disabled.out
 create mode 100644 test/pseudo-tty/test-trace-sigint-on-idle.js
 create mode 100644 test/pseudo-tty/test-trace-sigint-on-idle.out
 create mode 100644 test/pseudo-tty/test-trace-sigint.js
 create mode 100644 test/pseudo-tty/test-trace-sigint.out

diff --git a/doc/api/cli.md b/doc/api/cli.md
index 821f82be1d2a6e..731ddfbc95bd48 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -820,6 +820,13 @@ added: v12.16.0
 Prints a stack trace whenever an environment is exited proactively,
 i.e. invoking `process.exit()`.
 
+### `--trace-sigint`
+<!-- YAML
+added: REPLACEME
+-->
+
+Prints a stack trace on SIGINT.
+
 ### `--trace-sync-io`
 <!-- YAML
 added: v2.1.0
@@ -1149,6 +1156,7 @@ Node.js options that are allowed are:
 * `--trace-event-file-pattern`
 * `--trace-events-enabled`
 * `--trace-exit`
+* `--trace-sigint`
 * `--trace-sync-io`
 * `--trace-tls`
 * `--trace-uncaught`
diff --git a/doc/node.1 b/doc/node.1
index 8c9abd48435c6c..93ccf7f721a3c5 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -388,6 +388,8 @@ Enable the collection of trace event tracing information.
 .It Fl -trace-exit
 Prints a stack trace whenever an environment is exited proactively,
 i.e. invoking `process.exit()`.
+.It Fl -trace-sigint
+Prints a stack trace on SIGINT.
 .
 .It Fl -trace-sync-io
 Print a stack trace whenever synchronous I/O is detected after the first turn of the event loop.
diff --git a/lib/internal/bootstrap/pre_execution.js b/lib/internal/bootstrap/pre_execution.js
index 1f4d5c04b9b9d5..46af6c30e0daa8 100644
--- a/lib/internal/bootstrap/pre_execution.js
+++ b/lib/internal/bootstrap/pre_execution.js
@@ -37,6 +37,9 @@ function prepareMainThreadExecution(expandArgv1 = false) {
 
   setupDebugEnv();
 
+  // Print stack trace on `SIGINT` if option `--trace-sigint` presents.
+  setupStacktracePrinterOnSigint();
+
   // Process initial diagnostic reporting configuration, if present.
   initializeReport();
   initializeReportSignalHandlers();  // Main-thread-only.
@@ -149,6 +152,16 @@ function setupCoverageHooks(dir) {
   return coverageDirectory;
 }
 
+function setupStacktracePrinterOnSigint() {
+  if (!getOptionValue('--trace-sigint')) {
+    return;
+  }
+  const { SigintWatchdog } = require('internal/watchdog');
+
+  const watchdog = new SigintWatchdog();
+  watchdog.start();
+}
+
 function initializeReport() {
   if (!getOptionValue('--experimental-report')) {
     return;
diff --git a/lib/internal/watchdog.js b/lib/internal/watchdog.js
new file mode 100644
index 00000000000000..6a5b772111f4d6
--- /dev/null
+++ b/lib/internal/watchdog.js
@@ -0,0 +1,59 @@
+'use strict';
+
+const {
+  TraceSigintWatchdog
+} = internalBinding('watchdog');
+
+class SigintWatchdog extends TraceSigintWatchdog {
+  _started = false;
+  _effective = false;
+  _onNewListener = (eve) => {
+    if (eve === 'SIGINT' && this._effective) {
+      super.stop();
+      this._effective = false;
+    }
+  };
+  _onRemoveListener = (eve) => {
+    if (eve === 'SIGINT' && process.listenerCount('SIGINT') === 0 &&
+        !this._effective) {
+      super.start();
+      this._effective = true;
+    }
+  }
+
+  start() {
+    if (this._started) {
+      return;
+    }
+    this._started = true;
+    // Prepend sigint newListener to remove stop watchdog before signal wrap
+    // been activated. Also make sigint removeListener been ran after signal
+    // wrap been stopped.
+    process.prependListener('newListener', this._onNewListener);
+    process.addListener('removeListener', this._onRemoveListener);
+
+    if (process.listenerCount('SIGINT') === 0) {
+      super.start();
+      this._effective = true;
+    }
+  }
+
+  stop() {
+    if (!this._started) {
+      return;
+    }
+    this._started = false;
+    process.removeListener('newListener', this._onNewListener);
+    process.removeListener('removeListener', this._onRemoveListener);
+
+    if (this._effective) {
+      super.stop();
+      this._effective = false;
+    }
+  }
+}
+
+
+module.exports = {
+  SigintWatchdog
+};
diff --git a/node.gyp b/node.gyp
index 784ce677c3d081..ad2d1159b6e099 100644
--- a/node.gyp
+++ b/node.gyp
@@ -211,6 +211,7 @@
       'lib/internal/vm/module.js',
       'lib/internal/worker.js',
       'lib/internal/worker/io.js',
+      'lib/internal/watchdog.js',
       'lib/internal/streams/lazy_transform.js',
       'lib/internal/streams/async_iterator.js',
       'lib/internal/streams/buffer_list.js',
diff --git a/src/async_wrap.h b/src/async_wrap.h
index 521560c795768e..66de5f48aa0b7a 100644
--- a/src/async_wrap.h
+++ b/src/async_wrap.h
@@ -68,6 +68,7 @@ namespace node {
   V(TTYWRAP)                                                                  \
   V(UDPSENDWRAP)                                                              \
   V(UDPWRAP)                                                                  \
+  V(SIGINTWATCHDOG)                                                           \
   V(WORKER)                                                                   \
   V(WRITEWRAP)                                                                \
   V(ZLIB)
diff --git a/src/memory_tracker-inl.h b/src/memory_tracker-inl.h
index 2e39fa21a944d8..97be794bb86ba5 100644
--- a/src/memory_tracker-inl.h
+++ b/src/memory_tracker-inl.h
@@ -81,6 +81,14 @@ void MemoryTracker::TrackFieldWithSize(const char* edge_name,
   if (size > 0) AddNode(GetNodeName(node_name, edge_name), size, edge_name);
 }
 
+void MemoryTracker::TrackInlineFieldWithSize(const char* edge_name,
+                                             size_t size,
+                                             const char* node_name) {
+  if (size > 0) AddNode(GetNodeName(node_name, edge_name), size, edge_name);
+  CHECK(CurrentNode());
+  CurrentNode()->size_ -= size;
+}
+
 void MemoryTracker::TrackField(const char* edge_name,
                                const MemoryRetainer& value,
                                const char* node_name) {
@@ -232,6 +240,12 @@ void MemoryTracker::TrackField(const char* name,
   TrackFieldWithSize(name, sizeof(value), "uv_async_t");
 }
 
+void MemoryTracker::TrackInlineField(const char* name,
+                                     const uv_async_t& value,
+                                     const char* node_name) {
+  TrackInlineFieldWithSize(name, sizeof(value), "uv_async_t");
+}
+
 template <class NativeT, class V8T>
 void MemoryTracker::TrackField(const char* name,
                                const AliasedBufferBase<NativeT, V8T>& value,
diff --git a/src/memory_tracker.h b/src/memory_tracker.h
index a400009aad6728..6cd372f493c41d 100644
--- a/src/memory_tracker.h
+++ b/src/memory_tracker.h
@@ -135,6 +135,10 @@ class MemoryTracker {
   inline void TrackFieldWithSize(const char* edge_name,
                                  size_t size,
                                  const char* node_name = nullptr);
+  inline void TrackInlineFieldWithSize(const char* edge_name,
+                                       size_t size,
+                                       const char* node_name = nullptr);
+
   // Shortcut to extract the underlying object out of the smart pointer
   template <typename T, typename D>
   inline void TrackField(const char* edge_name,
@@ -220,6 +224,9 @@ class MemoryTracker {
   inline void TrackField(const char* edge_name,
                          const uv_async_t& value,
                          const char* node_name = nullptr);
+  inline void TrackInlineField(const char* edge_name,
+                               const uv_async_t& value,
+                               const char* node_name = nullptr);
   template <class NativeT, class V8T>
   inline void TrackField(const char* edge_name,
                          const AliasedBufferBase<NativeT, V8T>& value,
diff --git a/src/node_binding.cc b/src/node_binding.cc
index 0facfc36cbcb78..9f9695c2251b2e 100644
--- a/src/node_binding.cc
+++ b/src/node_binding.cc
@@ -88,6 +88,7 @@
   V(v8)                                                                        \
   V(wasi)                                                                      \
   V(worker)                                                                    \
+  V(watchdog)                                                                  \
   V(zlib)
 
 #define NODE_BUILTIN_MODULES(V)                                                \
diff --git a/src/node_options.cc b/src/node_options.cc
index d6b4ebe396cfeb..8f887404a03ce6 100644
--- a/src/node_options.cc
+++ b/src/node_options.cc
@@ -775,6 +775,11 @@ PerProcessOptionsParser::PerProcessOptionsParser(
   AddOption("--fast_calls_with_arguments_mismatches", "", NoOp{});
   AddOption("--harmony_numeric_separator", "", NoOp{});
 
+  AddOption("--trace-sigint",
+            "enable printing JavaScript stacktrace on SIGINT",
+            &PerProcessOptions::trace_sigint,
+            kAllowedInEnvironment);
+
   Insert(iop, &PerProcessOptions::get_per_isolate_options);
 }
 
diff --git a/src/node_options.h b/src/node_options.h
index 571ac305f9990c..6689a7292fa8c9 100644
--- a/src/node_options.h
+++ b/src/node_options.h
@@ -239,6 +239,7 @@ class PerProcessOptions : public Options {
   bool force_fips_crypto = false;
 #endif
 #endif
+  bool trace_sigint = false;
 
 #ifdef NODE_REPORT
   std::vector<std::string> cmdline;
diff --git a/src/node_watchdog.cc b/src/node_watchdog.cc
index f11902a1fa44bc..5cce8ffcb7b3b8 100644
--- a/src/node_watchdog.cc
+++ b/src/node_watchdog.cc
@@ -21,6 +21,7 @@
 
 #include <algorithm>
 
+#include "async_wrap-inl.h"
 #include "debug_utils-inl.h"
 #include "env-inl.h"
 #include "node_errors.h"
@@ -30,6 +31,12 @@
 
 namespace node {
 
+using v8::Context;
+using v8::FunctionCallbackInfo;
+using v8::FunctionTemplate;
+using v8::Object;
+using v8::Value;
+
 Watchdog::Watchdog(v8::Isolate* isolate, uint64_t ms, bool* timed_out)
     : isolate_(isolate), timed_out_(timed_out) {
 
@@ -106,10 +113,116 @@ SigintWatchdog::~SigintWatchdog() {
   SigintWatchdogHelper::GetInstance()->Stop();
 }
 
-
-void SigintWatchdog::HandleSigint() {
+SignalPropagation SigintWatchdog::HandleSigint() {
   *received_signal_ = true;
   isolate_->TerminateExecution();
+  return SignalPropagation::kStopPropagation;
+}
+
+void TraceSigintWatchdog::Init(Environment* env, Local<Object> target) {
+  Local<FunctionTemplate> constructor = env->NewFunctionTemplate(New);
+  constructor->InstanceTemplate()->SetInternalFieldCount(1);
+  Local<v8::String> js_sigint_watch_dog =
+      FIXED_ONE_BYTE_STRING(env->isolate(), "TraceSigintWatchdog");
+  constructor->SetClassName(js_sigint_watch_dog);
+  constructor->Inherit(HandleWrap::GetConstructorTemplate(env));
+
+  env->SetProtoMethod(constructor, "start", Start);
+  env->SetProtoMethod(constructor, "stop", Stop);
+
+  target
+      ->Set(env->context(),
+            js_sigint_watch_dog,
+            constructor->GetFunction(env->context()).ToLocalChecked())
+      .Check();
+}
+
+void TraceSigintWatchdog::New(const FunctionCallbackInfo<Value>& args) {
+  // This constructor should not be exposed to public javascript.
+  // Therefore we assert that we are not trying to call this as a
+  // normal function.
+  CHECK(args.IsConstructCall());
+  Environment* env = Environment::GetCurrent(args);
+  new TraceSigintWatchdog(env, args.This());
+}
+
+void TraceSigintWatchdog::Start(const FunctionCallbackInfo<Value>& args) {
+  TraceSigintWatchdog* watchdog;
+  ASSIGN_OR_RETURN_UNWRAP(&watchdog, args.Holder());
+  // Register this watchdog with the global SIGINT/Ctrl+C listener.
+  SigintWatchdogHelper::GetInstance()->Register(watchdog);
+  // Start the helper thread, if that has not already happened.
+  int r = SigintWatchdogHelper::GetInstance()->Start();
+  CHECK_EQ(r, 0);
+}
+
+void TraceSigintWatchdog::Stop(const FunctionCallbackInfo<Value>& args) {
+  TraceSigintWatchdog* watchdog;
+  ASSIGN_OR_RETURN_UNWRAP(&watchdog, args.Holder());
+  SigintWatchdogHelper::GetInstance()->Unregister(watchdog);
+  SigintWatchdogHelper::GetInstance()->Stop();
+}
+
+TraceSigintWatchdog::TraceSigintWatchdog(Environment* env, Local<Object> object)
+    : HandleWrap(env,
+                 object,
+                 reinterpret_cast<uv_handle_t*>(&handle_),
+                 AsyncWrap::PROVIDER_SIGINTWATCHDOG) {
+  int r = uv_async_init(env->event_loop(), &handle_, [](uv_async_t* handle) {
+    TraceSigintWatchdog* watchdog =
+        ContainerOf(&TraceSigintWatchdog::handle_, handle);
+    watchdog->signal_flag_ = SignalFlags::FromIdle;
+    watchdog->HandleInterrupt();
+  });
+  CHECK_EQ(r, 0);
+  uv_unref(reinterpret_cast<uv_handle_t*>(&handle_));
+}
+
+SignalPropagation TraceSigintWatchdog::HandleSigint() {
+  /**
+   * In case of uv loop polling, i.e. no JS currently running, activate the
+   * loop to run a piece of JS code to trigger interruption.
+   */
+  CHECK_EQ(uv_async_send(&handle_), 0);
+  env()->isolate()->RequestInterrupt(
+      [](v8::Isolate* isolate, void* data) {
+        TraceSigintWatchdog* self = static_cast<TraceSigintWatchdog*>(data);
+        if (self->signal_flag_ == SignalFlags::None) {
+          self->signal_flag_ = SignalFlags::FromInterrupt;
+        }
+        self->HandleInterrupt();
+      },
+      this);
+  return SignalPropagation::kContinuePropagation;
+}
+
+void TraceSigintWatchdog::HandleInterrupt() {
+  // Do not nest interrupts.
+  if (interrupting) {
+    return;
+  }
+  interrupting = true;
+  if (signal_flag_ == SignalFlags::None) {
+    return;
+  }
+  Environment* env_ = env();
+  // FIXME: Before
+  // https://github.com/nodejs/node/pull/29207#issuecomment-527667993 get
+  // fixed, additional JavaScript code evaluation shall be prevented from
+  // running during interruption.
+  FPrintF(stderr,
+      "KEYBOARD_INTERRUPT: Script execution was interrupted by `SIGINT`\n");
+  if (signal_flag_ == SignalFlags::FromInterrupt) {
+    PrintStackTrace(env_->isolate(),
+                    v8::StackTrace::CurrentStackTrace(
+                        env_->isolate(), 10, v8::StackTrace::kDetailed));
+  }
+  signal_flag_ = SignalFlags::None;
+  interrupting = false;
+
+  SigintWatchdogHelper::GetInstance()->Unregister(this);
+  SigintWatchdogHelper::GetInstance()->Stop();
+  raise(SIGINT);
 }
 
 #ifdef __POSIX__
@@ -162,8 +275,13 @@ bool SigintWatchdogHelper::InformWatchdogsAboutSignal() {
     instance.has_pending_signal_ = true;
   }
 
-  for (auto it : instance.watchdogs_)
-    it->HandleSigint();
+  for (auto it = instance.watchdogs_.rbegin(); it != instance.watchdogs_.rend();
+       it++) {
+    SignalPropagation wp = (*it)->HandleSigint();
+    if (wp == SignalPropagation::kStopPropagation) {
+      break;
+    }
+  }
 
   return is_stopping;
 }
@@ -259,15 +377,13 @@ bool SigintWatchdogHelper::HasPendingSignal() {
   return has_pending_signal_;
 }
 
-
-void SigintWatchdogHelper::Register(SigintWatchdog* wd) {
+void SigintWatchdogHelper::Register(SigintWatchdogBase* wd) {
   Mutex::ScopedLock lock(list_mutex_);
 
   watchdogs_.push_back(wd);
 }
 
-
-void SigintWatchdogHelper::Unregister(SigintWatchdog* wd) {
+void SigintWatchdogHelper::Unregister(SigintWatchdogBase* wd) {
   Mutex::ScopedLock lock(list_mutex_);
 
   auto it = std::find(watchdogs_.begin(), watchdogs_.end(), wd);
@@ -302,4 +418,16 @@ SigintWatchdogHelper::~SigintWatchdogHelper() {
 
 SigintWatchdogHelper SigintWatchdogHelper::instance;
 
+namespace watchdog {
+static void Initialize(Local<Object> target,
+                       Local<Value> unused,
+                       Local<Context> context,
+                       void* priv) {
+  Environment* env = Environment::GetCurrent(context);
+  TraceSigintWatchdog::Init(env, target);
+}
+}  // namespace watchdog
+
 }  // namespace node
+
+NODE_MODULE_CONTEXT_AWARE_INTERNAL(watchdog, node::watchdog::Initialize);
diff --git a/src/node_watchdog.h b/src/node_watchdog.h
index 14c5e19cca5c9a..bd960cb1ec8b18 100644
--- a/src/node_watchdog.h
+++ b/src/node_watchdog.h
@@ -24,9 +24,12 @@
 
 #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
 
-#include "uv.h"
-#include "node_mutex.h"
 #include <vector>
+#include "handle_wrap.h"
+#include "memory_tracker-inl.h"
+#include "node_mutex.h"
+#include "uv.h"
+#include "v8.h"
 
 #ifdef __POSIX__
 #include <pthread.h>
@@ -34,6 +37,11 @@
 
 namespace node {
 
+enum class SignalPropagation {
+  kContinuePropagation,
+  kStopPropagation,
+};
+
 class Watchdog {
  public:
   explicit Watchdog(v8::Isolate* isolate,
@@ -54,24 +62,56 @@ class Watchdog {
   bool* timed_out_;
 };
 
-class SigintWatchdog {
+class SigintWatchdogBase {
+ public:
+  virtual ~SigintWatchdogBase() = default;
+  virtual SignalPropagation HandleSigint() = 0;
+};
+
+class SigintWatchdog : public SigintWatchdogBase {
  public:
   explicit SigintWatchdog(v8::Isolate* isolate,
                           bool* received_signal = nullptr);
   ~SigintWatchdog();
   v8::Isolate* isolate() { return isolate_; }
-  void HandleSigint();
+  SignalPropagation HandleSigint() override;
 
  private:
   v8::Isolate* isolate_;
   bool* received_signal_;
 };
 
+class TraceSigintWatchdog : public HandleWrap, public SigintWatchdogBase {
+ public:
+  static void Init(Environment* env, Local<v8::Object> target);
+  static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void Start(const v8::FunctionCallbackInfo<Value>& args);
+  static void Stop(const v8::FunctionCallbackInfo<Value>& args);
+
+  SignalPropagation HandleSigint() override;
+
+  inline void MemoryInfo(node::MemoryTracker* tracker) const override {
+    tracker->TrackInlineField("handle_", handle_);
+  }
+  SET_MEMORY_INFO_NAME(TraceSigintWatchdog)
+  SET_SELF_SIZE(TraceSigintWatchdog)
+
+ private:
+  enum class SignalFlags { None, FromIdle, FromInterrupt };
+
+  TraceSigintWatchdog(Environment* env, Local<v8::Object> object);
+  void HandleInterrupt();
+
+  bool interrupting = false;
+  uv_async_t handle_;
+  SignalFlags signal_flag_ = SignalFlags::None;
+};
+
 class SigintWatchdogHelper {
  public:
   static SigintWatchdogHelper* GetInstance() { return &instance; }
-  void Register(SigintWatchdog* watchdog);
-  void Unregister(SigintWatchdog* watchdog);
+  void Register(SigintWatchdogBase* watchdog);
+  void Unregister(SigintWatchdogBase* watchdog);
   bool HasPendingSignal();
 
   int Start();
@@ -88,7 +128,7 @@ class SigintWatchdogHelper {
 
   Mutex mutex_;
   Mutex list_mutex_;
-  std::vector<SigintWatchdog*> watchdogs_;
+  std::vector<SigintWatchdogBase*> watchdogs_;
   bool has_pending_signal_;
 
 #ifdef __POSIX__
diff --git a/test/pseudo-tty/test-trace-sigint-disabled.js b/test/pseudo-tty/test-trace-sigint-disabled.js
new file mode 100644
index 00000000000000..5dc4dfa9b36afa
--- /dev/null
+++ b/test/pseudo-tty/test-trace-sigint-disabled.js
@@ -0,0 +1,39 @@
+'use strict';
+
+const { mustCall } = require('../common');
+const childProcess = require('child_process');
+const assert = require('assert');
+
+if (process.env.CHILD === 'true') {
+  main();
+} else {
+  // Use inherited stdio child process to prevent test tools from determining
+  // the case as crashed from SIGINT
+  const cp = childProcess.spawn(
+    process.execPath,
+    ['--trace-sigint', __filename],
+    {
+      env: { ...process.env, CHILD: 'true' },
+      stdio: 'inherit'
+    });
+  cp.on('exit', mustCall((code, signal) => {
+    assert.strictEqual(signal, null);
+    assert.strictEqual(code, 0);
+  }));
+}
+
+function main() {
+  // Deactivate colors even if the tty does support colors.
+  process.env.NODE_DISABLE_COLORS = '1';
+
+  const noop = mustCall(() => {
+    process.exit(0);
+  });
+  process.on('SIGINT', noop);
+  // Try testing re-add 'SIGINT' listeners
+  process.removeListener('SIGINT', noop);
+  process.on('SIGINT', noop);
+
+  process.kill(process.pid, 'SIGINT');
+  setTimeout(() => { assert.fail('unreachable path'); }, 10 * 1000);
+}
diff --git a/test/pseudo-tty/test-trace-sigint-disabled.out b/test/pseudo-tty/test-trace-sigint-disabled.out
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/test/pseudo-tty/test-trace-sigint-on-idle.js b/test/pseudo-tty/test-trace-sigint-on-idle.js
new file mode 100644
index 00000000000000..398aa8df097d2b
--- /dev/null
+++ b/test/pseudo-tty/test-trace-sigint-on-idle.js
@@ -0,0 +1,30 @@
+'use strict';
+
+const { mustCall } = require('../common');
+const childProcess = require('child_process');
+const assert = require('assert');
+
+if (process.env.CHILD === 'true') {
+  main();
+} else {
+  // Use inherited stdio child process to prevent test tools from determining
+  // the case as crashed from SIGINT
+  const cp = childProcess.spawn(
+    process.execPath,
+    ['--trace-sigint', __filename],
+    {
+      env: { ...process.env, CHILD: 'true' },
+      stdio: 'inherit'
+    });
+  setTimeout(() => cp.kill('SIGINT'), 1 * 1000);
+  cp.on('exit', mustCall((code, signal) => {
+    assert.strictEqual(signal, 'SIGINT');
+    assert.strictEqual(code, null);
+  }));
+}
+
+function main() {
+  // Deactivate colors even if the tty does support colors.
+  process.env.NODE_DISABLE_COLORS = '1';
+  setTimeout(() => {}, 10 * 1000);
+}
diff --git a/test/pseudo-tty/test-trace-sigint-on-idle.out b/test/pseudo-tty/test-trace-sigint-on-idle.out
new file mode 100644
index 00000000000000..faaef62fc79f70
--- /dev/null
+++ b/test/pseudo-tty/test-trace-sigint-on-idle.out
@@ -0,0 +1 @@
+KEYBOARD_INTERRUPT: Script execution was interrupted by `SIGINT`
diff --git a/test/pseudo-tty/test-trace-sigint.js b/test/pseudo-tty/test-trace-sigint.js
new file mode 100644
index 00000000000000..8b539bfc88d7b8
--- /dev/null
+++ b/test/pseudo-tty/test-trace-sigint.js
@@ -0,0 +1,30 @@
+'use strict';
+
+const { mustCall } = require('../common');
+const childProcess = require('child_process');
+const assert = require('assert');
+
+if (process.env.CHILD === 'true') {
+  main();
+} else {
+  // Use inherited stdio child process to prevent test tools from determining
+  // the case as crashed from SIGINT
+  const cp = childProcess.spawn(
+    process.execPath,
+    ['--trace-sigint', __filename],
+    {
+      env: { ...process.env, CHILD: 'true' },
+      stdio: 'inherit'
+    });
+  cp.on('exit', mustCall((code, signal) => {
+    assert.strictEqual(signal, 'SIGINT');
+    assert.strictEqual(code, null);
+  }));
+}
+
+function main() {
+  // Deactivate colors even if the tty does support colors.
+  process.env.NODE_DISABLE_COLORS = '1';
+  process.kill(process.pid, 'SIGINT');
+  while (true) {}
+}
diff --git a/test/pseudo-tty/test-trace-sigint.out b/test/pseudo-tty/test-trace-sigint.out
new file mode 100644
index 00000000000000..956cbafccc2d19
--- /dev/null
+++ b/test/pseudo-tty/test-trace-sigint.out
@@ -0,0 +1,9 @@
+KEYBOARD_INTERRUPT: Script execution was interrupted by `SIGINT`
+    at main (*/test-trace-sigint.js:*)
+    at */test-trace-sigint.js:*
+    at *
+    at *
+    at *
+    at *
+    at *
+    at *
diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js
index 7e9f77cd7a4cc2..8e532ec48c308d 100644
--- a/test/sequential/test-async-wrap-getasyncid.js
+++ b/test/sequential/test-async-wrap-getasyncid.js
@@ -51,6 +51,7 @@ const { getSystemErrorName } = require('util');
     delete providers.HTTPCLIENTREQUEST;
     delete providers.HTTPINCOMINGMESSAGE;
     delete providers.ELDHISTOGRAM;
+    delete providers.SIGINTWATCHDOG;
 
     const objKeys = Object.keys(providers);
     if (objKeys.length > 0)