current commit
261003.14 · src: fix --process-timeout message when exit waits
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

#include <algorithm>
#include <cstdlib>
#include <string>
#include <unordered_set>
#include <utility>

#include "async_wrap-inl.h"
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "node_errors.h"
#include "node_internals.h"
#include "node_sockaddr-inl.h"
#include "node_watchdog.h"
#include "node_worker.h"
#include "util-inl.h"

namespace node {

using v8::Context;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::StackFrame;
using v8::StackTrace;
using v8::Value;

Watchdog::Watchdog(v8::Isolate* isolate, uint64_t ms, bool* timed_out)
    : isolate_(isolate), timed_out_(timed_out) {

  int rc;
  rc = uv_loop_init(&loop_);
  if (rc != 0) {
    UNREACHABLE("Failed to initialize uv loop.");
  }

  rc = uv_async_init(&loop_, &async_, [](uv_async_t* signal) {
    Watchdog* w = ContainerOf(&Watchdog::async_, signal);
    uv_stop(&w->loop_);
  });

  CHECK_EQ(0, rc);

  rc = uv_timer_init(&loop_, &timer_);
  CHECK_EQ(0, rc);

  rc = uv_timer_start(&timer_, &Watchdog::Timer, ms, 0);
  CHECK_EQ(0, rc);

  rc = uv_thread_create(&thread_, &Watchdog::Run, this);
  CHECK_EQ(0, rc);
}


Watchdog::~Watchdog() {
  uv_async_send(&async_);
  uv_thread_join(&thread_);

  uv_close(reinterpret_cast<uv_handle_t*>(&async_), nullptr);

  // UV_RUN_DEFAULT so that libuv has a chance to clean up.
  uv_run(&loop_, UV_RUN_DEFAULT);

  CheckedUvLoopClose(&loop_);
}


void Watchdog::Run(void* arg) {
  uv_thread_setname("Watchdog");
  Watchdog* wd = static_cast<Watchdog*>(arg);

  // UV_RUN_DEFAULT the loop will be stopped either by the async or the
  // timer handle.
  uv_run(&wd->loop_, UV_RUN_DEFAULT);

  // Loop ref count reaches zero when both handles are closed.
  // Close the timer handle on this side and let ~Watchdog() close async_
  uv_close(reinterpret_cast<uv_handle_t*>(&wd->timer_), nullptr);
}

void Watchdog::Timer(uv_timer_t* timer) {
  Watchdog* w = ContainerOf(&Watchdog::timer_, timer);
  *w->timed_out_ = true;
  w->isolate()->TerminateExecution();
  uv_stop(&w->loop_);
}

namespace {

constexpr uint64_t kNanosecondsPerMillisecond = 1000 * 1000;
// How long printing the diagnostics, writing the report and exiting may take.
constexpr uint64_t kProcessTimeoutExitGraceMs = 5000;
// Printed when the process is still exiting, e.g. joining Worker threads, once
// --process-timeout has expired.
constexpr char kProcessNotFinishedExitingMessage[] =
    "The process did not finish exiting after the event loop had stopped.\n";

std::string FormatProcessTimeoutHeader(const std::string& duration) {
  return SPrintF("(node:%d) Process timed out after %s (--process-timeout). "
                 "Exiting with code %d.\n",
                 uv_os_getpid(),
                 duration,
                 static_cast<int>(ExitCode::kProcessTimeout));
}

// Called from the watchdog thread when the main thread cannot be trusted to
// exit on its own. It must not touch the Environment or V8.
//
// std::_Exit() does not run atexit handlers or static destructors, which is
// where code coverage data is written, so coverage is never recorded for a
// process that exits through here. The calls to this function are excluded
// from coverage reports for the same reason.
// LCOV_EXCL_START
[[noreturn]] void ForceProcessTimeoutExit(const std::string& message) {
  FPrintF(stderr, "%s", message);
  fflush(stderr);
  ResetStdio();
  std::_Exit(static_cast<int>(ExitCode::kProcessTimeout));
}
// LCOV_EXCL_STOP

std::string FormatJavaScriptStack(Isolate* isolate, Local<StackTrace> stack) {
  std::string result;
  for (int i = 0; i < stack->GetFrameCount(); i++) {
    Local<StackFrame> frame = stack->GetFrame(isolate, i);
    Utf8Value function_name(isolate, frame->GetFunctionName());
    Utf8Value script_name(isolate, frame->GetScriptName());
    const int line = frame->GetLineNumber();
    const int column = frame->GetColumn();
    if (function_name.length() == 0) {
      result += SPrintF("    at %s:%d:%d\n", script_name, line, column);
    } else {
      result += SPrintF(
          "    at %s (%s:%d:%d)\n", function_name, script_name, line, column);
    }
  }
  return result;
}

// Describes the details of a libuv handle that help identify where it comes
// from, e.g. the address a server listens on or the pid of a child process.
std::string DescribeHandle(uv_handle_t* handle, std::string_view name) {
  std::string description;
  switch (handle->type) {
    case UV_TCP: {
      const uv_tcp_t& tcp = *reinterpret_cast<uv_tcp_t*>(handle);
      const std::string local = SocketAddress::FromSockName(tcp).ToString();
      const std::string remote = SocketAddress::FromPeerName(tcp).ToString();
      if (!remote.empty()) {
        description = local + " -> " + remote;
      } else if (!local.empty()) {
        description =
            (name == "TCPServerWrap" ? "listening on " : "local ") + local;
      }
      break;
    }
    case UV_UDP: {
      const uv_udp_t& udp = *reinterpret_cast<uv_udp_t*>(handle);
      const std::string local = SocketAddress::FromSockName(udp).ToString();
      if (!local.empty()) description = "bound to " + local;
      break;
    }
    case UV_PROCESS:
      description =
          SPrintF("pid %d",
                  uv_process_get_pid(reinterpret_cast<uv_process_t*>(handle)));
      break;
    case UV_FS_EVENT:
    case UV_FS_POLL: {
      char path[PATH_MAX_BYTES];
      size_t size = sizeof(path);
      const int rc =
          handle->type == UV_FS_EVENT
              ? uv_fs_event_getpath(
                    reinterpret_cast<uv_fs_event_t*>(handle), path, &size)
              : uv_fs_poll_getpath(
                    reinterpret_cast<uv_fs_poll_t*>(handle), path, &size);
      if (rc == 0) description = "watching " + std::string(path, size);
      break;
    }
    default:
      break;
  }

#ifndef _WIN32
  uv_os_fd_t fd;
  if (handle->type != UV_PROCESS && uv_fileno(handle, &fd) == 0) {
    description += (description.empty() ? "" : ", ") + SPrintF("fd %d", fd);
  }
#endif

  return description;
}

// Collects resources in the order in which they are found, merging duplicates.
class ResourceList {
 public:
  void Add(std::string name, std::string details = "", size_t count = 1) {
    for (Entry& entry : entries_) {
      if (entry.name == name && entry.details == details) {
        entry.count += count;
        return;
      }
    }
    entries_.push_back({std::move(name), std::move(details), count});
  }

