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) 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 target_link_libraries(tux-dock
PRIVATE PRIVATE
ftxui::screen ftxui::screen
ftxui::dom ftxui::dom
ftxui::component 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 10. Attach Shell to Running Container
11. Run Detached Command in Container 11. Run Detached Command in Container
12. Spin Up MySQL Container 12. Spin Up MySQL Container
13. Get Container IP Address 13. Create Dockerfile & Build Image from Bash Script
14. Create Dockerfile & Build Image from Bash Script 14. About Tux-Dock
15. About Tux-Dock 15. Exit
16. Exit
--- ---
@ -99,7 +98,6 @@ public:
bool execShell(...); bool execShell(...);
bool execDetachedCommand(...); bool execDetachedCommand(...);
bool spinUpMySQL(...); bool spinUpMySQL(...);
bool showContainerIP(...);
bool createDockerfile(...); bool createDockerfile(...);
}; };

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