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:
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
|
||||
Reference in New Issue
Block a user