diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile | 3 | ||||
-rw-r--r-- | shell.nix | 4 | ||||
-rw-r--r-- | src/build-remote/build-remote.cc | 285 | ||||
-rw-r--r-- | src/build-remote/local.mk | 11 | ||||
-rw-r--r-- | src/libexpr/primops.cc | 2 | ||||
-rw-r--r-- | src/libmain/shared.cc | 22 | ||||
-rw-r--r-- | src/libstore/build.cc | 38 | ||||
-rw-r--r-- | src/libstore/derivations.cc | 3 | ||||
-rw-r--r-- | src/libstore/download.cc | 35 | ||||
-rw-r--r-- | src/libstore/gc.cc | 16 | ||||
-rw-r--r-- | src/libstore/optimise-store.cc | 8 | ||||
-rw-r--r-- | src/libstore/pathlocks.cc | 2 | ||||
-rw-r--r-- | src/libstore/remote-store.cc | 5 | ||||
-rw-r--r-- | src/libstore/ssh-store.cc | 2 | ||||
-rw-r--r-- | src/libstore/store-api.cc | 33 | ||||
-rw-r--r-- | src/libstore/store-api.hh | 2 | ||||
-rw-r--r-- | src/libutil/serialise.cc | 8 | ||||
-rw-r--r-- | src/libutil/util.cc | 156 | ||||
-rw-r--r-- | src/libutil/util.hh | 67 | ||||
-rwxr-xr-x | src/nix-build/nix-build.cc | 22 | ||||
-rw-r--r-- | src/nix/copy.cc | 28 | ||||
-rw-r--r-- | tests/remote-builds.nix | 1 | ||||
-rw-r--r-- | tests/timeout.builder.sh | 2 | ||||
-rw-r--r-- | tests/timeout.nix | 28 | ||||
-rw-r--r-- | tests/timeout.sh | 14 |
26 files changed, 576 insertions, 223 deletions
diff --git a/.gitignore b/.gitignore index 9b4ae6e151cd..92f95fe1fcb9 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,8 @@ Makefile.config # /src/nix-build/ /src/nix-build/nix-build +/src/build-remote/build-remote + # /tests/ /tests/test-tmp /tests/common.sh diff --git a/Makefile b/Makefile index 2ee40b56b1d3..14be271bb107 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ makefiles = \ src/resolve-system-dependencies/local.mk \ src/nix-channel/local.mk \ src/nix-build/local.mk \ + src/build-remote/local.mk \ perl/local.mk \ scripts/local.mk \ corepkgs/local.mk \ @@ -27,7 +28,7 @@ makefiles = \ tests/local.mk #src/download-via-ssh/local.mk \ -GLOBAL_CXXFLAGS += -std=c++11 -g -Wall +GLOBAL_CXXFLAGS += -std=c++14 -g -Wall -include Makefile.config diff --git a/shell.nix b/shell.nix index dd448410554e..4c1608230cee 100644 --- a/shell.nix +++ b/shell.nix @@ -1,6 +1,8 @@ +{ useClang ? false }: + with import <nixpkgs> {}; -stdenv.mkDerivation { +(if useClang then clangStdenv else stdenv).mkDerivation { name = "nix"; buildInputs = diff --git a/src/build-remote/build-remote.cc b/src/build-remote/build-remote.cc new file mode 100644 index 000000000000..1daf0b80ba70 --- /dev/null +++ b/src/build-remote/build-remote.cc @@ -0,0 +1,285 @@ +#include <cstdlib> +#include <cstring> +#include <algorithm> +#include <set> +#include <memory> +#include <tuple> +#include <iomanip> +#if __APPLE__ +#include <sys/time.h> +#endif + +#include "shared.hh" +#include "pathlocks.hh" +#include "globals.hh" +#include "serve-protocol.hh" +#include "serialise.hh" +#include "store-api.hh" +#include "derivations.hh" + +using namespace nix; +using std::cerr; +using std::cin; + +static void handle_alarm(int sig) { +} + +class machine { + const std::set<string> supportedFeatures; + const std::set<string> mandatoryFeatures; + +public: + const string hostName; + const std::vector<string> systemTypes; + const string sshKey; + const unsigned long long maxJobs; + const unsigned long long speedFactor; + bool enabled; + + bool allSupported(const std::set<string> & features) const { + return std::all_of(features.begin(), features.end(), + [&](const string & feature) { + return supportedFeatures.count(feature) || + mandatoryFeatures.count(feature); + }); + } + + bool mandatoryMet(const std::set<string> & features) const { + return std::all_of(mandatoryFeatures.begin(), mandatoryFeatures.end(), + [&](const string & feature) { + return features.count(feature); + }); + } + + machine(decltype(hostName) hostName, + decltype(systemTypes) systemTypes, + decltype(sshKey) sshKey, + decltype(maxJobs) maxJobs, + decltype(speedFactor) speedFactor, + decltype(supportedFeatures) supportedFeatures, + decltype(mandatoryFeatures) mandatoryFeatures) : + supportedFeatures{std::move(supportedFeatures)}, + mandatoryFeatures{std::move(mandatoryFeatures)}, + hostName{std::move(hostName)}, + systemTypes{std::move(systemTypes)}, + sshKey{std::move(sshKey)}, + maxJobs{std::move(maxJobs)}, + speedFactor{speedFactor == 0 ? 1 : std::move(speedFactor)}, + enabled{true} {}; +};; + +static std::vector<machine> read_conf() +{ + auto conf = getEnv("NIX_REMOTE_SYSTEMS", SYSCONFDIR "/nix/machines"); + + auto machines = std::vector<machine>{}; + auto lines = std::vector<string>{}; + try { + lines = tokenizeString<std::vector<string>>(readFile(conf), "\n"); + } catch (const SysError & e) { + if (e.errNo != ENOENT) + throw; + } + for (auto line : lines) { + chomp(line); + line.erase(std::find(line.begin(), line.end(), '#'), line.end()); + if (line.empty()) { + continue; + } + auto tokens = tokenizeString<std::vector<string>>(line); + auto sz = tokens.size(); + if (sz < 4) { + throw new FormatError(format("Bad machines.conf file %1%") + % conf); + } + machines.emplace_back(tokens[0], + tokenizeString<std::vector<string>>(tokens[1], ","), + tokens[2], + stoull(tokens[3]), + sz >= 5 ? stoull(tokens[4]) : 1LL, + sz >= 6 ? + tokenizeString<std::set<string>>(tokens[5], ",") : + std::set<string>{}, + sz >= 7 ? + tokenizeString<std::set<string>>(tokens[6], ",") : + std::set<string>{}); + } + return machines; +} + +static string currentLoad; + +static int openSlotLock(const machine & m, unsigned long long slot) +{ + std::ostringstream fn_stream(currentLoad, std::ios_base::ate | std::ios_base::out); + fn_stream << "/"; + for (auto t : m.systemTypes) { + fn_stream << t << "-"; + } + fn_stream << m.hostName << "-" << slot; + return openLockFile(fn_stream.str(), true); +} + +static char display_env[] = "DISPLAY="; +static char ssh_env[] = "SSH_ASKPASS="; + +int main (int argc, char * * argv) +{ + return handleExceptions(argv[0], [&]() { + initNix(); + /* Ensure we don't get any SSH passphrase or host key popups. */ + if (putenv(display_env) == -1 || + putenv(ssh_env) == -1) { + throw SysError("Setting SSH env vars"); + } + + if (argc != 4) { + throw UsageError("called without required arguments"); + } + + auto store = openStore(); + + auto localSystem = argv[1]; + settings.maxSilentTime = stoull(string(argv[2])); + settings.buildTimeout = stoull(string(argv[3])); + + currentLoad = getEnv("NIX_CURRENT_LOAD", "/run/nix/current-load"); + + std::shared_ptr<Store> sshStore; + AutoCloseFD bestSlotLock; + + auto machines = read_conf(); + string drvPath; + string hostName; + for (string line; getline(cin, line);) { + auto tokens = tokenizeString<std::vector<string>>(line); + auto sz = tokens.size(); + if (sz != 3 && sz != 4) { + throw Error(format("invalid build hook line %1%") % line); + } + auto amWilling = tokens[0] == "1"; + auto neededSystem = tokens[1]; + drvPath = tokens[2]; + auto requiredFeatures = sz == 3 ? + std::set<string>{} : + tokenizeString<std::set<string>>(tokens[3], ","); + auto canBuildLocally = amWilling && (neededSystem == localSystem); + + /* Error ignored here, will be caught later */ + mkdir(currentLoad.c_str(), 0777); + + while (true) { + bestSlotLock = -1; + AutoCloseFD lock = openLockFile(currentLoad + "/main-lock", true); + lockFile(lock.get(), ltWrite, true); + + bool rightType = false; + + machine * bestMachine = nullptr; + unsigned long long bestLoad = 0; + for (auto & m : machines) { + if (m.enabled && std::find(m.systemTypes.begin(), + m.systemTypes.end(), + neededSystem) != m.systemTypes.end() && + m.allSupported(requiredFeatures) && + m.mandatoryMet(requiredFeatures)) { + rightType = true; + AutoCloseFD free; + unsigned long long load = 0; + for (unsigned long long slot = 0; slot < m.maxJobs; ++slot) { + AutoCloseFD slotLock = openSlotLock(m, slot); + if (lockFile(slotLock.get(), ltWrite, false)) { + if (!free) { + free = std::move(slotLock); + } + } else { + ++load; + } + } + if (!free) { + continue; + } + bool best = false; + if (!bestSlotLock) { + best = true; + } else if (load / m.speedFactor < bestLoad / bestMachine->speedFactor) { + best = true; + } else if (load / m.speedFactor == bestLoad / bestMachine->speedFactor) { + if (m.speedFactor > bestMachine->speedFactor) { + best = true; + } else if (m.speedFactor == bestMachine->speedFactor) { + if (load < bestLoad) { + best = true; + } + } + } + if (best) { + bestLoad = load; + bestSlotLock = std::move(free); + bestMachine = &m; + } + } + } + + if (!bestSlotLock) { + if (rightType && !canBuildLocally) { + cerr << "# postpone\n"; + } else { + cerr << "# decline\n"; + } + break; + } + +#if __APPLE__ + futimes(bestSlotLock.get(), NULL); +#else + futimens(bestSlotLock.get(), NULL); +#endif + + lock = -1; + + try { + sshStore = openStore("ssh://" + bestMachine->hostName + "?key=" + bestMachine->sshKey); + hostName = bestMachine->hostName; + } catch (std::exception & e) { + cerr << e.what() << '\n'; + cerr << "unable to open SSH connection to ‘" << bestMachine->hostName << "’, trying other available machines...\n"; + bestMachine->enabled = false; + continue; + } + goto connected; + } + } +connected: + cerr << "# accept\n"; + string line; + if (!getline(cin, line)) { + throw Error("hook caller didn't send inputs"); + } + auto inputs = tokenizeString<std::list<string>>(line); + if (!getline(cin, line)) { + throw Error("hook caller didn't send outputs"); + } + auto outputs = tokenizeString<Strings>(line); + AutoCloseFD uploadLock = openLockFile(currentLoad + "/" + hostName + ".upload-lock", true); + auto old = signal(SIGALRM, handle_alarm); + alarm(15 * 60); + if (!lockFile(uploadLock.get(), ltWrite, true)) { + cerr << "somebody is hogging the upload lock for " << hostName << ", continuing...\n"; + } + alarm(0); + signal(SIGALRM, old); + copyPaths(store, ref<Store>(sshStore), inputs); + uploadLock = -1; + + cerr << "building ‘" << drvPath << "’ on ‘" << hostName << "’\n"; + sshStore->buildDerivation(drvPath, readDerivation(drvPath)); + + std::remove_if(outputs.begin(), outputs.end(), [=](const Path & path) { return store->isValidPath(path); }); + if (!outputs.empty()) { + setenv("NIX_HELD_LOCKS", concatStringsSep(" ", outputs).c_str(), 1); /* FIXME: ugly */ + copyPaths(ref<Store>(sshStore), store, outputs); + } + return; + }); +} diff --git a/src/build-remote/local.mk b/src/build-remote/local.mk new file mode 100644 index 000000000000..05b8cb451435 --- /dev/null +++ b/src/build-remote/local.mk @@ -0,0 +1,11 @@ +programs += build-remote + +build-remote_DIR := $(d) + +build-remote_INSTALL_DIR := $(libexecdir)/nix + +build-remote_LIBS = libmain libutil libformat libstore + +build-remote_SOURCES := $(d)/build-remote.cc + +build-remote_CXXFLAGS = -DSYSCONFDIR="\"$(sysconfdir)\"" -Isrc/nix-store diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 4398cc951da2..59623874c3f5 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -779,7 +779,7 @@ static void prim_readFile(EvalState & state, const Pos & pos, Value * * args, Va string s = readFile(state.checkSourcePath(path)); if (s.find((char) 0) != string::npos) throw Error(format("the contents of the file ‘%1%’ cannot be represented as a Nix string") % path); - mkString(v, s.c_str(), context); + mkString(v, s.c_str()); } diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc index 0c6e3fb76d64..12f083c7f794 100644 --- a/src/libmain/shared.cc +++ b/src/libmain/shared.cc @@ -24,12 +24,6 @@ namespace nix { -static void sigintHandler(int signo) -{ - _isInterrupted = 1; -} - - static bool gcWarning = true; void printGCWarning() @@ -120,19 +114,11 @@ void initNix() settings.processEnvironment(); settings.loadConfFile(); - /* Catch SIGINT. */ - struct sigaction act; - act.sa_handler = sigintHandler; - sigemptyset(&act.sa_mask); - act.sa_flags = 0; - if (sigaction(SIGINT, &act, 0)) - throw SysError("installing handler for SIGINT"); - if (sigaction(SIGTERM, &act, 0)) - throw SysError("installing handler for SIGTERM"); - if (sigaction(SIGHUP, &act, 0)) - throw SysError("installing handler for SIGHUP"); + startSignalHandlerThread(); /* Ignore SIGPIPE. */ + struct sigaction act; + sigemptyset(&act.sa_mask); act.sa_handler = SIG_IGN; act.sa_flags = 0; if (sigaction(SIGPIPE, &act, 0)) @@ -347,7 +333,7 @@ RunPager::~RunPager() if (pid != -1) { std::cout.flush(); close(STDOUT_FILENO); - pid.wait(true); + pid.wait(); } } catch (...) { ignoreException(); diff --git a/src/libstore/build.cc b/src/libstore/build.cc index c46b7cd641c4..7fc6ff0df0f8 100644 --- a/src/libstore/build.cc +++ b/src/libstore/build.cc @@ -257,7 +257,7 @@ public: LocalStore & store; - std::shared_ptr<HookInstance> hook; + std::unique_ptr<HookInstance> hook; Worker(LocalStore & store); ~Worker(); @@ -646,7 +646,7 @@ HookInstance::~HookInstance() { try { toHook.writeSide = -1; - pid.kill(true); + if (pid != -1) pid.kill(true); } catch (...) { ignoreException(); } @@ -751,7 +751,7 @@ private: Pipe userNamespaceSync; /* The build hook. */ - std::shared_ptr<HookInstance> hook; + std::unique_ptr<HookInstance> hook; /* Whether we're currently doing a chroot build. */ bool useChroot = false; @@ -960,7 +960,7 @@ void DerivationGoal::killChild() child. */ ::kill(-pid, SIGKILL); /* ignore the result */ buildUser.kill(); - pid.wait(true); + pid.wait(); } else pid.kill(); @@ -1416,11 +1416,9 @@ void DerivationGoal::buildDone() /* Since we got an EOF on the logger pipe, the builder is presumed to have terminated. In fact, the builder could also have - simply have closed its end of the pipe --- just don't do that - :-) */ - /* !!! this could block! security problem! solution: kill the - child */ - int status = hook ? hook->pid.wait(true) : pid.wait(true); + simply have closed its end of the pipe, so just to be sure, + kill it. */ + int status = hook ? hook->pid.kill(true) : pid.kill(true); debug(format("builder process for ‘%1%’ finished") % drvPath); @@ -1566,7 +1564,7 @@ HookReply DerivationGoal::tryBuildHook() if (!settings.useBuildHook || getEnv("NIX_BUILD_HOOK") == "" || !useDerivation) return rpDecline; if (!worker.hook) - worker.hook = std::make_shared<HookInstance>(); + worker.hook = std::make_unique<HookInstance>(); /* Tell the hook about system features (beyond the system type) required from the build machine. (The hook could parse the @@ -1601,8 +1599,7 @@ HookReply DerivationGoal::tryBuildHook() printMsg(lvlTalkative, format("using hook to build path(s) %1%") % showPaths(missingPaths)); - hook = worker.hook; - worker.hook.reset(); + hook = std::move(worker.hook); /* Tell the hook all the inputs that have to be copied to the remote system. This unfortunately has to contain the entire @@ -2113,6 +2110,8 @@ void DerivationGoal::startBuilder() result.startTime = time(0); /* Fork a child to build the package. */ + ProcessOptions options; + #if __linux__ if (useChroot) { /* Set up private namespaces for the build: @@ -2154,7 +2153,6 @@ void DerivationGoal::startBuilder() userNamespaceSync.create(); - ProcessOptions options; options.allowVfork = false; Pid helper = startProcess([&]() { @@ -2190,7 +2188,7 @@ void DerivationGoal::startBuilder() _exit(0); }, options); - if (helper.wait(true) != 0) + if (helper.wait() != 0) throw Error("unable to start build process"); userNamespaceSync.readSide = -1; @@ -2221,7 +2219,6 @@ void DerivationGoal::startBuilder() } else #endif { - ProcessOptions options; options.allowVfork = !buildUser.enabled() && !drv->isBuiltin(); pid = startProcess([&]() { runChild(); @@ -2295,12 +2292,8 @@ void DerivationGoal::runChild() outside of the namespace. Making a subtree private is local to the namespace, though, so setting MS_PRIVATE does not affect the outside world. */ - Strings mounts = tokenizeString<Strings>(readFile("/proc/self/mountinfo", true), "\n"); - for (auto & i : mounts) { - vector<string> fields = tokenizeString<vector<string> >(i, " "); - string fs = decodeOctalEscaped(fields.at(4)); - if (mount(0, fs.c_str(), 0, MS_PRIVATE, 0) == -1) - throw SysError(format("unable to make filesystem ‘%1%’ private") % fs); + if (mount(0, "/", 0, MS_REC|MS_PRIVATE, 0) == -1) { + throw SysError("unable to make ‘/’ private mount"); } /* Bind-mount chroot directory to itself, to treat it as a @@ -3707,7 +3700,8 @@ void Worker::waitForInput() auto after = steady_time_point::clock::now(); - /* Process all available file descriptors. */ + /* Process all available file descriptors. FIXME: this is + O(children * fds). */ decltype(children)::iterator i; for (auto j = children.begin(); j != children.end(); j = i) { i = std::next(j); diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc index d934bda38225..79526c594f71 100644 --- a/src/libstore/derivations.cc +++ b/src/libstore/derivations.cc @@ -88,9 +88,6 @@ Path writeDerivation(ref<Store> store, } -MakeError(FormatError, Error) - - /* Read string `s' from stream `str'. */ static void expect(std::istream & str, const string & s) { diff --git a/src/libstore/download.cc b/src/libstore/download.cc index 954044c2344f..8030e83b0dd5 100644 --- a/src/libstore/download.cc +++ b/src/libstore/download.cc @@ -172,6 +172,13 @@ struct CurlDownloader : public Downloader return ((DownloadItem *) userp)->progressCallback(dltotal, dlnow); } + static int debugCallback(CURL * handle, curl_infotype type, char * data, size_t size, void * userptr) + { + if (type == CURLINFO_TEXT) + vomit("curl: %s", chomp(std::string(data, size))); + return 0; + } + void init() { // FIXME: handle parallel downloads. @@ -184,6 +191,12 @@ struct CurlDownloader : public Downloader if (!req) req = curl_easy_init(); curl_easy_reset(req); + + if (verbosity >= lvlVomit) { + curl_easy_setopt(req, CURLOPT_VERBOSE, 1); + curl_easy_setopt(req, CURLOPT_DEBUGFUNCTION, DownloadItem::debugCallback); + } + curl_easy_setopt(req, CURLOPT_URL, request.uri.c_str()); curl_easy_setopt(req, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(req, CURLOPT_NOSIGNAL, 1); @@ -324,20 +337,30 @@ struct CurlDownloader : public Downloader ~CurlDownloader() { + stopWorkerThread(); + + workerThread.join(); + + if (curlm) curl_multi_cleanup(curlm); + } + + void stopWorkerThread() + { /* Signal the worker thread to exit. */ { auto state(state_.lock()); state->quit = true; } - writeFull(wakeupPipe.writeSide.get(), " "); - - workerThread.join(); - - if (curlm) curl_multi_cleanup(curlm); + writeFull(wakeupPipe.writeSide.get(), " ", false); } void workerThreadMain() { + /* Cause this thread to be notified on SIGINT. */ + auto callback = createInterruptCallback([&]() { + stopWorkerThread(); + }); + std::map<CURL *, std::shared_ptr<DownloadItem>> items; bool quit = false; @@ -377,7 +400,7 @@ struct CurlDownloader : public Downloader nextWakeup != std::chrono::steady_clock::time_point() ? std::max(0, (int) std::chrono::duration_cast<std::chrono::milliseconds>(nextWakeup - std::chrono::steady_clock::now()).count()) : 1000000000; - //printMsg(lvlVomit, format("download thread waiting for %d ms") % sleepTimeMs); + vomit("download thread waiting for %d ms", sleepTimeMs); mc = curl_multi_wait(curlm, extraFDs, 1, sleepTimeMs, &numfds); if (mc != CURLM_OK) throw nix::Error(format("unexpected error from curl_multi_wait(): %s") % curl_multi_strerror(mc)); diff --git a/src/libstore/gc.cc b/src/libstore/gc.cc index f8c4a07238c7..8e90913cc3f1 100644 --- a/src/libstore/gc.cc +++ b/src/libstore/gc.cc @@ -379,7 +379,7 @@ void LocalStore::findRuntimeRoots(PathSet & roots) auto digitsRegex = std::regex(R"(^\d+$)"); auto mapRegex = std::regex(R"(^\s*\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(/\S+)\s*$)"); auto storePathRegex = std::regex(quoteRegexChars(storeDir) + R"(/[0-9a-z]+[0-9a-zA-Z\+\-\._\?=]*)"); - while (errno = 0, ent = readdir(procDir)) { + while (errno = 0, ent = readdir(procDir.get())) { checkInterrupt(); if (std::regex_match(ent->d_name, digitsRegex)) { readProcLink((format("/proc/%1%/exe") % ent->d_name).str(), paths); @@ -393,14 +393,14 @@ void LocalStore::findRuntimeRoots(PathSet & roots) throw SysError(format("opening %1%") % fdStr); } struct dirent * fd_ent; - while (errno = 0, fd_ent = readdir(fdDir)) { + while (errno = 0, fd_ent = readdir(fdDir.get())) { if (fd_ent->d_name[0] != '.') { readProcLink((format("%1%/%2%") % fdStr % fd_ent->d_name).str(), paths); } } if (errno) throw SysError(format("iterating /proc/%1%/fd") % ent->d_name); - fdDir.close(); + fdDir.reset(); auto mapLines = tokenizeString<std::vector<string>>(readFile((format("/proc/%1%/maps") % ent->d_name).str(), true), "\n"); @@ -651,13 +651,13 @@ void LocalStore::tryToDelete(GCState & state, const Path & path) the link count. */ void LocalStore::removeUnusedLinks(const GCState & state) { - AutoCloseDir dir = opendir(linksDir.c_str()); + AutoCloseDir dir(opendir(linksDir.c_str())); if (!dir) throw SysError(format("opening directory ‘%1%’") % linksDir); long long actualSize = 0, unsharedSize = 0; struct dirent * dirent; - while (errno = 0, dirent = readdir(dir)) { + while (errno = 0, dirent = readdir(dir.get())) { checkInterrupt(); string name = dirent->d_name; if (name == "." || name == "..") continue; @@ -776,7 +776,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) try { - AutoCloseDir dir = opendir(realStoreDir.c_str()); + AutoCloseDir dir(opendir(realStoreDir.c_str())); if (!dir) throw SysError(format("opening directory ‘%1%’") % realStoreDir); /* Read the store and immediately delete all paths that @@ -787,7 +787,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) can start faster. */ Paths entries; struct dirent * dirent; - while (errno = 0, dirent = readdir(dir)) { + while (errno = 0, dirent = readdir(dir.get())) { checkInterrupt(); string name = dirent->d_name; if (name == "." || name == "..") continue; @@ -798,7 +798,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) tryToDelete(state, path); } - dir.close(); + dir.reset(); /* Now delete the unreachable valid paths. Randomise the order in which we delete entries to make the collector diff --git a/src/libstore/optimise-store.cc b/src/libstore/optimise-store.cc index 454c8b49d84b..b71c7e905ff1 100644 --- a/src/libstore/optimise-store.cc +++ b/src/libstore/optimise-store.cc @@ -47,11 +47,11 @@ LocalStore::InodeHash LocalStore::loadInodeHash() debug("loading hash inodes in memory"); InodeHash inodeHash; - AutoCloseDir dir = opendir(linksDir.c_str()); + AutoCloseDir dir(opendir(linksDir.c_str())); if (!dir) throw SysError(format("opening directory ‘%1%’") % linksDir); struct dirent * dirent; - while (errno = 0, dirent = readdir(dir)) { /* sic */ + while (errno = 0, dirent = readdir(dir.get())) { /* sic */ checkInterrupt(); // We don't care if we hit non-hash files, anything goes inodeHash.insert(dirent->d_ino); @@ -68,11 +68,11 @@ Strings LocalStore::readDirectoryIgnoringInodes(const Path & path, const InodeHa { Strings names; - AutoCloseDir dir = opendir(path.c_str()); + AutoCloseDir dir(opendir(path.c_str())); if (!dir) throw SysError(format("opening directory ‘%1%’") % path); struct dirent * dirent; - while (errno = 0, dirent = readdir(dir)) { /* sic */ + while (errno = 0, dirent = readdir(dir.get())) { /* sic */ checkInterrupt(); if (inodeHash.count(dirent->d_ino)) { diff --git a/src/libstore/pathlocks.cc b/src/libstore/pathlocks.cc index fecd636877af..620c9a6b752d 100644 --- a/src/libstore/pathlocks.cc +++ b/src/libstore/pathlocks.cc @@ -54,6 +54,8 @@ bool lockFile(int fd, LockType lockType, bool wait) checkInterrupt(); if (errno != EINTR) throw SysError(format("acquiring/releasing lock")); + else + return false; } } else { while (fcntl(fd, F_SETLK, &lock) != 0) { diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index 77faa2f801f1..816d95ba6075 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -599,9 +599,8 @@ void RemoteStore::Connection::processStderr(Sink * sink, Source * source) else if (msg == STDERR_READ) { if (!source) throw Error("no source"); size_t len = readInt(from); - unsigned char * buf = new unsigned char[len]; - AutoDeleteArray<unsigned char> d(buf); - writeString(buf, source->read(buf, len), to); + auto buf = std::make_unique<unsigned char[]>(len); + writeString(buf.get(), source->read(buf.get(), len), to); to.flush(); } else diff --git a/src/libstore/ssh-store.cc b/src/libstore/ssh-store.cc index 5166485226d9..3d01594009a0 100644 --- a/src/libstore/ssh-store.cc +++ b/src/libstore/ssh-store.cc @@ -49,6 +49,8 @@ SSHStore::SSHStore(string uri, const Params & params, size_t maxConnections) , uri(std::move(uri)) , key(get(params, "ssh-key", "")) { + /* open a connection and perform the handshake to verify all is well */ + connections->get(); } string SSHStore::getUri() diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 37a2d45fefe0..8fdd62771552 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -3,6 +3,7 @@ #include "store-api.hh" #include "util.hh" #include "nar-info-disk-cache.hh" +#include "thread-pool.hh" #include <future> @@ -698,4 +699,36 @@ std::list<ref<Store>> getDefaultSubstituters() } +void copyPaths(ref<Store> from, ref<Store> to, const Paths & storePaths) +{ + std::string copiedLabel = "copied"; + + logger->setExpected(copiedLabel, storePaths.size()); + + ThreadPool pool; + + processGraph<Path>(pool, + PathSet(storePaths.begin(), storePaths.end()), + + [&](const Path & storePath) { + return from->queryPathInfo(storePath)->references; + }, + + [&](const Path & storePath) { + checkInterrupt(); + + if (!to->isValidPath(storePath)) { + Activity act(*logger, lvlInfo, format("copying ‘%s’...") % storePath); + + copyStorePath(from, to, storePath); + + logger->incProgress(copiedLabel); + } else + logger->incExpected(copiedLabel, -1); + }); + + pool.process(); +} + + } diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index 789526cc2b70..ec3bf5a6fd83 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -625,6 +625,8 @@ void removeTempRoots(); ref<Store> openStore(const std::string & uri = getEnv("NIX_REMOTE")); +void copyPaths(ref<Store> from, ref<Store> to, const Paths & storePaths); + enum StoreType { tDaemon, tLocal, diff --git a/src/libutil/serialise.cc b/src/libutil/serialise.cc index 24c6d107359e..a68f7a0fa8ee 100644 --- a/src/libutil/serialise.cc +++ b/src/libutil/serialise.cc @@ -3,6 +3,7 @@ #include <cstring> #include <cerrno> +#include <memory> namespace nix { @@ -236,11 +237,10 @@ size_t readString(unsigned char * buf, size_t max, Source & source) string readString(Source & source) { size_t len = readInt(source); - unsigned char * buf = new unsigned char[len]; - AutoDeleteArray<unsigned char> d(buf); - source(buf, len); + auto buf = std::make_unique<unsigned char[]>(len); + source(buf.get(), len); readPadding(len, source); - return string((char *) buf, len); + return string((char *) buf.get(), len); } Source & operator >> (Source & in, string & s) diff --git a/src/libutil/util.cc b/src/libutil/util.cc index ce16cc30a5c7..52608ac2a016 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -2,14 +2,16 @@ #include "util.hh" #include "affinity.hh" +#include "sync.hh" -#include <iostream> +#include <cctype> #include <cerrno> #include <cstdio> #include <cstdlib> -#include <sstream> #include <cstring> -#include <cctype> +#include <iostream> +#include <sstream> +#include <thread> #include <sys/wait.h> #include <unistd.h> @@ -234,11 +236,11 @@ DirEntries readDirectory(const Path & path) DirEntries entries; entries.reserve(64); - AutoCloseDir dir = opendir(path.c_str()); + AutoCloseDir dir(opendir(path.c_str())); if (!dir) throw SysError(format("opening directory ‘%1%’") % path); struct dirent * dirent; - while (errno = 0, dirent = readdir(dir)) { /* sic */ + while (errno = 0, dirent = readdir(dir.get())) { /* sic */ checkInterrupt(); string name = dirent->d_name; if (name == "." || name == "..") continue; @@ -272,11 +274,10 @@ string readFile(int fd) if (fstat(fd, &st) == -1) throw SysError("statting file"); - unsigned char * buf = new unsigned char[st.st_size]; - AutoDeleteArray<unsigned char> d(buf); - readFull(fd, buf, st.st_size); + auto buf = std::make_unique<unsigned char[]>(st.st_size); + readFull(fd, buf.get(), st.st_size); - return string((char *) buf, st.st_size); + return string((char *) buf.get(), st.st_size); } @@ -646,69 +647,26 @@ void Pipe::create() ////////////////////////////////////////////////////////////////////// -AutoCloseDir::AutoCloseDir() -{ - dir = 0; -} - - -AutoCloseDir::AutoCloseDir(DIR * dir) -{ - this->dir = dir; -} - - -AutoCloseDir::~AutoCloseDir() -{ - close(); -} - - -void AutoCloseDir::operator =(DIR * dir) -{ - this->dir = dir; -} - - -AutoCloseDir::operator DIR *() -{ - return dir; -} - - -void AutoCloseDir::close() -{ - if (dir) { - closedir(dir); - dir = 0; - } -} - - -////////////////////////////////////////////////////////////////////// - - Pid::Pid() - : pid(-1), separatePG(false), killSignal(SIGKILL) { } Pid::Pid(pid_t pid) - : pid(pid), separatePG(false), killSignal(SIGKILL) + : pid(pid) { } Pid::~Pid() { - kill(); + if (pid != -1) kill(); } void Pid::operator =(pid_t pid) { - if (this->pid != pid) kill(); + if (this->pid != -1 && this->pid != pid) kill(); this->pid = pid; killSignal = SIGKILL; // reset signal to default } @@ -720,9 +678,9 @@ Pid::operator pid_t() } -void Pid::kill(bool quiet) +int Pid::kill(bool quiet) { - if (pid == -1 || pid == 0) return; + assert(pid != -1); if (!quiet) printError(format("killing process %1%") % pid); @@ -733,32 +691,20 @@ void Pid::kill(bool quiet) if (::kill(separatePG ? -pid : pid, killSignal) != 0) printError((SysError(format("killing process %1%") % pid).msg())); - /* Wait until the child dies, disregarding the exit status. */ - int status; - while (waitpid(pid, &status, 0) == -1) { - checkInterrupt(); - if (errno != EINTR) { - printError( - (SysError(format("waiting for process %1%") % pid).msg())); - break; - } - } - - pid = -1; + return wait(); } -int Pid::wait(bool block) +int Pid::wait() { assert(pid != -1); while (1) { int status; - int res = waitpid(pid, &status, block ? 0 : WNOHANG); + int res = waitpid(pid, &status, 0); if (res == pid) { pid = -1; return status; } - if (res == 0 && !block) return -1; if (errno != EINTR) throw SysError("cannot get child exit status"); checkInterrupt(); @@ -823,7 +769,7 @@ void killUser(uid_t uid) _exit(0); }, options); - int status = pid.wait(true); + int status = pid.wait(); if (status != 0) throw Error(format("cannot kill processes for uid ‘%1%’: %2%") % uid % statusToString(status)); @@ -934,7 +880,7 @@ string runProgram(Path program, bool searchPath, const Strings & args, string result = drainFD(out.readSide.get()); /* Wait for the child to finish. */ - int status = pid.wait(true); + int status = pid.wait(); if (!statusOk(status)) throw ExecError(status, format("program ‘%1%’ %2%") % program % statusToString(status)); @@ -976,7 +922,7 @@ void restoreSIGPIPE() ////////////////////////////////////////////////////////////////////// -volatile sig_atomic_t _isInterrupted = 0; +bool _isInterrupted = false; thread_local bool interruptThrown = false; @@ -1243,4 +1189,64 @@ void callFailure(const std::function<void(std::exception_ptr exc)> & failure, st } +static Sync<std::list<std::function<void()>>> _interruptCallbacks; + +static void signalHandlerThread(sigset_t set) +{ + while (true) { + int signal = 0; + sigwait(&set, &signal); + + if (signal == SIGINT || signal == SIGTERM || signal == SIGHUP) { + _isInterrupted = 1; + + { + auto interruptCallbacks(_interruptCallbacks.lock()); + for (auto & callback : *interruptCallbacks) { + try { + callback(); + } catch (...) { + ignoreException(); + } + } + } + } + } +} + +void startSignalHandlerThread() +{ + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGINT); + sigaddset(&set, SIGTERM); + sigaddset(&set, SIGHUP); + if (pthread_sigmask(SIG_BLOCK, &set, nullptr)) + throw SysError("blocking signals"); + + std::thread(signalHandlerThread, set).detach(); +} + +/* RAII helper to automatically deregister a callback. */ +struct InterruptCallbackImpl : InterruptCallback +{ + std::list<std::function<void()>>::iterator it; + ~InterruptCallbackImpl() override + { + _interruptCallbacks.lock()->erase(it); + } +}; + +std::unique_ptr<InterruptCallback> createInterruptCallback(std::function<void()> callback) +{ + auto interruptCallbacks(_interruptCallbacks.lock()); + interruptCallbacks->push_back(callback); + + auto res = std::make_unique<InterruptCallbackImpl>(); + res->it = interruptCallbacks->end(); + res->it--; + + return std::unique_ptr<InterruptCallback>(res.release()); +} + } diff --git a/src/libutil/util.hh b/src/libutil/util.hh index 50b96f7ed92c..b68d48582b34 100644 --- a/src/libutil/util.hh +++ b/src/libutil/util.hh @@ -139,18 +139,6 @@ string drainFD(int fd); /* Automatic cleanup of resources. */ -template <class T> -struct AutoDeleteArray -{ - T * p; - AutoDeleteArray(T * p) : p(p) { } - ~AutoDeleteArray() - { - delete [] p; - } -}; - - class AutoDelete { Path path; @@ -192,32 +180,30 @@ public: }; -class AutoCloseDir +struct DIRDeleter { - DIR * dir; -public: - AutoCloseDir(); - AutoCloseDir(DIR * dir); - ~AutoCloseDir(); - void operator =(DIR * dir); - operator DIR *(); - void close(); + void operator()(DIR * dir) const { + closedir(dir); + } }; +typedef std::unique_ptr<DIR, DIRDeleter> AutoCloseDir; + class Pid { - pid_t pid; - bool separatePG; - int killSignal; + pid_t pid = -1; + bool separatePG = false; + int killSignal = SIGKILL; public: Pid(); Pid(pid_t pid); ~Pid(); void operator =(pid_t pid); operator pid_t(); - void kill(bool quiet = false); - int wait(bool block); + int kill(bool quiet = false); + int wait(); + void setSeparatePG(bool separatePG); void setKillSignal(int signal); pid_t release(); @@ -233,11 +219,10 @@ void killUser(uid_t uid); pid to the caller. */ struct ProcessOptions { - string errorPrefix; - bool dieWithParent; - bool runExitHandlers; - bool allowVfork; - ProcessOptions() : errorPrefix("error: "), dieWithParent(true), runExitHandlers(false), allowVfork(true) { }; + string errorPrefix = "error: "; + bool dieWithParent = true; + bool runExitHandlers = false; + bool allowVfork = true; }; pid_t startProcess(std::function<void()> fun, const ProcessOptions & options = ProcessOptions()); @@ -278,7 +263,7 @@ void restoreSIGPIPE(); /* User interruption. */ -extern volatile sig_atomic_t _isInterrupted; +extern bool _isInterrupted; extern thread_local bool interruptThrown; @@ -292,6 +277,9 @@ void inline checkInterrupt() MakeError(Interrupted, BaseError) +MakeError(FormatError, Error) + + /* String tokenizer. */ template<class C> C tokenizeString(const string & s, const string & separators = " \t\n\r"); @@ -431,4 +419,19 @@ void callSuccess( } +/* Start a thread that handles various signals. Also block those signals + on the current thread (and thus any threads created by it). */ +void startSignalHandlerThread(); + +struct InterruptCallback +{ + virtual ~InterruptCallback() { }; +}; + +/* Register a function that gets called on SIGINT (in a non-signal + context). */ +std::unique_ptr<InterruptCallback> createInterruptCallback( + std::function<void()> callback); + + } diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc index c67148728722..3eb2d2c0b7a9 100755 --- a/src/nix-build/nix-build.cc +++ b/src/nix-build/nix-build.cc @@ -81,7 +81,8 @@ int main(int argc, char ** argv) auto pure = false; auto fromArgs = false; auto packages = false; - auto interactive = true; + // Same condition as bash uses for interactive shells + auto interactive = isatty(STDIN_FILENO) && isatty(STDERR_FILENO); Strings instArgs; Strings buildArgs; @@ -135,15 +136,11 @@ int main(int argc, char ** argv) if (arg == "--help") { deletePath(tmpDir); - tmpDir.cancel(); - execlp("man", "man", myName, NULL); - throw SysError("executing man"); + showManPage(myName); } - else if (arg == "--version") { - std::cout << myName << " (Nix) " << nixVersion << '\n'; - return; - } + else if (arg == "--version") + printVersion(myName); else if (arg == "--add-drv-link") { drvLink = "./derivation"; @@ -449,10 +446,13 @@ int main(int argc, char ** argv) ? Strings{"bash", "--rcfile", rcfile} : Strings{"bash", rcfile}; - environ = stringsToCharPtrs(envStrs).data(); + auto envPtrs = stringsToCharPtrs(envStrs); + + environ = envPtrs.data(); + + auto argPtrs = stringsToCharPtrs(args); - execvp(getEnv("NIX_BUILD_SHELL", "bash").c_str(), - stringsToCharPtrs(args).data()); + execvp(getEnv("NIX_BUILD_SHELL", "bash").c_str(), argPtrs.data()); throw SysError("executing shell"); } diff --git a/src/nix/copy.cc b/src/nix/copy.cc index e8317dc393fd..976b0d3e0b81 100644 --- a/src/nix/copy.cc +++ b/src/nix/copy.cc @@ -46,33 +46,7 @@ struct CmdCopy : StorePathsCommand ref<Store> srcStore = srcUri.empty() ? store : openStore(srcUri); ref<Store> dstStore = dstUri.empty() ? store : openStore(dstUri); - std::string copiedLabel = "copied"; - - logger->setExpected(copiedLabel, storePaths.size()); - - ThreadPool pool; - - processGraph<Path>(pool, - PathSet(storePaths.begin(), storePaths.end()), - - [&](const Path & storePath) { - return srcStore->queryPathInfo(storePath)->references; - }, - - [&](const Path & storePath) { - checkInterrupt(); - - if (!dstStore->isValidPath(storePath)) { - Activity act(*logger, lvlInfo, format("copying ‘%s’...") % storePath); - - copyStorePath(srcStore, dstStore, storePath); - - logger->incProgress(copiedLabel); - } else - logger->incExpected(copiedLabel, -1); - }); - - pool.process(); + copyPaths(srcStore, dstStore, storePaths); } }; diff --git a/tests/remote-builds.nix b/tests/remote-builds.nix index 34276e7d6981..d14d6ff7f056 100644 --- a/tests/remote-builds.nix +++ b/tests/remote-builds.nix @@ -43,6 +43,7 @@ in { config, pkgs, ... }: { nix.maxJobs = 0; # force remote building nix.distributedBuilds = true; + nix.envVars = pkgs.lib.mkAfter { NIX_BUILD_HOOK = "${nix}/libexec/nix/build-remote"; }; nix.buildMachines = [ { hostName = "slave1"; sshUser = "root"; diff --git a/tests/timeout.builder.sh b/tests/timeout.builder.sh deleted file mode 100644 index 3fbdd57946ad..000000000000 --- a/tests/timeout.builder.sh +++ /dev/null @@ -1,2 +0,0 @@ -echo "‘timeout’ builder entering an infinite loop" -while true ; do echo -n .; done diff --git a/tests/timeout.nix b/tests/timeout.nix index b41368bb38e2..540fba934ff6 100644 --- a/tests/timeout.nix +++ b/tests/timeout.nix @@ -1,6 +1,28 @@ with import ./config.nix; -mkDerivation { - name = "timeout"; - builder = ./timeout.builder.sh; +{ + + infiniteLoop = mkDerivation { + name = "timeout"; + buildCommand = '' + echo "‘timeout’ builder entering an infinite loop" + while true ; do echo -n .; done + ''; + }; + + silent = mkDerivation { + name = "silent"; + buildCommand = '' + sleep 60 + ''; + }; + + closeLog = mkDerivation { + name = "silent"; + buildCommand = '' + exec > /dev/null 2>&1 + sleep 1000000000 + ''; + }; + } diff --git a/tests/timeout.sh b/tests/timeout.sh index 2ebd06b9330c..ce1ae7d674a1 100644 --- a/tests/timeout.sh +++ b/tests/timeout.sh @@ -3,7 +3,7 @@ source common.sh failed=0 -messages="`nix-build -Q timeout.nix --timeout 2 2>&1 || failed=1`" +messages="`nix-build -Q timeout.nix -A infiniteLoop --timeout 2 2>&1 || failed=1`" if [ $failed -ne 0 ]; then echo "error: ‘nix-store’ succeeded; should have timed out" exit 1 @@ -15,7 +15,17 @@ if ! echo "$messages" | grep -q "timed out"; then exit 1 fi -if nix-build -Q timeout.nix --option build-max-log-size 100; then +if nix-build -Q timeout.nix -A infiniteLoop --option build-max-log-size 100; then + echo "build should have failed" + exit 1 +fi + +if nix-build timeout.nix -A silent --max-silent-time 2; then + echo "build should have failed" + exit 1 +fi + +if nix-build timeout.nix -A closeLog; then echo "build should have failed" exit 1 fi |