libutil: quit immediately on second sigint

we must be crash-safe *anyway*, and being unable to interrupt lix if it
gets stuck somewhere that never calls checkInterrupt is really annoying

Change-Id: I7c40271c3da7e69d8735e22b7b7c4751b5306ab6
This commit is contained in:
eldritch horrors
2025-07-05 16:27:47 +02:00
parent 0cc021ee15
commit b1544582af
4 changed files with 72 additions and 5 deletions
+25
View File
@@ -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
```
+1 -1
View File
@@ -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.
+40 -4
View File
@@ -4,6 +4,7 @@
#include "lix/libutil/sync.hh"
#include "lix/libutil/terminal.hh"
#include "lix/libutil/thread-name.hh"
#include "logging.hh"
#include <csignal>
#include <kj/time.h>
@@ -11,7 +12,9 @@
#include <kj/async.h>
#include <map>
#include <memory>
#include <optional>
#include <thread>
#include <unistd.h>
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<int> set)
setCurrentThreadName("signal handler");
AsyncIoRoot aio;
std::optional<kj::TimePoint> printInterruptMessageAt;
for (auto sig : set) {
AIO().unixEventPort.captureSignal(sig);
@@ -69,17 +79,43 @@ static void signalHandlerThread(const std::vector<int> 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();
}
}
+6
View File
@@ -48,6 +48,12 @@ extern thread_local std::function<bool()> 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()))