0.1 update, make the stop container code more robust and fix some bugs, update the about screen for version 0.1

This commit is contained in:
mrkmntal 2026-08-14 14:28:03 -04:00
commit e8e93d7b21
17 changed files with 449 additions and 43 deletions

View file

@ -60,14 +60,14 @@ EngineResponse DockerEngineClient::request(const std::string& method,
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"
std::ostringstream wire_request;
wire_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();
const std::string wire = wire_request.str();
std::size_t sent = 0;
while (sent < wire.size()) {
const ssize_t count = write(fd, wire.data() + sent, wire.size() - sent);
@ -82,18 +82,26 @@ EngineResponse DockerEngineClient::request(const std::string& method,
std::string raw;
char buffer[8192];
ssize_t count = 0;
while ((count = read(fd, buffer, sizeof(buffer))) > 0) {
while (!httpResponseComplete(raw, method) && (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;
if (errno == EINTR) return request(method, path, body);
if (errno != EAGAIN && errno != EWOULDBLOCK) {
response.error = std::strerror(errno);
return response;
}
if (!httpResponseComplete(raw, method)) {
response.error = "Docker Engine response timed out.";
return response;
}
}
const auto parsed = parseHttpResponse(raw);
response.status_code = parsed.status_code;
response.body = parsed.body;
response.error = parsed.error;
if (response.status_code == 304 && response.error.empty()) response.error.clear();
return response;
}

View file

@ -2,6 +2,7 @@
#include "process_runner.hpp"
#include "container_parser.hpp"
#include "stop_waiter.hpp"
#include <filesystem>
#include <fstream>
@ -133,9 +134,43 @@ bool DockerManager::deleteImage(const std::string& id, std::string& message) con
}
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.");
const auto stop_request = [this, &id] { return engine_.request("POST", "/containers/" + id + "/stop"); };
const auto acceptable = [](const EngineResponse& response) {
return response.status_code == 204 || response.status_code == 304 || response.status_code == 404 || response.ok();
};
const auto probe = [this, &id] {
const EngineResponse state = engine_.request("GET", "/containers/" + id + "/json");
if (!state.ok()) return StopProbe::Unknown;
try {
const bool running = nlohmann::json::parse(state.body)
.value("State", nlohmann::json::object())
.value("Running", true);
return running ? StopProbe::Running : StopProbe::Stopped;
} catch (const nlohmann::json::exception&) {
return StopProbe::Unknown;
}
};
const EngineResponse first = stop_request();
const bool first_timed_out = first.error == "Docker Engine response timed out.";
if (!acceptable(first) && !first_timed_out) {
message = apiError(first, "Could not stop that container.");
return false;
}
StopProbe state = first_timed_out ? StopProbe::Unknown : waitForStopped(probe);
if (state != StopProbe::Stopped) {
const EngineResponse retry = stop_request();
const bool retry_timed_out = retry.error == "Docker Engine response timed out.";
if (!acceptable(retry) && !retry_timed_out) {
message = apiError(retry, "Could not confirm that container stopped.");
return false;
}
state = waitForStopped(probe);
}
if (state != StopProbe::Stopped) {
message = "Stop requested, but container state could not be confirmed.";
return false;
}
message = "Container stopped.";

View file

@ -70,6 +70,9 @@ ParsedHttpResponse parseHttpResponse(const std::string& raw) {
return response;
}
const bool bodyless = (response.status_code >= 100 && response.status_code < 200) ||
response.status_code == 204 || response.status_code == 304;
std::size_t content_length = std::string::npos;
bool chunked = false;
std::size_t line_start = status_end + 2;
@ -90,7 +93,9 @@ ParsedHttpResponse parseHttpResponse(const std::string& raw) {
}
const std::string payload = raw.substr(header_end + 4);
if (chunked) {
if (bodyless) {
response.body.clear();
} else if (chunked) {
if (!decodeChunked(payload, response.body, response.error)) return response;
} else if (content_length != std::string::npos) {
if (payload.size() < content_length) {
@ -102,7 +107,36 @@ ParsedHttpResponse parseHttpResponse(const std::string& raw) {
response.body = payload;
}
if (response.status_code < 200 || response.status_code >= 300) {
if (response.status_code == 304) return response;
response.error = response.body.empty() ? "Docker Engine request failed." : response.body;
}
return response;
}
bool httpResponseComplete(const std::string& raw, const std::string& method) {
const auto header_end = raw.find("\r\n\r\n");
if (header_end == std::string::npos) return false;
const auto status_end = raw.find("\r\n");
if (status_end == std::string::npos || status_end > header_end) return false;
std::istringstream status_line(raw.substr(0, status_end));
std::string version;
int status_code = 0;
status_line >> version >> status_code;
if (method == "HEAD" || (status_code >= 100 && status_code < 200) || status_code == 204 || status_code == 304) return true;
const std::string headers = raw.substr(status_end + 2, header_end - status_end - 2);
const auto transfer = headers.find("Transfer-Encoding:");
if (transfer != std::string::npos && lower(headers.substr(transfer)).find("chunked") != std::string::npos) {
return raw.size() >= header_end + 7 && raw.find("\r\n0\r\n", header_end + 4) != std::string::npos;
}
const auto length = lower(headers).find("content-length:");
if (length == std::string::npos) return false;
const auto line_end = headers.find("\r\n", length);
const auto value_start = headers.find(':', length);
if (value_start == std::string::npos) return false;
try {
const auto expected = std::stoull(trim(headers.substr(value_start + 1, line_end - value_start - 1)));
return raw.size() >= header_end + 4 + expected;
} catch (...) {
return false;
}
}

View file

@ -7,7 +7,8 @@ struct ParsedHttpResponse {
std::string body;
std::string error;
bool ok() const { return status_code >= 200 && status_code < 300 && error.empty(); }
bool ok() const { return ((status_code >= 200 && status_code < 300) || status_code == 304) && error.empty(); }
};
ParsedHttpResponse parseHttpResponse(const std::string& raw);
bool httpResponseComplete(const std::string& raw, const std::string& method = {});

15
src/operation_state.cpp Normal file
View file

@ -0,0 +1,15 @@
#include "operation_state.hpp"
#include <utility>
void OperationState::begin(std::string title, std::string message) {
busy_ = true;
title_ = std::move(title);
message_ = std::move(message);
result_.clear();
}
void OperationState::complete(std::string result) {
busy_ = false;
result_ = std::move(result);
}

20
src/operation_state.hpp Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include <string>
class OperationState {
public:
void begin(std::string title, std::string message);
void complete(std::string result);
bool busy() const { return busy_; }
const std::string& title() const { return title_; }
const std::string& message() const { return message_; }
const std::string& result() const { return result_; }
private:
bool busy_ = false;
std::string title_;
std::string message_;
std::string result_;
};

14
src/stop_waiter.cpp Normal file
View file

@ -0,0 +1,14 @@
#include "stop_waiter.hpp"
#include <thread>
StopProbe waitForStopped(const std::function<StopProbe()>& probe,
std::chrono::milliseconds timeout,
std::chrono::milliseconds interval) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline) {
if (probe() == StopProbe::Stopped) return StopProbe::Stopped;
std::this_thread::sleep_for(interval);
}
return probe();
}

10
src/stop_waiter.hpp Normal file
View file

@ -0,0 +1,10 @@
#pragma once
#include <chrono>
#include <functional>
enum class StopProbe { Stopped, Running, Unknown };
StopProbe waitForStopped(const std::function<StopProbe()>& probe,
std::chrono::milliseconds timeout = std::chrono::milliseconds(2000),
std::chrono::milliseconds interval = std::chrono::milliseconds(100));