add option to forward environment to pipestream
[libi2ncommon] / src / pipestream.cpp
index e026851..fcd8161 100644 (file)
@@ -27,6 +27,7 @@ on this file might be covered by the GNU General Public License.
 #include <errno.h>
 #include <stdio.h>
 #include <string.h>
+#include <fcntl.h>
 #include <sys/wait.h>
 #include <unistd.h>
 
@@ -44,11 +45,15 @@ on this file might be covered by the GNU General Public License.
 /** @brief runs command and returns it's output as string
  *  @param command the full command with all parameters
  *  @param rescode struct containing the return code, if the program exited normally and so on
+ *  @param out Whether to collect \c stdout.
+ *  @param err Whether to collect \c stderr; combines with \c out.
+ *  @param path Wether to look up the executable in \c $PATH.
  *  @returns the output (stdout) of the called program
  */
 template <typename CmdT>
 std::string capture_exec(CmdT command, ExecResult &rescode,
-                         const bool out, const bool err)
+                         const bool out, const bool err,
+                         const bool path, const bool env)
 {
     std::string output;
 
@@ -62,7 +67,7 @@ std::string capture_exec(CmdT command, ExecResult &rescode,
     try
     {
         {
-            inpipestream ips(command, out, err);
+            inpipestream ips(command, out, err, path, env);
 
             ips.store_exit_status(&exit_set, &exit_status_waitpid);
 
@@ -110,7 +115,7 @@ std::string capture_exec(CmdT command, ExecResult &rescode,
  *                    passed.
  */
 std::string capture_exec (const std::string &command, ExecResult &res)
-{ return capture_exec<const std::string &>(command, res, true, false); }
+{ return capture_exec<const std::string &>(command, res, true, false, false, false); }
 
 /** @brief      Instantiation of \c capture_exec for argument lists. The
  *              pipestream used to run the command will not shell out.
@@ -124,12 +129,14 @@ std::string capture_exec (const std::string &command, ExecResult &res)
  *                    state in this struct.
  *  @param out        Whether to collect \c stdout.
  *  @param err        Whether to collect \c stderr; combines with \c out.
+ *  @param path       Wether to look up the executable in \c $PATH.
  *
  *  @returns          Captured output, combined into one string.
  */
 std::string capture_exec (const char *const *command, ExecResult &res,
-                          const bool out, const bool err)
-{ return capture_exec<const char *const *>(command, res, out, err); }
+                          const bool out, const bool err, const bool path,
+                          const bool env)
+{ return capture_exec<const char *const *>(command, res, out, err, path, env); }
 
 /** @brief      Instantiation of \c capture_exec for argument lists. The
  *              pipestream used to run the command will not shell out.
@@ -142,14 +149,16 @@ std::string capture_exec (const char *const *command, ExecResult &res,
  *                    state in this struct.
  *  @param out        Whether to collect \c stdout.
  *  @param err        Whether to collect \c stderr; combines with \c out.
+ *  @param path       Wether to look up the executable in \c $PATH.
  *
  *  @returns          Captured output, combined into one string.
  */
 std::string capture_exec (const std::vector<std::string> &command, ExecResult &res,
-                          const bool out, const bool err)
+                          const bool out, const bool err, const bool path,
+                          const bool env)
 {
     return capture_exec<const std::vector<std::string> &>
-        (command, res, out, err);
+        (command, res, out, err, path, env);
 }
 
 #define PIPE_CTOR_FAIL(where) \
@@ -197,6 +206,37 @@ mk_argv (const std::vector<std::string> &command)
     return boost::shared_array<char *> (ret);
 }
 
+/** @brief      Helper for redirecting a file descriptor to \c /dev/null.
+ *              This will only acquire an fd the first time it is called
+ *              or if it is called after unsuccessfully attempting to
+ *              acquire one.
+ *
+ *  @param fd         The open file descriptor to operate on.
+ *  @param save_errno Out parameter: stores errno here after a syscall failure.
+ *
+ *  @returns          \c true on success, \c false otherwise (the call to
+ *                    either \c open(2) or \c dup2(2) failed), with errno
+ *                    communicated through saved_errno.
+ */
+static bool
+redirect_devnull (const int fd, int &save_errno)
+{
+    static int nullfd = -1;
+    
+    errno = 0;
+    if (nullfd == -1 && (nullfd = open ("/dev/null", O_RDWR)) == -1) {
+        save_errno = errno;
+        return false;
+    }
+
+    errno = 0;
+    if (dup2 (nullfd, fd) == -1) {
+        save_errno = errno;
+        return false;
+    }
+
+    return true;
+}
 
 /** @brief      Helper aggregating common code for the shell-free ctors.
  *
@@ -207,13 +247,18 @@ mk_argv (const std::vector<std::string> &command)
  *  @returns          A \c FILE* handle for streaming if successful, \c NULL
  *                    otherwise.
  */
-FILE *
+std::pair <pid_t, FILE *>
 inpipebuf::init_without_shell (const char *const *argv,
                                const bool out,
-                               const bool err) const
+                               const bool err,
+                               const bool path,
+                               const bool env) const
 {
     FILE *pipeobj = NULL;
-    int pipefd [2];
+    int pipefd [2]; /* for reading output from the child */
+    int errfd  [2]; /* for determining a successful exec() */
+    sigset_t oldmask, newmask;
+    char *const *envp = env ? environ : NULL;
 
     if (!out && !err) {
         errno = EINVAL;
@@ -221,56 +266,128 @@ inpipebuf::init_without_shell (const char *const *argv,
     }
 
     errno = 0;
-    if (::pipe (pipefd) == -1) {
-        PIPE_CTOR_FAIL("pipe");
+    if (   ::pipe2 (pipefd, O_CLOEXEC) == -1
+        || ::pipe2 (errfd , O_CLOEXEC) == -1) {
+        PIPE_CTOR_FAIL("pipe2");
     }
 
+    sigfillset (&newmask);
+    sigprocmask (SIG_SETMASK, &newmask, &oldmask);
+
     errno = 0;
     pid_t childpid = fork ();
     switch (childpid) {
         case -1: {
+            sigprocmask (SIG_SETMASK, &oldmask, NULL);
             PIPE_CTOR_FAIL("fork");
             break;
         }
         case 0: {
             close (pipefd [0]);
+            close (errfd  [0]);
+
+            fcntl (pipefd [1], F_SETFD, 0);
 
+            int save_errno = 0;
             if (!out) {
-                close (STDOUT_FILENO);
+                if (!redirect_devnull (STDOUT_FILENO, save_errno)) {
+                    (void)write (errfd [1], (char *)&save_errno, sizeof(save_errno));
+                    exit (EXIT_FAILURE);
+                }
             } else if (dup2 (pipefd[1], STDOUT_FILENO) == -1) {
-                PIPE_CTOR_FAIL("dup2/stdout");
+                (void)write (errfd [1], (char *)&save_errno, sizeof(save_errno));
+                exit (EXIT_FAILURE);
             }
 
             if (!err) {
-                close (STDERR_FILENO);
+                if (!redirect_devnull (STDERR_FILENO, save_errno)) {
+                    (void)write (errfd [1], (char *)&save_errno, sizeof(save_errno));
+                    exit (EXIT_FAILURE);
+                }
             } else if (dup2 (pipefd[1], STDERR_FILENO) == -1) {
-                PIPE_CTOR_FAIL("dup2/stderr");
+                (void)write (errfd [1], (char *)&save_errno, sizeof(save_errno));
+                exit (EXIT_FAILURE);
             }
 
+            close (pipefd [1]);
+
+            sigprocmask (SIG_SETMASK, &oldmask, NULL);
+
             errno = 0;
-            if (execve (argv [0], const_cast <char *const *>(argv), NULL) == -1) {
-                PIPE_CTOR_FAIL("exec");
+            if (path) {
+                execvpe (argv [0], const_cast <char *const *>(argv), envp);
+            } else {
+                execve (argv [0], const_cast <char *const *>(argv), envp);
             }
+
+            (void)write (errfd [1], (char *)&errno, sizeof(errno));
+            exit (EXIT_FAILURE);
             break;
         }
         default: {
-            close (pipefd [1]);
-
-            errno = 0;
-            if ((pipeobj = fdopen (pipefd [0], "r")) == NULL) {
-                PIPE_CTOR_FAIL("fdopen");
-            }
             break;
         }
     }
 
-    return pipeobj;
+    close (pipefd [1]);
+    close (errfd  [1]);
+
+    /*
+     * Check whether the child exec()’ed by reading from the error pipe.
+     * The call to read(2) will block, uninterruptible due to signals being
+     * blocked. If all went well, the read(2) will return zero bytes and we can
+     * ditch the error channel.
+     *
+     * Otherwise either the read(2) failed or we actually received something
+     * through the error pipe. Both cases are treated as errors and cause an
+     * exit from the ctor.
+     */
+    char buf [sizeof (errno)];
+    int ret;
+    memset (buf, 0, sizeof (buf));
+    errno = 0;
+    if ((ret = read (errfd [0], buf, sizeof (buf))) != 0) {
+        close (pipefd [0]);
+        close (errfd  [0]);
+        sigprocmask (SIG_SETMASK, &oldmask, NULL);
+        if (ret == - 1) {
+            /* read(2) failed */
+            PIPE_CTOR_FAIL("read");
+        } else {
+            /*
+             * We received data on the error channel indicating the child
+             * process never successfully exec()’ed. We grab the error code
+             * from the buffer and bail.
+             */
+            errno = *((int *)&buf[0]);
+            PIPE_CTOR_FAIL("child failed to exec()");
+        }
+    }
+
+    /*
+     * read(2) yielded zero bytes; it’s safe to use the pipe so close our end
+     * and continue.
+     */
+    close (errfd [0]);
+
+    sigprocmask (SIG_SETMASK, &oldmask, NULL);
+
+    errno = 0;
+    if ((pipeobj = fdopen (pipefd [0], "r")) == NULL) {
+        close (pipefd [0]);
+        PIPE_CTOR_FAIL("fdopen");
+    }
+
+    return std::make_pair (childpid, pipeobj);
 }
 
 inpipebuf::inpipebuf(const char *const *command,
                      const bool out,
-                     const bool err)
+                     const bool err,
+                     const bool path,
+                     const bool env)
     : pipe (NULL) /* brr: shadowing global ident */
+    , pid (-1)
     , status_set (NULL)
     , exit_status (NULL)
 {
@@ -278,15 +395,21 @@ inpipebuf::inpipebuf(const char *const *command,
         PIPE_CTOR_FAIL("command");
     }
 
-    this->pipe = this->init_without_shell (command, out, err);
+    std::pair <pid_t, FILE *> tmp =
+        this->init_without_shell (command, out, err, path, env);
+    this->pid  = tmp.first; /* no std::tie :/ */
+    this->pipe = tmp.second;
 
     setg (&buffer, &buffer, &buffer);
 }
 
 inpipebuf::inpipebuf(const std::vector<std::string> &command,
                      const bool out,
-                     const bool err)
+                     const bool err,
+                     const bool path,
+                     const bool env)
     : pipe (NULL) /* brr: shadowing global ident */
+    , pid (-1)
     , status_set (NULL)
     , exit_status (NULL)
 {
@@ -299,18 +422,23 @@ inpipebuf::inpipebuf(const std::vector<std::string> &command,
         PIPE_CTOR_FAIL("malloc");
     }
 
-    this->pipe = this->init_without_shell (argv.get (), out, err);
+    std::pair <pid_t, FILE *> tmp =
+        this->init_without_shell (argv.get (), out, err, path, env);
+    this->pid  = tmp.first;
+    this->pipe = tmp.second;
 
     setg (&buffer, &buffer, &buffer);
 }
 
 inpipebuf::inpipebuf(const std::string& command,
                      const bool _ignored_out,
-                     const bool _ignored_err)
+                     const bool _ignored_err,
+                     const bool _ignored_path,
+                     const bool _ignored_env)
+    : pid (-1)
+    , status_set (NULL)
+    , exit_status (NULL)
 {
-    status_set = NULL;
-    exit_status = NULL;
-
     pipe = popen (command.c_str(), "r");
     if (pipe == NULL)
         throw EXCEPTION (pipestream_error, "can't open program or permission denied");
@@ -322,13 +450,47 @@ inpipebuf::inpipebuf(const std::string& command,
 inpipebuf::~inpipebuf()
 {
     if (pipe != NULL) {
-        int pclose_exit = pclose (pipe);
+        int status;
 
-        if (exit_status && pclose_exit != -1)
+        if (this->pid == -1)
         {
-            if (status_set)
-                *status_set = true;
-            *exit_status = pclose_exit;
+            errno = 0;
+            status = pclose (pipe);
+            if (status != -1) {
+                if (exit_status != NULL) {
+                    *exit_status = status;
+                    if (status_set != NULL) {
+                        *status_set = true;
+                    }
+                }
+            }
+        }
+        else
+        {
+            errno = 0;
+            status = fclose (pipe);
+            if (status != EOF) {
+                if (exit_status != NULL) {
+                    *exit_status = status; /* might be overwritten below */
+                    if (status_set != NULL) {
+                        *status_set = true;
+                    }
+                }
+            }
+
+            errno = 0;
+            while (waitpid (this->pid, &status, 0) == -1) {
+                if (errno != EINTR) {
+                    status = -1;
+                    break;
+                }
+            }
+            if (status != 0 && exit_status != NULL) {
+                *exit_status = status; /* might overwrite pipe status above */
+                if (status_set != NULL) {
+                    *status_set = true;
+                }
+            }
         }
 
         pipe = NULL;