Add interrupt handling classes.

This patch introduces a number of helper classes to
manage user interruption for both Win32 and Posix systems.

Fuchsia-Topic: advanced-ipc
Original-Change-Id: I97c658cc234e5f979aaa506246b4032893ba4fee
Original-Change-Id: Ie7ce9503a895b8e355927b93b80c68b139474790
Change-Id: Ie2e81f9db6ee8b37734ca85e66756d901488be8e
Reviewed-on: https://fuchsia-review.googlesource.com/c/third_party/github.com/ninja-build/ninja/+/975452
Reviewed-by: David Fang <fangism@google.com>
Reviewed-by: David Turner <digit@google.com>
Commit-Queue: David Turner <digit@google.com>
Reviewed-by: Tyler Mandry <tmandry@google.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5213814..012197f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -150,6 +150,7 @@
 	target_sources(libninja PRIVATE
 		src/subprocess-win32.cc
 		src/includes_normalize-win32.cc
+		src/interrupt_handling-win32.cc
 		src/ipc_handle-win32.cc
 		src/msvc_helper-win32.cc
 		src/msvc_helper_main-win32.cc
@@ -162,6 +163,7 @@
 	set_source_files_properties(src/getopt.c PROPERTIES LANGUAGE CXX)
 else()
 	target_sources(libninja PRIVATE
+		src/interrupt_handling-posix.cc
 		src/ipc_handle-posix.cc
 		src/subprocess-posix.cc
 	)
@@ -282,6 +284,7 @@
     src/dyndep_parser_test.cc
     src/edit_distance_test.cc
     src/graph_test.cc
+    src/interrupt_handling_test.cc
     src/ipc_handle_test.cc
     src/ipc_utils_test.cc
     src/json_test.cc
diff --git a/configure.py b/configure.py
index 238fcd2..e1b17e2 100755
--- a/configure.py
+++ b/configure.py
@@ -544,6 +544,7 @@
 if platform.is_windows():
     for name in ['subprocess-win32',
                  'includes_normalize-win32',
+                 'interrupt_handling-win32',
                  'ipc_handle-win32',
                  'msvc_helper-win32',
                  'msvc_helper_main-win32']:
@@ -552,7 +553,8 @@
         objs += cxx('minidump-win32', variables=cxxvariables)
     objs += cc('getopt')
 else:
-    for name in ['ipc_handle-posix',
+    for name in ['interrupt_handling-posix',
+                 'ipc_handle-posix',
                  'subprocess-posix']:
         objs += cxx(name, variables=cxxvariables)
 if platform.is_aix():
diff --git a/src/interrupt_handling-posix.cc b/src/interrupt_handling-posix.cc
new file mode 100644
index 0000000..fa47131
--- /dev/null
+++ b/src/interrupt_handling-posix.cc
@@ -0,0 +1,177 @@
+// Copyright 2023 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <stdio.h>
+#include <unistd.h>
+
+#include "interrupt_handling.h"
+#include "util.h"
+
+// Set to 1 to print debug messages to stderr during development
+#define DEBUG 0
+
+namespace {
+
+// Retrieve a signal mask for SIGINT/SIGHUP/SIGTERM
+sigset_t GetInterruptSignalMask() {
+  sigset_t mask;
+  sigemptyset(&mask);
+  sigaddset(&mask, SIGINT);
+  sigaddset(&mask, SIGHUP);
+  sigaddset(&mask, SIGTERM);
+  return mask;
+}
+
+// Set the signal action for a given |signum|, returning the previous one
+// in |*old_action| is |old_action != nullptr|.
+void SetSignalAction(int signum, const struct sigaction* action,
+                     struct sigaction* old_action) {
+  if (sigaction(signum, action, old_action) < 0)
+    ErrnoFatal("sigaction");
+}
+
+}  // namespace
+
+//////////////////////////////////////////////////////////////////////////
+///
+///  InterruptBlocker
+///
+
+InterruptBlocker::InterruptBlocker() {
+  sigset_t block_interrupts = GetInterruptSignalMask();
+  if (sigprocmask(SIG_BLOCK, &block_interrupts, &prev_signal_mask_) < 0)
+    ErrnoFatal("sigprocmask");
+}
+
+InterruptBlocker::~InterruptBlocker() {
+  if (sigprocmask(SIG_SETMASK, &prev_signal_mask_, nullptr) < 0)
+    ErrnoFatal("sigprocmask");
+}
+
+//////////////////////////////////////////////////////////////////////////
+///
+///  InterruptHandlerBase
+///
+
+InterruptHandlerBase::InterruptHandlerBase(const struct sigaction& action) {
+  // Block the signals before changing the handlers.
+  sigset_t mask = GetInterruptSignalMask();
+  sigprocmask(SIG_BLOCK, &mask, &old_mask_);
+
+  SetSignalAction(SIGINT, &action, &old_int_action_);
+  SetSignalAction(SIGHUP, &action, &old_hup_action_);
+  SetSignalAction(SIGTERM, &action, &old_term_action_);
+
+  // Unblock the signals now.
+  sigprocmask(SIG_UNBLOCK, &mask, nullptr);
+}
+
+InterruptHandlerBase::~InterruptHandlerBase() {
+  // Block the signal before changing the action handlers.
+  sigset_t mask = GetInterruptSignalMask();
+  sigprocmask(SIG_BLOCK, &mask, nullptr);
+
+  SetSignalAction(SIGINT, &old_int_action_, nullptr);
+  SetSignalAction(SIGHUP, &old_hup_action_, nullptr);
+  SetSignalAction(SIGTERM, &old_term_action_, nullptr);
+
+  // Restore the original signal mask.
+  sigprocmask(SIG_SETMASK, &old_mask_, nullptr);
+}
+
+//////////////////////////////////////////////////////////////////////////
+///
+///  InterruptCatcher
+///
+
+InterruptCatcher::InterruptCatcher() : InterruptHandlerBase(MakeAction()) {
+  s_interrupted_ = 0;
+  HandlePendingInterrupt();
+}
+
+InterruptCatcher::~InterruptCatcher() = default;
+
+#if DEBUG
+#define WRITE(msg) ::write(2, msg, sizeof(msg) - 1)
+#else
+#define WRITE(msg) (void)(msg)
+#endif
+
+// static
+struct sigaction InterruptCatcher::MakeAction() {
+  struct sigaction result = {};
+  result.sa_handler = [](int signum) {
+    s_interrupted_ = signum;
+    if (signum == SIGINT)
+      WRITE("\nSIGINT SIGNALED\n");
+    else if (signum == SIGHUP)
+      WRITE("\nSIGHUP SIGNALED\n");
+    else if (signum == SIGTERM)
+      WRITE("\nSIGTERM SIGNALED\n");
+  };
+  return result;
+}
+
+// static
+void InterruptCatcher::HandlePendingInterrupt() {
+  sigset_t pending;
+  sigemptyset(&pending);
+  if (sigpending(&pending) == -1) {
+    perror("ninja: sigpending");
+    return;
+  }
+  if (sigismember(&pending, SIGINT)) {
+    WRITE("\nSIGINT PENDING\n");
+    s_interrupted_ = SIGINT;
+  } else if (sigismember(&pending, SIGTERM)) {
+    WRITE("\nSIGTERM PENDING\n");
+    s_interrupted_ = SIGTERM;
+  } else if (sigismember(&pending, SIGHUP)) {
+    WRITE("\nSIGHUP PENDING\n");
+    s_interrupted_ = SIGHUP;
+  }
+}
+
+// static
+volatile sig_atomic_t InterruptCatcher::s_interrupted_ = 0;
+
+//////////////////////////////////////////////////////////////////////////
+///
+///  InterruptForwarder
+///
+
+#include <unistd.h>
+
+InterruptForwarder::InterruptForwarder(pid_t process_group)
+    : InterruptHandlerBase(MakeAction()), old_process_group_(s_process_group_) {
+  s_process_group_ = process_group;
+}
+
+InterruptForwarder::~InterruptForwarder() {
+  s_process_group_ = old_process_group_;
+}
+
+// static
+struct sigaction InterruptForwarder::MakeAction() {
+  struct sigaction result = {};
+  result.sa_handler = [](int signum) {
+    // Send the interrupt to the server's process group
+    kill(-s_process_group_, signum);
+    WRITE("\nINTERRUPT FORWARDED\n");
+  };
+  return result;
+}
+
+// static
+pid_t InterruptForwarder::s_process_group_ = 0;
diff --git a/src/interrupt_handling-win32.cc b/src/interrupt_handling-win32.cc
new file mode 100644
index 0000000..a0b866c
--- /dev/null
+++ b/src/interrupt_handling-win32.cc
@@ -0,0 +1,88 @@
+// Copyright 2023 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "interrupt_handling.h"
+#include "util.h"
+
+//////////////////////////////////////////////////////////////////////////
+///
+///  InterruptCompletionPortHandler
+///
+
+InterruptCompletionPortHandler::InterruptCompletionPortHandler(
+    HANDLE ioport, ULONG_PTR completion_key)
+    : old_ioport_(s_ioport_), old_completion_key_(s_completion_key_) {
+  s_ioport_ = ioport;
+  s_completion_key_ = completion_key;
+
+  if (!SetConsoleCtrlHandler(HandlerRoutine, TRUE))
+    Win32Fatal("SetConsoleCtrlHandler");
+}
+
+InterruptCompletionPortHandler::~InterruptCompletionPortHandler() {
+  s_ioport_ = old_ioport_;
+  s_completion_key_ = old_completion_key_;
+
+  if (!SetConsoleCtrlHandler(HandlerRoutine, FALSE))
+    Win32Fatal("SetConsoleCtrlHandler");
+}
+
+// static
+BOOL InterruptCompletionPortHandler::HandlerRoutine(DWORD dwCtrlType) {
+  if (dwCtrlType != CTRL_C_EVENT && dwCtrlType != CTRL_BREAK_EVENT)
+    return FALSE;
+
+  if (!PostQueuedCompletionStatus(s_ioport_, 0, s_completion_key_, nullptr))
+    Win32Fatal("PostQueuedCompletionStatus");
+
+  return TRUE;
+}
+
+// static
+HANDLE InterruptCompletionPortHandler::s_ioport_ = INVALID_HANDLE_VALUE;
+
+// static
+ULONG_PTR InterruptCompletionPortHandler::s_completion_key_ = 0;
+
+//////////////////////////////////////////////////////////////////////////
+///
+///  InterruptForwarder
+///
+
+InterruptForwarder::InterruptForwarder(DWORD pgid)
+    : old_process_group_id_(s_process_group_id_) {
+  s_process_group_id_ = pgid;
+  if (!SetConsoleCtrlHandler(InterruptForwarder::HandlerRoutine, TRUE))
+    Win32Fatal("SetConsoleCtrlHandler");
+}
+
+InterruptForwarder::~InterruptForwarder() {
+  s_process_group_id_ = old_process_group_id_;
+  if (!SetConsoleCtrlHandler(InterruptForwarder::HandlerRoutine, FALSE))
+    Win32Fatal("SetConsoleCtrlHandler");
+}
+
+// static
+BOOL InterruptForwarder::HandlerRoutine(DWORD dwCtrlType) {
+  if (dwCtrlType != CTRL_C_EVENT && dwCtrlType != CTRL_BREAK_EVENT)
+    return FALSE;
+
+  if (!GenerateConsoleCtrlEvent(dwCtrlType, s_process_group_id_))
+    Win32Fatal("GenerateConsoleCtrlEvent");
+
+  return TRUE;
+}
+
+// static
+DWORD InterruptForwarder::s_process_group_id_ = 0;
diff --git a/src/interrupt_handling.h b/src/interrupt_handling.h
new file mode 100644
index 0000000..6dc562c
--- /dev/null
+++ b/src/interrupt_handling.h
@@ -0,0 +1,125 @@
+// Copyright 2023 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+#ifndef NINJA_INTERRUPT_HANDLING_H_
+#define NINJA_INTERRUPT_HANDLING_H_
+
+/// Convenience classes used to control how user interruption
+/// is handled by Ninja at various times. This means Ctrl+C and Ctrl+Break
+/// events on Win32, and SIGINT/SIGHUP/SIGTERM ones on Posix.
+
+#ifdef _WIN32
+
+#include <windows.h>
+
+/// On Ctrl-C or Ctrl-Break, post en empty i/o completion packet
+/// to a given i/o completion queue. Useful to catch signals when
+/// waiting for overlapped i/o on Win32.
+struct InterruptCompletionPortHandler {
+  InterruptCompletionPortHandler(HANDLE ioport, ULONG_PTR completion_key = 0);
+  ~InterruptCompletionPortHandler();
+
+ private:
+  static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType);
+  HANDLE old_ioport_;
+  ULONG_PTR old_completion_key_;
+  static HANDLE s_ioport_;
+  static ULONG_PTR s_completion_key_;
+};
+
+/// Forward all Ctrl-C and Ctrl-Break signals to a different
+/// process group.
+struct InterruptForwarder {
+  InterruptForwarder(DWORD process_group_id);
+  ~InterruptForwarder();
+
+ private:
+  static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType);
+  DWORD old_process_group_id_;
+  static DWORD s_process_group_id_;
+};
+
+#else  // !_WIN32
+
+#include <signal.h>
+
+/// A class used to block all SIGINT/SIGHUP/SIGTERM signals
+/// in the current process. Restores the previous signal
+/// mask in the destructor.
+struct InterruptBlocker {
+  InterruptBlocker();
+  ~InterruptBlocker();
+
+  const sigset_t& old_mask() const { return prev_signal_mask_; }
+
+ private:
+  sigset_t prev_signal_mask_;
+};
+
+/// Base class for all interrupt handlers.
+struct InterruptHandlerBase {
+  /// Constructor sets a new signal handler for SIGINT/SIGHUP/SIGTERM
+  /// and unblocks these signals!
+  InterruptHandlerBase(const struct sigaction& action);
+
+  /// Destructor restores previous interrupt handlers and signal mask.
+  ~InterruptHandlerBase();
+
+  /// Return the signal mask before construction. This will be restored
+  /// on destruction as well.
+  sigset_t old_mask() const { return old_mask_; }
+
+ private:
+  struct sigaction old_int_action_;
+  struct sigaction old_hup_action_;
+  struct sigaction old_term_action_;
+  sigset_t old_mask_;
+};
+
+/// Catch all SIGINT/SIGHUP/SIGTERM signals and stores the
+/// corresponding signal number into a global interrupted()
+/// variable. This can also detect pending signals.
+struct InterruptCatcher : public InterruptHandlerBase {
+  InterruptCatcher();
+  ~InterruptCatcher();
+
+  /// Return interrupt signal number, or 0 if there were none.
+  int interrupted() const { return s_interrupted_; }
+
+  /// Clear the interrupted signal number.
+  void Clear() { s_interrupted_ = 0; }
+
+  /// Handle any pending interruption signal. This updates
+  /// the interrupted() value but does not cancel the signals.
+  static void HandlePendingInterrupt();
+
+ private:
+  static struct sigaction MakeAction();
+  static volatile sig_atomic_t s_interrupted_;
+};
+
+/// Forward all SIGINT/SIGHUP/SIGTERM signal to a different
+/// process group
+struct InterruptForwarder : public InterruptHandlerBase {
+  explicit InterruptForwarder(pid_t process_group);
+  ~InterruptForwarder();
+
+ private:
+  static struct sigaction MakeAction();
+  pid_t old_process_group_ = -1;
+  static pid_t s_process_group_;
+};
+
+#endif  // !_WIN32
+
+#endif  // NINJA_INTERRUPT_HANDLING_H_
diff --git a/src/interrupt_handling_test.cc b/src/interrupt_handling_test.cc
new file mode 100644
index 0000000..2df18db
--- /dev/null
+++ b/src/interrupt_handling_test.cc
@@ -0,0 +1,204 @@
+// Copyright 2023 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "interrupt_handling.h"
+
+#include "test.h"
+#include "util.h"
+
+#ifdef _WIN32
+
+// TODO(digit): Write real test for _WIN32
+char dummy_;
+
+#else  // !_WIN32
+
+#include <sys/signal.h>
+#include <unistd.h>
+
+namespace {
+
+// Retrieve a signal mask for SIGINT/SIGHUP/SIGTERM
+sigset_t GetInterruptSignalMask() {
+  sigset_t mask;
+  sigemptyset(&mask);
+  sigaddset(&mask, SIGINT);
+  sigaddset(&mask, SIGHUP);
+  sigaddset(&mask, SIGTERM);
+  return mask;
+}
+
+// Set the signal action for a given |signum|, returning the previous one
+// in |*old_action| is |old_action != nullptr|.
+void SetSignalAction(int signum, const struct sigaction* action,
+                     struct sigaction* old_action) {
+  if (sigaction(signum, action, old_action) < 0)
+    ErrnoFatal("sigaction");
+}
+
+// Base class for all tests, used to set the signal mask and actions for
+// interrupts to the same base state.
+class InterruptHandlingTest : public ::testing::Test {
+ public:
+  InterruptHandlingTest() {
+    // Block signals before changing the action handler + save previous mask.
+    sigset_t mask = GetInterruptSignalMask();
+    sigprocmask(SIG_BLOCK, &mask, &prev_mask_);
+
+    // Change signal handlers.
+    struct sigaction sigint_action = {};
+    sigint_action.sa_handler = [](int) { s_got_sigint = 1; };
+    SetSignalAction(SIGINT, &sigint_action, &prev_sigint_action_);
+
+    struct sigaction sighup_action = {};
+    sighup_action.sa_handler = [](int) { s_got_sighup = 1; };
+    SetSignalAction(SIGHUP, &sighup_action, &prev_sighup_action_);
+
+    struct sigaction sigterm_action = {};
+    sigterm_action.sa_handler = [](int) { s_got_sigterm = 1; };
+    SetSignalAction(SIGTERM, &sigterm_action, &prev_sigterm_action_);
+
+    // Unblock signals.
+    sigprocmask(SIG_UNBLOCK, &mask, nullptr);
+  }
+
+  ~InterruptHandlingTest() {
+    // Block signals before changing handlers.
+    sigset_t mask = GetInterruptSignalMask();
+    sigprocmask(SIG_BLOCK, &mask, nullptr);
+
+    // Restore previous handlers.
+    SetSignalAction(SIGINT, &prev_sigint_action_, nullptr);
+    SetSignalAction(SIGHUP, &prev_sighup_action_, nullptr);
+    SetSignalAction(SIGTERM, &prev_sigterm_action_, nullptr);
+
+    // Clear flags for next test.
+    Clear();
+
+    // Restore previous signal mask.
+    sigprocmask(SIG_SETMASK, &prev_mask_, nullptr);
+  }
+
+  void Clear() {
+    s_got_sigint = 0;
+    s_got_sighup = 0;
+    s_got_sigterm = 0;
+  }
+
+  void SendSelfSignal(int signum) { ::kill(getpid(), signum); }
+
+  sigset_t prev_mask_;
+  struct sigaction prev_sigint_action_;
+  struct sigaction prev_sighup_action_;
+  struct sigaction prev_sigterm_action_;
+
+  static volatile sig_atomic_t s_got_sigint;
+  static volatile sig_atomic_t s_got_sighup;
+  static volatile sig_atomic_t s_got_sigterm;
+};
+
+volatile sig_atomic_t InterruptHandlingTest::s_got_sigint = 0;
+volatile sig_atomic_t InterruptHandlingTest::s_got_sighup = 0;
+volatile sig_atomic_t InterruptHandlingTest::s_got_sigterm = 0;
+
+}  // namespace
+
+TEST_F(InterruptHandlingTest, SendSelfSignals) {
+  // Verify that the interrupt signals are not blocked
+  sigset_t empty_mask;
+  sigemptyset(&empty_mask);
+  sigset_t cur_mask;
+  sigprocmask(SIG_BLOCK, &empty_mask, &cur_mask);
+  ASSERT_FALSE(sigismember(&cur_mask, SIGINT));
+  ASSERT_FALSE(sigismember(&cur_mask, SIGHUP));
+  ASSERT_FALSE(sigismember(&cur_mask, SIGTERM));
+
+  SendSelfSignal(SIGINT);
+  ASSERT_TRUE(s_got_sigint);
+
+  SendSelfSignal(SIGHUP);
+  ASSERT_TRUE(s_got_sighup);
+
+  SendSelfSignal(SIGTERM);
+  ASSERT_TRUE(s_got_sigterm);
+
+  Clear();
+  ASSERT_FALSE(s_got_sigint);
+  ASSERT_FALSE(s_got_sighup);
+  ASSERT_FALSE(s_got_sigterm);
+}
+
+TEST_F(InterruptHandlingTest, InterruptBlocker) {
+  {
+    InterruptBlocker blocker;
+
+    SendSelfSignal(SIGINT);
+    ASSERT_FALSE(s_got_sigint);
+
+    SendSelfSignal(SIGHUP);
+    ASSERT_FALSE(s_got_sighup);
+
+    SendSelfSignal(SIGTERM);
+    ASSERT_FALSE(s_got_sigterm);
+  }
+  ASSERT_TRUE(s_got_sigint);
+  ASSERT_TRUE(s_got_sighup);
+  ASSERT_TRUE(s_got_sigterm);
+}
+
+TEST_F(InterruptHandlingTest, InterruptCatcher) {
+  {
+    InterruptCatcher catcher;
+
+    SendSelfSignal(SIGINT);
+    ASSERT_FALSE(s_got_sigint);
+    ASSERT_EQ(SIGINT, catcher.interrupted());
+
+    SendSelfSignal(SIGHUP);
+    ASSERT_FALSE(s_got_sighup);
+    ASSERT_EQ(SIGHUP, catcher.interrupted());
+
+    SendSelfSignal(SIGTERM);
+    ASSERT_FALSE(s_got_sigterm);
+    ASSERT_EQ(SIGTERM, catcher.interrupted());
+  }
+
+  // Verify that a second instance does not keep the old interrupted()
+  // value from a stale global variable.
+  {
+    InterruptCatcher catcher;
+    ASSERT_EQ(0, catcher.interrupted());
+  }
+}
+
+TEST_F(InterruptHandlingTest, InterruptForwarder) {
+  ASSERT_FALSE(s_got_sigint);
+
+  // Create child process with fork.
+  pid_t child_pid = fork();
+  ASSERT_GE(child_pid, 0);
+
+  if (child_pid == 0) {
+    // In the child process, do nothing, just wait for an interrupt.
+    sleep(10);
+    exit(0);
+  }
+
+  InterruptForwarder forwarder(child_pid);
+
+  SendSelfSignal(SIGINT);
+  ASSERT_FALSE(s_got_sigint);
+}
+
+#endif  // !_WIN32
diff --git a/src/subprocess-posix.cc b/src/subprocess-posix.cc
index fccf53c..7369f93 100644
--- a/src/subprocess-posix.cc
+++ b/src/subprocess-posix.cc
@@ -78,7 +78,7 @@
   short flags = 0;
 
   flags |= POSIX_SPAWN_SETSIGMASK;