  std::string ToString() const {
    std::string result;
    for (const Entry& entry : entries_) {
      result += "    " + entry.name;
      if (entry.count > 1) result += SPrintF(" x%d", entry.count);
      if (!entry.details.empty()) result += " (" + entry.details + ")";
      result += "\n";
    }
    return result;
  }

  bool empty() const { return entries_.empty(); }

 private:
  struct Entry {
    std::string name;
    std::string details;
    size_t count;
  };
  std::vector<Entry> entries_;
};

// Describes what the main thread was doing and what keeps the event loop
// alive. This must not call into JavaScript, as it may run from a V8 interrupt.
std::string FormatProcessTimeoutDiagnostics(Environment* env) {
  Isolate* isolate = env->isolate();
  std::string result;

  Local<StackTrace> stack;
  if (GetCurrentStackTrace(isolate, static_cast<int>(env->stack_trace_limit()))
          .ToLocal(&stack) &&
      stack->GetFrameCount() > 0) {
    result += "Main thread was executing JavaScript:\n";
    result += FormatJavaScriptStack(isolate, stack);
  } else {
    result += "Main thread was not executing JavaScript.\n";
  }

  ResourceList resources;

  for (ReqWrapBase* req_wrap : *env->req_wrap_queue()) {
    AsyncWrap* wrap = req_wrap->GetAsyncWrap();
    if (wrap->persistent().IsEmpty()) continue;
    resources.Add(wrap->MemoryInfoName());
  }

  std::unordered_set<const uv_handle_t*> handle_wraps;
  for (HandleWrap* wrap : *env->handle_wrap_queue()) {
    uv_handle_t* handle = wrap->GetHandle();
    handle_wraps.insert(handle);
    if (wrap->persistent().IsEmpty() || !HandleWrap::HasRef(wrap) ||
        !uv_is_active(handle)) {
      continue;
    }
    const std::string name = wrap->MemoryInfoName();
    resources.Add(name, DescribeHandle(handle, name));
  }

  // Timers and immediates are tracked in JavaScript and share one libuv handle
  // per kind, so report how many of them keep the event loop alive.
  const int32_t timeouts = env->timeout_info()[0];
  if (timeouts > 0) {
    // The timer handle is inactive while the timers are being processed.
    uv_timer_t* timer_handle = env->timer_handle();
    resources.Add(
        "Timeout",
        uv_is_active(reinterpret_cast<uv_handle_t*>(timer_handle))
            ? SPrintF("next due in %dms", uv_timer_get_due_in(timer_handle))
            : "",
        timeouts);
  }
  const uint32_t immediates = env->immediate_info()->ref_count();
  if (immediates > 0) resources.Add("Immediate", "", immediates);

  // Running Workers keep the event loop alive through the Environment's own
  // task queue handle rather than through a HandleWrap.
  env->ForEachWorker([&](worker::Worker* worker) {
    if (worker->is_internal() || !worker->has_ref() || worker->is_stopped()) {
      return;
    }
    std::string details = SPrintF("thread %d", worker->thread_id());
    if (!worker->name().empty()) {
      details += SPrintF(", name '%s'", worker->name());
    }
    resources.Add("Worker", std::move(details));
  });

  // Anything else, e.g. handles created by native addons.
  struct WalkData {
    Environment* env;
    const std::unordered_set<const uv_handle_t*>* handle_wraps;
    ResourceList* resources;
  } walk_data{env, &handle_wraps, &resources};
  uv_walk(
      env->event_loop(),
      [](uv_handle_t* handle, void* arg) {
        WalkData* data = static_cast<WalkData*>(arg);
        Environment* env = data->env;
        if (!uv_is_active(handle) || !uv_has_ref(handle) ||
            data->handle_wraps->contains(handle) ||
            handle == reinterpret_cast<uv_handle_t*>(env->timer_handle()) ||
            handle ==
                reinterpret_cast<uv_handle_t*>(env->immediate_idle_handle()) ||
            handle ==
                reinterpret_cast<uv_handle_t*>(env->task_queues_async())) {
          return;
        }
        data->resources->Add("libuv handle", uv_handle_type_name(handle->type));
      },
      &walk_data);

  if (resources.empty()) {
    result += "No resources keeping the event loop alive were found.\n";
  } else {
    result += "Resources keeping the event loop alive:\n";
    result += resources.ToString();
  }
  return result;
}

}  // namespace

struct ProcessTimeoutWatchdog::State {
  enum class Phase {
    // Waiting for the deadline.
    kArmed,
    // The deadline was reached and the main thread was interrupted.
    kFired,
    // The main thread is printing diagnostics and exiting.
    kHandling,
    // The event loop has stopped and the Environment is being torn down.
    kStopping,
    // The process is exiting normally.
    kDisarmed,
  };

  State(uint64_t deadline, std::string duration, bool report)
      : deadline(deadline), duration(std::move(duration)), report(report) {
    CHECK_EQ(uv_mutex_init(&mutex), 0);
    CHECK_EQ(uv_cond_init(&cond), 0);
  }

  ~State() {
    uv_cond_destroy(&cond);
    uv_mutex_destroy(&mutex);
  }

  State(const State&) = delete;
  State& operator=(const State&) = delete;

  // With `mutex` held, waits while the phase is `current`, but at most until
  // uv_hrtime() reaches `until`.
  void WaitWhile(Phase current, uint64_t until) {
    while (phase == current) {
      const uint64_t now = uv_hrtime();
      if (now >= until) return;
      uv_cond_timedwait(&cond, &mutex, until - now);
    }
  }

  uv_mutex_t mutex;
  uv_cond_t cond;
  Phase phase = Phase::kArmed;
  // In uv_hrtime() nanoseconds.
  const uint64_t deadline;
  // As passed to --process-timeout, e.g. "30s".
  const std::string duration;
  // --report-on-process-timeout
  const bool report;
};

bool ProcessTimeoutWatchdog::IsEnabled() {
  Mutex::ScopedLock lock(per_process::cli_options_mutex);
  return per_process::cli_options->process_timeout_ms != 0;
}

std::unique_ptr<ProcessTimeoutWatchdog> ProcessTimeoutWatchdog::MaybeStart(
    Environment* env) {
  CHECK(env->is_main_thread());
  uint64_t timeout_ms;
  std::string duration;
  bool report;
  {
    Mutex::ScopedLock lock(per_process::cli_options_mutex);
    timeout_ms = per_process::cli_options->process_timeout_ms;
    duration = per_process::cli_options->process_timeout;
    report = per_process::cli_options->report_on_process_timeout;
  }
  if (timeout_ms == 0) return nullptr;

  // In watch mode, this process only restarts the application in child
  // processes, which inherit --process-timeout and apply it to each run.
  if (env->options()->watch_mode) return nullptr;

  const uint64_t deadline =
      per_process::node_start_time + timeout_ms * kNanosecondsPerMillisecond;
  auto state = std::make_shared<State>(deadline, std::move(duration), report);
  return std::unique_ptr<ProcessTimeoutWatchdog>(
      new ProcessTimeoutWatchdog(env, std::move(state)));
}

ProcessTimeoutWatchdog::ProcessTimeoutWatchdog(Environment* env,
                                               std::shared_ptr<State> state)
    : env_(env), state_(std::move(state)) {
  CHECK_EQ(uv_thread_create(&thread_, Run, this), 0);
}

ProcessTimeoutWatchdog::~ProcessTimeoutWatchdog() {
  uv_mutex_lock(&state_->mutex);
  state_->phase = State::Phase::kDisarmed;
  uv_cond_signal(&state_->cond);
  uv_mutex_unlock(&state_->mutex);
  CHECK_EQ(uv_thread_join(&thread_), 0);
}

void ProcessTimeoutWatchdog::OnEnvironmentStopping() {
  uv_mutex_lock(&state_->mutex);
  // The phase is only kFired here if the event loop stopped right as the
  // deadline was reached, a race that tests cannot reproduce reliably.
  if (state_->phase == State::Phase::kArmed ||
      state_->phase == State::Phase::kFired) {  // LCOV_EXCL_LINE
    state_->phase = State::Phase::kStopping;
    uv_cond_signal(&state_->cond);
  }
  uv_mutex_unlock(&state_->mutex);
}

void ProcessTimeoutWatchdog::Run(void* arg) {
  uv_thread_setname("ProcessTimeout");
  using Phase = State::Phase;
  ProcessTimeoutWatchdog* self = static_cast<ProcessTimeoutWatchdog*>(arg);
  std::shared_ptr<State> state = self->state_;

  uv_mutex_lock(&state->mutex);
  state->WaitWhile(Phase::kArmed, state->deadline);

  // Environment::Exit(), e.g. from process.exit() or for an uncaught
  // exception, exits without returning to NodeMainInstance::Run(), so
  // OnEnvironmentStopping() is not called while it joins Worker threads. The
  // Environment is only freed after OnEnvironmentStopping(), so it is safe to
  // read while the phase is still kArmed.
  if (state->phase == Phase::kArmed && self->env_->is_stopping()) {
    state->phase = Phase::kStopping;
  }

  if (state->phase == Phase::kArmed) {
    // Barring the race described below, the process exits once the timeout
    // has fired, either through ForceProcessTimeoutExit() on this thread or
    // from the main thread while this thread is still executing this block.
    // clang's --coverage instrumentation reports a block that a thread has not
    // finished executing when the process exits as not executed, so coverage
    // cannot be collected for this block in either case.
    // LCOV_EXCL_START
    state->phase = Phase::kFired;
    // The Environment is only freed after OnEnvironmentStopping(), which
    // cannot happen while we hold the mutex. The callback runs from a V8
    // interrupt if JavaScript is executing, or from the event loop otherwise.
    self->env_->RequestInterrupt(
        [state](Environment* env) { OnTimeout(env, state); });

    state->WaitWhile(Phase::kFired,
                     uv_hrtime() + kProcessTimeoutResponseGraceMs *
                                       kNanosecondsPerMillisecond);
    if (state->phase == Phase::kFired) {
      // If Environment::Exit() started before the interrupt could run, the
      // main thread is not blocked by the application but by exiting.
      ForceProcessTimeoutExit(
          FormatProcessTimeoutHeader(state->duration) +
          (self->env_->is_stopping()
               ? std::string(kProcessNotFinishedExitingMessage)
               : SPrintF("The main thread did not respond within %dms. It is "
                         "likely blocked in a synchronous native operation, "
                         "e.g. child_process.execSync() or a native addon, so "
                         "no JavaScript stack or resource information is "
                         "available.\n",
                         kProcessTimeoutResponseGraceMs)));
    }

    if (state->phase == Phase::kHandling) {
      state->WaitWhile(Phase::kHandling,
                       uv_hrtime() + kProcessTimeoutExitGraceMs *
                                         kNanosecondsPerMillisecond);
      if (state->phase == Phase::kHandling) {
        ForceProcessTimeoutExit(
            SPrintF("(node:%d) The process did not finish exiting within %dms "
                    "after --process-timeout expired. Forcing exit.\n",
                    uv_os_getpid(),
                    kProcessTimeoutExitGraceMs));
      }
    }
  }
  // LCOV_EXCL_STOP

  if (state->phase == Phase::kStopping) {
    // The event loop has stopped, but tearing down the Environment, e.g.
    // joining Worker threads, can still take arbitrarily long. If the deadline
    // has already passed, give the process a moment to finish exiting. That
    // happens if Environment::Exit() was still in progress at the deadline, or
    // if the event loop stopped right as the deadline was reached.
    const uint64_t now = uv_hrtime();
    // LCOV_EXCL_START
    const uint64_t until =
        now < state->deadline
            ? state->deadline
            : now + kProcessTimeoutResponseGraceMs * kNanosecondsPerMillisecond;
    // LCOV_EXCL_STOP
    state->WaitWhile(Phase::kStopping, until);
    if (state->phase == Phase::kStopping) {
      // LCOV_EXCL_START
      ForceProcessTimeoutExit(FormatProcessTimeoutHeader(state->duration) +
                              kProcessNotFinishedExitingMessage);
      // LCOV_EXCL_STOP
    }
  }

  uv_mutex_unlock(&state->mutex);
}

void ProcessTimeoutWatchdog::OnTimeout(Environment* env,
                                       const std::shared_ptr<State>& state) {
  uv_mutex_lock(&state->mutex);
  const bool should_handle = state->phase == State::Phase::kFired;
  if (should_handle) {
    state->phase = State::Phase::kHandling;
    uv_cond_signal(&state->cond);
  }
  uv_mutex_unlock(&state->mutex);
  if (!should_handle) return;

  Isolate* isolate = env->isolate();
  HandleScope handle_scope(isolate);
  {
    Isolate::DisallowJavascriptExecutionScope disallow_js(
        isolate, Isolate::DisallowJavascriptExecutionScope::CRASH_ON_FAILURE);
    FPrintF(stderr,
            "%s%s",
            FormatProcessTimeoutHeader(state->duration),
            FormatProcessTimeoutDiagnostics(env));
    fflush(stderr);

    if (state->report) {
      TriggerNodeReport(env,
                        "Process timed out (--process-timeout)",
                        kProcessTimeoutReportTrigger,
                        "",
                        Local<Value>());
    }
  }

  // Like process.reallyExit(): flush coverage and profiles, but do not emit
  // 'exit', as the JavaScript code may be what keeps the process running.
  RunAtExit(env);
  env->Exit(ExitCode::kProcessTimeout);
}

SigintWatchdog::SigintWatchdog(
  v8::Isolate* isolate, bool* received_signal)
    : isolate_(isolate), received_signal_(received_signal) {
  Mutex::ScopedLock lock(SigintWatchdogHelper::GetInstanceActionMutex());
  // Register this watchdog with the global SIGINT/Ctrl+C listener.
  SigintWatchdogHelper::GetInstance()->Register(this);
  // Start the helper thread, if that has not already happened.
  SigintWatchdogHelper::GetInstance()->Start();
}


SigintWatchdog::~SigintWatchdog() {
  Mutex::ScopedLock lock(SigintWatchdogHelper::GetInstanceActionMutex());
  SigintWatchdogHelper::GetInstance()->Unregister(this);
  SigintWatchdogHelper::GetInstance()->Stop();
}

SignalPropagation SigintWatchdog::HandleSigint() {
  *received_signal_ = true;
  isolate_->TerminateExecution();
  return SignalPropagation::kStopPropagation;
}

void TraceSigintWatchdog::Init(Environment* env, Local<Object> target) {
  Isolate* isolate = env->isolate();
  Local<FunctionTemplate> constructor = NewFunctionTemplate(isolate, New);
  constructor->InstanceTemplate()->SetInternalFieldCount(
      TraceSigintWatchdog::kInternalFieldCount);
  constructor->Inherit(HandleWrap::GetConstructorTemplate(env));

  SetProtoMethod(isolate, constructor, "start", Start);
  SetProtoMethod(isolate, constructor, "stop", Stop);

  SetConstructorFunction(
      env->context(), target, "TraceSigintWatchdog", constructor);
}

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.This());
  Mutex::ScopedLock lock(SigintWatchdogHelper::GetInstanceActionMutex());
  // 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.This());
  Mutex::ScopedLock lock(SigintWatchdogHelper::GetInstanceActionMutex());
  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;

  Mutex::ScopedLock lock(SigintWatchdogHelper::GetInstanceActionMutex());
  SigintWatchdogHelper::GetInstance()->Unregister(this);
  SigintWatchdogHelper::GetInstance()->Stop();
  raise(SIGINT);
}

