aboutsummaryrefslogtreecommitdiff
path: root/src/libutil/signals.cc
diff options
context:
space:
mode:
authorJade Lovelace <lix@jade.fyi>2024-03-09 22:36:47 -0800
committerJade Lovelace <lix@jade.fyi>2024-03-11 00:52:09 -0700
commit8be7030299699edd3732411d8d97f237a67fbc08 (patch)
tree118f6c5c8dfac9ae9c65f54c5eb5942323324b17 /src/libutil/signals.cc
parenta9b813cc3bcf89f03de0db96fc2e88d1c83b8303 (diff)
util.hh: split out signals stuff
Copies part of the changes of ac89bb064aeea85a62b82a6daf0ecca7190a28b7 Change-Id: I9ce601875cd6d4db5eb1132d7835c5bab9f126d8
Diffstat (limited to 'src/libutil/signals.cc')
-rw-r--r--src/libutil/signals.cc184
1 files changed, 184 insertions, 0 deletions
diff --git a/src/libutil/signals.cc b/src/libutil/signals.cc
new file mode 100644
index 000000000..f5c79b325
--- /dev/null
+++ b/src/libutil/signals.cc
@@ -0,0 +1,184 @@
+#include "signals.hh"
+#include "util.hh"
+#include "error.hh"
+#include "sync.hh"
+
+#include <thread>
+
+namespace nix {
+
+std::atomic<bool> _isInterrupted = false;
+
+static thread_local bool interruptThrown = false;
+thread_local std::function<bool()> interruptCheck;
+
+void setInterruptThrown()
+{
+ interruptThrown = true;
+}
+
+void _interrupted()
+{
+ /* Block user interrupts while an exception is being handled.
+ Throwing an exception while another exception is being handled
+ kills the program! */
+ if (!interruptThrown && !std::uncaught_exceptions()) {
+ interruptThrown = true;
+ throw Interrupted("interrupted by the user");
+ }
+}
+
+
+//////////////////////////////////////////////////////////////////////
+
+/* We keep track of interrupt callbacks using integer tokens, so we can iterate
+ safely without having to lock the data structure while executing arbitrary
+ functions.
+ */
+struct InterruptCallbacks {
+ typedef int64_t Token;
+
+ /* We use unique tokens so that we can't accidentally delete the wrong
+ handler because of an erroneous double delete. */
+ Token nextToken = 0;
+
+ /* Used as a list, see InterruptCallbacks comment. */
+ std::map<Token, std::function<void()>> callbacks;
+};
+
+static Sync<InterruptCallbacks> _interruptCallbacks;
+
+static void signalHandlerThread(sigset_t set)
+{
+ while (true) {
+ int signal = 0;
+ sigwait(&set, &signal);
+
+ if (signal == SIGINT || signal == SIGTERM || signal == SIGHUP)
+ triggerInterrupt();
+
+ else if (signal == SIGWINCH) {
+ updateWindowSize();
+ }
+ }
+}
+
+void triggerInterrupt()
+{
+ _isInterrupted = true;
+
+ {
+ InterruptCallbacks::Token i = 0;
+ while (true) {
+ std::function<void()> callback;
+ {
+ auto interruptCallbacks(_interruptCallbacks.lock());
+ auto lb = interruptCallbacks->callbacks.lower_bound(i);
+ if (lb == interruptCallbacks->callbacks.end())
+ break;
+
+ callback = lb->second;
+ i = lb->first + 1;
+ }
+
+ try {
+ callback();
+ } catch (...) {
+ ignoreException();
+ }
+ }
+ }
+}
+
+static sigset_t savedSignalMask;
+static bool savedSignalMaskIsSet = false;
+
+void setChildSignalMask(sigset_t * sigs)
+{
+ assert(sigs); // C style function, but think of sigs as a reference
+
+#if _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE
+ sigemptyset(&savedSignalMask);
+ // There's no "assign" or "copy" function, so we rely on (math) idempotence
+ // of the or operator: a or a = a.
+ sigorset(&savedSignalMask, sigs, sigs);
+#else
+ // Without sigorset, our best bet is to assume that sigset_t is a type that
+ // can be assigned directly, such as is the case for a sigset_t defined as
+ // an integer type.
+ savedSignalMask = *sigs;
+#endif
+
+ savedSignalMaskIsSet = true;
+}
+
+void saveSignalMask() {
+ if (sigprocmask(SIG_BLOCK, nullptr, &savedSignalMask))
+ throw SysError("querying signal mask");
+
+ savedSignalMaskIsSet = true;
+}
+
+void startSignalHandlerThread()
+{
+ updateWindowSize();
+
+ saveSignalMask();
+
+ sigset_t set;
+ sigemptyset(&set);
+ sigaddset(&set, SIGINT);
+ sigaddset(&set, SIGTERM);
+ sigaddset(&set, SIGHUP);
+ sigaddset(&set, SIGPIPE);
+ sigaddset(&set, SIGWINCH);
+ if (pthread_sigmask(SIG_BLOCK, &set, nullptr))
+ throw SysError("blocking signals");
+
+ std::thread(signalHandlerThread, set).detach();
+}
+
+void restoreSignals()
+{
+ // If startSignalHandlerThread wasn't called, that means we're not running
+ // in a proper libmain process, but a process that presumably manages its
+ // own signal handlers. Such a process should call either
+ // - initNix(), to be a proper libmain process
+ // - startSignalHandlerThread(), to resemble libmain regarding signal
+ // handling only
+ // - saveSignalMask(), for processes that define their own signal handling
+ // thread
+ // TODO: Warn about this? Have a default signal mask? The latter depends on
+ // whether we should generally inherit signal masks from the caller.
+ // I don't know what the larger unix ecosystem expects from us here.
+ if (!savedSignalMaskIsSet)
+ return;
+
+ if (sigprocmask(SIG_SETMASK, &savedSignalMask, nullptr))
+ throw SysError("restoring signals");
+}
+
+/* RAII helper to automatically deregister a callback. */
+struct InterruptCallbackImpl : InterruptCallback
+{
+ InterruptCallbacks::Token token;
+ ~InterruptCallbackImpl() override
+ {
+ auto interruptCallbacks(_interruptCallbacks.lock());
+ interruptCallbacks->callbacks.erase(token);
+ }
+};
+
+std::unique_ptr<InterruptCallback> createInterruptCallback(std::function<void()> callback)
+{
+ auto interruptCallbacks(_interruptCallbacks.lock());
+ auto token = interruptCallbacks->nextToken++;
+ interruptCallbacks->callbacks.emplace(token, callback);
+
+ auto res = std::make_unique<InterruptCallbackImpl>();
+ res->token = token;
+
+ return std::unique_ptr<InterruptCallback>(res.release());
+}
+
+};