-  err = posix_spawnattr_setsigmask(&attr, &set->old_mask_);
+  err = posix_spawnattr_setsigmask(&attr, &set->old_signal_mask());
   if (err != 0)
     ErrnoFatal("posix_spawnattr_setsigmask", err);
   // Signals which are set to be caught in the calling process image are set to
@@ -185,58 +185,17 @@
   return buf_;
 }
 
-int SubprocessSet::interrupted_;
-
-void SubprocessSet::SetInterruptedFlag(int signum) {
-  interrupted_ = signum;
-}
-
-void SubprocessSet::HandlePendingInterruption() {
-  sigset_t pending;
-  sigemptyset(&pending);
-  if (sigpending(&pending) == -1) {
-    perror("ninja: sigpending");
-    return;
-  }
-  if (sigismember(&pending, SIGINT))
-    interrupted_ = SIGINT;
-  else if (sigismember(&pending, SIGTERM))
-    interrupted_ = SIGTERM;
-  else if (sigismember(&pending, SIGHUP))
-    interrupted_ = SIGHUP;
-}
-
 SubprocessSet::SubprocessSet() {
-  sigset_t set;
-  sigemptyset(&set);
-  sigaddset(&set, SIGINT);
-  sigaddset(&set, SIGTERM);
-  sigaddset(&set, SIGHUP);
-  if (sigprocmask(SIG_BLOCK, &set, &old_mask_) < 0)
-    Fatal("sigprocmask: %s", strerror(errno));
-
-  struct sigaction act;
-  memset(&act, 0, sizeof(act));
-  act.sa_handler = SetInterruptedFlag;
-  if (sigaction(SIGINT, &act, &old_int_act_) < 0)
-    Fatal("sigaction: %s", strerror(errno));
-  if (sigaction(SIGTERM, &act, &old_term_act_) < 0)
-    Fatal("sigaction: %s", strerror(errno));
-  if (sigaction(SIGHUP, &act, &old_hup_act_) < 0)
-    Fatal("sigaction: %s", strerror(errno));
+  // Allow ppoll()/pselect() to be interrupted by SIGINT/SIGHUP/SIGTERM
+  // even if these are blocked in the previous process mask.
+  wait_mask_ = interrupt_blocker_.old_mask();
+  sigdelset(&wait_mask_, SIGINT);
+  sigdelset(&wait_mask_, SIGHUP);
+  sigdelset(&wait_mask_, SIGTERM);
 }
 
 SubprocessSet::~SubprocessSet() {
   Clear();
-
-  if (sigaction(SIGINT, &old_int_act_, 0) < 0)
-    Fatal("sigaction: %s", strerror(errno));
-  if (sigaction(SIGTERM, &old_term_act_, 0) < 0)
-    Fatal("sigaction: %s", strerror(errno));
-  if (sigaction(SIGHUP, &old_hup_act_, 0) < 0)
-    Fatal("sigaction: %s", strerror(errno));
-  if (sigprocmask(SIG_SETMASK, &old_mask_, 0) < 0)
-    Fatal("sigprocmask: %s", strerror(errno));
 }
 
 Subprocess *SubprocessSet::Add(const string& command, bool use_console) {
@@ -264,20 +223,24 @@
     ++nfds;
   }
 
