commit fc24e168da60e543a18767bfcf600c848bba0d90 Author: arunmu Date: Tue Mar 15 11:35:07 2016 +0530 First commit of subprocess-cpp diff --git a/main b/main new file mode 100755 index 0000000..103c910 Binary files /dev/null and b/main differ diff --git a/main.cc b/main.cc new file mode 100755 index 0000000..e80c13d --- /dev/null +++ b/main.cc @@ -0,0 +1,9 @@ +#include "subprocess.hpp" + +using namespace subprocess; + +int main() { + auto p = Popen({"./script.sh"}, + output{"out.txt"}); + return 0; +} diff --git a/s.py b/s.py new file mode 100644 index 0000000..35dfefb --- /dev/null +++ b/s.py @@ -0,0 +1,3 @@ +import subprocess + +p = subprocess.Popen(["echo", "what the hell"]) diff --git a/script.sh b/script.sh new file mode 100755 index 0000000..419d311 --- /dev/null +++ b/script.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo "Hello, subprocess" diff --git a/subprocess.hpp b/subprocess.hpp new file mode 100755 index 0000000..7ceec2d --- /dev/null +++ b/subprocess.hpp @@ -0,0 +1,541 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { + #include + #include + #include + #include +} + +namespace subprocess { + +// Max buffer size allocated on stack for read error +// from pipe +static const size_t SP_MAX_ERR_BUF_SIZ = 1024; + +// Exception Classes +class CalledProcessError: public std::runtime_error +{ +public: + CalledProcessError(const std::string& error_msg): + std::runtime_error(error_msg) + {} +}; + + + +class OSError: public std::runtime_error +{ +public: + OSError(const std::string& err_msg, int err_code): + std::runtime_error( err_msg + " : " + std::strerror(err_code) ) + {} +}; + +//-------------------------------------------------------------------- + +namespace util +{ + std::vector + split(const std::string& str, const std::string& delims=" \t") + { + std::vector res; + size_t init = 0; + + while (true) { + auto pos = str.find_first_of(delims, init); + if (pos == std::string::npos) { + res.emplace_back(str.substr(init, str.length())); + break; + } + res.emplace_back(str.substr(init, pos - init)); + pos++; + init = pos; + } + + return res; + } + + std::string join(const std::vector& vec, + const std::string& sep = " ") + { + std::string res; + for (auto& elem : vec) res.append(elem + sep); + res.erase(--res.end()); + return res; + } + + void set_clo_on_exec(int fd, bool set = true) + { + int flags = fcntl(fd, F_GETFL, 0); + if (set) flags |= FD_CLOEXEC; + else flags &= ~FD_CLOEXEC; + //TODO: should check for errors + fcntl(fd, F_SETFL, flags); + } + + std::pair pipe_cloexec() + { + int pipe_fds[2]; + int res = pipe(pipe_fds); + if (res) { + throw OSError("pipe failure", errno); + } + + set_clo_on_exec(pipe_fds[0]); + set_clo_on_exec(pipe_fds[1]); + + return std::make_pair(pipe_fds[0], pipe_fds[1]); + } + + + int write_n(int fd, const char* buf, size_t length) + { + int nwritten = 0; + while (nwritten < length) { + int written = write(fd, buf + nwritten, length - nwritten); + if (written == -1) return -1; + nwritten += written; + } + return nwritten; + } + + int read_atmost_n(int fd, char* buf, size_t read_upto) + { + int rbytes = 0; + int eintr_cnter = 0; + + while (1) { + int read_bytes = read(fd, buf, read_upto); + if (read_bytes == -1) { + if (errno == EINTR) { + if (eintr_cnter >= 50) return -1; + eintr_cnter++; + continue; + } + return -1; + } + if (read_bytes == 0) return rbytes; + + rbytes += read_bytes; + } + return rbytes; + } + + int wait_for_child_exit(int pid) + { + int status; + int ret = -1; + while (1) { + ret = waitpid(pid, &status, WNOHANG); + if (ret == -1) break; + if (ret == 0) continue; + return pid; + } + + return ret; + } + + +}; // end namespace util + + +// Popen Arguments +struct bufsiz { int bufsiz; }; +struct defer_spawn { bool defer; }; + +struct string_arg +{ + string_arg(const char* arg): arg_value(arg) {} + string_arg(std::string&& arg): arg_value(std::move(arg)) {} + string_arg(std::string arg): arg_value(std::move(arg)) {} + std::string arg_value; +}; + +struct executable: string_arg +{ + template + executable(T&& arg): string_arg(std::forward(arg)) {} +}; + +struct cwd: string_arg +{ + template + cwd(T&& arg): string_arg(std::forward(arg)) {} +}; + +struct environment +{ + environment(std::map&& env): + env_(std::move(env)) {} + environment(const std::map& env): + env_(env) {} + std::map env_; +}; + +enum IOTYPE { + STDIN = 0, + STDOUT, + STDERR, + PIPE, +}; + +struct input +{ + input(int fd): rd_ch_(fd) {} + + input(const char* filename) { + int fd = open(filename, O_RDONLY); + if (fd == -1) throw OSError("File not found: ", errno); + rd_ch_ = fd; + } + input(IOTYPE typ) { + assert (typ == PIPE); + std::tie(rd_ch_, wr_ch_) = util::pipe_cloexec(); + } + + int rd_ch_ = -1; + int wr_ch_ = -1; +}; + +struct output +{ + output(int fd): wr_ch_(fd) {} + + output(const char* filename) { + int fd = open(filename, O_APPEND | O_CREAT | O_RDWR, 0640); + if (fd == -1) throw OSError("File not found: ", errno); + wr_ch_ = fd; + } + output(IOTYPE typ) { + assert (typ == PIPE); + std::tie(rd_ch_, wr_ch_) = util::pipe_cloexec(); + } + + int rd_ch_ = -1; + int wr_ch_ = -1; +}; + +struct error { + error(int fd): err_ch_(fd) {} + + error(const char* filename) { + int fd = open(filename, O_RDWR|O_APPEND); + if (fd == -1) throw OSError("File not found: ", errno); + err_ch_ = fd; + } + + int err_ch_ = -1; +}; + +// ~~~~ End Popen Args ~~~~ + +// Fwd Decl. +class Popen; + +namespace detail { + +struct ArgumentDeducer +{ + ArgumentDeducer(Popen* p): popen_(p) {} + + template + void set_options(T&& arg) + { + set_option(std::forward(arg)); + } + + template + void set_options(T&& farg, Args&&... rem_args) + { + set_option(std::forward(farg)); + set_options(std::forward(rem_args)...); + } + + void set_option(executable&& exe); + + void set_option(cwd&& cwdir); + + void set_option(bufsiz&& bsiz); + + void set_option(environment&& env); + + void set_option(defer_spawn&& defer); + + void set_option(input&& inp); + + void set_option(output&& out); + + void set_option(error&& err); + +private: + Popen* popen_ = nullptr; +}; + +}; // end namespace detail + + +class Popen +{ +public: + friend class detail::ArgumentDeducer; + + template + Popen(const std::string& cmd_args, Args&& ...args): + args_(cmd_args) + { + vargs_ = util::split(cmd_args); + init_args(std::forward(args)...); + + if (!defer_process_start_) execute_process(); + } + + template + Popen(std::initializer_list cmd_args, Args&& ...args) + { + vargs_.insert(vargs_.end(), cmd_args.begin(), cmd_args.end()); + init_args(std::forward(args)...); + if (!defer_process_start_) execute_process(); + } + + void start_process() throw (CalledProcessError, OSError) + { + // The process was started/tried to be started + // in the constructor itself. + // For explicitly calling this API to start the + // process, 'defer_spawn' argument must be set to + // true in the constructor. + if (!defer_process_start_) { + assert (0); + return; + } + execute_process(); + } + +private: + template + void init_args(Args&&... args); + void populate_c_argv(); + void execute_process() throw (CalledProcessError, OSError); + +private: + bool defer_process_start_ = false; + int bufsiz_ = 0; + std::string exe_name_; + std::string cwd_; + std::map env_; + + FILE* input_ = nullptr; + FILE* output_ = nullptr; + FILE* error_ = nullptr; + + // Pipes for communicating with child + + // Emulates stdin + int write_to_child_ = -1; // Parent owned descriptor + int read_from_parent_ = -1; // Child owned descriptor + + // Emulates stdout + int write_to_parent_ = -1; // Child owned descriptor + int read_from_child_ = -1; // Parent owned descriptor + + // Emulates stderr + int err_write_ = -1; // Write error to parent (Child owned) + int err_read_ = -1; // Read error from child (Parent owned) + + // Command in string format + std::string args_; + // Comamnd provided as sequence + std::vector vargs_; + std::vector cargv_; + + // Pid of the child process + int child_pid_ = -1; +}; + +template <> +void Popen::init_args() { + populate_c_argv(); +} + +template +void Popen::init_args(Args&&... args) +{ + detail::ArgumentDeducer argd(this); + argd.set_options(std::forward(args)...); +} + +void Popen::populate_c_argv() +{ + cargv_.reserve(vargs_.size()); + for (auto& arg : vargs_) cargv_.push_back(&arg[0]); +} + + +void Popen::execute_process() throw (CalledProcessError, OSError) +{ + int err_rd_pipe, err_wr_pipe; + int sys_ret = -1; + + std::tie(err_rd_pipe, err_wr_pipe) = util::pipe_cloexec(); + + if (!exe_name_.length()) { + exe_name_ = vargs_[0]; + } + + child_pid_ = fork(); + + if (child_pid_ < 0) { + close (err_rd_pipe); + close (err_wr_pipe); + throw OSError("fork failed", errno); + } + + if (child_pid_ == 0) {/* Child Part of Code */ + try { + // Close descriptors belonging to parent + if (write_to_child_ != -1) close(write_to_child_); + if (read_from_child_ != -1) close(read_from_child_); + if (err_read_ != -1) close(err_read_); + close(err_rd_pipe); + + // Make the child owned descriptors as the + // stdin, stdout and stderr for the child process + auto _dup2_ = [](int fd, int to_fd) { + if (fd == to_fd) { + // dup2 syscall does not reset the + // CLOEXEC flag if the descriptors + // provided to it are same. + // But, we need to reset the CLOEXEC + // flag as the provided descriptors + // are now going to be the standard + // input, output and error + util::set_clo_on_exec(fd, false); + } else if(fd != -1) { + int res = dup2(fd, to_fd); + if (res == -1) throw OSError("dup2 failed", errno); + } + }; + + // Create the standard streams + _dup2_(read_from_parent_, 0); // Input stream + _dup2_(write_to_parent_, 1); // Output stream + _dup2_(err_write_, 2); // Error stream + + // Close the extra sockets + if (read_from_parent_ != -1 && read_from_parent_ > 2) close(read_from_parent_); + if (write_to_parent_ != -1 && write_to_parent_ > 2) close(write_to_parent_); + if (err_write_ != -1 && err_write_ > 2) close(err_write_); + + // Close all the inherited fd's except the error write pipe + // TODO: Check for the option + int max_fd = sysconf(_SC_OPEN_MAX); + if (max_fd == -1) throw OSError("sysconf failed", errno); + for (int i = 3; i < max_fd; i++) { + if (i == err_wr_pipe) continue; + close(i); + } + + // Change the working directory if provided + if (cwd_.length()) { + sys_ret = chdir(cwd_.c_str()); + if (sys_ret == -1) throw OSError("chdir failed", errno); + } + + // Replace the current image with the executable + if (env_.size()) { + for (auto& kv : env_) setenv(kv.first.c_str(), kv.second.c_str(), 1); + sys_ret = execvp(exe_name_.c_str(), cargv_.data()); + } else { + sys_ret = execvp(exe_name_.c_str(), cargv_.data()); + } + if (sys_ret == -1) throw OSError("execve failed", errno); + + } catch (const OSError& exp) { + // Just write the exception message + // TODO: Give back stack trace ? + std::string err_msg(exp.what()); + //ATTN: Can we do something on error here ? + util::write_n(err_wr_pipe, err_msg.c_str(), err_msg.length()); + close(err_wr_pipe); + } + + // Calling application would not get this + // exit failure + exit (EXIT_FAILURE); + } else { // Parent code + // Close the err_wr_pipe + // Else get stuck forever!! + close (err_wr_pipe); + // Read the error from child if at all + char err_buf[SP_MAX_ERR_BUF_SIZ] = {0,}; + int read_bytes = util::read_atmost_n(err_rd_pipe, err_buf, SP_MAX_ERR_BUF_SIZ); + + close(err_rd_pipe); + + if (read_bytes || strlen(err_buf)) { + // Call waitpid to reap the child process + // waitpid suspends the calling process until the + // child terminates. + sys_ret = util::wait_for_child_exit(child_pid_); + // If the child could not be reaped successfully + // raise that error instead of child error + if (sys_ret == -1) throw OSError("child exit", errno); + + // Throw whatever information we have about child failure + throw CalledProcessError(err_buf); + } + } +} + + +namespace detail { + + void ArgumentDeducer::set_option(executable&& exe) { + popen_->exe_name_ = std::move(exe.arg_value); + } + + void ArgumentDeducer::set_option(cwd&& cwdir) { + popen_->cwd_ = std::move(cwdir.arg_value); + } + + void ArgumentDeducer::set_option(bufsiz&& bsiz) { + popen_->bufsiz_ = bsiz.bufsiz; + } + + void ArgumentDeducer::set_option(environment&& env) { + popen_->env_ = std::move(env.env_); + } + + void ArgumentDeducer::set_option(defer_spawn&& defer) { + popen_->defer_process_start_ = defer.defer; + } + + void ArgumentDeducer::set_option(input&& inp) { + popen_->read_from_parent_ = inp.rd_ch_; + if (inp.wr_ch_ != -1) popen_->write_to_child_ = inp.wr_ch_; + } + + void ArgumentDeducer::set_option(output&& out) { + popen_->write_to_parent_ = out.wr_ch_; + if (out.rd_ch_ != -1) popen_->read_from_child_ = out.rd_ch_; + } + + void ArgumentDeducer::set_option(error&& err) { + } + + +}; // end namespace detail + + +}; diff --git a/test/test.cc b/test/test.cc new file mode 100644 index 0000000..9cd453a --- /dev/null +++ b/test/test.cc @@ -0,0 +1,42 @@ +#include +#include + +struct bufsize { int siz_; }; +struct executable { std::string exe_; }; + +class Ex { +public: + template + Ex(Args&&... args) { + set_options(std::forward(args)...); + } + + template + void set_options(T&& arg) { + set_option(std::forward(arg)); + } + + template + void set_options(T&& first, Args&&... rem_args) { + set_option(std::forward(first)); + set_options(std::forward(rem_args)...); + } + + void set_option(bufsize&& bufs) { + std::cout << "bufsize opt" << std::endl; + bufsiz_ = bufs.siz_; + } + void set_option(executable&& exe) { + std::cout << "exe opt" << std::endl; + exe_name_ = std::move(exe.exe_); + } + +private: + int bufsiz_ = 0; + std::string exe_name_; +}; + +int main() { + Ex e(bufsize{1}, executable{"finger"}); + return 0; +} diff --git a/test/test_split b/test/test_split new file mode 100755 index 0000000..5736605 Binary files /dev/null and b/test/test_split differ diff --git a/test/test_split.cc b/test/test_split.cc new file mode 100644 index 0000000..4de56c2 --- /dev/null +++ b/test/test_split.cc @@ -0,0 +1,42 @@ +#include +#include +#include + +std::vector +split(const std::string& str, const std::string& delims=" \t") +{ + std::vector res; + size_t init = 0; + + while (true) { + auto pos = str.find_first_of(delims, init); + if (pos == std::string::npos) { + res.emplace_back(str.substr(init, str.length())); + break; + } + res.emplace_back(str.substr(init, pos - init)); + pos++; + init = pos; + } + + return res; +} + +std::string join(const std::vector& vec) +{ + std::string res; + for (auto& elem : vec) { + res.append(elem + " "); + } + res.erase(--res.end()); + return res; +} + +int main() { + + auto vec = split ("a b c"); + for (auto elem : vec) { std::cout << elem << std::endl; } + + std::cout << join(vec).length() << std::endl; + return 0; +}