179 lines
4.2 KiB
C++
179 lines
4.2 KiB
C++
#include "subprocess.h"
|
|
#include <array>
|
|
#include <boost/asio.hpp>
|
|
#include <boost/process.hpp>
|
|
#include <cerrno>
|
|
#include <cstddef>
|
|
#include <cstdio>
|
|
#include <stdexcept>
|
|
|
|
using std::string;
|
|
using std::vector;
|
|
|
|
namespace bp = boost::process;
|
|
namespace bf = boost::filesystem;
|
|
namespace ba = boost::asio;
|
|
namespace bs = boost::system;
|
|
|
|
#if defined(_MSC_VER)
|
|
constexpr auto PIPE_CLOSED = ba::error::broken_pipe;
|
|
#else
|
|
constexpr auto PIPE_CLOSED = ba::error::eof;
|
|
#endif
|
|
|
|
namespace cpputils
|
|
{
|
|
namespace
|
|
{
|
|
bf::path _find_executable(const char *command)
|
|
{
|
|
bf::path executable = bp::search_path(command);
|
|
if (executable == "")
|
|
{
|
|
throw std::runtime_error("Tried to run command " + std::string(command) + " but didn't find it in the PATH");
|
|
}
|
|
return executable;
|
|
}
|
|
|
|
class OutputPipeHandler final
|
|
{
|
|
public:
|
|
explicit OutputPipeHandler(ba::io_context* ctx)
|
|
: vOut_(128 * 1024)
|
|
, buffer_(ba::buffer(vOut_))
|
|
, pipe_(*ctx)
|
|
, output_() {
|
|
}
|
|
|
|
void async_read()
|
|
{
|
|
std::function<void(const bs::error_code & ec, std::size_t n)> onOutput;
|
|
onOutput = [&](const bs::error_code & ec, size_t n)
|
|
{
|
|
output_.reserve(output_.size() + n);
|
|
output_.insert(output_.end(), vOut_.begin(), vOut_.begin() + n);
|
|
if (ec) {
|
|
if (ec != PIPE_CLOSED) {
|
|
throw SubprocessError(std::string() + "Error getting output from subprocess. Error code: " + std::to_string(ec.value()) + " : " + ec.message());
|
|
}
|
|
} else {
|
|
ba::async_read(pipe_, buffer_, onOutput);
|
|
}
|
|
};
|
|
ba::async_read(pipe_, buffer_, onOutput);
|
|
}
|
|
|
|
bp::async_pipe& pipe()
|
|
{
|
|
return pipe_;
|
|
}
|
|
|
|
std::string output() &&
|
|
{
|
|
return std::move(output_);
|
|
}
|
|
|
|
|
|
private:
|
|
std::vector<char> vOut_;
|
|
ba::mutable_buffer buffer_;
|
|
bp::async_pipe pipe_;
|
|
std::string output_;
|
|
};
|
|
|
|
class InputPipeHandler final
|
|
{
|
|
public:
|
|
explicit InputPipeHandler(ba::io_context* ctx, const std::string& input)
|
|
: input_(input)
|
|
, buffer_(ba::buffer(input_))
|
|
, pipe_(*ctx) {
|
|
|
|
}
|
|
|
|
bp::async_pipe& pipe()
|
|
{
|
|
return pipe_;
|
|
}
|
|
|
|
void async_write()
|
|
{
|
|
ba::async_write(pipe_, buffer_,
|
|
[&](const bs::error_code & ec, std::size_t /*n*/)
|
|
{
|
|
if (ec) {
|
|
throw SubprocessError(std::string() + "Error sending input to subprocess. Error code: " + std::to_string(ec.value()) + " : " + ec.message());
|
|
}
|
|
pipe_.async_close();
|
|
}
|
|
);
|
|
}
|
|
private:
|
|
const std::string& input_;
|
|
ba::const_buffer buffer_;
|
|
bp::async_pipe pipe_;
|
|
};
|
|
}
|
|
|
|
SubprocessResult Subprocess::call(const char *command, const vector<string> &args, const string &input)
|
|
{
|
|
return call(_find_executable(command), args, input);
|
|
}
|
|
|
|
SubprocessResult Subprocess::check_call(const char *command, const vector<string> &args, const string& input)
|
|
{
|
|
return check_call(_find_executable(command), args, input);
|
|
}
|
|
|
|
SubprocessResult Subprocess::call(const bf::path& executable, const vector<string>& args, const string& input)
|
|
{
|
|
if (!bf::exists(executable))
|
|
{
|
|
throw std::runtime_error("Tried to run executable " + executable.string() + " but didn't find it");
|
|
}
|
|
|
|
// Process I/O needs to use the async API to avoid deadlocks, see
|
|
// - https://www.boost.org/doc/libs/1_78_0/doc/html/boost_process/faq.html
|
|
// - Code taken from https://www.py4u.net/discuss/97014 and modified
|
|
|
|
ba::io_context ctx;
|
|
|
|
OutputPipeHandler stdout_handler(&ctx);
|
|
OutputPipeHandler stderr_handler(&ctx);
|
|
InputPipeHandler stdin_handler(&ctx, input);
|
|
|
|
bp::child child(
|
|
bp::exe = executable.string(),
|
|
bp::args(args),
|
|
bp::std_out > stdout_handler.pipe(),
|
|
bp::std_err > stderr_handler.pipe(),
|
|
bp::std_in < stdin_handler.pipe()
|
|
);
|
|
|
|
stdin_handler.async_write();
|
|
stdout_handler.async_read();
|
|
stderr_handler.async_read();
|
|
|
|
ctx.run();
|
|
|
|
child.wait();
|
|
|
|
return SubprocessResult{
|
|
std::move(stdout_handler).output(),
|
|
std::move(stderr_handler).output(),
|
|
child.exit_code(),
|
|
};
|
|
}
|
|
|
|
SubprocessResult Subprocess::check_call(const bf::path &executable, const vector<string> &args, const string& input)
|
|
{
|
|
auto result = call(executable, args, input);
|
|
if (result.exitcode != 0)
|
|
{
|
|
throw SubprocessError("Subprocess \"" + executable.string() + "\" exited with code " + std::to_string(result.exitcode));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
}
|