// system headers #include #include #include #include #include #include #include #include #include // local headers #include "includes/process.h" #include "includes/subprocess.h" #include "includes/assert.h" // shorter than using namespace ... using namespace linuxdeploy::subprocess; int process::pid() const { return child_pid_; } int process::stdout_fd() const { return stdout_fd_; } int process::stderr_fd() const { return stderr_fd_; } process::process(std::initializer_list args, const subprocess_env_map_t& env) : process(std::vector(args), env) {} process::process(const std::vector& args, const subprocess_env_map_t& env) { // preconditions util::assert::assert_not_empty(args); // pipes for both stdout and stderr // the order is, as seen from the child: [read, write] int stdout_pipe_fds[2]; int stderr_pipe_fds[2]; // FIXME: for debugging of #150 auto create_pipe = [](int fds[]) { const auto rv = pipe(fds); if (rv != 0) { const auto error = errno; throw std::logic_error("failed to create pipe: " + std::string(strerror(error))); } }; // create actual pipes create_pipe(stdout_pipe_fds); create_pipe(stderr_pipe_fds); // create child process child_pid_ = fork(); if (child_pid_ < 0) { throw std::runtime_error{"fork() failed"}; } if (child_pid_ == 0) { // we're in the child process // first step: close the read end of both pipes close_pipe_fd_(stdout_pipe_fds[READ_END_]); close_pipe_fd_(stderr_pipe_fds[READ_END_]); auto connect_fd = [](int fd, int fileno) { for (;;) { if (dup2(fd, fileno) == -1) { const auto error = errno; if (error != EINTR) { throw std::logic_error{"failed to connect pipes: " + std::string(strerror(error))}; } continue; } break; } }; connect_fd(stdout_pipe_fds[WRITE_END_], STDOUT_FILENO); connect_fd(stderr_pipe_fds[WRITE_END_], STDERR_FILENO); // now, we also have to close the write end of both pipes close_pipe_fd_(stdout_pipe_fds[WRITE_END_]); close_pipe_fd_(stderr_pipe_fds[WRITE_END_]); // prepare arguments for exec* auto exec_args = make_args_vector_(args); auto exec_env = make_env_vector_(env); // call subprocess execvpe(args.front().c_str(), exec_args.data(), exec_env.data()); // only reached if exec* fails // clean up memory if exec should ever return // prevents memleaks if the exception below would be handled by a caller auto deleter = [](char* ptr) { free(ptr); ptr = nullptr; }; std::for_each(exec_args.begin(), exec_args.end(), deleter); std::for_each(exec_env.begin(), exec_env.end(), deleter); throw std::runtime_error{"exec() failed: " + std::string(strerror(errno))}; } // parent code // we do not intend to write to the processes close_pipe_fd_(stdout_pipe_fds[WRITE_END_]); close_pipe_fd_(stderr_pipe_fds[WRITE_END_]); // store file descriptors stdout_fd_ = stdout_pipe_fds[READ_END_]; stderr_fd_ = stderr_pipe_fds[READ_END_]; } int process::close() { if (!exited_) { close_pipe_fd_(stdout_fd_); stdout_fd_ = -1; close_pipe_fd_(stderr_fd_); stderr_fd_ = -1; { int status; if (waitpid(child_pid_, &status, 0) == -1) { throw std::logic_error{"waitpid() failed"}; } exited_ = true; exit_code_ = check_waitpid_status_(status); } } return exit_code_; } process::~process() { (void) close(); } std::vector process::make_args_vector_(const std::vector& args) { std::vector rv{}; rv.reserve(args.size()); for (const auto& arg : args) { rv.emplace_back(strdup(arg.c_str())); } // execv* want a nullptr-terminated array rv.emplace_back(nullptr); return rv; } std::vector process::make_env_vector_(const subprocess_env_map_t& env) { std::vector rv; // first, copy existing environment // we cannot reserve space in the vector unfortunately, as we don't know the size of environ before the iteration if (environ != nullptr) { for (auto** current_env_var = environ; *current_env_var != nullptr; ++current_env_var) { rv.emplace_back(strdup(*current_env_var)); } } // add own environment variables, overwriting existing ones if necessary for (const auto& env_var : env) { const auto& key = env_var.first; const auto& value = env_var.second; auto predicate = [&key](char* existing_env_var) { char* equal_sign = strstr(existing_env_var, "="); if (equal_sign == nullptr) { throw std::runtime_error{"no equal sign in environment variable"}; } return strncmp(existing_env_var, key.c_str(), std::distance(equal_sign, existing_env_var)) == 0; }; // delete existing env var, if any rv.erase(std::remove_if(rv.begin(), rv.end(), predicate), rv.end()); // insert new value std::ostringstream oss; oss << key; oss << "="; oss << value; rv.emplace_back(strdup(oss.str().c_str())); } // exec*e want a nullptr-terminated array rv.emplace_back(nullptr); return rv; } void process::kill(int signal) const { if (::kill(child_pid_, signal) != 0) { throw std::logic_error{"failed to kill child process"}; } if (waitpid(child_pid_, nullptr, 0)) { throw std::logic_error{"failed to wait for killed child"}; } } bool process::is_running() { if (exited_) { return false; } int status; auto result = waitpid(child_pid_, &status, WNOHANG); if (result == 0) { return true; } if (result == child_pid_) { // TODO: extract the following lines from both this method and close() to eliminate duplicate code close_pipe_fd_(stdout_fd_); stdout_fd_ = -1; close_pipe_fd_(stderr_fd_); stderr_fd_ = -1; exited_ = true; exit_code_ = check_waitpid_status_(status); return false; } if (result < 0) { // TODO: check errno == ECHILD throw std::logic_error{"waitpid() failed: " + std::string(strerror(errno))}; } // can only happen if waitpid() returns an unknown process ID throw std::logic_error{"unknown error occured"}; } int process::check_waitpid_status_(int status) { if (WIFSIGNALED(status) != 0) { // TODO: consider treating child exit caused by signals separately return WTERMSIG(status); } else if (WIFEXITED(status) != 0) { return WEXITSTATUS(status); } throw std::logic_error{"unknown child process state"}; } void process::close_pipe_fd_(int fd) { const auto rv = ::close(fd); if (rv != 0) { const auto error = errno; throw std::logic_error("failed to close pipe fd: " + std::string(strerror(error))); } }