Refactoring to use Docker sockets directly as well as fork() to call processes instead of opening shells
This commit is contained in:
parent
6635446438
commit
802550f6e7
9 changed files with 720 additions and 1229 deletions
|
|
@ -20,11 +20,28 @@ FetchContent_Declare(
|
|||
|
||||
FetchContent_MakeAvailable(ftxui)
|
||||
|
||||
add_executable(tux-dock main.cpp)
|
||||
include(FetchContent)
|
||||
|
||||
FetchContent_Declare(
|
||||
json
|
||||
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(json)
|
||||
|
||||
add_executable(tux-dock
|
||||
main.cpp
|
||||
src/process_runner.cpp
|
||||
src/docker_engine_client.cpp
|
||||
src/docker_manager.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(tux-dock
|
||||
PRIVATE
|
||||
ftxui::screen
|
||||
ftxui::dom
|
||||
ftxui::component
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
target_compile_options(tux-dock PRIVATE -Wall -Wextra -Wpedantic)
|
||||
|
|
|
|||
|
|
@ -64,10 +64,9 @@ Current TUI actions:
|
|||
10. Attach Shell to Running Container
|
||||
11. Run Detached Command in Container
|
||||
12. Spin Up MySQL Container
|
||||
13. Get Container IP Address
|
||||
14. Create Dockerfile & Build Image from Bash Script
|
||||
15. About Tux-Dock
|
||||
16. Exit
|
||||
13. Create Dockerfile & Build Image from Bash Script
|
||||
14. About Tux-Dock
|
||||
15. Exit
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -99,7 +98,6 @@ public:
|
|||
bool execShell(...);
|
||||
bool execDetachedCommand(...);
|
||||
bool spinUpMySQL(...);
|
||||
bool showContainerIP(...);
|
||||
bool createDockerfile(...);
|
||||
};
|
||||
|
||||
|
|
|
|||
93
src/docker_engine_client.cpp
Normal file
93
src/docker_engine_client.cpp
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#include "docker_engine_client.hpp"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
DockerEngineClient::DockerEngineClient(std::string socket_path)
|
||||
: socket_path_(std::move(socket_path)) {}
|
||||
|
||||
EngineResponse DockerEngineClient::request(const std::string& method,
|
||||
const std::string& path,
|
||||
const std::string& body) const {
|
||||
EngineResponse response;
|
||||
const int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd == -1) {
|
||||
response.error = std::strerror(errno);
|
||||
return response;
|
||||
}
|
||||
|
||||
sockaddr_un address{};
|
||||
address.sun_family = AF_UNIX;
|
||||
if (socket_path_.size() >= sizeof(address.sun_path)) {
|
||||
response.error = "Docker socket path is too long.";
|
||||
close(fd);
|
||||
return response;
|
||||
}
|
||||
std::strncpy(address.sun_path, socket_path_.c_str(), sizeof(address.sun_path) - 1);
|
||||
if (connect(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0) {
|
||||
response.error = std::strerror(errno);
|
||||
close(fd);
|
||||
return response;
|
||||
}
|
||||
|
||||
timeval timeout{};
|
||||
timeout.tv_sec = 10;
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
|
||||
std::ostringstream request;
|
||||
request << method << " " << path << " HTTP/1.1\r\n"
|
||||
<< "Host: docker\r\n"
|
||||
<< "Connection: close\r\n"
|
||||
<< "Content-Type: application/json\r\n"
|
||||
<< "Content-Length: " << body.size() << "\r\n\r\n"
|
||||
<< body;
|
||||
const std::string wire = request.str();
|
||||
std::size_t sent = 0;
|
||||
while (sent < wire.size()) {
|
||||
const ssize_t count = write(fd, wire.data() + sent, wire.size() - sent);
|
||||
if (count <= 0) {
|
||||
response.error = std::strerror(errno);
|
||||
close(fd);
|
||||
return response;
|
||||
}
|
||||
sent += static_cast<std::size_t>(count);
|
||||
}
|
||||
|
||||
std::string raw;
|
||||
char buffer[8192];
|
||||
ssize_t count = 0;
|
||||
while ((count = read(fd, buffer, sizeof(buffer))) > 0) {
|
||||
raw.append(buffer, static_cast<std::size_t>(count));
|
||||
}
|
||||
close(fd);
|
||||
if (count < 0) {
|
||||
response.error = std::strerror(errno);
|
||||
return response;
|
||||
}
|
||||
|
||||
const std::size_t header_end = raw.find("\r\n\r\n");
|
||||
if (header_end == std::string::npos) {
|
||||
response.error = "Invalid response from Docker Engine.";
|
||||
return response;
|
||||
}
|
||||
const std::size_t status_end = raw.find("\r\n");
|
||||
std::istringstream status_line(raw.substr(0, status_end));
|
||||
std::string http_version;
|
||||
status_line >> http_version >> response.status_code;
|
||||
if (response.status_code == 0) {
|
||||
response.error = "Invalid HTTP status from Docker Engine.";
|
||||
return response;
|
||||
}
|
||||
response.body = raw.substr(header_end + 4);
|
||||
if (!response.ok()) {
|
||||
response.error = response.body.empty() ? "Docker Engine request failed." : response.body;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
23
src/docker_engine_client.hpp
Normal file
23
src/docker_engine_client.hpp
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
struct EngineResponse {
|
||||
int status_code = 0;
|
||||
std::string body;
|
||||
std::string error;
|
||||
|
||||
bool ok() const { return status_code >= 200 && status_code < 300 && error.empty(); }
|
||||
};
|
||||
|
||||
class DockerEngineClient {
|
||||
public:
|
||||
explicit DockerEngineClient(std::string socket_path = "/var/run/docker.sock");
|
||||
|
||||
EngineResponse request(const std::string& method,
|
||||
const std::string& path,
|
||||
const std::string& body = {}) const;
|
||||
|
||||
private:
|
||||
std::string socket_path_;
|
||||
};
|
||||
234
src/docker_manager.cpp
Normal file
234
src/docker_manager.cpp
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
#include "docker_manager.hpp"
|
||||
|
||||
#include "process_runner.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace {
|
||||
|
||||
std::string joinPorts(const json& ports) {
|
||||
std::ostringstream result;
|
||||
bool first = true;
|
||||
for (const auto& port : ports) {
|
||||
const auto public_port = port.value("PublicPort", 0);
|
||||
const auto private_port = port.value("PrivatePort", 0);
|
||||
if (public_port == 0 || private_port == 0) continue;
|
||||
if (!first) result << ", ";
|
||||
result << public_port << ":" << private_port;
|
||||
first = false;
|
||||
}
|
||||
return result.str();
|
||||
}
|
||||
|
||||
std::string apiError(const EngineResponse& response, const std::string& fallback) {
|
||||
if (!response.error.empty()) return response.error;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string DockerManager::processError(const std::string& fallback,
|
||||
const std::string& stderr_text) {
|
||||
return stderr_text.empty() ? fallback : stderr_text;
|
||||
}
|
||||
|
||||
bool DockerManager::runProcess(const std::vector<std::string>& args,
|
||||
std::string& message,
|
||||
bool inherit_stdio) {
|
||||
ProcessOptions options;
|
||||
options.inherit_stdio = inherit_stdio;
|
||||
options.capture_stdout = !inherit_stdio;
|
||||
options.capture_stderr = !inherit_stdio;
|
||||
const ProcessResult result = ProcessRunner::run(args, options);
|
||||
if (!result.ok()) {
|
||||
message = processError("Docker command failed.", result.stderr_text);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<DockerManager::ContainerInfo> DockerManager::getContainerList() const {
|
||||
std::vector<ContainerInfo> containers;
|
||||
const EngineResponse response = engine_.request("GET", "/containers/json?all=true");
|
||||
if (!response.ok()) return containers;
|
||||
|
||||
try {
|
||||
for (const auto& item : json::parse(response.body)) {
|
||||
ContainerInfo info;
|
||||
info.id = item.value("Id", "");
|
||||
info.name = item.value("Names", std::vector<std::string>{}).empty()
|
||||
? ""
|
||||
: item.value("Names", std::vector<std::string>{}).front();
|
||||
if (!info.name.empty() && info.name.front() == '/') info.name.erase(0, 1);
|
||||
info.status = item.value("Status", "");
|
||||
info.running = item.value("State", "") == "running";
|
||||
info.ports = joinPorts(item.value("Ports", json::array()));
|
||||
if (!info.id.empty() && !info.name.empty()) containers.push_back(std::move(info));
|
||||
}
|
||||
} catch (const json::exception&) {
|
||||
return {};
|
||||
}
|
||||
return containers;
|
||||
}
|
||||
|
||||
std::vector<DockerManager::ImageInfo> DockerManager::getImageList() const {
|
||||
std::vector<ImageInfo> images;
|
||||
const EngineResponse response = engine_.request("GET", "/images/json");
|
||||
if (!response.ok()) return images;
|
||||
|
||||
try {
|
||||
for (const auto& item : json::parse(response.body)) {
|
||||
const std::string id = item.value("Id", "");
|
||||
const auto tags = item.value("RepoTags", std::vector<std::string>{});
|
||||
if (id.empty()) continue;
|
||||
if (tags.empty()) images.emplace_back(id, "<untagged>");
|
||||
else for (const auto& tag : tags) images.emplace_back(id, tag);
|
||||
}
|
||||
} catch (const json::exception&) {
|
||||
return {};
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
bool DockerManager::pullImage(const std::string& image, std::string& message) const {
|
||||
if (image.empty()) {
|
||||
message = "Please provide an image name.";
|
||||
return false;
|
||||
}
|
||||
const bool ok = runProcess({"docker", "pull", image}, message);
|
||||
if (ok) message = "Image pulled successfully.";
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool DockerManager::runContainerInteractive(const std::string& image,
|
||||
const std::vector<std::string>& ports,
|
||||
std::string& message) const {
|
||||
if (image.empty()) {
|
||||
message = "Please choose an image first.";
|
||||
return false;
|
||||
}
|
||||
std::vector<std::string> args{"docker", "run", "-it"};
|
||||
for (const auto& port : ports) {
|
||||
args.push_back("-p");
|
||||
args.push_back(port);
|
||||
}
|
||||
args.insert(args.end(), {image, "/bin/sh"});
|
||||
const bool ok = runProcess(args, message, true);
|
||||
if (ok) message = "Interactive container session finished.";
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool DockerManager::startInteractive(const std::string& id, std::string& message) const {
|
||||
const bool ok = runProcess({"docker", "start", "-ai", id}, message, true);
|
||||
if (ok) message = "Interactive container session finished.";
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool DockerManager::startDetached(const std::string& id, std::string& message) const {
|
||||
const EngineResponse response = engine_.request("POST", "/containers/" + id + "/start");
|
||||
if (!response.ok()) {
|
||||
message = apiError(response, "Could not start that container.");
|
||||
return false;
|
||||
}
|
||||
message = "Container started in detached mode.";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DockerManager::deleteImage(const std::string& id, std::string& message) const {
|
||||
const EngineResponse response = engine_.request("DELETE", "/images/" + id);
|
||||
if (!response.ok()) {
|
||||
message = apiError(response, "Could not delete that image.");
|
||||
return false;
|
||||
}
|
||||
message = "Image deleted.";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DockerManager::stopContainer(const std::string& id, std::string& message) const {
|
||||
const EngineResponse response = engine_.request("POST", "/containers/" + id + "/stop");
|
||||
if (!response.ok()) {
|
||||
message = apiError(response, "Could not stop that container.");
|
||||
return false;
|
||||
}
|
||||
message = "Container stopped.";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DockerManager::removeContainer(const std::string& id, std::string& message) const {
|
||||
const EngineResponse response = engine_.request("DELETE", "/containers/" + id);
|
||||
if (!response.ok()) {
|
||||
message = apiError(response, "Could not remove that container.");
|
||||
return false;
|
||||
}
|
||||
message = "Container removed.";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DockerManager::execShell(const std::string& id, std::string& message) const {
|
||||
const bool ok = runProcess({"docker", "exec", "-it", id, "/bin/sh"}, message, true);
|
||||
if (ok) message = "Shell session finished.";
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool DockerManager::execDetachedCommand(const std::string& id,
|
||||
const std::string& command,
|
||||
std::string& message) const {
|
||||
if (command.empty()) {
|
||||
message = "Please provide a command to run.";
|
||||
return false;
|
||||
}
|
||||
const bool ok = runProcess({"docker", "exec", "-d", id, "/bin/sh", "-c", command}, message);
|
||||
if (ok) message = "Command dispatched in detached mode.";
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool DockerManager::spinUpMySQL(const std::string& port,
|
||||
const std::string& password,
|
||||
const std::string& version,
|
||||
std::string& message) const {
|
||||
const bool ok = runProcess({"docker", "run", "-p", port, "--name", "mysql-container",
|
||||
"-e", "MYSQL_ROOT_PASSWORD=" + password, "-d", "mysql:" + version}, message);
|
||||
if (ok) message = "MySQL container launched.";
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool DockerManager::createDockerfile(const std::string& baseImage,
|
||||
const std::string& bashScriptPath,
|
||||
std::string outputFile,
|
||||
const std::string& imageName,
|
||||
std::string& message) const {
|
||||
if (baseImage.empty() || bashScriptPath.empty()) {
|
||||
message = "Base image and script path are required.";
|
||||
return false;
|
||||
}
|
||||
if (!std::filesystem::exists(bashScriptPath)) {
|
||||
message = "Could not find that script file.";
|
||||
return false;
|
||||
}
|
||||
if (outputFile.empty()) outputFile = "Dockerfile";
|
||||
std::ifstream scriptFile(bashScriptPath);
|
||||
std::ofstream dockerfile(outputFile);
|
||||
if (!scriptFile.is_open() || !dockerfile.is_open()) {
|
||||
message = "Could not open one of the files.";
|
||||
return false;
|
||||
}
|
||||
dockerfile << "FROM " << baseImage << "\nWORKDIR /app\n\n# Auto-generated by Tux-Dock\n";
|
||||
std::string line;
|
||||
while (std::getline(scriptFile, line)) {
|
||||
if (!line.empty() && line.rfind("#", 0) != 0) dockerfile << "RUN " << line << "\n";
|
||||
}
|
||||
dockerfile << "\nCMD [\"/bin/bash\"]\n";
|
||||
if (imageName.empty()) {
|
||||
message = "Dockerfile created. Build skipped because no image name was provided.";
|
||||
return true;
|
||||
}
|
||||
const bool ok = runProcess({"docker", "build", "-t", imageName, "-f", outputFile, "."}, message);
|
||||
if (ok) message = "Dockerfile created and image build completed.";
|
||||
return ok;
|
||||
}
|
||||
55
src/docker_manager.hpp
Normal file
55
src/docker_manager.hpp
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#pragma once
|
||||
|
||||
#include "docker_engine_client.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
class DockerManager {
|
||||
public:
|
||||
struct ContainerInfo {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string status;
|
||||
std::string ports;
|
||||
bool running = false;
|
||||
};
|
||||
|
||||
using ImageInfo = std::pair<std::string, std::string>;
|
||||
|
||||
std::vector<ContainerInfo> getContainerList() const;
|
||||
std::vector<ImageInfo> getImageList() const;
|
||||
|
||||
bool pullImage(const std::string& image, std::string& message) const;
|
||||
bool runContainerInteractive(const std::string& image,
|
||||
const std::vector<std::string>& ports,
|
||||
std::string& message) const;
|
||||
bool startInteractive(const std::string& containerId, std::string& message) const;
|
||||
bool startDetached(const std::string& containerId, std::string& message) const;
|
||||
bool deleteImage(const std::string& imageId, std::string& message) const;
|
||||
bool stopContainer(const std::string& containerId, std::string& message) const;
|
||||
bool removeContainer(const std::string& containerId, std::string& message) const;
|
||||
bool execShell(const std::string& containerId, std::string& message) const;
|
||||
bool execDetachedCommand(const std::string& containerId,
|
||||
const std::string& command,
|
||||
std::string& message) const;
|
||||
bool spinUpMySQL(const std::string& port,
|
||||
const std::string& password,
|
||||
const std::string& version,
|
||||
std::string& message) const;
|
||||
bool createDockerfile(const std::string& baseImage,
|
||||
const std::string& bashScriptPath,
|
||||
std::string outputFile,
|
||||
const std::string& imageName,
|
||||
std::string& message) const;
|
||||
|
||||
private:
|
||||
DockerEngineClient engine_;
|
||||
|
||||
static std::string processError(const std::string& fallback,
|
||||
const std::string& stderr_text);
|
||||
static bool runProcess(const std::vector<std::string>& args,
|
||||
std::string& message,
|
||||
bool inherit_stdio = false);
|
||||
};
|
||||
122
src/process_runner.cpp
Normal file
122
src/process_runner.cpp
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
#include "process_runner.hpp"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <array>
|
||||
#include <poll.h>
|
||||
namespace {
|
||||
|
||||
struct Pipe {
|
||||
int read = -1;
|
||||
int write = -1;
|
||||
|
||||
~Pipe() {
|
||||
if (read != -1) close(read);
|
||||
if (write != -1) close(write);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ProcessResult ProcessRunner::run(const std::vector<std::string>& args,
|
||||
const ProcessOptions& options) {
|
||||
ProcessResult result;
|
||||
if (args.empty()) {
|
||||
result.stderr_text = "No executable specified.";
|
||||
return result;
|
||||
}
|
||||
|
||||
Pipe stdout_pipe;
|
||||
Pipe stderr_pipe;
|
||||
if (!options.inherit_stdio && options.capture_stdout) {
|
||||
int fds[2];
|
||||
if (pipe(fds) != 0) {
|
||||
result.stderr_text = std::strerror(errno);
|
||||
return result;
|
||||
}
|
||||
stdout_pipe.read = fds[0];
|
||||
stdout_pipe.write = fds[1];
|
||||
}
|
||||
if (!options.inherit_stdio && options.capture_stderr) {
|
||||
int fds[2];
|
||||
if (pipe(fds) != 0) {
|
||||
result.stderr_text = std::strerror(errno);
|
||||
return result;
|
||||
}
|
||||
stderr_pipe.read = fds[0];
|
||||
stderr_pipe.write = fds[1];
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == -1) {
|
||||
result.stderr_text = std::strerror(errno);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
if (!options.inherit_stdio && options.capture_stdout) {
|
||||
if (dup2(stdout_pipe.write, STDOUT_FILENO) == -1) _exit(127);
|
||||
}
|
||||
if (!options.inherit_stdio && options.capture_stderr) {
|
||||
if (dup2(stderr_pipe.write, STDERR_FILENO) == -1) _exit(127);
|
||||
}
|
||||
if (stdout_pipe.read != -1) close(stdout_pipe.read);
|
||||
if (stdout_pipe.write != -1) close(stdout_pipe.write);
|
||||
if (stderr_pipe.read != -1) close(stderr_pipe.read);
|
||||
if (stderr_pipe.write != -1) close(stderr_pipe.write);
|
||||
|
||||
std::vector<char*> argv;
|
||||
argv.reserve(args.size() + 1);
|
||||
for (const auto& arg : args) argv.push_back(const_cast<char*>(arg.c_str()));
|
||||
argv.push_back(nullptr);
|
||||
execvp(argv[0], argv.data());
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
if (stdout_pipe.write != -1) close(stdout_pipe.write);
|
||||
stdout_pipe.write = -1;
|
||||
if (stderr_pipe.write != -1) close(stderr_pipe.write);
|
||||
stderr_pipe.write = -1;
|
||||
std::array<char, 4096> buffer{};
|
||||
pollfd poll_fds[2]{};
|
||||
int active = 0;
|
||||
if (stdout_pipe.read != -1) poll_fds[active++] = {stdout_pipe.read, POLLIN, 0};
|
||||
if (stderr_pipe.read != -1) poll_fds[active++] = {stderr_pipe.read, POLLIN, 0};
|
||||
while (active > 0) {
|
||||
if (poll(poll_fds, static_cast<nfds_t>(active), -1) == -1) {
|
||||
if (errno == EINTR) continue;
|
||||
break;
|
||||
}
|
||||
for (int i = 0; i < active;) {
|
||||
if ((poll_fds[i].revents & (POLLIN | POLLHUP)) == 0) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
const ssize_t count = read(poll_fds[i].fd, buffer.data(), buffer.size());
|
||||
if (count > 0) {
|
||||
if (poll_fds[i].fd == stdout_pipe.read) result.stdout_text.append(buffer.data(), static_cast<std::size_t>(count));
|
||||
else result.stderr_text.append(buffer.data(), static_cast<std::size_t>(count));
|
||||
++i;
|
||||
} else {
|
||||
close(poll_fds[i].fd);
|
||||
poll_fds[i] = poll_fds[--active];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
if (waitpid(pid, &status, 0) == -1) {
|
||||
result.stderr_text = std::strerror(errno);
|
||||
return result;
|
||||
}
|
||||
if (WIFEXITED(status)) {
|
||||
result.exit_code = WEXITSTATUS(status);
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
result.signaled = true;
|
||||
result.signal = WTERMSIG(status);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
26
src/process_runner.hpp
Normal file
26
src/process_runner.hpp
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct ProcessResult {
|
||||
int exit_code = -1;
|
||||
bool signaled = false;
|
||||
int signal = 0;
|
||||
std::string stdout_text;
|
||||
std::string stderr_text;
|
||||
|
||||
bool ok() const { return !signaled && exit_code == 0; }
|
||||
};
|
||||
|
||||
struct ProcessOptions {
|
||||
bool capture_stdout = true;
|
||||
bool capture_stderr = true;
|
||||
bool inherit_stdio = false;
|
||||
};
|
||||
|
||||
class ProcessRunner {
|
||||
public:
|
||||
static ProcessResult run(const std::vector<std::string>& args,
|
||||
const ProcessOptions& options = {});
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue