allow path lookup for pipestream
[libi2ncommon] / src / pipestream.cpp
index d6bf13a..66ee39b 100644 (file)
@@ -24,22 +24,317 @@ on this file might be covered by the GNU General Public License.
     copyright            : (C) 2001 by Intra2net AG
  ***************************************************************************/
 
+#include <errno.h>
 #include <stdio.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <unistd.h>
 
-#include <string>
 #include <streambuf>
 #include <istream>
 #include <ostream>
 #include <cstdio>
+#include <boost/foreach.hpp>
+#include <boost/shared_array.hpp>
 
 #include "exception.hxx"
+#include "stringfunc.hxx"
 #include "pipestream.hxx"
 
-inpipebuf::inpipebuf(const std::string& command)
+/** @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 path)
 {
-    status_set = NULL;
-    exit_status = NULL;
+    std::string output;
+
+    bool exit_set = false;
+    int exit_status_waitpid;
+
+    // set the results to false until we are sure we have proper values
+    rescode.normal_exit = false;
+    rescode.terminated_by_signal = false;
+
+    try
+    {
+        {
+            inpipestream ips(command, out, err, path);
+
+            ips.store_exit_status(&exit_set, &exit_status_waitpid);
+
+            char buffer[2048];
+            while (ips.good())
+            {
+                ips.read(buffer, sizeof(buffer));
+                output.append(buffer, ips.gcount());
+            }
+        }
+
+        // exit_status_waitpid only valid after destruction of the inpipestream
+
+        if (exit_set)
+        {
+            rescode.normal_exit = WIFEXITED(exit_status_waitpid);
+            if (rescode.normal_exit)
+                rescode.return_code = WEXITSTATUS(exit_status_waitpid);
+
+            rescode.terminated_by_signal = WIFSIGNALED(exit_status_waitpid);
+            if (rescode.terminated_by_signal)
+                rescode.signal = WTERMSIG(exit_status_waitpid);
+        }
+    }
+    catch (pipestream_error &e)
+    {
+        rescode.error_message = e.what();
+    }
+
+    return output;
+}
+
+/** @brief      Instantiation of \c capture_exec for STL string arguments.
+ *              Caveat emptor: this will cause the backing stream to use \c
+ *              popen(3). To avoid shelling out, please refer to one of the
+ *              variants that allow passing an argument list.
+ *
+ *  @param command    String specifying the shell expression to be executed.
+ *  @param res        (Out parameter) Store information about the termination
+ *                    state in this struct.
+ *
+ *  @returns          Result of \c stdout. Note that due to the use of \c
+ *                    popen, the correct way to collect stderr output as
+ *                    well is to use shell redirection inside the expression
+ *                    passed.
+ */
+std::string capture_exec (const std::string &command, ExecResult &res)
+{ return capture_exec<const std::string &>(command, res, true, false, false); }
+
+/** @brief      Instantiation of \c capture_exec for argument lists. The
+ *              pipestream used to run the command will not shell out.
+ *              One of \c out or \c err must be set.
+ *
+ *  @param command    List of \c char* specifying the \c argv array of the
+ *                    command to run. Note that the binary to executed is
+ *                    assumed to be present at index 0 and that the input
+ *                    is properly \c NULL terminated.
+ *  @param res        (Out parameter) Store information about the termination
+ *                    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, const bool path)
+{ return capture_exec<const char *const *>(command, res, out, err, path); }
+
+/** @brief      Instantiation of \c capture_exec for argument lists. The
+ *              pipestream used to run the command will not shell out.
+ *              One of \c out or \c err must be set.
+ *
+ *  @param command    String vector specifying the \c argv array of the
+ *                    command to run. Note that the binary to executed is
+ *                    assumed to be present at index 0.
+ *  @param res        (Out parameter) Store information about the termination
+ *                    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 path)
+{
+    return capture_exec<const std::vector<std::string> &>
+        (command, res, out, err, path);
+}
+
+#define PIPE_CTOR_FAIL(where) \
+    do { \
+        throw EXCEPTION (pipestream_error, \
+                         std::string (where) + ": error " \
+                         + I2n::to_string (errno) \
+                         + " (" + std::string (strerror (errno)) + ")"); \
+    } while (0)
+
+/** @brief      Convert a string vector to a refcounted \c char**
+ *              that is \c NULL terminated for use with e. g. \c execve(2).
+ *
+ *  @param command    List of arguments including the binary at index 0.
+ *
+ *  @returns          A \c boost::shared_array of pointers to the
+ *                    arguments plus a trailing \c NULL. Note that
+ *                    while the array itself is refcounted, the
+ *                    pointees are assumed owned by the caller and
+ *                    *not copyied*. I. e. they lose validity if the
+ *                    original strings are freed.
+ */
+static boost::shared_array <char *>
+mk_argv (const std::vector<std::string> &command)
+{
+    char **ret = NULL;
+
+    try {
+        ret = new char *[command.size () * sizeof (ret[0]) + 1];
+    } catch (std::bad_alloc &) {
+        return boost::shared_array<char *> ();
+    }
+
+    size_t cur = 0;
+    BOOST_FOREACH(const std::string &arg, command) {
+        /*
+         * Casting away constness is safe since the data is always
+         * kept alive until after exec().
+         */
+        ret [cur++] = const_cast<char *> (arg.c_str ());
+    }
+
+    ret [cur] = NULL;
+
+    return boost::shared_array<char *> (ret);
+}
+
+/** @brief      Helper aggregating common code for the shell-free ctors.
+ *
+ *  @param argv       Argument list prepared for \c execve(2).
+ *  @param out        Whether to capture \c stdout.
+ *  @param err        Whether to capture \c stderr.
+ *
+ *  @returns          A \c FILE* handle for streaming if successful, \c NULL
+ *                    otherwise.
+ */
+std::pair <pid_t, FILE *>
+inpipebuf::init_without_shell (const char *const *argv,
+                               const bool out,
+                               const bool err,
+                               const bool path) const
+{
+    FILE *pipeobj = NULL;
+    int pipefd [2];
+
+    if (!out && !err) {
+        errno = EINVAL;
+        PIPE_CTOR_FAIL("ctor");
+    }
+
+    errno = 0;
+    if (::pipe (pipefd) == -1) {
+        PIPE_CTOR_FAIL("pipe");
+    }
+
+    errno = 0;
+    pid_t childpid = fork ();
+    switch (childpid) {
+        case -1: {
+            PIPE_CTOR_FAIL("fork");
+            break;
+        }
+        case 0: {
+            close (pipefd [0]);
+
+            if (!out) {
+                close (STDOUT_FILENO);
+            } else if (dup2 (pipefd[1], STDOUT_FILENO) == -1) {
+                fprintf(stderr, "dup2/stdout: %m\n");
+                exit(EXIT_FAILURE);
+            }
 
+            if (!err) {
+                close (STDERR_FILENO);
+            } else if (dup2 (pipefd[1], STDERR_FILENO) == -1) {
+                fprintf(stderr, "dup2/stderr: %m\n");
+                exit(EXIT_FAILURE);
+            }
+
+            close (pipefd [1]);
+
+            errno = 0;
+            if (path) {
+                execvpe (argv [0], const_cast <char *const *>(argv), environ);
+            } else {
+                execve (argv [0], const_cast <char *const *>(argv), NULL);
+            }
+            exit(EXIT_FAILURE);
+            break;
+        }
+        default: {
+            close (pipefd [1]);
+
+            errno = 0;
+            if ((pipeobj = fdopen (pipefd [0], "r")) == NULL) {
+                PIPE_CTOR_FAIL("fdopen");
+            }
+            break;
+        }
+    }
+
+    return std::make_pair (childpid, pipeobj);
+}
+
+inpipebuf::inpipebuf(const char *const *command,
+                     const bool out,
+                     const bool err,
+                     const bool path)
+    : pipe (NULL) /* brr: shadowing global ident */
+    , pid (-1)
+    , status_set (NULL)
+    , exit_status (NULL)
+{
+    if (command == NULL || command [0] == NULL) {
+        PIPE_CTOR_FAIL("command");
+    }
+
+    std::pair <pid_t, FILE *> tmp =
+        this->init_without_shell (command, out, err, path);
+    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 path)
+    : pipe (NULL) /* brr: shadowing global ident */
+    , pid (-1)
+    , status_set (NULL)
+    , exit_status (NULL)
+{
+    if (command.empty ()) {
+        PIPE_CTOR_FAIL("command");
+    }
+
+    const boost::shared_array <char *> argv = mk_argv (command);
+    if (!argv) {
+        PIPE_CTOR_FAIL("malloc");
+    }
+
+    std::pair <pid_t, FILE *> tmp =
+        this->init_without_shell (argv.get (), out, err, path);
+    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_path)
+    : pid (-1)
+    , 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");
@@ -51,11 +346,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) {
-            *status_set = true;
-            *exit_status = pclose_exit;
+        if (this->pid == -1)
+        {
+            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;
@@ -86,43 +417,6 @@ inpipebuf::int_type inpipebuf::underflow()
     return traits_type::to_int_type(*gptr());
 }
 
-/** @brief runs command and returns it's output as string
- *  @param command the full command with all parameters
- *  @param exit_status the full exit status, use WEXITSTATUS to get the "regular" return code
- *  @returns the output (stderr) of the called program
- */
-std::string pipe_to_string(const std::string& command, std::string *error, int *_exit_status)
-{
-    std::string result;
-    bool exit_set;
-
-    try
-    {
-        inpipestream ips(command);
-
-        ips.store_exit_status(&exit_set, _exit_status);
-
-        char buffer[2048];
-        while (ips.good())
-        {
-            ips.read(buffer, sizeof(buffer));
-            result.append(buffer, ips.gcount());
-        }
-    }
-    catch (pipestream_error &e)
-    {
-        if (error)
-            *error=e.what();
-        return "";
-    }
-    catch(...)
-    {
-        throw;
-    }
-
-    return result;
-}
-
 outpipebuf::outpipebuf(const std::string& command)
 {
     status_set = NULL;
@@ -138,8 +432,10 @@ outpipebuf::~outpipebuf()
     if (pipe != NULL) {
         int pclose_exit = pclose (pipe);
 
-        if (exit_status && pclose_exit != -1) {
-            *status_set = true;
+        if (exit_status && pclose_exit != -1)
+        {
+            if (status_set)
+                *status_set = true;
             *exit_status = pclose_exit;
         }