mirror of
https://github.com/audacity/linuxdeploy.git
synced 2026-04-13 15:19:49 -05:00
Extract process handling into separate class
Needed for plugins' subprocess handling.
This commit is contained in:
71
include/linuxdeploy/subprocess/process.h
Normal file
71
include/linuxdeploy/subprocess/process.h
Normal file
@@ -0,0 +1,71 @@
|
||||
// system headers
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// local headers
|
||||
#include "linuxdeploy/subprocess/subprocess.h"
|
||||
|
||||
namespace linuxdeploy {
|
||||
namespace subprocess {
|
||||
class process {
|
||||
private:
|
||||
// child process ID
|
||||
int child_pid_;
|
||||
|
||||
// pipes to child process's stdout/stderr
|
||||
int stdout_fd_;
|
||||
int stderr_fd_;
|
||||
|
||||
// process exited
|
||||
bool exited_ = false;
|
||||
// exit code -- will be initialized by close()
|
||||
int exit_code_;
|
||||
|
||||
// these constants help make the pipe code more readable
|
||||
static constexpr int READ_END_ = 0, WRITE_END_ = 1;
|
||||
|
||||
static std::vector<char*> make_args_vector_(const std::vector<std::string>& args) ;
|
||||
|
||||
static std::vector<char*> make_env_vector_(const subprocess_env_map_t& env) ;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Create a child process.
|
||||
* @param args parameters for process
|
||||
* @param env additional environment variables (current environment will be copied)
|
||||
*/
|
||||
process(std::initializer_list<std::string> args, const subprocess_env_map_t& env);
|
||||
|
||||
/**
|
||||
* Create a child process.
|
||||
* @param args parameters for process
|
||||
* @param env additional environment variables (current environment will be copied)
|
||||
*/
|
||||
process(const std::vector<std::string>& args, const subprocess_env_map_t& env);
|
||||
|
||||
~process();
|
||||
|
||||
/**
|
||||
* @return child process's ID
|
||||
*/
|
||||
int pid() const;
|
||||
|
||||
/**
|
||||
* @return child process's stdout file descriptor ID
|
||||
*/
|
||||
int stdout_fd() const;
|
||||
|
||||
|
||||
/**
|
||||
* @return child process's stderr file descriptor ID
|
||||
*/
|
||||
int stderr_fd() const;
|
||||
|
||||
/**
|
||||
* Close all pipes and wait for process to exit. If process was closed already, just returns exit code.
|
||||
* @return child process's exit code
|
||||
*/
|
||||
int close();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,6 @@ namespace linuxdeploy {
|
||||
std::vector<std::string> args_{};
|
||||
std::unordered_map<std::string, std::string> env_{};
|
||||
|
||||
void assert_args_not_empty_() const;
|
||||
|
||||
std::vector<char*> make_args_vector_() const;
|
||||
|
||||
std::vector<char*> make_env_vector_() const;
|
||||
|
||||
public:
|
||||
subprocess(std::initializer_list<std::string> args, subprocess_env_map_t env = {});
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ set(headers_dir ${PROJECT_SOURCE_DIR}/include/linuxdeploy/subprocess)
|
||||
add_library(linuxdeploy_subprocess STATIC
|
||||
subprocess.cpp
|
||||
subprocess_result.cpp
|
||||
process.cpp
|
||||
${headers_dir}/subprocess.h
|
||||
${headers_dir}/subprocess_result.h
|
||||
${headers_dir}/process.h
|
||||
)
|
||||
target_include_directories(linuxdeploy_subprocess PUBLIC ${headers_dir})
|
||||
target_include_directories(linuxdeploy_subprocess PUBLIC ${PROJECT_SOURCE_DIR}/include)
|
||||
|
||||
add_executable(subprocess_demo subprocess_demo.cpp)
|
||||
target_link_libraries(subprocess_demo PUBLIC linuxdeploy_subprocess)
|
||||
|
||||
188
src/subprocess/process.cpp
Normal file
188
src/subprocess/process.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
// system headers
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <unistd.h>
|
||||
#include <memory.h>
|
||||
#include <wait.h>
|
||||
|
||||
// local headers
|
||||
#include "linuxdeploy/subprocess/process.h"
|
||||
#include "linuxdeploy/subprocess/subprocess.h"
|
||||
#include "linuxdeploy/util/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<std::string> args, const subprocess_env_map_t& env)
|
||||
: process(std::vector<std::string>(args), env) {}
|
||||
|
||||
process::process(const std::vector<std::string>& args, const subprocess_env_map_t& env)
|
||||
: exited_(false), exit_code_(0) {
|
||||
// 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];
|
||||
|
||||
// create actual pipes
|
||||
if (pipe(stdout_pipe_fds) != 0 || pipe(stderr_pipe_fds) != 0) {
|
||||
throw std::logic_error{"failed to create pipes"};
|
||||
}
|
||||
|
||||
// create child process
|
||||
child_pid_ = fork();
|
||||
|
||||
if (child_pid_ < 0) {
|
||||
throw std::runtime_error{"fork() failed"};
|
||||
}
|
||||
|
||||
std::cout << "child pid: " << std::to_string(child_pid_) << std::endl;
|
||||
|
||||
if (child_pid_ == 0) {
|
||||
// we're in the child process
|
||||
|
||||
// first step: close the read end of both pipes
|
||||
::close(stdout_pipe_fds[READ_END_]);
|
||||
::close(stderr_pipe_fds[READ_END_]);
|
||||
|
||||
auto connect_fd = [](int fds[], int fileno) {
|
||||
for (;;) {
|
||||
if (dup2(fds[WRITE_END_], fileno) == -1) {
|
||||
if (errno != EINTR) {
|
||||
throw std::logic_error{"failed to connect pipes"};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
connect_fd(stdout_pipe_fds, STDOUT_FILENO);
|
||||
connect_fd(stderr_pipe_fds, STDERR_FILENO);
|
||||
|
||||
// now, we also have to close the write end of both pipes
|
||||
::close(stdout_pipe_fds[WRITE_END_]);
|
||||
::close(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
|
||||
throw std::runtime_error{"exec() failed: " + std::string(strerror(errno))};
|
||||
}
|
||||
|
||||
// parent code
|
||||
|
||||
// we do not intend to write to the processes
|
||||
::close(stdout_pipe_fds[WRITE_END_]);
|
||||
::close(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(stdout_fd_);
|
||||
stdout_fd_ = -1;
|
||||
|
||||
::close(stderr_fd_);
|
||||
stderr_fd_ = -1;
|
||||
|
||||
{
|
||||
int temporary;
|
||||
waitpid(child_pid_, &temporary, 0);
|
||||
exit_code_ = WEXITSTATUS(temporary);
|
||||
}
|
||||
}
|
||||
|
||||
return exit_code_;
|
||||
}
|
||||
|
||||
process::~process() {
|
||||
(void) close();
|
||||
}
|
||||
|
||||
std::vector<char*> process::make_args_vector_(const std::vector<std::string>& args) {
|
||||
std::vector<char*> 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<char*> process::make_env_vector_(const subprocess_env_map_t& env) {
|
||||
std::vector<char*> 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"};
|
||||
}
|
||||
|
||||
std::string existing_env_var_name{existing_env_var_name, 0, static_cast<size_t>(existing_env_var - equal_sign)};
|
||||
|
||||
return existing_env_var_name == key;
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -2,94 +2,29 @@
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <unistd.h>
|
||||
#include <memory.h>
|
||||
#include <wait.h>
|
||||
|
||||
// local headers
|
||||
#include "subprocess.h"
|
||||
#include "linuxdeploy/subprocess/subprocess.h"
|
||||
#include "linuxdeploy/subprocess/process.h"
|
||||
#include "linuxdeploy/util/assert.h"
|
||||
|
||||
// shorter than using namespace ...
|
||||
using namespace linuxdeploy::subprocess;
|
||||
|
||||
subprocess::subprocess(std::initializer_list<std::string> args, subprocess_env_map_t env)
|
||||
: args_(args), env_(std::move(env)) {
|
||||
assert_args_not_empty_();
|
||||
}
|
||||
: subprocess(std::vector<std::string>(args), std::move(env)) {}
|
||||
|
||||
subprocess::subprocess(std::vector<std::string> args, subprocess_env_map_t env)
|
||||
: args_(std::move(args)), env_(std::move(env)) {
|
||||
assert_args_not_empty_();
|
||||
// preconditions
|
||||
util::assert::assert_not_empty(args_);
|
||||
}
|
||||
|
||||
subprocess_result subprocess::run() const {
|
||||
// these constants help make the code below more readable
|
||||
static constexpr int READ_END = 0, WRITE_END = 1;
|
||||
|
||||
// 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];
|
||||
|
||||
// create actual pipes
|
||||
if (pipe(stdout_pipe_fds) != 0 || pipe(stderr_pipe_fds) != 0) {
|
||||
throw std::logic_error{"failed to create pipes"};
|
||||
}
|
||||
|
||||
// create child process
|
||||
pid_t child_pid = fork();
|
||||
|
||||
if (child_pid < 0) {
|
||||
throw std::runtime_error{"fork() failed"};
|
||||
}
|
||||
|
||||
std::cout << "child pid: " << std::to_string(child_pid) << std::endl;
|
||||
|
||||
if (child_pid == 0) {
|
||||
// we're in the child process
|
||||
|
||||
// first step: close the read end of both pipes
|
||||
close(stdout_pipe_fds[READ_END]);
|
||||
close(stderr_pipe_fds[READ_END]);
|
||||
|
||||
auto connect_fd = [](int fds[], int fileno) {
|
||||
for (;;) {
|
||||
if (dup2(fds[WRITE_END], fileno) == -1) {
|
||||
if (errno != EINTR) {
|
||||
throw std::logic_error{"failed to connect pipes"};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
connect_fd(stdout_pipe_fds, STDOUT_FILENO);
|
||||
connect_fd(stderr_pipe_fds, STDERR_FILENO);
|
||||
|
||||
// now, we also have to close the write end of both pipes
|
||||
close(stdout_pipe_fds[WRITE_END]);
|
||||
close(stderr_pipe_fds[WRITE_END]);
|
||||
|
||||
// prepare arguments for exec*
|
||||
auto exec_args = make_args_vector_();
|
||||
auto exec_env = make_env_vector_();
|
||||
|
||||
// call subprocess
|
||||
execvpe(args_.front().c_str(), exec_args.data(), exec_env.data());
|
||||
|
||||
throw std::runtime_error{"exec() failed: " + std::string(strerror(errno))};
|
||||
}
|
||||
|
||||
// parent code
|
||||
|
||||
// we do not intend to write to the processes
|
||||
close(stdout_pipe_fds[WRITE_END]);
|
||||
close(stderr_pipe_fds[WRITE_END]);
|
||||
process proc{args_, env_};
|
||||
|
||||
subprocess_result_buffer_t stdout_contents{};
|
||||
subprocess_result_buffer_t stderr_contents{};
|
||||
@@ -118,94 +53,19 @@ subprocess_result subprocess::run() const {
|
||||
}
|
||||
};
|
||||
|
||||
read_into_buffer(stdout_pipe_fds[READ_END], stdout_contents);
|
||||
read_into_buffer(stderr_pipe_fds[READ_END], stderr_contents);
|
||||
|
||||
// done reading -> close read end
|
||||
close(stdout_pipe_fds[READ_END]);
|
||||
close(stderr_pipe_fds[READ_END]);
|
||||
// TODO: make sure neither stderr nor stdout overflow
|
||||
read_into_buffer(proc.stdout_fd(), stdout_contents);
|
||||
read_into_buffer(proc.stderr_fd(), stderr_contents);
|
||||
|
||||
// make sure contents are null-terminated
|
||||
stdout_contents.emplace_back('\0');
|
||||
stderr_contents.emplace_back('\0');
|
||||
|
||||
// wait for child to exit and fetch its exit code
|
||||
int exit_code;
|
||||
{
|
||||
int temporary;
|
||||
waitpid(child_pid, &temporary, 0);
|
||||
exit_code = WEXITSTATUS(temporary);
|
||||
}
|
||||
auto exit_code = proc.close();
|
||||
|
||||
return subprocess_result{exit_code, stdout_contents, stderr_contents};
|
||||
}
|
||||
|
||||
void subprocess::assert_args_not_empty_() const {
|
||||
if(args_.empty()) {
|
||||
throw std::invalid_argument("args may not be empty");
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<char*> subprocess::make_args_vector_() const {
|
||||
std::vector<char*> 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<char*> subprocess::make_env_vector_() const {
|
||||
std::vector<char*> 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"};
|
||||
}
|
||||
|
||||
std::string existing_env_var_name{existing_env_var_name, 0, static_cast<size_t>(existing_env_var - equal_sign)};
|
||||
|
||||
return existing_env_var_name == key;
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
std::string subprocess::check_output() const {
|
||||
const auto result = run();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include <iostream>
|
||||
|
||||
#include "subprocess.h"
|
||||
#include "linuxdeploy/subprocess/subprocess.h"
|
||||
|
||||
using namespace linuxdeploy::subprocess;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// local headers
|
||||
#include "subprocess_result.h"
|
||||
#include "linuxdeploy/subprocess/subprocess_result.h"
|
||||
|
||||
// shorter than using namespace ...
|
||||
using namespace linuxdeploy::subprocess;
|
||||
|
||||
Reference in New Issue
Block a user