#ifdef __POSIX__
void* SigintWatchdogHelper::RunSigintWatchdog(void* arg) {
  uv_thread_setname("SigintWatchdog");
  // Inside the helper thread.
  bool is_stopping;
  do {
    uv_sem_wait(&instance.sem_);
    is_stopping = InformWatchdogsAboutSignal();
  } while (!is_stopping);

  return nullptr;
}

void SigintWatchdogHelper::HandleSignal(int signum,
                                        siginfo_t* info,
                                        void* ucontext) {
  uv_sem_post(&instance.sem_);
}

#else

// Windows starts a separate thread for executing the handler, so no extra
// helper thread is required.
BOOL WINAPI SigintWatchdogHelper::WinCtrlCHandlerRoutine(DWORD dwCtrlType) {
  if (!instance.watchdog_disabled_ &&
      (dwCtrlType == CTRL_C_EVENT || dwCtrlType == CTRL_BREAK_EVENT)) {
    InformWatchdogsAboutSignal();

    // Return true because the signal has been handled.
    return TRUE;
  } else {
    return FALSE;
  }
}
#endif


bool SigintWatchdogHelper::InformWatchdogsAboutSignal() {
  Mutex::ScopedLock list_lock(instance.list_mutex_);

  bool is_stopping = false;
#ifdef __POSIX__
  is_stopping = instance.stopping_;
#endif

  // If there are no listeners and the helper thread has been awoken by a signal
  // (= not when stopping it), indicate that by setting has_pending_signal_.
  if (instance.watchdogs_.empty() && !is_stopping) {
    instance.has_pending_signal_ = true;
  }

  for (auto it = instance.watchdogs_.rbegin(); it != instance.watchdogs_.rend();
       it++) {
    SignalPropagation wp = (*it)->HandleSigint();
    if (wp == SignalPropagation::kStopPropagation) {
      break;
    }
  }

  return is_stopping;
}


