diff --git a/doc/manual/rl-next/double-sigint.md b/doc/manual/rl-next/double-sigint.md new file mode 100644 index 000000000..455e4faf3 --- /dev/null +++ b/doc/manual/rl-next/double-sigint.md @@ -0,0 +1,25 @@ +--- +synopsis: "Hitting Control-C twice always terminates Lix" +cls: [3574] +issues: [] +category: "Improvements" +credits: [horrors] +--- + +Hitting Control-C or sending `SIGINT` to Lix now prints an informational message +if it is still running after on second, the second Control-C/`SIGINT` terminates +Lix immediately without waiting for any shutdown code to finish running. Lix did +not treat the second such event differently from first in the past; this made it +impossible to easily terminate running Lix processes that got stuck in e.g. very +expensive Nixlang code that never interacted with the store. We now terminate as +soon as the user hits Control-C again without waiting any more, to much the same +effect as putting Lix into the background and killing it immediately afterwards. + +This means you can now more conveniently break out of stuck Nixlang evaluations: +``` +❯ nix-instantiate --eval --expr 'let f = n: if n == 0 then 0 else f (n - 1) + f (n - 1); in f 32' +^CStill shutting down. Press ^C again to abort all operations immediately. +^C + +❌130 ❯ +``` diff --git a/lix/libcmd/repl.cc b/lix/libcmd/repl.cc index f232c0ced..76e6c9d2b 100644 --- a/lix/libcmd/repl.cc +++ b/lix/libcmd/repl.cc @@ -306,7 +306,7 @@ ReplExitStatus NixRepl::mainLoop() std::string input; while (true) { - _isInterrupted = false; + unsetUserInterruptRequest(); // When continuing input from previous lines, don't print a prompt, just align to the same // number of chars as the prompt. diff --git a/lix/libutil/signals.cc b/lix/libutil/signals.cc index 33cc4a8ed..993e52d7a 100644 --- a/lix/libutil/signals.cc +++ b/lix/libutil/signals.cc @@ -4,6 +4,7 @@ #include "lix/libutil/sync.hh" #include "lix/libutil/terminal.hh" #include "lix/libutil/thread-name.hh" +#include "logging.hh" #include #include @@ -11,7 +12,9 @@ #include #include #include +#include #include +#include namespace nix { @@ -34,6 +37,12 @@ void _interrupted() } } +void unsetUserInterruptRequest() +{ + _isInterrupted = false; + // recapture the signal as the signal handler thread will have released it + AIO().unixEventPort.captureSignal(SIGINT); +} ////////////////////////////////////////////////////////////////////// @@ -59,6 +68,7 @@ static void signalHandlerThread(const std::vector set) setCurrentThreadName("signal handler"); AsyncIoRoot aio; + std::optional printInterruptMessageAt; for (auto sig : set) { AIO().unixEventPort.captureSignal(sig); @@ -69,17 +79,43 @@ static void signalHandlerThread(const std::vector set) for (auto sig : set) { promise = promise.exclusiveJoin(AIO().unixEventPort.onSignal(sig)); } - return promise; + if (printInterruptMessageAt) { + return AIO().provider.getTimer().atTime(*printInterruptMessageAt).then([] { + return siginfo_t{.si_signo = -1}; + }); + } else { + return promise; + } }; while (true) { auto info = onSignal().wait(aio.kj.waitScope); int signal = info.si_signo; - if (signal == SIGINT || signal == SIGTERM || signal == SIGHUP) - triggerInterrupt(); + if (printInterruptMessageAt && *printInterruptMessageAt <= AIO().provider.getTimer().now()) { + // we only print to a terminal, and only by bypassing the logger, to + // ensure that it's both a *user* who is sending us this signal, and + // that the user will get a notification that isn't mixed with logs. + if (_isInterrupted && isatty(STDERR_FILENO)) { + writeLogsToStderr( + "Still shutting down. Press ^C again to abort all operations immediately.\n" + ); + } + printInterruptMessageAt = std::nullopt; + } - else if (signal == SIGWINCH) { + // treat SIGINT specially. SIGINT is usually sent interactively, SIGTERM only to daemons + if (signal == SIGINT) { + sigset_t unblock; + sigemptyset(&unblock); + sigaddset(&unblock, signal); + pthread_sigmask(SIG_UNBLOCK, &unblock, nullptr); + ::signal(SIGINT, SIG_DFL); + printInterruptMessageAt = AIO().provider.getTimer().now() + 1 * kj::SECONDS; + triggerInterrupt(); + } else if (signal == SIGTERM || signal == SIGHUP) { + triggerInterrupt(); + } else if (signal == SIGWINCH) { updateWindowSize(); } } diff --git a/lix/libutil/signals.hh b/lix/libutil/signals.hh index f9d9bc93e..0778ee905 100644 --- a/lix/libutil/signals.hh +++ b/lix/libutil/signals.hh @@ -48,6 +48,12 @@ extern thread_local std::function interruptCheck; Interrupted makeInterrupted(); void _interrupted(); +/** + * Clear a pending `checkInterrupt()` request. Mainly useful for the REPL which + * can safely continue after a user interruption of eg. some hung Nixlang code. + */ +void unsetUserInterruptRequest(); + void inline checkInterrupt() { if (_isInterrupted || (interruptCheck && interruptCheck()))