mirror of
https://github.com/privatevoid-net/nix-super.git
synced 2024-11-30 17:46:15 +02:00
ac89bb064a
All OS and IO operations should be moved out, leaving only some misc portable pure functions. This is useful to avoid copious CPP when doing things like Windows and Emscripten ports. Newly exposed functions to break cycles: - `restoreSignals` - `updateWindowSize`
421 lines
11 KiB
C++
421 lines
11 KiB
C++
#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'. */
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
/* Wrapper around vfork to prevent the child process from clobbering
|
|
the caller's stack frame in the parent. */
|
|
static pid_t doFork(bool allowVfork, std::function<void()> fun) __attribute__((noinline));
|
|
static pid_t doFork(bool allowVfork, std::function<void()> 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 main = (std::function<void()> *) arg;
|
|
(*main)();
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
|
|
pid_t startProcess(std::function<void()> fun, const ProcessOptions & options)
|
|
{
|
|
std::function<void()> 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 = (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;
|
|
}
|
|
|
|
}
|