int SigintWatchdogHelper::Start() {
  Mutex::ScopedLock lock(mutex_);

  if (start_stop_count_++ > 0) {
    return 0;
  }

#ifdef __POSIX__
  CHECK_EQ(has_running_thread_, false);
  has_pending_signal_ = false;
  stopping_ = false;

  sigset_t sigmask;
  sigfillset(&sigmask);
  sigset_t savemask;
  CHECK_EQ(0, pthread_sigmask(SIG_SETMASK, &sigmask, &savemask));
  sigmask = savemask;
  int ret = pthread_create(&thread_, nullptr, RunSigintWatchdog, nullptr);

  auto cleanup = OnScopeLeave(
      [&]() { CHECK_EQ(0, pthread_sigmask(SIG_SETMASK, &sigmask, nullptr)); });

  if (ret != 0) {
    return ret;
  }
  has_running_thread_ = true;

  RegisterSignalHandler(SIGINT, HandleSignal);
#else
  if (watchdog_disabled_) {
    watchdog_disabled_ = false;
  } else {
    SetConsoleCtrlHandler(WinCtrlCHandlerRoutine, TRUE);
  }
#endif

  return 0;
}


bool SigintWatchdogHelper::Stop() {
  bool had_pending_signal;
  Mutex::ScopedLock lock(mutex_);

  {
    Mutex::ScopedLock list_lock(list_mutex_);

    had_pending_signal = has_pending_signal_;

    if (--start_stop_count_ > 0) {
      has_pending_signal_ = false;
      return had_pending_signal;
    }

#ifdef __POSIX__
    // Set stopping now because it's only protected by list_mutex_.
    stopping_ = true;
#endif

    watchdogs_.clear();
  }

#ifdef __POSIX__
  if (!has_running_thread_) {
    has_pending_signal_ = false;
    return had_pending_signal;
  }

  // Wake up the helper thread.
  uv_sem_post(&sem_);

  // Wait for the helper thread to finish.
  CHECK_EQ(0, pthread_join(thread_, nullptr));
  has_running_thread_ = false;

  RegisterSignalHandler(SIGINT, SignalExit, true);
#else
  watchdog_disabled_ = true;
#endif

  had_pending_signal = has_pending_signal_;
  has_pending_signal_ = false;

  return had_pending_signal;
}


