mirror of
https://github.com/NixOS/nix
synced 2025-06-29 06:21:14 +02:00
Start factoring out Unix assumptions
This splits files and adds new identifiers in preperation for supporting windows, but no Windows-specific code is actually added yet. Co-authored-by: Robert Hensing <roberth@users.noreply.github.com>
This commit is contained in:
parent
852391765d
commit
02fa20622f
34 changed files with 352 additions and 296 deletions
21
src/libutil/unix/environment-variables.cc
Normal file
21
src/libutil/unix/environment-variables.cc
Normal file
|
@ -0,0 +1,21 @@
|
|||
#include "util.hh"
|
||||
#include "environment-variables.hh"
|
||||
|
||||
extern char * * environ __attribute__((weak));
|
||||
|
||||
namespace nix {
|
||||
|
||||
void clearEnv()
|
||||
{
|
||||
for (auto & name : getEnv())
|
||||
unsetenv(name.first.c_str());
|
||||
}
|
||||
|
||||
void replaceEnv(const std::map<std::string, std::string> & newEnv)
|
||||
{
|
||||
clearEnv();
|
||||
for (auto & newEnvVar : newEnv)
|
||||
setenv(newEnvVar.first.c_str(), newEnvVar.second.c_str(), 1);
|
||||
}
|
||||
|
||||
}
|
155
src/libutil/unix/file-descriptor.cc
Normal file
155
src/libutil/unix/file-descriptor.cc
Normal file
|
@ -0,0 +1,155 @@
|
|||
#include "file-system.hh"
|
||||
#include "signals.hh"
|
||||
#include "finally.hh"
|
||||
#include "serialise.hh"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace nix {
|
||||
|
||||
std::string readFile(int fd)
|
||||
{
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) == -1)
|
||||
throw SysError("statting file");
|
||||
|
||||
return drainFD(fd, true, st.st_size);
|
||||
}
|
||||
|
||||
|
||||
void readFull(int fd, char * buf, size_t count)
|
||||
{
|
||||
while (count) {
|
||||
checkInterrupt();
|
||||
ssize_t res = read(fd, buf, count);
|
||||
if (res == -1) {
|
||||
if (errno == EINTR) continue;
|
||||
throw SysError("reading from file");
|
||||
}
|
||||
if (res == 0) throw EndOfFile("unexpected end-of-file");
|
||||
count -= res;
|
||||
buf += res;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void writeFull(int fd, std::string_view s, bool allowInterrupts)
|
||||
{
|
||||
while (!s.empty()) {
|
||||
if (allowInterrupts) checkInterrupt();
|
||||
ssize_t res = write(fd, s.data(), s.size());
|
||||
if (res == -1 && errno != EINTR)
|
||||
throw SysError("writing to file");
|
||||
if (res > 0)
|
||||
s.remove_prefix(res);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::string readLine(int fd)
|
||||
{
|
||||
std::string s;
|
||||
while (1) {
|
||||
checkInterrupt();
|
||||
char ch;
|
||||
// FIXME: inefficient
|
||||
ssize_t rd = read(fd, &ch, 1);
|
||||
if (rd == -1) {
|
||||
if (errno != EINTR)
|
||||
throw SysError("reading a line");
|
||||
} else if (rd == 0)
|
||||
throw EndOfFile("unexpected EOF reading a line");
|
||||
else {
|
||||
if (ch == '\n') return s;
|
||||
s += ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void drainFD(int fd, Sink & sink, bool block)
|
||||
{
|
||||
// silence GCC maybe-uninitialized warning in finally
|
||||
int saved = 0;
|
||||
|
||||
if (!block) {
|
||||
saved = fcntl(fd, F_GETFL);
|
||||
if (fcntl(fd, F_SETFL, saved | O_NONBLOCK) == -1)
|
||||
throw SysError("making file descriptor non-blocking");
|
||||
}
|
||||
|
||||
Finally finally([&]() {
|
||||
if (!block) {
|
||||
if (fcntl(fd, F_SETFL, saved) == -1)
|
||||
throw SysError("making file descriptor blocking");
|
||||
}
|
||||
});
|
||||
|
||||
std::vector<unsigned char> buf(64 * 1024);
|
||||
while (1) {
|
||||
checkInterrupt();
|
||||
ssize_t rd = read(fd, buf.data(), buf.size());
|
||||
if (rd == -1) {
|
||||
if (!block && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
break;
|
||||
if (errno != EINTR)
|
||||
throw SysError("reading from file");
|
||||
}
|
||||
else if (rd == 0) break;
|
||||
else sink({reinterpret_cast<char *>(buf.data()), (size_t) rd});
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
void Pipe::create()
|
||||
{
|
||||
int fds[2];
|
||||
#if HAVE_PIPE2
|
||||
if (pipe2(fds, O_CLOEXEC) != 0) throw SysError("creating pipe");
|
||||
#else
|
||||
if (pipe(fds) != 0) throw SysError("creating pipe");
|
||||
closeOnExec(fds[0]);
|
||||
closeOnExec(fds[1]);
|
||||
#endif
|
||||
readSide = fds[0];
|
||||
writeSide = fds[1];
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
void closeMostFDs(const std::set<int> & exceptions)
|
||||
{
|
||||
#if __linux__
|
||||
try {
|
||||
for (auto & s : readDirectory("/proc/self/fd")) {
|
||||
auto fd = std::stoi(s.name);
|
||||
if (!exceptions.count(fd)) {
|
||||
debug("closing leaked FD %d", fd);
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
return;
|
||||
} catch (SysError &) {
|
||||
}
|
||||
#endif
|
||||
|
||||
int maxFD = 0;
|
||||
maxFD = sysconf(_SC_OPEN_MAX);
|
||||
for (int fd = 0; fd < maxFD; ++fd)
|
||||
if (!exceptions.count(fd))
|
||||
close(fd); /* ignore result */
|
||||
}
|
||||
|
||||
|
||||
void closeOnExec(int fd)
|
||||
{
|
||||
int prev;
|
||||
if ((prev = fcntl(fd, F_GETFD, 0)) == -1 ||
|
||||
fcntl(fd, F_SETFD, prev | FD_CLOEXEC) == -1)
|
||||
throw SysError("setting close-on-exec flag");
|
||||
}
|
||||
|
||||
}
|
72
src/libutil/unix/monitor-fd.hh
Normal file
72
src/libutil/unix/monitor-fd.hh
Normal file
|
@ -0,0 +1,72 @@
|
|||
#pragma once
|
||||
///@file
|
||||
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <poll.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
|
||||
#include "signals.hh"
|
||||
|
||||
namespace nix {
|
||||
|
||||
|
||||
class MonitorFdHup
|
||||
{
|
||||
private:
|
||||
std::thread thread;
|
||||
|
||||
public:
|
||||
MonitorFdHup(int fd)
|
||||
{
|
||||
thread = std::thread([fd]() {
|
||||
while (true) {
|
||||
/* Wait indefinitely until a POLLHUP occurs. */
|
||||
struct pollfd fds[1];
|
||||
fds[0].fd = fd;
|
||||
/* Polling for no specific events (i.e. just waiting
|
||||
for an error/hangup) doesn't work on macOS
|
||||
anymore. So wait for read events and ignore
|
||||
them. */
|
||||
fds[0].events =
|
||||
#ifdef __APPLE__
|
||||
POLLRDNORM
|
||||
#else
|
||||
0
|
||||
#endif
|
||||
;
|
||||
auto count = poll(fds, 1, -1);
|
||||
if (count == -1) abort(); // can't happen
|
||||
/* This shouldn't happen, but can on macOS due to a bug.
|
||||
See rdar://37550628.
|
||||
|
||||
This may eventually need a delay or further
|
||||
coordination with the main thread if spinning proves
|
||||
too harmful.
|
||||
*/
|
||||
if (count == 0) continue;
|
||||
if (fds[0].revents & POLLHUP) {
|
||||
triggerInterrupt();
|
||||
break;
|
||||
}
|
||||
/* This will only happen on macOS. We sleep a bit to
|
||||
avoid waking up too often if the client is sending
|
||||
input. */
|
||||
sleep(1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
~MonitorFdHup()
|
||||
{
|
||||
pthread_cancel(thread.native_handle());
|
||||
thread.join();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
422
src/libutil/unix/processes.cc
Normal file
422
src/libutil/unix/processes.cc
Normal file
|
@ -0,0 +1,422 @@
|
|||
#include "current-process.hh"
|
||||
#include "environment-variables.hh"
|
||||
#include "signals.hh"
|
||||
#include "processes.hh"
|
||||
#include "finally.hh"
|
||||
#include "serialise.hh"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
#include <grp.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __APPLE__
|
||||
# include <sys/syscall.h>
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
# include <sys/prctl.h>
|
||||
# include <sys/mman.h>
|
||||
#endif
|
||||
|
||||
|
||||
namespace nix {
|
||||
|
||||
Pid::Pid()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
Pid::Pid(pid_t pid)
|
||||
: pid(pid)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
Pid::~Pid()
|
||||
{
|
||||
if (pid != -1) kill();
|
||||
}
|
||||
|
||||
|
||||
void Pid::operator =(pid_t pid)
|
||||
{
|
||||
if (this->pid != -1 && this->pid != pid) kill();
|
||||
this->pid = pid;
|
||||
killSignal = SIGKILL; // reset signal to default
|
||||
}
|
||||
|
||||
|
||||
Pid::operator pid_t()
|
||||
{
|
||||
return pid;
|
||||
}
|
||||
|
||||
|
||||
int Pid::kill()
|
||||
{
|
||||
assert(pid != -1);
|
||||
|
||||
debug("killing process %1%", pid);
|
||||
|
||||
/* 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). */
|
||||
if (::kill(separatePG ? -pid : pid, killSignal) != 0) {
|
||||
/* On BSDs, killing a process group will return EPERM if all
|
||||
processes in the group are zombies (or something like
|
||||
that). So try to detect and ignore that situation. */
|
||||
#if __FreeBSD__ || __APPLE__
|
||||
if (errno != EPERM || ::kill(pid, 0) != 0)
|
||||
#endif
|
||||
logError(SysError("killing process %d", pid).info());
|
||||
}
|
||||
|
||||
return wait();
|
||||
}
|
||||
|
||||
|
||||
int Pid::wait()
|
||||
{
|
||||
assert(pid != -1);
|
||||
while (1) {
|
||||
int status;
|
||||
int res = waitpid(pid, &status, 0);
|
||||
if (res == pid) {
|
||||
pid = -1;
|
||||
return status;
|
||||
}
|
||||
if (errno != EINTR)
|
||||
throw SysError("cannot get exit status of PID %d", pid);
|
||||
checkInterrupt();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Pid::setSeparatePG(bool separatePG)
|
||||
{
|
||||
this->separatePG = separatePG;
|
||||
}
|
||||
|
||||
|
||||
void Pid::setKillSignal(int signal)
|
||||
{
|
||||
this->killSignal = signal;
|
||||
}
|
||||
|
||||
|
||||
pid_t Pid::release()
|
||||
{
|
||||
pid_t p = pid;
|
||||
pid = -1;
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
void killUser(uid_t uid)
|
||||
{
|
||||
debug("killing all processes running under uid '%1%'", uid);
|
||||
|
||||
assert(uid != 0); /* just to be safe... */
|
||||
|
||||
/* The system call kill(-1, sig) sends the signal `sig' to all
|
||||
users to which the current process can send signals. So we
|
||||
fork a process, switch to uid, and send a mass kill. */
|
||||
|
||||
Pid pid = startProcess([&] {
|
||||
|
||||
if (setuid(uid) == -1)
|
||||
throw SysError("setting uid");
|
||||
|
||||
while (true) {
|
||||
#ifdef __APPLE__
|
||||
/* OSX's kill syscall takes a third parameter that, among
|
||||
other things, determines if kill(-1, signo) affects the
|
||||
calling process. In the OSX libc, it's set to true,
|
||||
which means "follow POSIX", which we don't want here
|
||||
*/
|
||||
if (syscall(SYS_kill, -1, SIGKILL, false) == 0) break;
|
||||
#else
|
||||
if (kill(-1, SIGKILL) == 0) break;
|
||||
#endif
|
||||
if (errno == ESRCH || errno == EPERM) break; /* no more processes */
|
||||
if (errno != EINTR)
|
||||
throw SysError("cannot kill processes for uid '%1%'", uid);
|
||||
}
|
||||
|
||||
_exit(0);
|
||||
});
|
||||
|
||||
int status = pid.wait();
|
||||
if (status != 0)
|
||||
throw Error("cannot kill processes for uid '%1%': %2%", uid, statusToString(status));
|
||||
|
||||
/* !!! We should really do some check to make sure that there are
|
||||
no processes left running under `uid', but there is no portable
|
||||
way to do so (I think). The most reliable way may be `ps -eo
|
||||
uid | grep -q $uid'. */
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
using ChildWrapperFunction = std::function<void()>;
|
||||
|
||||
/* Wrapper around vfork to prevent the child process from clobbering
|
||||
the caller's stack frame in the parent. */
|
||||
static pid_t doFork(bool allowVfork, ChildWrapperFunction & fun) __attribute__((noinline));
|
||||
static pid_t doFork(bool allowVfork, ChildWrapperFunction & fun)
|
||||
{
|
||||
#ifdef __linux__
|
||||
pid_t pid = allowVfork ? vfork() : fork();
|
||||
#else
|
||||
pid_t pid = fork();
|
||||
#endif
|
||||
if (pid != 0) return pid;
|
||||
fun();
|
||||
abort();
|
||||
}
|
||||
|
||||
|
||||
#if __linux__
|
||||
static int childEntry(void * arg)
|
||||
{
|
||||
auto & fun = *reinterpret_cast<ChildWrapperFunction*>(arg);
|
||||
fun();
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
pid_t startProcess(std::function<void()> fun, const ProcessOptions & options)
|
||||
{
|
||||
ChildWrapperFunction wrapper = [&] {
|
||||
if (!options.allowVfork)
|
||||
logger = makeSimpleLogger();
|
||||
try {
|
||||
#if __linux__
|
||||
if (options.dieWithParent && prctl(PR_SET_PDEATHSIG, SIGKILL) == -1)
|
||||
throw SysError("setting death signal");
|
||||
#endif
|
||||
fun();
|
||||
} catch (std::exception & e) {
|
||||
try {
|
||||
std::cerr << options.errorPrefix << e.what() << "\n";
|
||||
} catch (...) { }
|
||||
} catch (...) { }
|
||||
if (options.runExitHandlers)
|
||||
exit(1);
|
||||
else
|
||||
_exit(1);
|
||||
};
|
||||
|
||||
pid_t pid = -1;
|
||||
|
||||
if (options.cloneFlags) {
|
||||
#ifdef __linux__
|
||||
// Not supported, since then we don't know when to free the stack.
|
||||
assert(!(options.cloneFlags & CLONE_VM));
|
||||
|
||||
size_t stackSize = 1 * 1024 * 1024;
|
||||
auto stack = static_cast<char *>(mmap(0, stackSize,
|
||||
PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0));
|
||||
if (stack == MAP_FAILED) throw SysError("allocating stack");
|
||||
|
||||
Finally freeStack([&] { munmap(stack, stackSize); });
|
||||
|
||||
pid = clone(childEntry, stack + stackSize, options.cloneFlags | SIGCHLD, &wrapper);
|
||||
#else
|
||||
throw Error("clone flags are only supported on Linux");
|
||||
#endif
|
||||
} else
|
||||
pid = doFork(options.allowVfork, wrapper);
|
||||
|
||||
if (pid == -1) throw SysError("unable to fork");
|
||||
|
||||
return pid;
|
||||
}
|
||||
|
||||
|
||||
std::string runProgram(Path program, bool searchPath, const Strings & args,
|
||||
const std::optional<std::string> & input, bool isInteractive)
|
||||
{
|
||||
auto res = runProgram(RunOptions {.program = program, .searchPath = searchPath, .args = args, .input = input, .isInteractive = isInteractive});
|
||||
|
||||
if (!statusOk(res.first))
|
||||
throw ExecError(res.first, "program '%1%' %2%", program, statusToString(res.first));
|
||||
|
||||
return res.second;
|
||||
}
|
||||
|
||||
// Output = error code + "standard out" output stream
|
||||
std::pair<int, std::string> runProgram(RunOptions && options)
|
||||
{
|
||||
StringSink sink;
|
||||
options.standardOut = &sink;
|
||||
|
||||
int status = 0;
|
||||
|
||||
try {
|
||||
runProgram2(options);
|
||||
} catch (ExecError & e) {
|
||||
status = e.status;
|
||||
}
|
||||
|
||||
return {status, std::move(sink.s)};
|
||||
}
|
||||
|
||||
void runProgram2(const RunOptions & options)
|
||||
{
|
||||
checkInterrupt();
|
||||
|
||||
assert(!(options.standardIn && options.input));
|
||||
|
||||
std::unique_ptr<Source> source_;
|
||||
Source * source = options.standardIn;
|
||||
|
||||
if (options.input) {
|
||||
source_ = std::make_unique<StringSource>(*options.input);
|
||||
source = source_.get();
|
||||
}
|
||||
|
||||
/* Create a pipe. */
|
||||
Pipe out, in;
|
||||
if (options.standardOut) out.create();
|
||||
if (source) in.create();
|
||||
|
||||
ProcessOptions processOptions;
|
||||
// vfork implies that the environment of the main process and the fork will
|
||||
// be shared (technically this is undefined, but in practice that's the
|
||||
// case), so we can't use it if we alter the environment
|
||||
processOptions.allowVfork = !options.environment;
|
||||
|
||||
std::optional<Finally<std::function<void()>>> resumeLoggerDefer;
|
||||
if (options.isInteractive) {
|
||||
logger->pause();
|
||||
resumeLoggerDefer.emplace(
|
||||
[]() {
|
||||
logger->resume();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* Fork. */
|
||||
Pid pid = startProcess([&] {
|
||||
if (options.environment)
|
||||
replaceEnv(*options.environment);
|
||||
if (options.standardOut && dup2(out.writeSide.get(), STDOUT_FILENO) == -1)
|
||||
throw SysError("dupping stdout");
|
||||
if (options.mergeStderrToStdout)
|
||||
if (dup2(STDOUT_FILENO, STDERR_FILENO) == -1)
|
||||
throw SysError("cannot dup stdout into stderr");
|
||||
if (source && dup2(in.readSide.get(), STDIN_FILENO) == -1)
|
||||
throw SysError("dupping stdin");
|
||||
|
||||
if (options.chdir && chdir((*options.chdir).c_str()) == -1)
|
||||
throw SysError("chdir failed");
|
||||
if (options.gid && setgid(*options.gid) == -1)
|
||||
throw SysError("setgid failed");
|
||||
/* Drop all other groups if we're setgid. */
|
||||
if (options.gid && setgroups(0, 0) == -1)
|
||||
throw SysError("setgroups failed");
|
||||
if (options.uid && setuid(*options.uid) == -1)
|
||||
throw SysError("setuid failed");
|
||||
|
||||
Strings args_(options.args);
|
||||
args_.push_front(options.program);
|
||||
|
||||
restoreProcessContext();
|
||||
|
||||
if (options.searchPath)
|
||||
execvp(options.program.c_str(), stringsToCharPtrs(args_).data());
|
||||
// This allows you to refer to a program with a pathname relative
|
||||
// to the PATH variable.
|
||||
else
|
||||
execv(options.program.c_str(), stringsToCharPtrs(args_).data());
|
||||
|
||||
throw SysError("executing '%1%'", options.program);
|
||||
}, processOptions);
|
||||
|
||||
out.writeSide.close();
|
||||
|
||||
std::thread writerThread;
|
||||
|
||||
std::promise<void> promise;
|
||||
|
||||
Finally doJoin([&] {
|
||||
if (writerThread.joinable())
|
||||
writerThread.join();
|
||||
});
|
||||
|
||||
|
||||
if (source) {
|
||||
in.readSide.close();
|
||||
writerThread = std::thread([&] {
|
||||
try {
|
||||
std::vector<char> buf(8 * 1024);
|
||||
while (true) {
|
||||
size_t n;
|
||||
try {
|
||||
n = source->read(buf.data(), buf.size());
|
||||
} catch (EndOfFile &) {
|
||||
break;
|
||||
}
|
||||
writeFull(in.writeSide.get(), {buf.data(), n});
|
||||
}
|
||||
promise.set_value();
|
||||
} catch (...) {
|
||||
promise.set_exception(std::current_exception());
|
||||
}
|
||||
in.writeSide.close();
|
||||
});
|
||||
}
|
||||
|
||||
if (options.standardOut)
|
||||
drainFD(out.readSide.get(), *options.standardOut);
|
||||
|
||||
/* Wait for the child to finish. */
|
||||
int status = pid.wait();
|
||||
|
||||
/* Wait for the writer thread to finish. */
|
||||
if (source) promise.get_future().get();
|
||||
|
||||
if (status)
|
||||
throw ExecError(status, "program '%1%' %2%", options.program, statusToString(status));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
std::string statusToString(int status)
|
||||
{
|
||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
||||
if (WIFEXITED(status))
|
||||
return fmt("failed with exit code %1%", WEXITSTATUS(status));
|
||||
else if (WIFSIGNALED(status)) {
|
||||
int sig = WTERMSIG(status);
|
||||
#if HAVE_STRSIGNAL
|
||||
const char * description = strsignal(sig);
|
||||
return fmt("failed due to signal %1% (%2%)", sig, description);
|
||||
#else
|
||||
return fmt("failed due to signal %1%", sig);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
return "died abnormally";
|
||||
} else return "succeeded";
|
||||
}
|
||||
|
||||
|
||||
bool statusOk(int status)
|
||||
{
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
}
|
||||
|
||||
}
|
123
src/libutil/unix/processes.hh
Normal file
123
src/libutil/unix/processes.hh
Normal file
|
@ -0,0 +1,123 @@
|
|||
#pragma once
|
||||
///@file
|
||||
|
||||
#include "types.hh"
|
||||
#include "error.hh"
|
||||
#include "logging.hh"
|
||||
#include "ansicolor.hh"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <dirent.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
|
||||
#include <boost/lexical_cast.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <optional>
|
||||
|
||||
namespace nix {
|
||||
|
||||
struct Sink;
|
||||
struct Source;
|
||||
|
||||
class Pid
|
||||
{
|
||||
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();
|
||||
int kill();
|
||||
int wait();
|
||||
|
||||
void setSeparatePG(bool separatePG);
|
||||
void setKillSignal(int signal);
|
||||
pid_t release();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Kill all processes running under the specified uid by sending them
|
||||
* a SIGKILL.
|
||||
*/
|
||||
void killUser(uid_t uid);
|
||||
|
||||
|
||||
/**
|
||||
* Fork a process that runs the given function, and return the child
|
||||
* pid to the caller.
|
||||
*/
|
||||
struct ProcessOptions
|
||||
{
|
||||
std::string errorPrefix = "";
|
||||
bool dieWithParent = true;
|
||||
bool runExitHandlers = false;
|
||||
bool allowVfork = false;
|
||||
/**
|
||||
* use clone() with the specified flags (Linux only)
|
||||
*/
|
||||
int cloneFlags = 0;
|
||||
};
|
||||
|
||||
pid_t startProcess(std::function<void()> fun, const ProcessOptions & options = ProcessOptions());
|
||||
|
||||
|
||||
/**
|
||||
* Run a program and return its stdout in a string (i.e., like the
|
||||
* shell backtick operator).
|
||||
*/
|
||||
std::string runProgram(Path program, bool searchPath = false,
|
||||
const Strings & args = Strings(),
|
||||
const std::optional<std::string> & input = {}, bool isInteractive = false);
|
||||
|
||||
struct RunOptions
|
||||
{
|
||||
Path program;
|
||||
bool searchPath = true;
|
||||
Strings args;
|
||||
std::optional<uid_t> uid;
|
||||
std::optional<uid_t> gid;
|
||||
std::optional<Path> chdir;
|
||||
std::optional<std::map<std::string, std::string>> environment;
|
||||
std::optional<std::string> input;
|
||||
Source * standardIn = nullptr;
|
||||
Sink * standardOut = nullptr;
|
||||
bool mergeStderrToStdout = false;
|
||||
bool isInteractive = false;
|
||||
};
|
||||
|
||||
std::pair<int, std::string> runProgram(RunOptions && options);
|
||||
|
||||
void runProgram2(const RunOptions & options);
|
||||
|
||||
|
||||
class ExecError : public Error
|
||||
{
|
||||
public:
|
||||
int status;
|
||||
|
||||
template<typename... Args>
|
||||
ExecError(int status, const Args & ... args)
|
||||
: Error(args...), status(status)
|
||||
{ }
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Convert the exit status of a child as returned by wait() into an
|
||||
* error string.
|
||||
*/
|
||||
std::string statusToString(int status);
|
||||
|
||||
bool statusOk(int status);
|
||||
|
||||
}
|
188
src/libutil/unix/signals.cc
Normal file
188
src/libutil/unix/signals.cc
Normal file
|
@ -0,0 +1,188 @@
|
|||
#include "signals.hh"
|
||||
#include "util.hh"
|
||||
#include "error.hh"
|
||||
#include "sync.hh"
|
||||
#include "terminal.hh"
|
||||
|
||||
#include <thread>
|
||||
|
||||
namespace nix {
|
||||
|
||||
std::atomic<bool> _isInterrupted = false;
|
||||
|
||||
static thread_local bool interruptThrown = false;
|
||||
thread_local std::function<bool()> interruptCheck;
|
||||
|
||||
void setInterruptThrown()
|
||||
{
|
||||
interruptThrown = true;
|
||||
}
|
||||
|
||||
void _interrupted()
|
||||
{
|
||||
/* Block user interrupts while an exception is being handled.
|
||||
Throwing an exception while another exception is being handled
|
||||
kills the program! */
|
||||
if (!interruptThrown && !std::uncaught_exceptions()) {
|
||||
interruptThrown = true;
|
||||
throw Interrupted("interrupted by the user");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
/* We keep track of interrupt callbacks using integer tokens, so we can iterate
|
||||
safely without having to lock the data structure while executing arbitrary
|
||||
functions.
|
||||
*/
|
||||
struct InterruptCallbacks {
|
||||
typedef int64_t Token;
|
||||
|
||||
/* We use unique tokens so that we can't accidentally delete the wrong
|
||||
handler because of an erroneous double delete. */
|
||||
Token nextToken = 0;
|
||||
|
||||
/* Used as a list, see InterruptCallbacks comment. */
|
||||
std::map<Token, std::function<void()>> callbacks;
|
||||
};
|
||||
|
||||
static Sync<InterruptCallbacks> _interruptCallbacks;
|
||||
|
||||
static void signalHandlerThread(sigset_t set)
|
||||
{
|
||||
while (true) {
|
||||
int signal = 0;
|
||||
sigwait(&set, &signal);
|
||||
|
||||
if (signal == SIGINT || signal == SIGTERM || signal == SIGHUP)
|
||||
triggerInterrupt();
|
||||
|
||||
else if (signal == SIGWINCH) {
|
||||
updateWindowSize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void triggerInterrupt()
|
||||
{
|
||||
_isInterrupted = true;
|
||||
|
||||
{
|
||||
InterruptCallbacks::Token i = 0;
|
||||
while (true) {
|
||||
std::function<void()> callback;
|
||||
{
|
||||
auto interruptCallbacks(_interruptCallbacks.lock());
|
||||
auto lb = interruptCallbacks->callbacks.lower_bound(i);
|
||||
if (lb == interruptCallbacks->callbacks.end())
|
||||
break;
|
||||
|
||||
callback = lb->second;
|
||||
i = lb->first + 1;
|
||||
}
|
||||
|
||||
try {
|
||||
callback();
|
||||
} catch (...) {
|
||||
ignoreException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static sigset_t savedSignalMask;
|
||||
static bool savedSignalMaskIsSet = false;
|
||||
|
||||
void setChildSignalMask(sigset_t * sigs)
|
||||
{
|
||||
assert(sigs); // C style function, but think of sigs as a reference
|
||||
|
||||
#if _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE
|
||||
sigemptyset(&savedSignalMask);
|
||||
// There's no "assign" or "copy" function, so we rely on (math) idempotence
|
||||
// of the or operator: a or a = a.
|
||||
sigorset(&savedSignalMask, sigs, sigs);
|
||||
#else
|
||||
// Without sigorset, our best bet is to assume that sigset_t is a type that
|
||||
// can be assigned directly, such as is the case for a sigset_t defined as
|
||||
// an integer type.
|
||||
savedSignalMask = *sigs;
|
||||
#endif
|
||||
|
||||
savedSignalMaskIsSet = true;
|
||||
}
|
||||
|
||||
void saveSignalMask() {
|
||||
if (sigprocmask(SIG_BLOCK, nullptr, &savedSignalMask))
|
||||
throw SysError("querying signal mask");
|
||||
|
||||
savedSignalMaskIsSet = true;
|
||||
}
|
||||
|
||||
void startSignalHandlerThread()
|
||||
{
|
||||
updateWindowSize();
|
||||
|
||||
saveSignalMask();
|
||||
|
||||
sigset_t set;
|
||||
sigemptyset(&set);
|
||||
sigaddset(&set, SIGINT);
|
||||
sigaddset(&set, SIGTERM);
|
||||
sigaddset(&set, SIGHUP);
|
||||
sigaddset(&set, SIGPIPE);
|
||||
sigaddset(&set, SIGWINCH);
|
||||
if (pthread_sigmask(SIG_BLOCK, &set, nullptr))
|
||||
throw SysError("blocking signals");
|
||||
|
||||
std::thread(signalHandlerThread, set).detach();
|
||||
}
|
||||
|
||||
void restoreSignals()
|
||||
{
|
||||
// If startSignalHandlerThread wasn't called, that means we're not running
|
||||
// in a proper libmain process, but a process that presumably manages its
|
||||
// own signal handlers. Such a process should call either
|
||||
// - initNix(), to be a proper libmain process
|
||||
// - startSignalHandlerThread(), to resemble libmain regarding signal
|
||||
// handling only
|
||||
// - saveSignalMask(), for processes that define their own signal handling
|
||||
// thread
|
||||
// TODO: Warn about this? Have a default signal mask? The latter depends on
|
||||
// whether we should generally inherit signal masks from the caller.
|
||||
// I don't know what the larger unix ecosystem expects from us here.
|
||||
if (!savedSignalMaskIsSet)
|
||||
return;
|
||||
|
||||
if (sigprocmask(SIG_SETMASK, &savedSignalMask, nullptr))
|
||||
throw SysError("restoring signals");
|
||||
}
|
||||
|
||||
|
||||
/* RAII helper to automatically deregister a callback. */
|
||||
struct InterruptCallbackImpl : InterruptCallback
|
||||
{
|
||||
InterruptCallbacks::Token token;
|
||||
~InterruptCallbackImpl() override
|
||||
{
|
||||
auto interruptCallbacks(_interruptCallbacks.lock());
|
||||
interruptCallbacks->callbacks.erase(token);
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<InterruptCallback> createInterruptCallback(std::function<void()> callback)
|
||||
{
|
||||
auto interruptCallbacks(_interruptCallbacks.lock());
|
||||
auto token = interruptCallbacks->nextToken++;
|
||||
interruptCallbacks->callbacks.emplace(token, callback);
|
||||
|
||||
std::unique_ptr<InterruptCallbackImpl> res {new InterruptCallbackImpl{}};
|
||||
res->token = token;
|
||||
|
||||
return std::unique_ptr<InterruptCallback>(res.release());
|
||||
}
|
||||
|
||||
}
|
104
src/libutil/unix/signals.hh
Normal file
104
src/libutil/unix/signals.hh
Normal file
|
@ -0,0 +1,104 @@
|
|||
#pragma once
|
||||
///@file
|
||||
|
||||
#include "types.hh"
|
||||
#include "error.hh"
|
||||
#include "logging.hh"
|
||||
#include "ansicolor.hh"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <dirent.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
|
||||
#include <boost/lexical_cast.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <optional>
|
||||
|
||||
namespace nix {
|
||||
|
||||
/* User interruption. */
|
||||
|
||||
extern std::atomic<bool> _isInterrupted;
|
||||
|
||||
extern thread_local std::function<bool()> interruptCheck;
|
||||
|
||||
void setInterruptThrown();
|
||||
|
||||
void _interrupted();
|
||||
|
||||
void inline checkInterrupt()
|
||||
{
|
||||
if (_isInterrupted || (interruptCheck && interruptCheck()))
|
||||
_interrupted();
|
||||
}
|
||||
|
||||
MakeError(Interrupted, BaseError);
|
||||
|
||||
|
||||
/**
|
||||
* Start a thread that handles various signals. Also block those signals
|
||||
* on the current thread (and thus any threads created by it).
|
||||
* Saves the signal mask before changing the mask to block those signals.
|
||||
* See saveSignalMask().
|
||||
*/
|
||||
void startSignalHandlerThread();
|
||||
|
||||
/**
|
||||
* Saves the signal mask, which is the signal mask that nix will restore
|
||||
* before creating child processes.
|
||||
* See setChildSignalMask() to set an arbitrary signal mask instead of the
|
||||
* current mask.
|
||||
*/
|
||||
void saveSignalMask();
|
||||
|
||||
/**
|
||||
* To use in a process that already called `startSignalHandlerThread()`
|
||||
* or `saveSignalMask()` first.
|
||||
*/
|
||||
void restoreSignals();
|
||||
|
||||
/**
|
||||
* Sets the signal mask. Like saveSignalMask() but for a signal set that doesn't
|
||||
* necessarily match the current thread's mask.
|
||||
* See saveSignalMask() to set the saved mask to the current mask.
|
||||
*/
|
||||
void setChildSignalMask(sigset_t *sigs);
|
||||
|
||||
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);
|
||||
|
||||
void triggerInterrupt();
|
||||
|
||||
/**
|
||||
* A RAII class that causes the current thread to receive SIGUSR1 when
|
||||
* the signal handler thread receives SIGINT. That is, this allows
|
||||
* SIGINT to be multiplexed to multiple threads.
|
||||
*/
|
||||
struct ReceiveInterrupts
|
||||
{
|
||||
pthread_t target;
|
||||
std::unique_ptr<InterruptCallback> callback;
|
||||
|
||||
ReceiveInterrupts()
|
||||
: target(pthread_self())
|
||||
, callback(createInterruptCallback([&]() { pthread_kill(target, SIGUSR1); }))
|
||||
{ }
|
||||
};
|
||||
|
||||
|
||||
}
|
107
src/libutil/unix/unix-domain-socket.cc
Normal file
107
src/libutil/unix/unix-domain-socket.cc
Normal file
|
@ -0,0 +1,107 @@
|
|||
#include "file-system.hh"
|
||||
#include "processes.hh"
|
||||
#include "unix-domain-socket.hh"
|
||||
#include "util.hh"
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace nix {
|
||||
|
||||
AutoCloseFD createUnixDomainSocket()
|
||||
{
|
||||
AutoCloseFD fdSocket = socket(PF_UNIX, SOCK_STREAM
|
||||
#ifdef SOCK_CLOEXEC
|
||||
| SOCK_CLOEXEC
|
||||
#endif
|
||||
, 0);
|
||||
if (!fdSocket)
|
||||
throw SysError("cannot create Unix domain socket");
|
||||
closeOnExec(fdSocket.get());
|
||||
return fdSocket;
|
||||
}
|
||||
|
||||
|
||||
AutoCloseFD createUnixDomainSocket(const Path & path, mode_t mode)
|
||||
{
|
||||
auto fdSocket = nix::createUnixDomainSocket();
|
||||
|
||||
bind(fdSocket.get(), path);
|
||||
|
||||
if (chmod(path.c_str(), mode) == -1)
|
||||
throw SysError("changing permissions on '%1%'", path);
|
||||
|
||||
if (listen(fdSocket.get(), 100) == -1)
|
||||
throw SysError("cannot listen on socket '%1%'", path);
|
||||
|
||||
return fdSocket;
|
||||
}
|
||||
|
||||
|
||||
static void bindConnectProcHelper(
|
||||
std::string_view operationName, auto && operation,
|
||||
int fd, const std::string & path)
|
||||
{
|
||||
struct sockaddr_un addr;
|
||||
addr.sun_family = AF_UNIX;
|
||||
|
||||
// Casting between types like these legacy C library interfaces
|
||||
// require is forbidden in C++. To maintain backwards
|
||||
// compatibility, the implementation of the bind/connect functions
|
||||
// contains some hints to the compiler that allow for this
|
||||
// special case.
|
||||
auto * psaddr = reinterpret_cast<struct sockaddr *>(&addr);
|
||||
|
||||
if (path.size() + 1 >= sizeof(addr.sun_path)) {
|
||||
Pipe pipe;
|
||||
pipe.create();
|
||||
Pid pid = startProcess([&] {
|
||||
try {
|
||||
pipe.readSide.close();
|
||||
Path dir = dirOf(path);
|
||||
if (chdir(dir.c_str()) == -1)
|
||||
throw SysError("chdir to '%s' failed", dir);
|
||||
std::string base(baseNameOf(path));
|
||||
if (base.size() + 1 >= sizeof(addr.sun_path))
|
||||
throw Error("socket path '%s' is too long", base);
|
||||
memcpy(addr.sun_path, base.c_str(), base.size() + 1);
|
||||
if (operation(fd, psaddr, sizeof(addr)) == -1)
|
||||
throw SysError("cannot %s to socket at '%s'", operationName, path);
|
||||
writeFull(pipe.writeSide.get(), "0\n");
|
||||
} catch (SysError & e) {
|
||||
writeFull(pipe.writeSide.get(), fmt("%d\n", e.errNo));
|
||||
} catch (...) {
|
||||
writeFull(pipe.writeSide.get(), "-1\n");
|
||||
}
|
||||
});
|
||||
pipe.writeSide.close();
|
||||
auto errNo = string2Int<int>(chomp(drainFD(pipe.readSide.get())));
|
||||
if (!errNo || *errNo == -1)
|
||||
throw Error("cannot %s to socket at '%s'", operationName, path);
|
||||
else if (*errNo > 0) {
|
||||
errno = *errNo;
|
||||
throw SysError("cannot %s to socket at '%s'", operationName, path);
|
||||
}
|
||||
} else {
|
||||
memcpy(addr.sun_path, path.c_str(), path.size() + 1);
|
||||
if (operation(fd, psaddr, sizeof(addr)) == -1)
|
||||
throw SysError("cannot %s to socket at '%s'", operationName, path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void bind(int fd, const std::string & path)
|
||||
{
|
||||
unlink(path.c_str());
|
||||
|
||||
bindConnectProcHelper("bind", ::bind, fd, path);
|
||||
}
|
||||
|
||||
|
||||
void connect(int fd, const std::string & path)
|
||||
{
|
||||
bindConnectProcHelper("connect", ::connect, fd, path);
|
||||
}
|
||||
|
||||
}
|
31
src/libutil/unix/unix-domain-socket.hh
Normal file
31
src/libutil/unix/unix-domain-socket.hh
Normal file
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
///@file
|
||||
|
||||
#include "types.hh"
|
||||
#include "file-descriptor.hh"
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
namespace nix {
|
||||
|
||||
/**
|
||||
* Create a Unix domain socket.
|
||||
*/
|
||||
AutoCloseFD createUnixDomainSocket();
|
||||
|
||||
/**
|
||||
* Create a Unix domain socket in listen mode.
|
||||
*/
|
||||
AutoCloseFD createUnixDomainSocket(const Path & path, mode_t mode);
|
||||
|
||||
/**
|
||||
* Bind a Unix domain socket to a path.
|
||||
*/
|
||||
void bind(int fd, const std::string & path);
|
||||
|
||||
/**
|
||||
* Connect to a Unix domain socket.
|
||||
*/
|
||||
void connect(int fd, const std::string & path);
|
||||
|
||||
}
|
66
src/libutil/unix/users.cc
Normal file
66
src/libutil/unix/users.cc
Normal file
|
@ -0,0 +1,66 @@
|
|||
#include "util.hh"
|
||||
#include "users.hh"
|
||||
#include "environment-variables.hh"
|
||||
#include "file-system.hh"
|
||||
|
||||
#include <pwd.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace nix {
|
||||
|
||||
std::string getUserName()
|
||||
{
|
||||
auto pw = getpwuid(geteuid());
|
||||
std::string name = pw ? pw->pw_name : getEnv("USER").value_or("");
|
||||
if (name.empty())
|
||||
throw Error("cannot figure out user name");
|
||||
return name;
|
||||
}
|
||||
|
||||
Path getHomeOf(uid_t userId)
|
||||
{
|
||||
std::vector<char> buf(16384);
|
||||
struct passwd pwbuf;
|
||||
struct passwd * pw;
|
||||
if (getpwuid_r(userId, &pwbuf, buf.data(), buf.size(), &pw) != 0
|
||||
|| !pw || !pw->pw_dir || !pw->pw_dir[0])
|
||||
throw Error("cannot determine user's home directory");
|
||||
return pw->pw_dir;
|
||||
}
|
||||
|
||||
Path getHome()
|
||||
{
|
||||
static Path homeDir = []()
|
||||
{
|
||||
std::optional<std::string> unownedUserHomeDir = {};
|
||||
auto homeDir = getEnv("HOME");
|
||||
if (homeDir) {
|
||||
// Only use $HOME if doesn't exist or is owned by the current user.
|
||||
struct stat st;
|
||||
int result = stat(homeDir->c_str(), &st);
|
||||
if (result != 0) {
|
||||
if (errno != ENOENT) {
|
||||
warn("couldn't stat $HOME ('%s') for reason other than not existing ('%d'), falling back to the one defined in the 'passwd' file", *homeDir, errno);
|
||||
homeDir.reset();
|
||||
}
|
||||
} else if (st.st_uid != geteuid()) {
|
||||
unownedUserHomeDir.swap(homeDir);
|
||||
}
|
||||
}
|
||||
if (!homeDir) {
|
||||
homeDir = getHomeOf(geteuid());
|
||||
if (unownedUserHomeDir.has_value() && unownedUserHomeDir != homeDir) {
|
||||
warn("$HOME ('%s') is not owned by you, falling back to the one defined in the 'passwd' file ('%s')", *unownedUserHomeDir, *homeDir);
|
||||
}
|
||||
}
|
||||
return *homeDir;
|
||||
}();
|
||||
return homeDir;
|
||||
}
|
||||
|
||||
bool isRootUser() {
|
||||
return getuid() == 0;
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue