#include "config.h" #include "util.hh" #include "affinity.hh" #include #include #include #include #include #include #include #include #include #include #ifdef __APPLE__ #include #endif #ifdef __linux__ #include #endif extern char * * environ; namespace nix { BaseError::BaseError(const FormatOrString & fs, unsigned int status) : status(status) { err = fs.s; } BaseError & BaseError::addPrefix(const FormatOrString & fs) { prefix_ = fs.s + prefix_; return *this; } SysError::SysError(const FormatOrString & fs) : Error(format("%1%: %2%") % fs.s % strerror(errno)) , errNo(errno) { } string getEnv(const string & key, const string & def) { char * value = getenv(key.c_str()); return value ? string(value) : def; } Path absPath(Path path, Path dir) { if (path[0] != '/') { if (dir == "") { #ifdef __GNU__
aboutsummaryrefslogtreecommitdiff
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2017, 2021, 2023 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2021 muradm <mail@muradm.net>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu tests desktop)
  #:use-module (gnu tests)
  #:use-module (gnu packages shells)
  #:use-module (gnu services)
  #:use-module (gnu services base)
  #:use-module (gnu services dbus)
  #:use-module (gnu services desktop)
  #:use-module (gnu system)
  #:use-module (gnu system vm)
  #:use-module (guix gexp)
  #:use-module (srfi srfi-1)
  #:export (%test-elogind
            %test-minimal-desktop))


;;;
;;; Elogind.
;;;

(define (run-elogind-test vm)
  (define test
    (with-imported-modules '((gnu build marionette)
                             (guix build syscalls))
      #~(begin
          (use-modules (gnu build marionette)
                       (guix build syscalls)
                       (srfi srfi-64))

          (define marionette
            (make-marionette '(#$vm)))

          (test-runner-current (system-test-runner #$output))
          (test-begin "elogind")

          ;; Log in as root on tty1, and check what 'loginctl' returns.
          (test-equal "login on tty1"
            '(("c1" "0" "root" "seat0" "tty1")      ;session
              ("seat0")                             ;seat
              ("0" "root" "no"))                    ;user

            (begin
              ;; Wait for tty1.
              (marionette-eval
               '(begin
                  (use-modules (gnu services herd))
                  (start-service 'term-tty1)
                  (start-service 'elogind))
               marionette)
              (marionette-control "sendkey ctrl-alt-f1" marionette)

              ;; Now we can type.
              (marionette-type "root\n" marionette)
              (marionette-type "loginctl list-users --no-legend > users\n"
                               marionette)
              (marionette-type "loginctl list-seats --no-legend > seats\n"
                               marionette)
              (marionette-type "loginctl list-sessions --no-legend > sessions\n"
                               marionette)


              ;; Read the three files.
              (marionette-eval '(use-modules (rnrs io ports)) marionette)
              (let ((guest-file (lambda (file)
                                  (string-tokenize
                                   (wait-for-file file marionette
                                                  #:read 'get-string-all)))))
                (list (guest-file "/root/sessions")
                      (guest-file "/root/seats")
                      (guest-file "/root/users")))))

          (test-assert "screendump"
            (begin
              (let ((capture (string-append #$output "/tty1.ppm")))
                (marionette-control
                 (string-append "screendump " capture) marionette)
                (file-exists? capture))))

          (test-end))))

  (gexp->derivation "elogind" test))

(define %test-elogind
  (system-test
   (name "elogind")
   (description
    "Test whether we can log in when elogind is enabled, and whether
'loginctl' reports accurate user, session, and seat information.")
   (value
    (let ((os (marionette-operating-system
               (simple-operating-system
                (service elogind-service-type)
                (service polkit-service-type)
                (service dbus-root-service-type))
               #:imported-modules '((gnu services herd)
                                    (guix combinators)))))
      (run-elogind-test (virtual-machine os))))))


;;;
;;; Seatd/greetd based minimal desktop
;;;

(define %minimal-services
  (append
   (modify-services %base-services
     ;; greetd-service-type provides "greetd" PAM service
     (delete login-service-type)
     ;; and can be used in place of mingetty-service-type
     (delete mingetty-service-type))
   (list
    (service seatd-service-type)
    (service greetd-service-type
             (greetd-configuration
              (greeter-supplementary-groups '("input" "video"))
              (terminals
               (list
                ;; we can make any terminal active by default
                (greetd-terminal-configuration (terminal-vt "1") (terminal-switch #t))
                ;; we can make environment without XDG_RUNTIME_DIR set
                ;; even provide our own environment variables
                (greetd-terminal-configuration
                 (terminal-vt "2")
                 (default-session-command
                   (greetd-agreety-session
                    (extra-env '(("MY_VAR" . "1")))
                    (xdg-env? #f))))
                ;; we can use different shell instead of default bash
                (greetd-terminal-configuration
                 (terminal-vt "3")
                 (default-session-command
                   (greetd-agreety-session (command (file-append zsh "/bin/zsh")))))
                ;; we can use any other executable command as greeter
                (greetd-terminal-configuration
                 (terminal-vt "4")
                 (default-session-command (program-file "my-noop-greeter" #~(exit))))
                (greetd-terminal-configuration (terminal-vt "5"))
                (greetd-terminal-configuration (terminal-vt "6"))))))
    ;; mingetty-service-type can be used in parallel
    ;; if needed to do so, do not (delete login-service-type)
    ;; as illustrated above
    #| (service mingetty-service-type (mingetty-configuration (tty "tty8"))) |#)))

(define-syntax-rule (minimal-operating-system user-services ...)
  "Return an operating system that includes USER-SERVICES in addition to
minimal %BASE-SERVICES."
  (operating-system (inherit %simple-os)
                    (services (cons* user-services ... %minimal-services))))

(define (run-minimal-desktop-test os vm)
  (define test
    (with-imported-modules '((gnu build marionette)
                             (guix build syscalls))
      #~(begin
          (use-modules (gnu build marionette)
                       (guix build syscalls)
                       (srfi srfi-1)
                       (srfi srfi-64)
                       (ice-9 pretty-print))

          (define marionette
            (make-marionette #$vm))

          (define (file-get-all-strings fname)
            (marionette-eval '(use-modules (rnrs io ports)) marionette)
            (wait-for-file fname marionette #:read 'get-string-all))

          (define (wait-for-unix-socket-m socket)
            (wait-for-unix-socket socket marionette))

          (mkdir #$output)
          (chdir #$output)

          (test-runner-current (system-test-runner #$output))
          (test-begin "minimal-desktop")

          (test-assert "seatd is ready"
            (wait-for-unix-socket-m "/run/seatd.sock"))

          (test-equal "login user on tty1"
            "alice\n"
            (begin
              ;; Wait for tty1.
              (marionette-eval
               '(begin
                  (use-modules (gnu services herd))
                  (start-service 'term-tty1))
               marionette)
              (marionette-control "sendkey ctrl-alt-f1" marionette)

              ;; login as root change alice password and exit
              ;; then login as alice
              (for-each
               (lambda (cmd) (marionette-type cmd marionette) (sleep 1))
               (list
                "root\n"
                "passwd alice\n"
                "alice\n"
                "alice\n"
                "exit\n"
                "alice\n"
                "alice\n"
                "id -un > logged-in\n"))

              (file-get-all-strings "/home/alice/logged-in")))

          (test-equal "validate user environment"
            '("SEATD_SOCK=/run/seatd.sock"
              "XDG_RUNTIME_DIR=/run/user/1000"
              "XDG_SEAT=seat0"
              "XDG_VTNR=1")

            (begin
              (marionette-type "env > env\n" marionette)
              (sleep 1)

              (define user-env (string-tokenize
                                (file-get-all-strings "/home/alice/env")))

              (define (expected-var var)
                (any (lambda (s) (string-contains var s))
                     '("SEATD_SOCK"
                       "XDG_RUNTIME_DIR"
                       "XDG_SEAT"
                       "XDG_VTNR")))

              (sort (filter expected-var user-env) string<?)))

          (test-assert "validate SEATD_SOCK and GREETD_SOCK"
            (begin
              (marionette-type "env > env\n" marionette)
              (sleep 1)

              (define (sock-var? var)
                (any (lambda (s) (string-contains var s))
                     '("SEATD_SOCK" "GREETD_SOCK")))

              (define (sock-var-sock var)
                (car (cdr (string-split var #\=))))

              (let*
                  ((out (file-get-all-strings "/home/alice/env"))
                   (out (string-tokenize out))
                   (out (filter sock-var? out))
                   (socks (map sock-var-sock out))
                   (socks (map wait-for-unix-socket-m socks)))
                (and (= 2 (length socks)) (every identity socks)))))

          (test-equal "seatd.sock ownership"
            '("root" "seat")
            `(,(marionette-eval
                '(passwd:name (getpwuid (stat:uid (stat "/run/seatd.sock"))))
                marionette)
              ,(marionette-eval
                '(group:name (getgrgid (stat:gid (stat "/run/seatd.sock"))))
                marionette)))

          (test-assert "greetd is ready"
            (begin
              (marionette-type "ps -C greetd -o pid,args --no-headers > ps-greetd\n"
                               marionette)
              (sleep 1)

              (define (greetd-daemon? cmd)
                (string-contains cmd "config"))

              (define (greetd-cmd-to-pid cmd)
                (car (string-split cmd #\space)))

              (define (greetd-pid-to-sock pid)
                (string-append "/run/greetd-" pid ".sock"))

              (let* ((out (file-get-all-strings "/home/alice/ps-greetd"))
                     (out (string-split out #\newline))
                     (out (map string-trim-both out))
                     (out (filter greetd-daemon? out))
                     (pids (map greetd-cmd-to-pid out))
                     (socks (map greetd-pid-to-sock pids))
                     (socks (map wait-for-unix-socket-m socks)))
                (every identity socks))))

          ;; a bit weak, but tests everything at once actually
          (test-equal "check /run/user/<uid> mounted and writable"
            "alice\n"
            (begin
              (marionette-type "echo alice > /run/user/1000/test\n" marionette)
              (file-get-all-strings "/run/user/1000/test")))

          (test-equal "check greeter user has correct groups"
            "greeter input video\n"
            (begin
              (marionette-type "id -Gn greeter > /run/user/1000/greeter-groups\n"
                               marionette)
              (file-get-all-strings "/run/user/1000/greeter-groups")))

          (test-assert "screendump"
            (begin
              (marionette-control (string-append "screendump " #$output
                                                 "/tty1.ppm")
                                  marionette)
              (file-exists? "tty1.ppm")))

          (test-end))))

  (gexp->derivation "minimal-desktop" test))

(define %test-minimal-desktop
  (system-test
   (name "minimal-desktop")
   (description
    "Test whether we can log in when seatd and greetd is enabled")
   (value
    (let* ((os (marionette-operating-system
                (minimal-operating-system)
                #:imported-modules '((gnu services herd)
                                     (guix combinators))))
           (vm (virtual-machine os)))
      (run-minimal-desktop-test (virtualized-operating-system os '())
                                #~(list #$vm))))))
ingLevel; i++) prefix += "| "; else if (logType == ltEscapes && level != lvlInfo) prefix = "\033[" + escVerbosity(level) + "s"; string s = (format("%1%%2%\n") % prefix % fs.s).str(); writeToStderr(s); } void warnOnce(bool & haveWarned, const FormatOrString & fs) { if (!haveWarned) { printMsg(lvlError, format("warning: %1%") % fs.s); haveWarned = true; } } void writeToStderr(const string & s) { try { if (_writeToStderr) _writeToStderr((const unsigned char *) s.data(), s.size()); else writeFull(STDERR_FILENO, s); } catch (SysError & e) { /* Ignore failing writes to stderr if we're in an exception handler, otherwise throw an exception. We need to ignore write errors in exception handlers to ensure that cleanup code runs to completion if the other side of stderr has been closed unexpectedly. */ if (!std::uncaught_exception()) throw; } } void (*_writeToStderr) (const unsigned char * buf, size_t count) = 0; void readFull(int fd, unsigned char * buf, size_t count) { while (count) { checkInterrupt(); ssize_t res = read(fd, (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) { while (count) { checkInterrupt(); ssize_t res = write(fd, (char *) buf, count); if (res == -1) { if (errno == EINTR) continue; throw SysError("writing to file"); } count -= res; buf += res; } } void writeFull(int fd, const string & s) { writeFull(fd, (const unsigned char *) s.data(), s.size()); } string drainFD(int fd) { string result; unsigned char buffer[4096]; while (1) { checkInterrupt(); ssize_t rd = read(fd, buffer, sizeof buffer); if (rd == -1) { if (errno != EINTR) throw SysError("reading from file"); } else if (rd == 0) break; else result.append((char *) buffer, rd); } return result; } ////////////////////////////////////////////////////////////////////// AutoDelete::AutoDelete(const string & p, bool recursive) : path(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; } ////////////////////////////////////////////////////////////////////// AutoCloseFD::AutoCloseFD() { fd = -1; } AutoCloseFD::AutoCloseFD(int fd) { this->fd = fd; } AutoCloseFD::AutoCloseFD(const AutoCloseFD & fd) { /* Copying an AutoCloseFD isn't allowed (who should get to close it?). But as an edge case, allow copying of closed AutoCloseFDs. This is necessary due to tiresome reasons involving copy constructor use on default object values in STL containers (like when you do `map[value]' where value isn't in the map yet). */ this->fd = fd.fd; if (this->fd != -1) abort(); } AutoCloseFD::~AutoCloseFD() { try { close(); } catch (...) { ignoreException(); } } void AutoCloseFD::operator =(int fd) { if (this->fd != fd) close(); this->fd = fd; } AutoCloseFD::operator int() 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); fd = -1; } } bool AutoCloseFD::isOpen() { return fd != -1; } /* Pass responsibility for closing this fd to the caller. */ int AutoCloseFD::borrow() { int oldFD = fd; fd = -1; return oldFD; } void Pipe::create() { int fds[2]; if (pipe(fds) != 0) throw SysError("creating pipe"); readSide = fds[0]; writeSide = fds[1]; closeOnExec(readSide); closeOnExec(writeSide); } ////////////////////////////////////////////////////////////////////// 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() { kill(); } void Pid::operator =(pid_t pid) { if (this->pid != pid) kill(); this->pid = pid; killSignal = SIGKILL; // reset signal to default } Pid::operator pid_t() { return pid; } void Pid::kill(bool quiet) { if (pid == -1 || pid == 0) return; if (!quiet) printMsg(lvlError, format("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) printMsg(lvlError, (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) { printMsg(lvlError, (SysError(format("waiting for process %1%") % pid).msg())); break; } } pid = -1; } int Pid::wait(bool block) { assert(pid != -1); while (1) { int status; int res = waitpid(pid, &status, block ? 0 : WNOHANG); if (res == pid) { pid = -1; return status; } if (res == 0 && !block) return -1; 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; } void killUser(uid_t uid) { debug(format("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; #elif __GNU__ /* Killing all a user's processes using PID=-1 does currently not work on the Hurd. */ if (kill(getpid(), SIGKILL) == 0) break; #else if (kill(-1, SIGKILL) == 0) break; #endif if (errno == ESRCH) break; /* no more processes */ if (errno != EINTR) throw SysError(format("cannot kill processes for uid `%1%'") % uid); } _exit(0); }); int status = pid.wait(true); #if __GNU__ /* When the child killed itself, status = SIGKILL. */ if (status == SIGKILL) return; #endif 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'. */ } ////////////////////////////////////////////////////////////////////// pid_t startProcess(std::function fun, bool dieWithParent, const string & errorPrefix, bool runExitHandlers) { pid_t pid = fork(); if (pid == -1) throw SysError("unable to fork"); if (pid == 0) { _writeToStderr = 0; try { #if __linux__ if (dieWithParent && prctl(PR_SET_PDEATHSIG, SIGKILL) == -1) throw SysError("setting death signal"); #endif restoreAffinity(); fun(); } catch (std::exception & e) { try { std::cerr << errorPrefix << e.what() << "\n"; } catch (...) { } } catch (...) { } if (runExitHandlers) exit(1); else _exit(1); } return pid; } std::vector stringsToCharPtrs(const Strings & ss) { std::vector res; for (auto & s : ss) res.push_back((char *) s.c_str()); res.push_back(0); return res; } string runProgram(Path program, bool searchPath, const Strings & args) { checkInterrupt(); /* Create a pipe. */ Pipe pipe; pipe.create(); /* Fork. */ Pid pid = startProcess([&]() { if (dup2(pipe.writeSide, STDOUT_FILENO) == -1) throw SysError("dupping stdout"); Strings args_(args); args_.push_front(program); if (searchPath) execvp(program.c_str(), stringsToCharPtrs(args_).data()); else execv(program.c_str(), stringsToCharPtrs(args_).data()); throw SysError(format("executing `%1%'") % program); }); pipe.writeSide.close(); string result = drainFD(pipe.readSide); /* Wait for the child to finish. */ int status = pid.wait(true); if (!statusOk(status)) throw ExecError(format("program `%1%' %2%") % program % statusToString(status)); return result; } void closeMostFDs(const set & exceptions) { int maxFD = 0; maxFD = sysconf(_SC_OPEN_MAX); for (int fd = 0; fd < maxFD; ++fd) if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO && exceptions.find(fd) == exceptions.end()) 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"); } ////////////////////////////////////////////////////////////////////// volatile sig_atomic_t _isInterrupted = 0; void _interrupted() { /* Block user interrupts while an exception is being handled. Throwing an exception while another exception is being handled kills the program! */ if (!std::uncaught_exception()) { _isInterrupted = 0; throw Interrupted("interrupted by the user"); } } ////////////////////////////////////////////////////////////////////// template C tokenizeString(const string & s, const string & separators) { C result; string::size_type pos = s.find_first_not_of(separators, 0); while (pos != string::npos) { string::size_type end = s.find_first_of(separators, pos + 1); if (end == string::npos) end = s.size(); string token(s, pos, end - pos); result.insert(result.end(), token); pos = s.find_first_not_of(separators, end); } return result; } template Strings tokenizeString(const string & s, const string & separators); template StringSet tokenizeString(const string & s, const string & separators); template vector tokenizeString(const string & s, const string & separators); string concatStringsSep(const string & sep, const Strings & ss) { string s; foreach (Strings::const_iterator, i, ss) { if (s.size() != 0) s += sep; s += *i; } return s; } string concatStringsSep(const string & sep, const StringSet & ss) { string s; foreach (StringSet::const_iterator, i, ss) { if (s.size() != 0) s += sep; s += *i; } return s; } string chomp(const string & s) { size_t i = s.find_last_not_of(" \n\r\t"); return i == string::npos ? "" : string(s, 0, i + 1); } string statusToString(int status) { if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { if (WIFEXITED(status)) return (format("failed with exit code %1%") % WEXITSTATUS(status)).str(); else 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; } bool hasSuffix(const string & s, const string & suffix) { return s.size() >= suffix.size() && string(s, s.size() - suffix.size()) == suffix; } void expect(std::istream & str, const string & s) { char s2[s.size()]; str.read(s2, s.size()); if (string(s2, s.size()) != s) throw FormatError(format("expected string `%1%'") % s); } string parseString(std::istream & str) { string res; expect(str, "\""); int c; while ((c = str.get()) != '"') if (c == '\\') { c = str.get(); if (c == 'n') res += '\n'; else if (c == 'r') res += '\r'; else if (c == 't') res += '\t'; else res += c; } else res += c; return res; } bool endOfList(std::istream & str) { if (str.peek() == ',') { str.get(); return false; } if (str.peek() == ']') { str.get(); return true; } return false; } void ignoreException() { try { throw; } catch (std::exception & e) { printMsg(lvlError, format("error (ignored): %1%") % e.what()); } } static const string pathNullDevice = "/dev/null"; /* Common initialisation performed in child processes. */ void commonChildInit(Pipe & logPipe) { /* Put the child in a separate session (and thus a separate process group) so that it has no controlling terminal (meaning that e.g. ssh cannot open /dev/tty) and it doesn't receive terminal signals. */ if (setsid() == -1) throw SysError(format("creating a new session")); /* Dup the write side of the logger pipe into stderr. */ if (dup2(logPipe.writeSide, STDERR_FILENO) == -1) throw SysError("cannot pipe standard error into log file"); /* Dup stderr to stdout. */ if (dup2(STDERR_FILENO, STDOUT_FILENO) == -1) throw SysError("cannot dup stderr into stdout"); /* Reroute stdin to /dev/null. */ int fdDevNull = open(pathNullDevice.c_str(), O_RDWR); if (fdDevNull == -1) throw SysError(format("cannot open `%1%'") % pathNullDevice); if (dup2(fdDevNull, STDIN_FILENO) == -1) throw SysError("cannot dup null device into stdin"); close(fdDevNull); } ////////////////////////////////////////////////////////////////////// Agent::Agent(const string &command, const Strings &args, const std::map &env) { debug(format("starting agent '%1%'") % command); /* Create a pipe to get the output of the child. */ fromAgent.create(); /* Create the communication pipes. */ toAgent.create(); /* Create a pipe to get the output of the builder. */ builderOut.create(); /* Fork the hook. */ pid = startProcess([&]() { commonChildInit(fromAgent); for (auto pair: env) { setenv(pair.first.c_str(), pair.second.c_str(), 1); } if (chdir("/") == -1) throw SysError("changing into `/"); /* Dup the communication pipes. */ if (dup2(toAgent.readSide, STDIN_FILENO) == -1) throw SysError("dupping to-hook read side"); /* Use fd 4 for the builder's stdout/stderr. */ if (dup2(builderOut.writeSide, 4) == -1) throw SysError("dupping builder's stdout/stderr"); Strings allArgs; allArgs.push_back(command); allArgs.insert(allArgs.end(), args.begin(), args.end()); // append execv(command.c_str(), stringsToCharPtrs(allArgs).data()); throw SysError(format("executing `%1%'") % command); }); pid.setSeparatePG(true); fromAgent.writeSide.close(); toAgent.readSide.close(); } Agent::~Agent() { try { toAgent.writeSide.close(); pid.kill(true); } catch (...) { ignoreException(); } } }