-  interrupted_ = 0;
-  int ret = ppoll(&fds.front(), nfds, NULL, &old_mask_);
+  interrupt_catcher_.Clear();
+
+  int ret = ppoll(&fds.front(), nfds, NULL, &wait_mask_);
   if (ret == -1) {
     if (errno != EINTR) {
       perror("ninja: ppoll");
       return false;
     }
-    return IsInterrupted();
+    return interrupt_catcher_.interrupted();
   }
 
-  HandlePendingInterruption();
-  if (IsInterrupted())
+  interrupt_catcher_.HandlePendingInterrupt();
+  if (interrupt_catcher_.interrupted())
     return true;
 
+  if (ret == 0)
+    return false;
+
   nfds_t cur_nfd = 0;
   for (vector<Subprocess*>::iterator i = running_.begin();
        i != running_.end(); ) {
@@ -296,7 +259,7 @@
     ++i;
   }
 
-  return IsInterrupted();
+  return interrupt_catcher_.interrupted();
 }
 
 #else  // !defined(USE_PPOLL)
@@ -315,20 +278,24 @@
     }
   }
 
-  interrupted_ = 0;
-  int ret = pselect(nfds, &set, 0, 0, 0, &old_mask_);
+  interrupt_catcher_.Clear();
+
+  int ret = pselect(nfds, &set, 0, 0, 0, &wait_mask_);
   if (ret == -1) {
     if (errno != EINTR) {
       perror("ninja: pselect");
       return false;
     }
-    return IsInterrupted();
+    return interrupt_catcher_.interrupted();
   }
 
-  HandlePendingInterruption();
-  if (IsInterrupted())
+  interrupt_catcher_.HandlePendingInterrupt();
+  if (interrupt_catcher_.interrupted())
     return true;
 
+  if (ret == 0)
+    return false;
+
   for (vector<Subprocess*>::iterator i = running_.begin();
        i != running_.end(); ) {
     int fd = (*i)->fd_;
@@ -343,7 +310,7 @@
     ++i;
   }
 
-  return IsInterrupted();
+  return interrupt_catcher_.interrupted();
 }
 #endif  // !defined(USE_PPOLL)
 
@@ -356,14 +323,19 @@
 }
 
 void SubprocessSet::Clear() {
-  for (vector<Subprocess*>::iterator i = running_.begin();
-       i != running_.end(); ++i)
-    // Since the foreground process is in our process group, it will receive
-    // the interruption signal (i.e. SIGINT or SIGTERM) at the same time as us.
-    if (!(*i)->use_console_)
-      kill(-(*i)->pid_, interrupted_);
-  for (vector<Subprocess*>::iterator i = running_.begin();
-       i != running_.end(); ++i)
-    delete *i;
+  int interrupted = interrupt_catcher_.interrupted();
+  if (interrupted) {
+    for (auto* subproc : running_) {
+      // Since the foreground process is in our process group, it will receive
+      // the interruption signal (i.e. SIGINT or SIGTERM) at the same time as
+      // us.
+      if (!subproc->use_console_)
+        kill(-subproc->pid_, interrupted);
+    }
+  }
+
+  for (auto* subproc : running_)
+    delete subproc;
+
   running_.clear();
 }