bool SigintWatchdogHelper::HasPendingSignal() {
  Mutex::ScopedLock lock(list_mutex_);

  return has_pending_signal_;
}

void SigintWatchdogHelper::Register(SigintWatchdogBase* wd) {
  Mutex::ScopedLock lock(list_mutex_);

  watchdogs_.push_back(wd);
}

void SigintWatchdogHelper::Unregister(SigintWatchdogBase* wd) {
  Mutex::ScopedLock lock(list_mutex_);

  auto it = std::ranges::find(watchdogs_, wd);

  CHECK_NE(it, watchdogs_.end());
  watchdogs_.erase(it);
}


SigintWatchdogHelper::SigintWatchdogHelper()
    : start_stop_count_(0),
      has_pending_signal_(false) {
#ifdef __POSIX__
  has_running_thread_ = false;
  stopping_ = false;
  CHECK_EQ(0, uv_sem_init(&sem_, 0));
#else
  watchdog_disabled_ = false;
#endif
}


SigintWatchdogHelper::~SigintWatchdogHelper() {
  start_stop_count_ = 0;
  Stop();

#ifdef __POSIX__
  CHECK_EQ(has_running_thread_, false);
  uv_sem_destroy(&sem_);
#endif
}

SigintWatchdogHelper SigintWatchdogHelper::instance;
Mutex SigintWatchdogHelper::instance_action_mutex_;

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_BINDING_CONTEXT_AWARE_INTERNAL(watchdog, node::watchdog::Initialize)