Refactoring to use Docker sockets directly as well as fork() to call processes instead of opening shells

This commit is contained in:
mrkmntal 2026-08-14 12:49:10 -04:00
commit 802550f6e7
9 changed files with 720 additions and 1229 deletions

View file

@ -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)

View file

@ -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(...);
};

1357
main.cpp

File diff suppressed because it is too large Load diff

View 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;
}

View 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
View 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
View 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
View 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
View 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 = {});
};