diff options
Diffstat (limited to 'third_party/nix/src/libutil/util.cc')
-rw-r--r-- | third_party/nix/src/libutil/util.cc | 1426 |
1 files changed, 1426 insertions, 0 deletions
diff --git a/third_party/nix/src/libutil/util.cc b/third_party/nix/src/libutil/util.cc new file mode 100644 index 000000000000..aea1e68e3c77 --- /dev/null +++ b/third_party/nix/src/libutil/util.cc @@ -0,0 +1,1426 @@ +#include "libutil/util.hh" + +#include <cctype> +#include <cerrno> +#include <climits> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <future> +#include <iostream> +#include <sstream> +#include <thread> +#include <utility> + +#include <absl/strings/str_split.h> +#include <absl/strings/string_view.h> +#include <fcntl.h> +#include <glog/logging.h> +#include <grp.h> +#include <pwd.h> +#include <sys/ioctl.h> +#include <sys/prctl.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "libutil/affinity.hh" +#include "libutil/finally.hh" +#include "libutil/lazy.hh" +#include "libutil/serialise.hh" +#include "libutil/sync.hh" +#include "nix_config.h" + +namespace nix { + +const std::string nativeSystem = SYSTEM; + +BaseError& BaseError::addPrefix(const FormatOrString& fs) { + prefix_ = fs.s + prefix_; + return *this; +} + +std::string SysError::addErrno(const std::string& s) { + errNo = errno; + return s + ": " + strerror(errNo); +} + +std::optional<std::string> getEnv(const std::string& key) { + char* value = getenv(key.c_str()); + if (value == nullptr) return {}; + return std::string(value); +} + +std::map<std::string, std::string> getEnv() { + std::map<std::string, std::string> env; + for (size_t i = 0; environ[i] != nullptr; ++i) { + auto s = environ[i]; + auto eq = strchr(s, '='); + if (eq == nullptr) { + // invalid env, just keep going + continue; + } + env.emplace(std::string(s, eq), std::string(eq + 1)); + } + return env; +} + +void clearEnv() { + for (auto& name : getEnv()) { + unsetenv(name.first.c_str()); + } +} + +void replaceEnv(const std::map<std::string, std::string>& newEnv) { + clearEnv(); + for (const auto& newEnvVar : newEnv) { + setenv(newEnvVar.first.c_str(), newEnvVar.second.c_str(), 1); + } +} + +Path absPath(Path path, Path dir) { + if (path[0] != '/') { + if (dir.empty()) { +#ifdef __GNU__ + /* GNU (aka. GNU/Hurd) doesn't have any limitation on path + lengths and doesn't define `PATH_MAX'. */ + char* buf = getcwd(NULL, 0); + if (buf == NULL) +#else + char buf[PATH_MAX]; + if (getcwd(buf, sizeof(buf)) == nullptr) { +#endif + throw SysError("cannot get cwd"); + } + dir = buf; +#ifdef __GNU__ + free(buf); +#endif + } + path = dir + "/" + path; +} +return canonPath(path); +} // namespace nix + +Path canonPath(const Path& path, bool resolveSymlinks) { + assert(!path.empty()); + + std::string s; + + if (path[0] != '/') { + throw Error(format("not an absolute path: '%1%'") % path); + } + + std::string::const_iterator i = path.begin(); + std::string::const_iterator end = path.end(); + std::string temp; + + /* Count the number of times we follow a symlink and stop at some + arbitrary (but high) limit to prevent infinite loops. */ + unsigned int followCount = 0; + unsigned int maxFollow = 1024; + + while (true) { + /* Skip slashes. */ + while (i != end && *i == '/') { + i++; + } + if (i == end) { + break; + } + + /* Ignore `.'. */ + if (*i == '.' && (i + 1 == end || i[1] == '/')) { + i++; + } + + /* If `..', delete the last component. */ + else if (*i == '.' && i + 1 < end && i[1] == '.' && + (i + 2 == end || i[2] == '/')) { + if (!s.empty()) { + s.erase(s.rfind('/')); + } + i += 2; + } + + /* Normal component; copy it. */ + else { + s += '/'; + while (i != end && *i != '/') { + s += *i++; + } + + /* If s points to a symlink, resolve it and restart (since + the symlink target might contain new symlinks). */ + if (resolveSymlinks && isLink(s)) { + if (++followCount >= maxFollow) { + throw Error(format("infinite symlink recursion in path '%1%'") % + path); + } + temp = absPath(readLink(s), dirOf(s)) + std::string(i, end); + i = temp.begin(); /* restart */ + end = temp.end(); + s = ""; + } + } + } + + return s.empty() ? "/" : s; +} + +// TODO(grfn) remove in favor of std::filesystem::path::parent_path() +Path dirOf(absl::string_view path) { + Path::size_type pos = path.rfind('/'); + if (pos == std::string::npos) { + return "."; + } + return pos == 0 ? "/" : Path(path, 0, pos); +} + +// TODO(grfn) remove in favor of std::filesystem::path::root_name() +std::string baseNameOf(const Path& path) { + if (path.empty()) { + return ""; + } + + Path::size_type last = path.length() - 1; + if (path[last] == '/' && last > 0) { + last -= 1; + } + + Path::size_type pos = path.rfind('/', last); + if (pos == std::string::npos) { + pos = 0; + } else { + pos += 1; + } + + return std::string(path, pos, last - pos + 1); +} + +bool isInDir(const Path& path, const Path& dir) { + return path[0] == '/' && std::string(path, 0, dir.size()) == dir && + path.size() >= dir.size() + 2 && path[dir.size()] == '/'; +} + +bool isDirOrInDir(const Path& path, const Path& dir) { + return path == dir || isInDir(path, dir); +} + +struct stat lstat(const Path& path) { + struct stat st; + if (lstat(path.c_str(), &st) != 0) { + throw SysError(format("getting status of '%1%'") % path); + } + return st; +} + +bool pathExists(const Path& path) { + int res; + struct stat st; + res = lstat(path.c_str(), &st); + if (res == 0) { + return true; + } + if (errno != ENOENT && errno != ENOTDIR) { + throw SysError(format("getting status of %1%") % path); + } + return false; +} + +Path readLink(const Path& path) { + checkInterrupt(); + std::vector<char> buf; + for (ssize_t bufSize = PATH_MAX / 4; true; bufSize += bufSize / 2) { + buf.resize(bufSize); + ssize_t rlSize = readlink(path.c_str(), buf.data(), bufSize); + if (rlSize == -1) { + if (errno == EINVAL) { + throw Error("'%1%' is not a symlink", path); + } + throw SysError("reading symbolic link '%1%'", path); + + } else if (rlSize < bufSize) { + return std::string(buf.data(), rlSize); + } + } +} + +bool isLink(const Path& path) { + struct stat st = lstat(path); + return S_ISLNK(st.st_mode); +} + +DirEntries readDirectory(DIR* dir, const Path& path) { + DirEntries entries; + entries.reserve(64); + + struct dirent* dirent; + while (errno = 0, dirent = readdir(dir)) { /* sic */ + checkInterrupt(); + std::string name = dirent->d_name; + if (name == "." || name == "..") { + continue; + } + entries.emplace_back(name, dirent->d_ino, +#ifdef HAVE_STRUCT_DIRENT_D_TYPE + dirent->d_type +#else + DT_UNKNOWN +#endif + ); + } + if (errno) { + throw SysError(format("reading directory '%1%'") % path); + } + + return entries; +} + +DirEntries readDirectory(const Path& path) { + AutoCloseDir dir(opendir(path.c_str())); + if (!dir) { + throw SysError(format("opening directory '%1%'") % path); + } + + return readDirectory(dir.get(), path); +} + +unsigned char getFileType(const Path& path) { + struct stat st = lstat(path); + if (S_ISDIR(st.st_mode)) { + return DT_DIR; + } + if (S_ISLNK(st.st_mode)) { + return DT_LNK; + } + if (S_ISREG(st.st_mode)) { + return DT_REG; + } + return DT_UNKNOWN; +} + +std::string readFile(int fd) { + struct stat st; + if (fstat(fd, &st) == -1) { + throw SysError("statting file"); + } + + std::vector<unsigned char> buf(st.st_size); + readFull(fd, buf.data(), st.st_size); + + return std::string(reinterpret_cast<char*>(buf.data()), st.st_size); +} + +std::string readFile(absl::string_view path, bool drain) { + AutoCloseFD fd(open(std::string(path).c_str(), O_RDONLY | O_CLOEXEC)); + if (!fd) { + throw SysError(format("opening file '%1%'") % path); + } + return drain ? drainFD(fd.get()) : readFile(fd.get()); +} + +void readFile(absl::string_view path, Sink& sink) { + // TODO(tazjin): use stdlib functions for this stuff + AutoCloseFD fd(open(std::string(path).c_str(), O_RDONLY | O_CLOEXEC)); + if (!fd) { + throw SysError("opening file '%s'", path); + } + drainFD(fd.get(), sink); +} + +void writeFile(const Path& path, const std::string& s, mode_t mode) { + AutoCloseFD fd( + open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)); + if (!fd) { + throw SysError(format("opening file '%1%'") % path); + } + writeFull(fd.get(), s); +} + +void writeFile(const Path& path, Source& source, mode_t mode) { + AutoCloseFD fd( + open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, mode)); + if (!fd) { + throw SysError(format("opening file '%1%'") % path); + } + + std::vector<unsigned char> buf(64 * 1024); + + while (true) { + try { + auto n = source.read(buf.data(), buf.size()); + writeFull(fd.get(), static_cast<unsigned char*>(buf.data()), n); + } catch (EndOfFile&) { + break; + } + } +} + +std::string readLine(int fd) { + std::string s; + while (true) { + 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 writeLine(int fd, std::string s) { + s += '\n'; + writeFull(fd, s); +} + +static void _deletePath(int parentfd, const Path& path, + unsigned long long& bytesFreed) { + checkInterrupt(); + + std::string name(baseNameOf(path)); + + struct stat st; + if (fstatat(parentfd, name.c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1) { + if (errno == ENOENT) { + return; + } + throw SysError(format("getting status of '%1%'") % path); + } + + if (!S_ISDIR(st.st_mode) && st.st_nlink == 1) { + bytesFreed += st.st_size; + } + + if (S_ISDIR(st.st_mode)) { + /* Make the directory accessible. */ + const auto PERM_MASK = S_IRUSR | S_IWUSR | S_IXUSR; + if ((st.st_mode & PERM_MASK) != PERM_MASK) { + if (fchmodat(parentfd, name.c_str(), st.st_mode | PERM_MASK, 0) == -1) { + throw SysError(format("chmod '%1%'") % path); + } + } + + int fd = openat(parentfd, path.c_str(), O_RDONLY); + if (!fd) { + throw SysError(format("opening directory '%1%'") % path); + } + AutoCloseDir dir(fdopendir(fd)); + if (!dir) { + throw SysError(format("opening directory '%1%'") % path); + } + for (auto& i : readDirectory(dir.get(), path)) { + _deletePath(dirfd(dir.get()), path + "/" + i.name, bytesFreed); + } + } + + int flags = S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0; + if (unlinkat(parentfd, name.c_str(), flags) == -1) { + if (errno == ENOENT) { + return; + } + throw SysError(format("cannot unlink '%1%'") % path); + } +} + +static void _deletePath(const Path& path, unsigned long long& bytesFreed) { + Path dir = dirOf(path); + if (dir == "") { + dir = "/"; + } + + AutoCloseFD dirfd(open(dir.c_str(), O_RDONLY)); + if (!dirfd) { + // This really shouldn't fail silently, but it's left this way + // for backwards compatibility. + if (errno == ENOENT) { + return; + } + + throw SysError(format("opening directory '%1%'") % path); + } + + _deletePath(dirfd.get(), path, bytesFreed); +} + +void deletePath(const Path& path) { + unsigned long long dummy; + deletePath(path, dummy); +} + +void deletePath(const Path& path, unsigned long long& bytesFreed) { + // Activity act(*logger, lvlDebug, format("recursively deleting path '%1%'") % + // path); + bytesFreed = 0; + _deletePath(path, bytesFreed); +} + +static Path tempName(Path tmpRoot, const Path& prefix, bool includePid, + int& counter) { + tmpRoot = canonPath( + tmpRoot.empty() ? getEnv("TMPDIR").value_or("/tmp") : tmpRoot, true); + + if (includePid) { + return (format("%1%/%2%-%3%-%4%") % tmpRoot % prefix % getpid() % counter++) + .str(); + } + return (format("%1%/%2%-%3%") % tmpRoot % prefix % counter++).str(); +} + +Path createTempDir(const Path& tmpRoot, const Path& prefix, bool includePid, + bool useGlobalCounter, mode_t mode) { + static int globalCounter = 0; + int localCounter = 0; + int& counter(useGlobalCounter ? globalCounter : localCounter); + + while (true) { + checkInterrupt(); + Path tmpDir = tempName(tmpRoot, prefix, includePid, counter); + if (mkdir(tmpDir.c_str(), mode) == 0) { +#if __FreeBSD__ + /* Explicitly set the group of the directory. This is to + work around around problems caused by BSD's group + ownership semantics (directories inherit the group of + the parent). For instance, the group of /tmp on + FreeBSD is "wheel", so all directories created in /tmp + will be owned by "wheel"; but if the user is not in + "wheel", then "tar" will fail to unpack archives that + have the setgid bit set on directories. */ + if (chown(tmpDir.c_str(), (uid_t)-1, getegid()) != 0) + throw SysError(format("setting group of directory '%1%'") % tmpDir); +#endif + return tmpDir; + } + if (errno != EEXIST) { + throw SysError(format("creating directory '%1%'") % tmpDir); + } + } +} + +std::string getUserName() { + auto pw = getpwuid(geteuid()); + std::optional<std::string> name = + pw != nullptr ? pw->pw_name : getEnv("USER"); + if (!name.has_value()) { + throw Error("cannot figure out user name"); + } + return *name; +} + +static Lazy<Path> getHome2([]() { + std::optional<Path> homeDir = getEnv("HOME"); + if (!homeDir) { + std::vector<char> buf(16384); + struct passwd pwbuf; + struct passwd* pw; + if (getpwuid_r(geteuid(), &pwbuf, buf.data(), buf.size(), &pw) != 0 || + (pw == nullptr) || (pw->pw_dir == nullptr) || (pw->pw_dir[0] == 0)) { + throw Error("cannot determine user's home directory"); + } + return std::string(pw->pw_dir); + } + return homeDir.value(); +}); + +Path getHome() { return getHome2(); } + +Path getCacheDir() { + return getEnv("XDG_CACHE_HOME").value_or(getHome() + "/.cache"); +} + +Path getConfigDir() { + return getEnv("XDG_CONFIG_HOME").value_or(getHome() + "/.config"); +} + +std::vector<Path> getConfigDirs() { + Path configHome = getConfigDir(); + std::string configDirs = getEnv("XDG_CONFIG_DIRS").value_or(""); + std::vector<std::string> result = + absl::StrSplit(configDirs, absl::ByChar(':'), absl::SkipEmpty()); + result.insert(result.begin(), configHome); + return result; +} + +Path getDataDir() { + return getEnv("XDG_DATA_HOME").value_or(getHome() + "/.local/share"); +} + +// TODO(grfn): Remove in favor of std::filesystem::create_directories +Paths createDirs(const Path& path) { + Paths created; + if (path == "/") { + return created; + } + + struct stat st; + if (lstat(path.c_str(), &st) == -1) { + created = createDirs(dirOf(path)); + if (mkdir(path.c_str(), 0777) == -1 && errno != EEXIST) { + throw SysError(format("creating directory '%1%'") % path); + } + st = lstat(path); + created.push_back(path); + } + + if (S_ISLNK(st.st_mode) && stat(path.c_str(), &st) == -1) { + throw SysError(format("statting symlink '%1%'") % path); + } + + if (!S_ISDIR(st.st_mode)) { + throw Error(format("'%1%' is not a directory") % path); + } + + return created; +} + +void createSymlink(const Path& target, const Path& link) { + if (symlink(target.c_str(), link.c_str()) != 0) { + throw SysError(format("creating symlink from '%1%' to '%2%'") % link % + target); + } +} + +void replaceSymlink(const Path& target, const Path& link) { + for (unsigned int n = 0; true; n++) { + Path tmp = canonPath(fmt("%s/.%d_%s", dirOf(link), n, baseNameOf(link))); + + try { + createSymlink(target, tmp); + } catch (SysError& e) { + if (e.errNo == EEXIST) { + continue; + } + throw; + } + + if (rename(tmp.c_str(), link.c_str()) != 0) { + throw SysError(format("renaming '%1%' to '%2%'") % tmp % link); + } + + break; + } +} + +void readFull(int fd, unsigned char* buf, size_t count) { + while (count != 0u) { + checkInterrupt(); + ssize_t res = read(fd, reinterpret_cast<char*>(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, const unsigned char* buf, size_t count, + bool allowInterrupts) { + while (count != 0u) { + if (allowInterrupts) { + checkInterrupt(); + } + ssize_t res = write(fd, (char*)buf, count); + if (res == -1 && errno != EINTR) { + throw SysError("writing to file"); + } + if (res > 0) { + count -= res; + buf += res; + } + } +} + +void writeFull(int fd, const std::string& s, bool allowInterrupts) { + writeFull(fd, reinterpret_cast<const unsigned char*>(s.data()), s.size(), + allowInterrupts); +} + +std::string drainFD(int fd, bool block) { + StringSink sink; + drainFD(fd, sink, block); + return std::move(*sink.s); +} + +void drainFD(int fd, Sink& sink, bool block) { + int saved; + + Finally finally([&]() { + if (!block) { + if (fcntl(fd, F_SETFL, saved) == -1) { + throw SysError("making file descriptor blocking"); + } + } + }); + + if (!block) { + saved = fcntl(fd, F_GETFL); + if (fcntl(fd, F_SETFL, saved | O_NONBLOCK) == -1) { + throw SysError("making file descriptor non-blocking"); + } + } + + std::vector<unsigned char> buf(64 * 1024); + while (true) { + 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(buf.data(), rd); + } + } +} + +////////////////////////////////////////////////////////////////////// + +AutoDelete::AutoDelete() : del{false} {} + +AutoDelete::AutoDelete(std::string p, bool recursive) : path(std::move(p)) { + del = true; + this->recursive = recursive; +} + +AutoDelete::~AutoDelete() { + try { + if (del) { + if (recursive) { + deletePath(path); + } else { + if (remove(path.c_str()) == -1) { + throw SysError(format("cannot unlink '%1%'") % path); + } + } + } + } catch (...) { + ignoreException(); + } +} + +void AutoDelete::cancel() { del = false; } + +void AutoDelete::reset(const Path& p, bool recursive) { + path = p; + this->recursive = recursive; + del = true; +} + +////////////////////////////////////////////////////////////////////// + +AutoCloseFD::AutoCloseFD() : fd{-1} {} + +AutoCloseFD::AutoCloseFD(int fd) : fd{fd} {} + +AutoCloseFD::AutoCloseFD(AutoCloseFD&& that) : fd{that.fd} { that.fd = -1; } + +AutoCloseFD& AutoCloseFD::operator=(AutoCloseFD&& that) { + close(); + fd = that.fd; + that.fd = -1; + return *this; +} + +AutoCloseFD::~AutoCloseFD() { + try { + close(); + } catch (...) { + ignoreException(); + } +} + +int AutoCloseFD::get() const { return fd; } + +void AutoCloseFD::close() { + if (fd != -1) { + if (::close(fd) == -1) { /* This should never happen. */ + throw SysError(format("closing file descriptor %1%") % fd); + } + } +} + +AutoCloseFD::operator bool() const { return fd != -1; } + +int AutoCloseFD::release() { + int oldFD = fd; + fd = -1; + return oldFD; +} + +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 = AutoCloseFD(fds[0]); + writeSide = AutoCloseFD(fds[1]); +} + +////////////////////////////////////////////////////////////////////// + +Pid::Pid() = default; + +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); + + DLOG(INFO) << "killing process " << 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) { + LOG(ERROR) << SysError("killing process %d", pid).msg(); + } + + return wait(); +} + +int Pid::wait() { + assert(pid != -1); + while (true) { + int status; + int res = waitpid(pid, &status, 0); + if (res == pid) { + pid = -1; + return status; + } + if (errno != EINTR) { + throw SysError("cannot get child exit status"); + } + 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) { + DLOG(INFO) << "killing all processes running under UID " << 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. */ + + ProcessOptions options; + + Pid pid(startProcess( + [&]() { + if (setuid(uid) == -1) { + throw SysError("setting uid"); + } + + while (true) { + if (kill(-1, SIGKILL) == 0) { + break; + } + if (errno == ESRCH) { + break; + } /* no more processes */ + if (errno != EINTR) { + throw SysError(format("cannot kill processes for uid '%1%'") % uid); + } + } + + _exit(0); + }, + options)); + + int status = pid.wait(); + if (status != 0) { + throw Error(format("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'. */ +} + +////////////////////////////////////////////////////////////////////// + +/* + * Please note that it is not legal for this function to call vfork(). If the + * process created by vfork() returns from the function in which vfork() was + * called, or calls any other function before successfully calling _exit() or + * one of the exec*() family of functions, the behavior is undefined. + */ +static pid_t doFork(const std::function<void()>& fun) __attribute__((noinline)); +static pid_t doFork(const std::function<void()>& fun) { +#ifdef __linux__ + // TODO(kanepyork): call clone() instead for faster forking +#endif + + pid_t pid = fork(); + if (pid != 0) { + return pid; + } + fun(); + abort(); +} + +pid_t startProcess(std::function<void()> fun, const ProcessOptions& options) { + auto wrapper = [&]() { + try { +#if __linux__ + if (options.dieWithParent && prctl(PR_SET_PDEATHSIG, SIGKILL) == -1) { + throw SysError("setting death signal"); + } +#endif + restoreAffinity(); + fun(); + } catch (std::exception& e) { + try { + LOG(ERROR) << options.errorPrefix << e.what(); + } catch (...) { + } + } catch (...) { + } + if (options.runExitHandlers) { + exit(1); + } else { + _exit(1); + } + }; + + pid_t pid = doFork(wrapper); + if (pid == -1) { + throw SysError("unable to fork"); + } + + return pid; +} + +std::vector<char*> stringsToCharPtrs(const Strings& ss) { + std::vector<char*> res; + for (auto& s : ss) { + res.push_back(const_cast<char*>(s.c_str())); + } + res.push_back(nullptr); + return res; +} + +std::string runProgram(const Path& program, bool searchPath, + const Strings& args, + const std::optional<std::string>& input) { + RunOptions opts(program, args); + opts.searchPath = searchPath; + opts.input = input; + + auto res = runProgram(opts); + + if (!statusOk(res.first)) { + throw ExecError(res.first, fmt("program '%1%' %2%", program, + statusToString(res.first))); + } + + return res.second; +} + +std::pair<int, std::string> runProgram(const RunOptions& options_) { + RunOptions options(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; + Pipe in; + if (options.standardOut != nullptr) { + out.create(); + } + if (source != nullptr) { + in.create(); + } + + ProcessOptions processOptions; + + /* Fork. */ + Pid pid(startProcess( + [&]() { + if (options.environment) { + replaceEnv(*options.environment); + } + if ((options.standardOut != nullptr) && + 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 != nullptr) && + 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, nullptr) == -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); + + restoreSignals(); + + if (options.searchPath) { + execvp(options.program.c_str(), stringsToCharPtrs(args_).data()); + } else { + execv(options.program.c_str(), stringsToCharPtrs(args_).data()); + } + + throw SysError("executing '%1%'", options.program); + }, + processOptions)); + + out.writeSide = AutoCloseFD(-1); + + std::thread writerThread; + + std::promise<void> promise; + + Finally doJoin([&]() { + if (writerThread.joinable()) { + writerThread.join(); + } + }); + + if (source != nullptr) { + in.readSide = AutoCloseFD(-1); + writerThread = std::thread([&]() { + try { + std::vector<unsigned 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 = AutoCloseFD(-1); + }); + } + + if (options.standardOut != nullptr) { + 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 != nullptr) { + promise.get_future().get(); + } + + if (status != 0) { + throw ExecError(status, fmt("program '%1%' %2%", options.program, + statusToString(status))); + } +} + +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) == 0u) { + DLOG(INFO) << "closing leaked FD " << 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) == 0u) { + 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"); + } +} + +////////////////////////////////////////////////////////////////////// + +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() == 0)) { + interruptThrown = true; + throw Interrupted("interrupted by the user"); + } +} + +////////////////////////////////////////////////////////////////////// + +std::string concatStringsSep(const std::string& sep, const Strings& ss) { + std::string s; + for (auto& i : ss) { + if (!s.empty()) { + s += sep; + } + s += i; + } + return s; +} + +std::string concatStringsSep(const std::string& sep, const StringSet& ss) { + std::string s; + for (auto& i : ss) { + if (!s.empty()) { + s += sep; + } + s += i; + } + return s; +} + +std::string replaceStrings(const std::string& s, const std::string& from, + const std::string& to) { + if (from.empty()) { + return s; + } + std::string res = s; + size_t pos = 0; + while ((pos = res.find(from, pos)) != std::string::npos) { + res.replace(pos, from.size(), to); + pos += to.size(); + } + return res; +} + +std::string statusToString(int status) { + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + if (WIFEXITED(status)) { + return (format("failed with exit code %1%") % WEXITSTATUS(status)).str(); + } + if (WIFSIGNALED(status)) { + int sig = WTERMSIG(status); +#if HAVE_STRSIGNAL + const char* description = strsignal(sig); + return (format("failed due to signal %1% (%2%)") % sig % description) + .str(); +#else + return (format("failed due to signal %1%") % sig).str(); +#endif + } else { + return "died abnormally"; + } + } else { + return "succeeded"; + } +} + +bool statusOk(int status) { + return WIFEXITED(status) && WEXITSTATUS(status) == 0; +} + +std::string toLower(const std::string& s) { + std::string r(s); + for (auto& c : r) { + c = std::tolower(c); + } + return r; +} + +std::string shellEscape(const std::string& s) { + std::string r = "'"; + for (auto& i : s) { + if (i == '\'') { + r += "'\\''"; + } else { + r += i; + } + } + r += '\''; + return r; +} + +void ignoreException() { + try { + throw; + } catch (std::exception& e) { + LOG(ERROR) << "error (ignored): " << e.what(); + } +} + +std::string filterANSIEscapes(const std::string& s, bool filterAll, + unsigned int width) { + std::string t; + std::string e; + size_t w = 0; + auto i = s.begin(); + + while (w < static_cast<size_t>(width) && i != s.end()) { + if (*i == '\e') { + std::string e; + e += *i++; + char last = 0; + + if (i != s.end() && *i == '[') { + e += *i++; + // eat parameter bytes + while (i != s.end() && *i >= 0x30 && *i <= 0x3f) { + e += *i++; + } + // eat intermediate bytes + while (i != s.end() && *i >= 0x20 && *i <= 0x2f) { + e += *i++; + } + // eat final byte + if (i != s.end() && *i >= 0x40 && *i <= 0x7e) { + e += last = *i++; + } + } else { + if (i != s.end() && *i >= 0x40 && *i <= 0x5f) { + e += *i++; + } + } + + if (!filterAll && last == 'm') { + t += e; + } + } + + else if (*i == '\t') { + i++; + t += ' '; + w++; + while (w < static_cast<size_t>(width) && ((w % 8) != 0u)) { + t += ' '; + w++; + } + } + + else if (*i == '\r') { + // do nothing for now + i++; + + } else { + t += *i++; + w++; + } + } + + return t; +} + +void callFailure(const std::function<void(std::exception_ptr exc)>& failure, + const std::exception_ptr& exc) { + try { + failure(exc); + } catch (std::exception& e) { + LOG(ERROR) << "uncaught exception: " << e.what(); + abort(); + } +} + +static Sync<std::pair<unsigned short, unsigned short>> windowSize{{0, 0}}; + +static void updateWindowSize() { + struct winsize ws; + if (ioctl(2, TIOCGWINSZ, &ws) == 0) { + auto windowSize_(windowSize.lock()); + windowSize_->first = ws.ws_row; + windowSize_->second = ws.ws_col; + } +} + +std::pair<unsigned short, unsigned short> getWindowSize() { + return *windowSize.lock(); +} + +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) { + triggerInterrupt(); + + } else if (signal == SIGWINCH) { + updateWindowSize(); + } + } +} + +void triggerInterrupt() { + _isInterrupted = true; + + { + auto interruptCallbacks(_interruptCallbacks.lock()); + for (auto& callback : *interruptCallbacks) { + try { + callback(); + } catch (...) { + ignoreException(); + } + } + } +} + +static sigset_t savedSignalMask; + +void startSignalHandlerThread() { + updateWindowSize(); + + if (sigprocmask(SIG_BLOCK, nullptr, &savedSignalMask) != 0) { + throw SysError("quering signal mask"); + } + + 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) != 0) { + throw SysError("blocking signals"); + } + + std::thread(signalHandlerThread, set).detach(); +} + +void restoreSignals() { + if (sigprocmask(SIG_SETMASK, &savedSignalMask, nullptr) != 0) { + throw SysError("restoring signals"); + } +} + +/* 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( + const 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()); +} + +} // namespace nix |