Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/libmain/progress-bar.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "nix/main/progress-bar.hh"
#include "nix/util/terminal.hh"
#include "nix/util/sync.hh"
#include "nix/util/signals.hh"
#include "nix/store/store-api.hh"
#include "nix/store/names.hh"

Expand Down Expand Up @@ -94,10 +95,16 @@ class ProgressBar : public Logger
bool printBuildLogs = false;
bool isTTY;

std::unique_ptr<InterruptCallback> interruptCallback;

public:

ProgressBar(bool isTTY)
: isTTY(isTTY)
, interruptCallback(createInterruptCallback([&]() {
pause();
redraw("\rshutting down\e[K");
}))
{
state_.lock()->active = isTTY;
updateThread = std::thread([&]() {
Expand Down
6 changes: 6 additions & 0 deletions src/libstore/unix/build/hook-instance.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include "nix/util/strings.hh"
#include "nix/util/executable-path.hh"

using namespace std::chrono_literals;

namespace nix {

HookInstance::HookInstance()
Expand Down Expand Up @@ -72,6 +74,10 @@ HookInstance::HookInstance()
throw SysError("executing %s", PathFmt(buildHook));
});

/* Give custom build hooks the chance to cleanup. */
pid.setKillSignal(SIGTERM);
Comment on lines +77 to +78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm doesn't this mean that we'd use SIGTERM in the destructor and could get stuck on waitpid after an interrupt indefinitely for a misbehaving build hook? Shouldn't we still use SIGKILL in the destructor and have a timeout in the happy path (busywait?).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How's this? The alternative is adding a busywait + kill directly to Pid::wait, which seems messy.

pid.setKillTimeout(500ms);

pid.setSeparatePG(true);
fromHook.writeSide = -1;
toHook.readSide = -1;
Expand Down
4 changes: 4 additions & 0 deletions src/libutil/include/nix/util/processes.hh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <map>
#include <sstream>
#include <optional>
#include <thread>

namespace nix {

Expand All @@ -30,6 +31,8 @@ class Pid
pid_t pid = -1;
bool separatePG = false;
int killSignal = SIGKILL;
std::chrono::milliseconds killTimeout;
std::thread killThread;
#else
AutoCloseFD pid = INVALID_DESCRIPTOR;
#endif
Expand All @@ -55,6 +58,7 @@ public:
#ifndef _WIN32
void setSeparatePG(bool separatePG);
void setKillSignal(int signal);
void setKillTimeout(std::chrono::milliseconds duration);
pid_t release();
#endif

Expand Down
29 changes: 27 additions & 2 deletions src/libutil/unix/processes.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
#include <future>
#include <iostream>
#include <sstream>
#include <thread>
#include <atomic>
using namespace std::chrono_literals;

#include <grp.h>
#include <sys/types.h>
Expand Down Expand Up @@ -75,6 +76,20 @@ int Pid::kill(bool allowInterrupts)

debug("killing process %1%", pid);

std::atomic<bool> killed = false;

if (killTimeout > 0ms && killSignal != SIGKILL)
killThread = std::thread([&]() {
auto elapsed = 0ms;
while (elapsed < killTimeout) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will block the Worker::run() loop from making progress for up to killTimeout milliseconds, right? That's probably not too bad, but not ideal...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In an ideal world we could try doing async cleanup if we had better facilities for async things (think a task that either polls pidfd or hits a timeout). I think ASIO + Boost.process2 might have some helpers for this sort of thing

Copy link
Contributor Author

@lisanna-dettwyler lisanna-dettwyler Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only if these conditions are met:

  • The user interrupts nix
  • The user is using a nonstandard build hook (the regular build hook responds timely to SIGTERM)
  • The build hook is misbehaving during shutdown

std::this_thread::sleep_for(25ms);
elapsed += 25ms;
if (killed)
return;
}
::kill(separatePG ? -pid : pid, SIGKILL);
});

/* Send the requested signal to the child. If it has its own
process group, send the signal to every process in the child
process group (which hopefully includes *all* its children). */
Expand All @@ -88,7 +103,12 @@ int Pid::kill(bool allowInterrupts)
logError(SysError("killing process %d", pid).info());
}

return wait(allowInterrupts);
int ret = wait(allowInterrupts);
if (killThread.joinable()) {
killed = true;
killThread.join();
}
return ret;
}

int Pid::wait(bool allowInterrupts)
Expand Down Expand Up @@ -118,6 +138,11 @@ void Pid::setKillSignal(int signal)
this->killSignal = signal;
}

void Pid::setKillTimeout(std::chrono::milliseconds duration)
{
this->killTimeout = duration;
}

pid_t Pid::release()
{
pid_t p = pid;
Expand Down
17 changes: 13 additions & 4 deletions src/libutil/unix/signals.cc
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,16 @@ struct InterruptCallbacks
std::map<Token, std::function<void()>> callbacks;
};

static Sync<InterruptCallbacks> _interruptCallbacks;
/* Required to avoid static initialization order fiasco. This allows global
objects to safely register callbacks. */
static Sync<InterruptCallbacks> & getInterruptCallbacks()
{
/* Intentionally leak, according to the Construct On First Use Idiom.
An alternative is to use the Nifty Counter Idiom, but
InterruptCallbacks' destructor is not very important. */
static Sync<InterruptCallbacks> * _interruptCallbacks = new Sync<InterruptCallbacks>();
return *_interruptCallbacks;
}

static void signalHandlerThread(sigset_t set)
{
Expand All @@ -68,7 +77,7 @@ void unix::triggerInterrupt()
while (true) {
std::function<void()> callback;
{
auto interruptCallbacks(_interruptCallbacks.lock());
auto interruptCallbacks(getInterruptCallbacks().lock());
auto lb = interruptCallbacks->callbacks.lower_bound(i);
if (lb == interruptCallbacks->callbacks.end())
break;
Expand Down Expand Up @@ -153,14 +162,14 @@ struct InterruptCallbackImpl : InterruptCallback

~InterruptCallbackImpl() override
{
auto interruptCallbacks(_interruptCallbacks.lock());
auto interruptCallbacks(getInterruptCallbacks().lock());
interruptCallbacks->callbacks.erase(token);
}
};

std::unique_ptr<InterruptCallback> createInterruptCallback(std::function<void()> callback)
{
auto interruptCallbacks(_interruptCallbacks.lock());
auto interruptCallbacks(getInterruptCallbacks().lock());
auto token = interruptCallbacks->nextToken++;
interruptCallbacks->callbacks.emplace(token, callback);
return std::make_unique<InterruptCallbackImpl>(token);
Expand Down
17 changes: 17 additions & 0 deletions src/nix/build-remote/build-remote.cc
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ static bool allSupportedLocally(Store & store, const StringSet & requiredFeature
static int main_build_remote(int argc, char ** argv)
{
{
/* Upon exiting, Nix will attempt to terminate this process with
SIGTERM. initNix will block or handle SIGTERM, so we need to unblock
and unhandle it here.
*/
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = SIG_DFL;
if (sigaction(SIGTERM, &act, 0))
throw SysError("resetting SIGTERM");

sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGTERM);
if (pthread_sigmask(SIG_UNBLOCK, &set, nullptr))
throw SysError("unblocking SIGTERM");

logger = makeJSONLogger(getStandardError());

/* Ensure we don't get any SSH passphrase or host key popups. */
Expand Down
Loading