[M4] add exec::run subprocess wrapper
This commit is contained in:
@@ -1,3 +1,31 @@
|
||||
export module cargoxx.exec;
|
||||
|
||||
import std;
|
||||
import cargoxx.util;
|
||||
|
||||
export namespace cargoxx::exec {
|
||||
|
||||
struct ExecResult {
|
||||
int exit_code;
|
||||
std::string stdout_text;
|
||||
std::string stderr_text;
|
||||
};
|
||||
|
||||
struct ExecOptions {
|
||||
std::filesystem::path cwd;
|
||||
std::vector<std::pair<std::string, std::string>> env_overrides;
|
||||
std::optional<std::chrono::seconds> timeout;
|
||||
bool inherit_stdio = false; // true for `cargoxx run` and `cargoxx test`
|
||||
};
|
||||
|
||||
// Spawns `program` with `args` (argv only — never a shell string), waits for
|
||||
// completion, and returns its exit code plus captured stdout/stderr. With
|
||||
// `opts.inherit_stdio = true`, the child's stdio is wired straight to the
|
||||
// parent's; the returned ExecResult has empty stdout_text/stderr_text.
|
||||
//
|
||||
// Returns `ExecToolNotFound` when the program cannot be located, and
|
||||
// `ExecCommandFailed` for any other start/wait failure (including timeout).
|
||||
auto run(const std::string& program, const std::vector<std::string>& args,
|
||||
const ExecOptions& opts = {}) -> util::Result<ExecResult>;
|
||||
|
||||
} // namespace cargoxx::exec
|
||||
|
||||
145
src/exec/subprocess.cpp
Normal file
145
src/exec/subprocess.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
module;
|
||||
|
||||
#include <cerrno>
|
||||
#include <reproc/drain.h>
|
||||
#include <reproc/reproc.h>
|
||||
|
||||
module cargoxx.exec;
|
||||
|
||||
import std;
|
||||
import cargoxx.util;
|
||||
|
||||
namespace cargoxx::exec {
|
||||
|
||||
namespace {
|
||||
|
||||
auto string_sink_fn(REPROC_STREAM /*stream*/, const uint8_t* buffer, size_t size, void* ctx)
|
||||
-> int {
|
||||
if (size == 0) {
|
||||
return 0;
|
||||
}
|
||||
auto* s = static_cast<std::string*>(ctx);
|
||||
s->append(reinterpret_cast<const char*>(buffer), size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto exec_error(util::ErrorCode code, std::string msg) -> util::Error {
|
||||
return util::Error{code, std::move(msg), "", std::nullopt, std::nullopt};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto run(const std::string& program, const std::vector<std::string>& args,
|
||||
const ExecOptions& opts) -> util::Result<ExecResult> {
|
||||
std::vector<const char*> argv;
|
||||
argv.reserve(args.size() + 2);
|
||||
argv.push_back(program.c_str());
|
||||
for (const auto& a : args) {
|
||||
argv.push_back(a.c_str());
|
||||
}
|
||||
argv.push_back(nullptr);
|
||||
|
||||
std::vector<std::string> env_storage;
|
||||
std::vector<const char*> env_ptrs;
|
||||
env_storage.reserve(opts.env_overrides.size());
|
||||
for (const auto& [k, v] : opts.env_overrides) {
|
||||
env_storage.push_back(std::format("{}={}", k, v));
|
||||
}
|
||||
env_ptrs.reserve(env_storage.size() + 1);
|
||||
for (const auto& e : env_storage) {
|
||||
env_ptrs.push_back(e.c_str());
|
||||
}
|
||||
env_ptrs.push_back(nullptr);
|
||||
|
||||
reproc_options ropts{};
|
||||
std::string cwd_storage;
|
||||
if (!opts.cwd.empty()) {
|
||||
cwd_storage = opts.cwd.string();
|
||||
ropts.working_directory = cwd_storage.c_str();
|
||||
}
|
||||
ropts.env.behavior = REPROC_ENV_EXTEND;
|
||||
ropts.env.extra = env_storage.empty() ? nullptr : env_ptrs.data();
|
||||
|
||||
if (opts.inherit_stdio) {
|
||||
ropts.redirect.in.type = REPROC_REDIRECT_PARENT;
|
||||
ropts.redirect.out.type = REPROC_REDIRECT_PARENT;
|
||||
ropts.redirect.err.type = REPROC_REDIRECT_PARENT;
|
||||
} else {
|
||||
ropts.redirect.in.type = REPROC_REDIRECT_DISCARD;
|
||||
ropts.redirect.out.type = REPROC_REDIRECT_PIPE;
|
||||
ropts.redirect.err.type = REPROC_REDIRECT_PIPE;
|
||||
}
|
||||
|
||||
// The deadline applies to drain/read/write; reproc returns REPROC_ETIMEDOUT
|
||||
// once it is hit. Without this, draining can outlive the wait timeout.
|
||||
if (opts.timeout) {
|
||||
ropts.deadline =
|
||||
static_cast<int>(std::chrono::milliseconds{*opts.timeout}.count());
|
||||
}
|
||||
// If reproc_destroy runs while the child is still alive (e.g. after a
|
||||
// timeout), gracefully terminate then kill rather than leaking the process.
|
||||
ropts.stop.first = {REPROC_STOP_TERMINATE, 100};
|
||||
ropts.stop.second = {REPROC_STOP_KILL, 100};
|
||||
ropts.stop.third = {REPROC_STOP_NOOP, 0};
|
||||
|
||||
reproc_t* p = reproc_new();
|
||||
if (!p) {
|
||||
return std::unexpected(exec_error(util::ErrorCode::ExecCommandFailed,
|
||||
"reproc_new failed (out of memory)"));
|
||||
}
|
||||
|
||||
int rc = reproc_start(p, argv.data(), ropts);
|
||||
if (rc < 0) {
|
||||
reproc_destroy(p);
|
||||
if (rc == -ENOENT) {
|
||||
return std::unexpected(exec_error(
|
||||
util::ErrorCode::ExecToolNotFound,
|
||||
std::format("program not found: {}", program)));
|
||||
}
|
||||
return std::unexpected(exec_error(
|
||||
util::ErrorCode::ExecCommandFailed,
|
||||
std::format("failed to start '{}': {} (errno {})", program,
|
||||
std::strerror(-rc), -rc)));
|
||||
}
|
||||
|
||||
ExecResult result{};
|
||||
|
||||
if (!opts.inherit_stdio) {
|
||||
reproc_sink out_sink{string_sink_fn, &result.stdout_text};
|
||||
reproc_sink err_sink{string_sink_fn, &result.stderr_text};
|
||||
rc = reproc_drain(p, out_sink, err_sink);
|
||||
if (rc < 0 && rc != REPROC_ETIMEDOUT) {
|
||||
reproc_destroy(p);
|
||||
return std::unexpected(exec_error(
|
||||
util::ErrorCode::ExecCommandFailed,
|
||||
std::format("drain failed for '{}': {}", program, std::strerror(-rc))));
|
||||
}
|
||||
}
|
||||
|
||||
int timeout_ms = REPROC_INFINITE;
|
||||
if (opts.timeout) {
|
||||
timeout_ms =
|
||||
static_cast<int>(std::chrono::milliseconds{*opts.timeout}.count());
|
||||
}
|
||||
|
||||
int wait_rc = reproc_wait(p, timeout_ms);
|
||||
if (wait_rc == REPROC_ETIMEDOUT) {
|
||||
reproc_destroy(p);
|
||||
return std::unexpected(exec_error(
|
||||
util::ErrorCode::ExecCommandFailed,
|
||||
std::format("'{}' timed out after {} seconds", program,
|
||||
opts.timeout ? opts.timeout->count() : 0)));
|
||||
}
|
||||
if (wait_rc < 0) {
|
||||
reproc_destroy(p);
|
||||
return std::unexpected(exec_error(
|
||||
util::ErrorCode::ExecCommandFailed,
|
||||
std::format("wait failed for '{}': {}", program, std::strerror(-wait_rc))));
|
||||
}
|
||||
|
||||
result.exit_code = wait_rc;
|
||||
reproc_destroy(p);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace cargoxx::exec
|
||||
Reference in New Issue
Block a user