diff --git a/src/subprocess-win32.cc b/src/subprocess-win32.cc
index ff3baac..2325773 100644
--- a/src/subprocess-win32.cc
+++ b/src/subprocess-win32.cc
@@ -75,7 +75,7 @@
 }
 
 bool Subprocess::Start(SubprocessSet* set, const string& command) {
-  HANDLE child_pipe = SetupPipe(set->ioport_);
+  HANDLE child_pipe = SetupPipe(set->ioport_.get());
 
   SECURITY_ATTRIBUTES security_attributes;
   memset(&security_attributes, 0, sizeof(SECURITY_ATTRIBUTES));
@@ -211,31 +211,23 @@
   return buf_;
 }
 
-HANDLE SubprocessSet::ioport_;
-
-SubprocessSet::SubprocessSet() {
-  ioport_ = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 1);
-  if (!ioport_)
+SubprocessSet::ScopedIoPort::ScopedIoPort() {
+  s_handle_ = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 1);
+  if (!s_handle_)
     Win32Fatal("CreateIoCompletionPort");
-  if (!SetConsoleCtrlHandler(NotifyInterrupted, TRUE))
-    Win32Fatal("SetConsoleCtrlHandler");
 }
 
+SubprocessSet::ScopedIoPort::~ScopedIoPort() {
+  ::CloseHandle(s_handle_);
+}
+
+// static
+HANDLE SubprocessSet::ScopedIoPort::s_handle_ = INVALID_HANDLE_VALUE;
+
+SubprocessSet::SubprocessSet() : ioport_(), interrupt_handler_(ioport_.get()) {}
+
 SubprocessSet::~SubprocessSet() {
   Clear();
-
-  SetConsoleCtrlHandler(NotifyInterrupted, FALSE);
-  CloseHandle(ioport_);
-}
-
-BOOL WINAPI SubprocessSet::NotifyInterrupted(DWORD dwCtrlType) {
-  if (dwCtrlType == CTRL_C_EVENT || dwCtrlType == CTRL_BREAK_EVENT) {
-    if (!PostQueuedCompletionStatus(ioport_, 0, 0, NULL))
-      Win32Fatal("PostQueuedCompletionStatus");
-    return TRUE;
-  }
-
-  return FALSE;
 }
 
 Subprocess *SubprocessSet::Add(const string& command, bool use_console) {
@@ -256,14 +248,14 @@
   Subprocess* subproc;
   OVERLAPPED* overlapped;
 
-  if (!GetQueuedCompletionStatus(ioport_, &bytes_read, (PULONG_PTR)&subproc,
-                                 &overlapped, INFINITE)) {
+  if (!GetQueuedCompletionStatus(ioport_.get(), &bytes_read,
+                                 (PULONG_PTR)&subproc, &overlapped, INFINITE)) {
     if (GetLastError() != ERROR_BROKEN_PIPE)
       Win32Fatal("GetQueuedCompletionStatus");
   }
 
-  if (!subproc) // A NULL subproc indicates that we were interrupted and is
-                // delivered by NotifyInterrupted above.
+  if (!subproc)  // A NULL subproc indicates that we were interrupted and is
+                 // delivered by an InterruptCompletionPortHandler.
     return true;
 
   subproc->OnPipeReady();
diff --git a/src/subprocess.h b/src/subprocess.h
index 9e3d2ee..dbf9a72 100644
--- a/src/subprocess.h
+++ b/src/subprocess.h
@@ -15,9 +15,10 @@
 #ifndef NINJA_SUBPROCESS_H_
 #define NINJA_SUBPROCESS_H_
 
+#include <memory>
+#include <queue>
 #include <string>
 #include <vector>
-#include <queue>
 
 #ifdef _WIN32
 #include <windows.h>
@@ -34,6 +35,7 @@
 #endif
 
 #include "exit_status.h"
+#include "interrupt_handling.h"
 
 /// Subprocess wraps a single async subprocess.  It is entirely
 /// passive: it expects the caller to notify it when its fds are ready
@@ -92,21 +94,26 @@
   std::queue<Subprocess*> finished_;
 
 #ifdef _WIN32
-  static BOOL WINAPI NotifyInterrupted(DWORD dwCtrlType);
-  static HANDLE ioport_;
+  // Helper class to create and close a static I/O completion port handle
+  // in the right order.
+  struct ScopedIoPort {
+    ScopedIoPort();
+    ~ScopedIoPort();
+    HANDLE get() const { return s_handle_; }
+
+   private:
+    static HANDLE s_handle_;
+  };
+  ScopedIoPort ioport_;
+  InterruptCompletionPortHandler interrupt_handler_;
 #else
-  static void SetInterruptedFlag(int signum);
-  static void HandlePendingInterruption();
-  /// Store the signal number that causes the interruption.
-  /// 0 if not interruption.
-  static int interrupted_;
+  const sigset_t& old_signal_mask() const {
+    return interrupt_blocker_.old_mask();
+  }
 
-  static bool IsInterrupted() { return interrupted_ != 0; }
-
-  struct sigaction old_int_act_;
-  struct sigaction old_term_act_;
-  struct sigaction old_hup_act_;
-  sigset_t old_mask_;
+  InterruptBlocker interrupt_blocker_;
+  InterruptCatcher interrupt_catcher_;
+  sigset_t wait_mask_;
 #endif
 };