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:
parent
91fa50370a
commit
e8e93d7b21
17 changed files with 449 additions and 43 deletions
|
|
@ -35,6 +35,8 @@ add_executable(tux-dock
|
||||||
src/docker_engine_client.cpp
|
src/docker_engine_client.cpp
|
||||||
src/http_response_parser.cpp
|
src/http_response_parser.cpp
|
||||||
src/container_parser.cpp
|
src/container_parser.cpp
|
||||||
|
src/operation_state.cpp
|
||||||
|
src/stop_waiter.cpp
|
||||||
src/docker_manager.cpp
|
src/docker_manager.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -66,4 +68,28 @@ if(BUILD_TESTING)
|
||||||
target_link_libraries(tux-dock-container-tests PRIVATE nlohmann_json::nlohmann_json)
|
target_link_libraries(tux-dock-container-tests PRIVATE nlohmann_json::nlohmann_json)
|
||||||
target_compile_options(tux-dock-container-tests PRIVATE -Wall -Wextra -Wpedantic)
|
target_compile_options(tux-dock-container-tests PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
add_test(NAME tux-dock-container-tests COMMAND tux-dock-container-tests)
|
add_test(NAME tux-dock-container-tests COMMAND tux-dock-container-tests)
|
||||||
|
|
||||||
|
add_executable(tux-dock-operation-tests
|
||||||
|
tests/test_operation_state.cpp
|
||||||
|
src/operation_state.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(tux-dock-operation-tests PRIVATE src)
|
||||||
|
target_compile_options(tux-dock-operation-tests PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
add_test(NAME tux-dock-operation-tests COMMAND tux-dock-operation-tests)
|
||||||
|
|
||||||
|
add_executable(tux-dock-stop-tests
|
||||||
|
tests/test_stop_waiter.cpp
|
||||||
|
src/stop_waiter.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(tux-dock-stop-tests PRIVATE src)
|
||||||
|
target_compile_options(tux-dock-stop-tests PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
add_test(NAME tux-dock-stop-tests COMMAND tux-dock-stop-tests)
|
||||||
|
|
||||||
|
add_executable(tux-dock-stop-sequence-tests
|
||||||
|
tests/test_stop_sequence.cpp
|
||||||
|
src/stop_waiter.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(tux-dock-stop-sequence-tests PRIVATE src)
|
||||||
|
target_compile_options(tux-dock-stop-sequence-tests PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
add_test(NAME tux-dock-stop-sequence-tests COMMAND tux-dock-stop-sequence-tests)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
42
DEVLOG.md
Normal file
42
DEVLOG.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Tux-Dock Development Log
|
||||||
|
|
||||||
|
## 0.1-beta
|
||||||
|
|
||||||
|
This release establishes the first beta-quality Docker integration and TUI workflow.
|
||||||
|
|
||||||
|
### Docker integration
|
||||||
|
|
||||||
|
- Added direct Docker Engine API access through `/var/run/docker.sock`.
|
||||||
|
- Added startup Docker preflight using `GET /_ping` before launching the TUI.
|
||||||
|
- Added synchronous initial container and image loading.
|
||||||
|
- Added asynchronous cache refreshes after mutations and interactive sessions.
|
||||||
|
- Preserved cached state when a refresh fails.
|
||||||
|
- Added structured container/image JSON mapping with exited-container support.
|
||||||
|
|
||||||
|
### Process execution
|
||||||
|
|
||||||
|
- Replaced `system()` and `popen()` with a `fork`/`exec` process runner.
|
||||||
|
- Added captured stdout/stderr and inherited terminal I/O modes.
|
||||||
|
- Kept direct CLI execution for interactive terminal handoffs.
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
|
||||||
|
- Added HTTP response parsing for Content-Length, chunked transfer, and bodyless responses.
|
||||||
|
- Added robust handling for Docker `204` and idempotent `304` responses.
|
||||||
|
- Added tri-state stop polling: stopped, running, and unknown.
|
||||||
|
- Added bounded stop retries after transport timeouts.
|
||||||
|
- Stop completion now refreshes cached state before showing the result modal.
|
||||||
|
|
||||||
|
### TUI
|
||||||
|
|
||||||
|
- Replaced the persistent status panel with a centered actions panel.
|
||||||
|
- Added busy-operation modals with spinner feedback.
|
||||||
|
- Blocked input during long-running Docker operations.
|
||||||
|
- Added structured, scrollable container and image list dialogs.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Added HTTP response parser tests.
|
||||||
|
- Added container JSON mapping tests.
|
||||||
|
- Added operation-state tests.
|
||||||
|
- Added stop polling and timeout-sequence regression tests.
|
||||||
58
README.md
58
README.md
|
|
@ -2,7 +2,7 @@
|
||||||
### A lightweight C++ Docker TUI
|
### A lightweight C++ Docker TUI
|
||||||
|
|
||||||
Tux-Dock is a modern **C++17** Docker terminal frontend built with **FTXUI**.
|
Tux-Dock is a modern **C++17** Docker terminal frontend built with **FTXUI**.
|
||||||
It gives you a guided, keyboard-first TUI for common Docker operations like pulling images, running containers, inspecting IPs, and managing images/containers without memorizing long CLI flags.
|
It gives you a guided, keyboard-first TUI for common Docker operations without memorizing long CLI flags.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -10,12 +10,16 @@ It gives you a guided, keyboard-first TUI for common Docker operations like pull
|
||||||
|
|
||||||
- Interactive Docker workflows through a single-screen TUI with modal steps.
|
- Interactive Docker workflows through a single-screen TUI with modal steps.
|
||||||
- Picker-based selection (arrow keys + Enter) for containers/images instead of numeric menus.
|
- Picker-based selection (arrow keys + Enter) for containers/images instead of numeric menus.
|
||||||
- High-level status panel with responsive wait states for slower operations.
|
- Busy-operation modals with a spinner and input blocking while Docker work completes.
|
||||||
- Rich container display with state and forwarded ports.
|
- Rich container display with state and forwarded ports.
|
||||||
- Interactive shell handoff with clean terminal clear before/after shell transitions.
|
- Interactive shell handoff with clean terminal clear before/after shell transitions.
|
||||||
- Image operations: pull/list/delete with curated quick picks and custom image support.
|
- Image operations: pull/list/delete with curated quick picks and custom image support.
|
||||||
- Script-to-image workflow: generate Dockerfile from bash script, then optionally build.
|
- Script-to-image workflow: generate Dockerfile from bash script, then optionally build.
|
||||||
- MySQL quick start flow with version/password/port prompts.
|
- MySQL quick start flow with version/password/port prompts.
|
||||||
|
- Docker Engine API access through `/var/run/docker.sock` for structured list and lifecycle operations.
|
||||||
|
- Direct `fork`/`exec` process execution for CLI-backed streaming and interactive commands.
|
||||||
|
- Persistent container listings that retain exited containers.
|
||||||
|
- Robust stop handling with state polling, timeout retry, and idempotent stop responses.
|
||||||
- About screen in-app with project/version/repository info.
|
- About screen in-app with project/version/repository info.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -35,12 +39,14 @@ It gives you a guided, keyboard-first TUI for common Docker operations like pull
|
||||||
git clone https://mentalnet.xyz/forgejo/markmental/tuxdock.git
|
git clone https://mentalnet.xyz/forgejo/markmental/tuxdock.git
|
||||||
cd tuxdock
|
cd tuxdock
|
||||||
|
|
||||||
# Configure & build (FTXUI is fetched automatically)
|
# Configure, build, and test (FTXUI and nlohmann/json are fetched automatically)
|
||||||
cmake -S . -B build
|
./compile.sh
|
||||||
cmake --build build -j
|
|
||||||
|
|
||||||
# Run it (requires Docker permissions)
|
# Run it (requires Docker permissions)
|
||||||
sudo ./build/tux-dock
|
sudo ./build/tux-dock
|
||||||
|
|
||||||
|
# Build without running tests
|
||||||
|
./compile.sh --no-test
|
||||||
```
|
```
|
||||||
|
|
||||||
Prefer a prebuilt binary? CI artifacts are published at:
|
Prefer a prebuilt binary? CI artifacts are published at:
|
||||||
|
|
@ -72,7 +78,7 @@ Current TUI actions:
|
||||||
|
|
||||||
## Design Overview
|
## Design Overview
|
||||||
|
|
||||||
Tux-Dock is now structured around **two main classes**:
|
Tux-Dock is organized around the TUI, Docker manager, Engine API client, and direct process runner:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
class DockerManager {
|
class DockerManager {
|
||||||
|
|
@ -85,9 +91,9 @@ public:
|
||||||
bool running;
|
bool running;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Docker command/data layer
|
bool checkConnection(...);
|
||||||
std::vector<ContainerInfo> getContainerList() const;
|
ListResult<ContainerInfo> getContainerList() const;
|
||||||
std::vector<std::pair<std::string, std::string>> getImageList() const;
|
ListResult<ImageInfo> getImageList() const;
|
||||||
bool pullImage(...);
|
bool pullImage(...);
|
||||||
bool runContainerInteractive(...);
|
bool runContainerInteractive(...);
|
||||||
bool startInteractive(...);
|
bool startInteractive(...);
|
||||||
|
|
@ -106,11 +112,11 @@ public:
|
||||||
void Run();
|
void Run();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// TUI orchestration layer
|
// TUI orchestration layer with modal busy states
|
||||||
void OpenInput(...);
|
void OpenInput(...);
|
||||||
void OpenSelect(...);
|
void OpenSelect(...);
|
||||||
void OpenConfirm(...);
|
void OpenConfirm(...);
|
||||||
void RunDeferredStatusAction(...);
|
void BeginBusyOperation(...);
|
||||||
void RunWithRestoredIO(...);
|
void RunWithRestoredIO(...);
|
||||||
void ExecuteSelectedAction();
|
void ExecuteSelectedAction();
|
||||||
// action handlers bridge UI -> DockerManager
|
// action handlers bridge UI -> DockerManager
|
||||||
|
|
@ -119,24 +125,44 @@ private:
|
||||||
|
|
||||||
### Responsibilities
|
### Responsibilities
|
||||||
|
|
||||||
|
- `DockerEngineClient`
|
||||||
|
- Talks directly to Docker over the Unix socket.
|
||||||
|
- Parses HTTP responses, including chunked and bodyless responses.
|
||||||
|
|
||||||
|
- `ProcessRunner`
|
||||||
|
- Executes direct argument vectors using `fork` and `exec`.
|
||||||
|
- Supports captured output and inherited terminal I/O.
|
||||||
|
|
||||||
- `DockerManager`
|
- `DockerManager`
|
||||||
- Executes Docker commands.
|
- Maps Engine API JSON into application data.
|
||||||
- Escapes shell arguments and returns success/failure + user-facing messages.
|
- Performs lifecycle operations and robust stop-state confirmation.
|
||||||
- Collects container/image data used by the UI.
|
- Preserves cached state when refreshes fail.
|
||||||
|
|
||||||
- `TuxDockApp`
|
- `TuxDockApp`
|
||||||
- Renders the FTXUI interface.
|
- Renders the FTXUI interface.
|
||||||
- Manages modal flows (input/select/confirm/message).
|
- Manages modal flows (input/select/confirm/message).
|
||||||
- Handles status updates, deferred actions, and interactive shell transitions.
|
- Handles modal flows, busy operations, blocked input, and interactive shell transitions.
|
||||||
- Coordinates end-to-end user flows by calling `DockerManager` methods.
|
- Coordinates end-to-end user flows by calling `DockerManager` methods.
|
||||||
|
|
||||||
This split keeps Docker behavior isolated while making UI behavior easier to extend.
|
This split keeps Docker behavior isolated while making UI behavior easier to extend.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -S . -B build -DBUILD_TESTING=ON
|
||||||
|
cmake --build build -j
|
||||||
|
ctest --test-dir build --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
`compile.sh` runs these tests by default. Pass `--no-test` to skip them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## About / Version
|
## About / Version
|
||||||
|
|
||||||
- Version: `022526-dev`
|
- Version: `0.1-beta`
|
||||||
- Created by: `markmental`
|
- Created by: `markmental`
|
||||||
- GitHub: https://github.com/MARKMENTAL/tuxdock
|
- GitHub: https://github.com/MARKMENTAL/tuxdock
|
||||||
- Forgejo: https://mentalnet.xyz/forgejo/markmental/tuxdock
|
- Forgejo: https://mentalnet.xyz/forgejo/markmental/tuxdock
|
||||||
|
|
|
||||||
26
compile.sh
26
compile.sh
|
|
@ -1,3 +1,27 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
cmake -S . -B build && cmake --build build -j && echo "tux-dock successfully compiled!"
|
set -eu
|
||||||
|
|
||||||
|
if [ "$#" -gt 1 ] || { [ "$#" -eq 1 ] && [ "$1" != "--no-test" ]; }; then
|
||||||
|
printf '%s\n' "Usage: $0 [--no-test]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_tests=1
|
||||||
|
if [ "$#" -eq 1 ]; then
|
||||||
|
run_tests=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$run_tests" -eq 1 ]; then
|
||||||
|
cmake -S . -B build -DBUILD_TESTING=ON
|
||||||
|
else
|
||||||
|
cmake -S . -B build -DBUILD_TESTING=OFF
|
||||||
|
fi
|
||||||
|
|
||||||
|
cmake --build build -j
|
||||||
|
|
||||||
|
if [ "$run_tests" -eq 1 ]; then
|
||||||
|
ctest --test-dir build --output-on-failure
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "tux-dock successfully compiled!"
|
||||||
|
|
|
||||||
85
main.cpp
85
main.cpp
|
|
@ -1,4 +1,5 @@
|
||||||
#include "src/docker_manager.hpp"
|
#include "src/docker_manager.hpp"
|
||||||
|
#include "src/operation_state.hpp"
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
@ -21,7 +22,7 @@ public:
|
||||||
int Run();
|
int Run();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class ModalMode { None, Input, Confirm, Select, Message };
|
enum class ModalMode { None, Input, Confirm, Select, Message, Busy };
|
||||||
struct RunContainerContext { std::string image; int port_count = 0; std::vector<std::string> ports; };
|
struct RunContainerContext { std::string image; int port_count = 0; std::vector<std::string> ports; };
|
||||||
struct MySqlContext { std::string port; std::string password; std::string version; };
|
struct MySqlContext { std::string port; std::string password; std::string version; };
|
||||||
struct DockerfileContext { std::string base_image; std::string script_path; std::string output_file; std::string image_name; };
|
struct DockerfileContext { std::string base_image; std::string script_path; std::string output_file; std::string image_name; };
|
||||||
|
|
@ -46,6 +47,8 @@ private:
|
||||||
ftxui::ScreenInteractive* screen_ = nullptr;
|
ftxui::ScreenInteractive* screen_ = nullptr;
|
||||||
std::thread refresh_thread_;
|
std::thread refresh_thread_;
|
||||||
std::vector<std::thread> action_threads_;
|
std::vector<std::thread> action_threads_;
|
||||||
|
OperationState operation_state_;
|
||||||
|
std::size_t spinner_frame_ = 0;
|
||||||
|
|
||||||
static bool IsDigits(const std::string& value);
|
static bool IsDigits(const std::string& value);
|
||||||
static std::string ShortId(const std::string& id);
|
static std::string ShortId(const std::string& id);
|
||||||
|
|
@ -68,6 +71,8 @@ private:
|
||||||
void PromptContainerSelection(const std::string&, std::function<void(const std::string&, const std::string&)>);
|
void PromptContainerSelection(const std::string&, std::function<void(const std::string&, const std::string&)>);
|
||||||
void PromptImageSelection(const std::string&, std::function<void(const std::string&, const std::string&)>);
|
void PromptImageSelection(const std::string&, std::function<void(const std::string&, const std::string&)>);
|
||||||
void RunDeferredStatusAction(const std::string&, std::function<std::string()>);
|
void RunDeferredStatusAction(const std::string&, std::function<std::string()>);
|
||||||
|
void BeginBusyOperation(const std::string&, const std::string&, std::function<std::string()>);
|
||||||
|
void BeginStopOperation(const std::string& id);
|
||||||
void RefreshState(const std::string& message = "Refreshing Docker state...");
|
void RefreshState(const std::string& message = "Refreshing Docker state...");
|
||||||
void ApplyRefreshResults(DockerManager::ListResult<DockerManager::ContainerInfo> containers,
|
void ApplyRefreshResults(DockerManager::ListResult<DockerManager::ContainerInfo> containers,
|
||||||
DockerManager::ListResult<DockerManager::ImageInfo> images);
|
DockerManager::ListResult<DockerManager::ImageInfo> images);
|
||||||
|
|
@ -155,16 +160,49 @@ void TuxDockApp::PromptContainerSelection(const std::string& title, std::functio
|
||||||
void TuxDockApp::PromptImageSelection(const std::string& title, std::function<void(const std::string&, const std::string&)> callback) { if (images_.empty()) { RefreshState("No cached images. Refreshing..."); return; } std::vector<std::string> options; for (const auto& image : images_) options.push_back(image.second + " (" + ShortId(image.first) + ")"); OpenSelect(title, "Select an image with arrows and press Enter.", std::move(options), [this, callback = std::move(callback)](bool ok, int selected) { if (!ok) return SetStatus("Action cancelled."); if (selected < 0 || selected >= static_cast<int>(images_.size())) return SetStatus("Please choose a valid image."); const auto& image = images_[static_cast<std::size_t>(selected)]; callback(image.first, image.second); }); }
|
void TuxDockApp::PromptImageSelection(const std::string& title, std::function<void(const std::string&, const std::string&)> callback) { if (images_.empty()) { RefreshState("No cached images. Refreshing..."); return; } std::vector<std::string> options; for (const auto& image : images_) options.push_back(image.second + " (" + ShortId(image.first) + ")"); OpenSelect(title, "Select an image with arrows and press Enter.", std::move(options), [this, callback = std::move(callback)](bool ok, int selected) { if (!ok) return SetStatus("Action cancelled."); if (selected < 0 || selected >= static_cast<int>(images_.size())) return SetStatus("Please choose a valid image."); const auto& image = images_[static_cast<std::size_t>(selected)]; callback(image.first, image.second); }); }
|
||||||
|
|
||||||
void TuxDockApp::RunDeferredStatusAction(const std::string& wait, std::function<std::string()> action) {
|
void TuxDockApp::RunDeferredStatusAction(const std::string& wait, std::function<std::string()> action) {
|
||||||
SetStatus(wait);
|
BeginBusyOperation("Working", wait, std::move(action));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TuxDockApp::BeginBusyOperation(const std::string& title,
|
||||||
|
const std::string& message,
|
||||||
|
std::function<std::string()> action) {
|
||||||
|
operation_state_.begin(title, message);
|
||||||
|
modal_mode_ = ModalMode::Busy;
|
||||||
|
spinner_frame_ = 0;
|
||||||
auto* active = screen_;
|
auto* active = screen_;
|
||||||
|
if (active) active->PostEvent(ftxui::Event::Custom);
|
||||||
action_threads_.emplace_back([this, active, action = std::move(action)]() mutable {
|
action_threads_.emplace_back([this, active, action = std::move(action)]() mutable {
|
||||||
const auto message = action();
|
const auto message = action();
|
||||||
if (active && ftxui::ScreenInteractive::Active() == active) {
|
if (active && ftxui::ScreenInteractive::Active() == active) {
|
||||||
active->Post([this, message] { SetStatus(message); RefreshState(); });
|
active->Post([this, message] {
|
||||||
|
operation_state_.complete(message);
|
||||||
|
modal_mode_ = ModalMode::None;
|
||||||
|
OpenMessage("Operation complete", message);
|
||||||
|
RefreshState();
|
||||||
|
});
|
||||||
active->PostEvent(ftxui::Event::Custom);
|
active->PostEvent(ftxui::Event::Custom);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
void TuxDockApp::BeginStopOperation(const std::string& id) {
|
||||||
|
operation_state_.begin("Stopping container", "Stopping and refreshing state...");
|
||||||
|
modal_mode_ = ModalMode::Busy;
|
||||||
|
auto* active = screen_;
|
||||||
|
action_threads_.emplace_back([this, active, id] {
|
||||||
|
std::string message;
|
||||||
|
const bool stopped = docker_.stopContainer(id, message);
|
||||||
|
auto containers = docker_.getContainerList();
|
||||||
|
auto images = docker_.getImageList();
|
||||||
|
if (!active || ftxui::ScreenInteractive::Active() != active) return;
|
||||||
|
active->Post([this, stopped, message, containers = std::move(containers), images = std::move(images)]() mutable {
|
||||||
|
ApplyRefreshResults(std::move(containers), std::move(images));
|
||||||
|
operation_state_.complete(message);
|
||||||
|
modal_mode_ = ModalMode::None;
|
||||||
|
OpenMessage(stopped ? "Container stopped" : "Stop failed", message);
|
||||||
|
});
|
||||||
|
active->PostEvent(ftxui::Event::Custom);
|
||||||
|
});
|
||||||
|
}
|
||||||
void TuxDockApp::ClearTerminal() { std::cout << "\x1b[2J\x1b[H" << std::flush; }
|
void TuxDockApp::ClearTerminal() { std::cout << "\x1b[2J\x1b[H" << std::flush; }
|
||||||
void TuxDockApp::RunWithRestoredIO(const std::function<void()>& action, bool before, bool after) { if (screen_) screen_->WithRestoredIO([&] { if (before) ClearTerminal(); action(); if (after) ClearTerminal(); })(); else action(); }
|
void TuxDockApp::RunWithRestoredIO(const std::function<void()>& action, bool before, bool after) { if (screen_) screen_->WithRestoredIO([&] { if (before) ClearTerminal(); action(); if (after) ClearTerminal(); })(); else action(); }
|
||||||
|
|
||||||
|
|
@ -183,18 +221,18 @@ void TuxDockApp::ActionListImages() {
|
||||||
}
|
}
|
||||||
OpenListMessage("Images", FormatImageList(images_));
|
OpenListMessage("Images", FormatImageList(images_));
|
||||||
}
|
}
|
||||||
void TuxDockApp::ActionPullImage() { OpenInput("Pull Docker Image", "Enter image name:", [this](bool ok, const std::string& image) { if (!ok) return; RunDeferredStatusAction("Please wait, pulling image...", [this, image] { std::string m; docker_.pullImage(image, m); return m; }); }); }
|
void TuxDockApp::ActionPullImage() { OpenInput("Pull Docker Image", "Enter image name:", [this](bool ok, const std::string& image) { if (!ok) return; BeginBusyOperation("Pulling image", "Please wait...", [this, image] { std::string m; docker_.pullImage(image, m); return m; }); }); }
|
||||||
void TuxDockApp::ActionRunContainer() { PromptImageSelection("Run Interactive Container", [this](const std::string&, const std::string& tag) { auto context = std::make_shared<RunContainerContext>(); context->image = tag; PromptPortCountAndRun(context); }); }
|
void TuxDockApp::ActionRunContainer() { PromptImageSelection("Run Interactive Container", [this](const std::string&, const std::string& tag) { auto context = std::make_shared<RunContainerContext>(); context->image = tag; PromptPortCountAndRun(context); }); }
|
||||||
void TuxDockApp::PromptPortCountAndRun(const std::shared_ptr<RunContainerContext>& c) { OpenInput("Port Mappings", "How many port mappings? (0 for none)", [this, c](bool ok, const std::string& value) { if (!ok) return; if (!IsDigits(value)) return SetStatus("Please enter a valid number."); c->port_count = std::stoi(value); PromptNextPort(c, 0); }); }
|
void TuxDockApp::PromptPortCountAndRun(const std::shared_ptr<RunContainerContext>& c) { OpenInput("Port Mappings", "How many port mappings? (0 for none)", [this, c](bool ok, const std::string& value) { if (!ok) return; if (!IsDigits(value)) return SetStatus("Please enter a valid number."); c->port_count = std::stoi(value); PromptNextPort(c, 0); }); }
|
||||||
void TuxDockApp::PromptNextPort(const std::shared_ptr<RunContainerContext>& c, int index) { if (index >= c->port_count) { std::string m; RunWithRestoredIO([this, c, &m] { docker_.runContainerInteractive(c->image, c->ports, m); }, true, true); SetStatus(m); RefreshState(); return; } OpenInput("Port Mapping", "Enter mapping #" + std::to_string(index + 1), [this, c, index](bool ok, const std::string& value) { if (!ok) return; if (!IsValidPortMapping(value)) return SetStatus("Use host:container format."); c->ports.push_back(value); PromptNextPort(c, index + 1); }); }
|
void TuxDockApp::PromptNextPort(const std::shared_ptr<RunContainerContext>& c, int index) { if (index >= c->port_count) { std::string m; RunWithRestoredIO([this, c, &m] { docker_.runContainerInteractive(c->image, c->ports, m); }, true, true); SetStatus(m); RefreshState(); return; } OpenInput("Port Mapping", "Enter mapping #" + std::to_string(index + 1), [this, c, index](bool ok, const std::string& value) { if (!ok) return; if (!IsValidPortMapping(value)) return SetStatus("Use host:container format."); c->ports.push_back(value); PromptNextPort(c, index + 1); }); }
|
||||||
void TuxDockApp::ActionStartInteractive() { PromptContainerSelection("Start Interactively", [this](const std::string& id, const std::string&) { std::string m; RunWithRestoredIO([this, &id, &m] { docker_.startInteractive(id, m); }, true, true); SetStatus(m); RefreshState(); }); }
|
void TuxDockApp::ActionStartInteractive() { PromptContainerSelection("Start Interactively", [this](const std::string& id, const std::string&) { std::string m; RunWithRestoredIO([this, &id, &m] { docker_.startInteractive(id, m); }, true, true); SetStatus(m); RefreshState(); }); }
|
||||||
void TuxDockApp::ActionStartDetached() { PromptContainerSelection("Start Detached", [this](const std::string& id, const std::string&) { RunDeferredStatusAction("Starting container...", [this, id] { std::string m; docker_.startDetached(id, m); return m; }); }); }
|
void TuxDockApp::ActionStartDetached() { PromptContainerSelection("Start Detached", [this](const std::string& id, const std::string&) { BeginBusyOperation("Starting container", "Please wait...", [this, id] { std::string m; docker_.startDetached(id, m); return m; }); }); }
|
||||||
void TuxDockApp::ActionDeleteImage() { PromptImageSelection("Delete Image", [this](const std::string& id, const std::string& tag) { OpenConfirm("Delete Image", "Delete image " + tag + "?", [this, id](bool ok) { if (ok) RunDeferredStatusAction("Deleting image...", [this, id] { std::string m; docker_.deleteImage(id, m); return m; }); }); }); }
|
void TuxDockApp::ActionDeleteImage() { PromptImageSelection("Delete Image", [this](const std::string& id, const std::string& tag) { OpenConfirm("Delete Image", "Delete image " + tag + "?", [this, id](bool ok) { if (ok) BeginBusyOperation("Deleting image", "Please wait...", [this, id] { std::string m; docker_.deleteImage(id, m); return m; }); }); }); }
|
||||||
void TuxDockApp::ActionStopContainer() { PromptContainerSelection("Stop Container", [this](const std::string& id, const std::string&) { RunDeferredStatusAction("Stopping container...", [this, id] { std::string m; docker_.stopContainer(id, m); return m; }); }); }
|
void TuxDockApp::ActionStopContainer() { PromptContainerSelection("Stop Container", [this](const std::string& id, const std::string&) { BeginStopOperation(id); }); }
|
||||||
void TuxDockApp::ActionRemoveContainer() { PromptContainerSelection("Remove Container", [this](const std::string& id, const std::string& name) { OpenConfirm("Remove Container", "Remove container " + name + "?", [this, id](bool ok) { if (ok) RunDeferredStatusAction("Removing container...", [this, id] { std::string m; docker_.removeContainer(id, m); return m; }); }); }); }
|
void TuxDockApp::ActionRemoveContainer() { PromptContainerSelection("Remove Container", [this](const std::string& id, const std::string& name) { OpenConfirm("Remove Container", "Remove container " + name + "?", [this, id](bool ok) { if (ok) BeginBusyOperation("Removing container", "Please wait...", [this, id] { std::string m; docker_.removeContainer(id, m); return m; }); }); }); }
|
||||||
void TuxDockApp::ActionExecShell() { PromptContainerSelection("Open Shell", [this](const std::string& id, const std::string&) { std::string m; RunWithRestoredIO([this, &id, &m] { docker_.execShell(id, m); }, true, true); SetStatus(m); }); }
|
void TuxDockApp::ActionExecShell() { PromptContainerSelection("Open Shell", [this](const std::string& id, const std::string&) { std::string m; RunWithRestoredIO([this, &id, &m] { docker_.execShell(id, m); }, true, true); SetStatus(m); }); }
|
||||||
void TuxDockApp::ActionExecDetachedCommand() { PromptContainerSelection("Run Detached Command", [this](const std::string& id, const std::string& name) { OpenInput("Detached Command", "Enter command to run in " + name + ":", [this, id](bool ok, const std::string& command) { if (!ok) return; RunDeferredStatusAction("Running command...", [this, id, command] { std::string m; docker_.execDetachedCommand(id, command, m); return m; }); }); }); }
|
void TuxDockApp::ActionExecDetachedCommand() { PromptContainerSelection("Run Detached Command", [this](const std::string& id, const std::string& name) { OpenInput("Detached Command", "Enter command to run in " + name + ":", [this, id](bool ok, const std::string& command) { if (!ok) return; BeginBusyOperation("Running command", "Please wait...", [this, id, command] { std::string m; docker_.execDetachedCommand(id, command, m); return m; }); }); }); }
|
||||||
void TuxDockApp::ActionSpinUpMySQL() { OpenInput("MySQL Setup", "Enter port mapping:", [this](bool ok, const std::string& port) { if (!ok || !IsValidPortMapping(port)) return SetStatus("Use host:container format."); OpenInput("MySQL Setup", "Enter root password:", [this, port](bool ok2, const std::string& password) { if (!ok2) return; OpenInput("MySQL Setup", "Enter version tag:", [this, port, password](bool ok3, const std::string& version) { if (!ok3) return; RunDeferredStatusAction("Launching MySQL...", [this, port, password, version] { std::string m; docker_.spinUpMySQL(port, password, version, m); return m; }); }); }, true); }); }
|
void TuxDockApp::ActionSpinUpMySQL() { OpenInput("MySQL Setup", "Enter port mapping:", [this](bool ok, const std::string& port) { if (!ok || !IsValidPortMapping(port)) return SetStatus("Use host:container format."); OpenInput("MySQL Setup", "Enter root password:", [this, port](bool ok2, const std::string& password) { if (!ok2) return; OpenInput("MySQL Setup", "Enter version tag:", [this, port, password](bool ok3, const std::string& version) { if (!ok3) return; BeginBusyOperation("Launching MySQL", "Please wait...", [this, port, password, version] { std::string m; docker_.spinUpMySQL(port, password, version, m); return m; }); }); }, true); }); }
|
||||||
void TuxDockApp::ActionCreateDockerfile() {
|
void TuxDockApp::ActionCreateDockerfile() {
|
||||||
OpenInput("Dockerfile Builder", "Enter base image:", [this](bool ok, const std::string& base) {
|
OpenInput("Dockerfile Builder", "Enter base image:", [this](bool ok, const std::string& base) {
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
|
|
@ -204,7 +242,7 @@ void TuxDockApp::ActionCreateDockerfile() {
|
||||||
if (!ok3) return;
|
if (!ok3) return;
|
||||||
OpenInput("Dockerfile Builder", "Enter image name (empty skips build):", [this, base, script, output](bool ok4, const std::string& image) {
|
OpenInput("Dockerfile Builder", "Enter image name (empty skips build):", [this, base, script, output](bool ok4, const std::string& image) {
|
||||||
if (!ok4) return;
|
if (!ok4) return;
|
||||||
RunDeferredStatusAction("Creating Dockerfile...", [this, base, script, output, image] {
|
BeginBusyOperation("Building image", "Creating Dockerfile and running build...", [this, base, script, output, image] {
|
||||||
std::string m;
|
std::string m;
|
||||||
docker_.createDockerfile(base, script, output, image, m);
|
docker_.createDockerfile(base, script, output, image, m);
|
||||||
return m;
|
return m;
|
||||||
|
|
@ -214,11 +252,19 @@ void TuxDockApp::ActionCreateDockerfile() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
void TuxDockApp::ActionAbout() { SetStatus("Tux-Dock 022526-dev\nCreated by markmental"); }
|
void TuxDockApp::ActionAbout() { OpenMessage("About Tux-Dock", "Tux-Dock 0.1-beta | Created by markmental"); }
|
||||||
|
|
||||||
void TuxDockApp::ExecuteSelectedAction() { switch (menu_selected_) { case 0: ActionPullImage(); break; case 1: ActionRunContainer(); break; case 2: ActionListContainers(); break; case 3: ActionListImages(); break; case 4: ActionStartInteractive(); break; case 5: ActionStartDetached(); break; case 6: ActionDeleteImage(); break; case 7: ActionStopContainer(); break; case 8: ActionRemoveContainer(); break; case 9: ActionExecShell(); break; case 10: ActionExecDetachedCommand(); break; case 11: ActionSpinUpMySQL(); break; case 12: ActionCreateDockerfile(); break; case 13: ActionAbout(); break; case 14: if (screen_) screen_->ExitLoopClosure()(); break; default: break; } }
|
void TuxDockApp::ExecuteSelectedAction() { switch (menu_selected_) { case 0: ActionPullImage(); break; case 1: ActionRunContainer(); break; case 2: ActionListContainers(); break; case 3: ActionListImages(); break; case 4: ActionStartInteractive(); break; case 5: ActionStartDetached(); break; case 6: ActionDeleteImage(); break; case 7: ActionStopContainer(); break; case 8: ActionRemoveContainer(); break; case 9: ActionExecShell(); break; case 10: ActionExecDetachedCommand(); break; case 11: ActionSpinUpMySQL(); break; case 12: ActionCreateDockerfile(); break; case 13: ActionAbout(); break; case 14: if (screen_) screen_->ExitLoopClosure()(); break; default: break; } }
|
||||||
|
|
||||||
bool TuxDockApp::OnEvent(ftxui::Event event) { if (modal_mode_ == ModalMode::Input) { if (event == ftxui::Event::Return) { ResolveInput(true); return true; } if (event == ftxui::Event::Escape) { ResolveInput(false); return true; } return input_component_->OnEvent(event); } if (modal_mode_ == ModalMode::Confirm) { if (event == ftxui::Event::Return || event == ftxui::Event::Character("y")) { ResolveConfirm(true); return true; } if (event == ftxui::Event::Escape || event == ftxui::Event::Character("n")) { ResolveConfirm(false); return true; } return true; } if (modal_mode_ == ModalMode::Select) { if (event == ftxui::Event::Return) { ResolveSelect(true); return true; } if (event == ftxui::Event::Escape) { ResolveSelect(false); return true; } return select_component_->OnEvent(event); } if (modal_mode_ == ModalMode::Message) { if (event == ftxui::Event::Return || event == ftxui::Event::Escape) { CloseMessage(); return true; } return true; } if (event == ftxui::Event::Return) { ExecuteSelectedAction(); return true; } return false; }
|
bool TuxDockApp::OnEvent(ftxui::Event event) {
|
||||||
|
if (modal_mode_ == ModalMode::Busy) return true;
|
||||||
|
if (modal_mode_ == ModalMode::Input) { if (event == ftxui::Event::Return) { ResolveInput(true); return true; } if (event == ftxui::Event::Escape) { ResolveInput(false); return true; } return input_component_->OnEvent(event); }
|
||||||
|
if (modal_mode_ == ModalMode::Confirm) { if (event == ftxui::Event::Return || event == ftxui::Event::Character("y")) { ResolveConfirm(true); return true; } if (event == ftxui::Event::Escape || event == ftxui::Event::Character("n")) { ResolveConfirm(false); return true; } return true; }
|
||||||
|
if (modal_mode_ == ModalMode::Select) { if (event == ftxui::Event::Return) { ResolveSelect(true); return true; } if (event == ftxui::Event::Escape) { ResolveSelect(false); return true; } return select_component_->OnEvent(event); }
|
||||||
|
if (modal_mode_ == ModalMode::Message) { if (event == ftxui::Event::Return || event == ftxui::Event::Escape) { CloseMessage(); return true; } return true; }
|
||||||
|
if (event == ftxui::Event::Return) { ExecuteSelectedAction(); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
ftxui::Element TuxDockApp::RenderModal() const {
|
ftxui::Element TuxDockApp::RenderModal() const {
|
||||||
using namespace ftxui;
|
using namespace ftxui;
|
||||||
Element body;
|
Element body;
|
||||||
|
|
@ -232,17 +278,26 @@ ftxui::Element TuxDockApp::RenderModal() const {
|
||||||
} else if (modal_mode_ == ModalMode::Select) {
|
} else if (modal_mode_ == ModalMode::Select) {
|
||||||
body = vbox(Elements{paragraph(modal_text_), separator(), select_component_->Render() | frame | vscroll_indicator});
|
body = vbox(Elements{paragraph(modal_text_), separator(), select_component_->Render() | frame | vscroll_indicator});
|
||||||
footer = text("Up/Down: choose Enter: confirm Esc: cancel") | dim;
|
footer = text("Up/Down: choose Enter: confirm Esc: cancel") | dim;
|
||||||
|
} else if (modal_mode_ == ModalMode::Busy) {
|
||||||
|
static const std::string spinner = "|/-\\";
|
||||||
|
body = vbox(Elements{
|
||||||
|
text(operation_state_.message()),
|
||||||
|
separator(),
|
||||||
|
text(std::string(" ") + spinner[spinner_frame_ % spinner.size()]) | bold,
|
||||||
|
});
|
||||||
|
footer = text("Please wait; input is disabled") | dim;
|
||||||
} else {
|
} else {
|
||||||
body = (modal_content_ ? std::move(modal_content_) : paragraph(modal_text_)) |
|
body = (modal_content_ ? std::move(modal_content_) : paragraph(modal_text_)) |
|
||||||
frame | vscroll_indicator |
|
frame | vscroll_indicator |
|
||||||
size(HEIGHT, LESS_THAN, 18) | size(WIDTH, LESS_THAN, 96);
|
size(HEIGHT, LESS_THAN, 18) | size(WIDTH, LESS_THAN, 96);
|
||||||
footer = text("Enter/Esc: close") | dim;
|
footer = text("Enter/Esc: close") | dim;
|
||||||
}
|
}
|
||||||
return window(text(modal_title_), vbox(Elements{body, separator(), footer})) |
|
const std::string title = modal_mode_ == ModalMode::Busy ? operation_state_.title() : modal_title_;
|
||||||
|
return window(text(title), vbox(Elements{body, separator(), footer})) |
|
||||||
size(WIDTH, LESS_THAN, 96) | size(WIDTH, GREATER_THAN, 42) |
|
size(WIDTH, LESS_THAN, 96) | size(WIDTH, GREATER_THAN, 42) |
|
||||||
size(HEIGHT, LESS_THAN, 24) | size(HEIGHT, GREATER_THAN, 10) | center;
|
size(HEIGHT, LESS_THAN, 24) | size(HEIGHT, GREATER_THAN, 10) | center;
|
||||||
}
|
}
|
||||||
ftxui::Element TuxDockApp::Render() const { using namespace ftxui; Elements lines; std::stringstream s(status_); std::string line; while (std::getline(s, line)) lines.push_back(line.empty() ? text(" ") : text(line)); auto base = hbox(Elements{window(text("Actions"), vbox(Elements{menu_component_->Render() | frame | vscroll_indicator, separator(), text("Up/Down: navigate Enter: select") | dim})) | size(WIDTH, GREATER_THAN, 48) | flex, separator(), window(text("Status"), vbox(Elements{vbox(std::move(lines)) | yflex | frame | vscroll_indicator, separator(), text("Engine API cache") | dim})) | size(WIDTH, GREATER_THAN, 48) | flex}) | border; return modal_mode_ == ModalMode::None ? base : dbox({base, RenderModal() | clear_under | center}); }
|
ftxui::Element TuxDockApp::Render() const { using namespace ftxui; auto base = window(text("Actions"), vbox(Elements{menu_component_->Render() | frame | vscroll_indicator, separator(), text("Up/Down: navigate Enter: select") | dim})) | size(WIDTH, GREATER_THAN, 56) | size(WIDTH, LESS_THAN, 90) | center; return modal_mode_ == ModalMode::None ? base : dbox({base, RenderModal() | clear_under | center}); }
|
||||||
int TuxDockApp::Run() {
|
int TuxDockApp::Run() {
|
||||||
std::string connection_error;
|
std::string connection_error;
|
||||||
if (!docker_.checkConnection(connection_error)) {
|
if (!docker_.checkConnection(connection_error)) {
|
||||||
|
|
|
||||||
|
|
@ -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_SNDTIMEO, &timeout, sizeof(timeout));
|
||||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||||
|
|
||||||
std::ostringstream request;
|
std::ostringstream wire_request;
|
||||||
request << method << " " << path << " HTTP/1.1\r\n"
|
wire_request << method << " " << path << " HTTP/1.1\r\n"
|
||||||
<< "Host: docker\r\n"
|
<< "Host: docker\r\n"
|
||||||
<< "Connection: close\r\n"
|
<< "Connection: close\r\n"
|
||||||
<< "Content-Type: application/json\r\n"
|
<< "Content-Type: application/json\r\n"
|
||||||
<< "Content-Length: " << body.size() << "\r\n\r\n"
|
<< "Content-Length: " << body.size() << "\r\n\r\n"
|
||||||
<< body;
|
<< body;
|
||||||
const std::string wire = request.str();
|
const std::string wire = wire_request.str();
|
||||||
std::size_t sent = 0;
|
std::size_t sent = 0;
|
||||||
while (sent < wire.size()) {
|
while (sent < wire.size()) {
|
||||||
const ssize_t count = write(fd, wire.data() + sent, wire.size() - sent);
|
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;
|
std::string raw;
|
||||||
char buffer[8192];
|
char buffer[8192];
|
||||||
ssize_t count = 0;
|
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));
|
raw.append(buffer, static_cast<std::size_t>(count));
|
||||||
}
|
}
|
||||||
close(fd);
|
close(fd);
|
||||||
if (count < 0) {
|
if (count < 0) {
|
||||||
response.error = std::strerror(errno);
|
if (errno == EINTR) return request(method, path, body);
|
||||||
return response;
|
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);
|
const auto parsed = parseHttpResponse(raw);
|
||||||
response.status_code = parsed.status_code;
|
response.status_code = parsed.status_code;
|
||||||
response.body = parsed.body;
|
response.body = parsed.body;
|
||||||
response.error = parsed.error;
|
response.error = parsed.error;
|
||||||
|
if (response.status_code == 304 && response.error.empty()) response.error.clear();
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "process_runner.hpp"
|
#include "process_runner.hpp"
|
||||||
#include "container_parser.hpp"
|
#include "container_parser.hpp"
|
||||||
|
#include "stop_waiter.hpp"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#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 {
|
bool DockerManager::stopContainer(const std::string& id, std::string& message) const {
|
||||||
const EngineResponse response = engine_.request("POST", "/containers/" + id + "/stop");
|
const auto stop_request = [this, &id] { return engine_.request("POST", "/containers/" + id + "/stop"); };
|
||||||
if (!response.ok()) {
|
const auto acceptable = [](const EngineResponse& response) {
|
||||||
message = apiError(response, "Could not stop that container.");
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
message = "Container stopped.";
|
message = "Container stopped.";
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,9 @@ ParsedHttpResponse parseHttpResponse(const std::string& raw) {
|
||||||
return response;
|
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;
|
std::size_t content_length = std::string::npos;
|
||||||
bool chunked = false;
|
bool chunked = false;
|
||||||
std::size_t line_start = status_end + 2;
|
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);
|
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;
|
if (!decodeChunked(payload, response.body, response.error)) return response;
|
||||||
} else if (content_length != std::string::npos) {
|
} else if (content_length != std::string::npos) {
|
||||||
if (payload.size() < content_length) {
|
if (payload.size() < content_length) {
|
||||||
|
|
@ -102,7 +107,36 @@ ParsedHttpResponse parseHttpResponse(const std::string& raw) {
|
||||||
response.body = payload;
|
response.body = payload;
|
||||||
}
|
}
|
||||||
if (response.status_code < 200 || response.status_code >= 300) {
|
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;
|
response.error = response.body.empty() ? "Docker Engine request failed." : response.body;
|
||||||
}
|
}
|
||||||
return response;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ struct ParsedHttpResponse {
|
||||||
std::string body;
|
std::string body;
|
||||||
std::string error;
|
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);
|
ParsedHttpResponse parseHttpResponse(const std::string& raw);
|
||||||
|
bool httpResponseComplete(const std::string& raw, const std::string& method = {});
|
||||||
|
|
|
||||||
15
src/operation_state.cpp
Normal file
15
src/operation_state.cpp
Normal 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
20
src/operation_state.hpp
Normal 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
14
src/stop_waiter.cpp
Normal 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
10
src/stop_waiter.hpp
Normal 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));
|
||||||
|
|
@ -34,10 +34,32 @@ void testErrorResponse() {
|
||||||
assert(response.error == "bad");
|
assert(response.error == "bad");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void testCompleteResponseDoesNotNeedPeerClose() {
|
||||||
|
const std::string raw = "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\n\r\n";
|
||||||
|
assert(httpResponseComplete(raw));
|
||||||
|
const auto response = parseHttpResponse(raw);
|
||||||
|
assert(response.ok());
|
||||||
|
assert(response.status_code == 204);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testBodylessStatuses() {
|
||||||
|
for (const int status : {204, 304}) {
|
||||||
|
const std::string raw = "HTTP/1.1 " + std::to_string(status) + " Status\r\n\r\n";
|
||||||
|
assert(httpResponseComplete(raw));
|
||||||
|
const auto parsed = parseHttpResponse(raw);
|
||||||
|
if (!parsed.ok()) std::cerr << "status=" << status << " error=" << parsed.error << "\n";
|
||||||
|
assert(parsed.ok());
|
||||||
|
}
|
||||||
|
const std::string head = "HTTP/1.1 200 OK\r\n\r\n";
|
||||||
|
assert(httpResponseComplete(head, "HEAD"));
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
testContentLength();
|
testContentLength();
|
||||||
testChunked();
|
testChunked();
|
||||||
testTruncatedBody();
|
testTruncatedBody();
|
||||||
testErrorResponse();
|
testErrorResponse();
|
||||||
|
testCompleteResponseDoesNotNeedPeerClose();
|
||||||
|
testBodylessStatuses();
|
||||||
std::cout << "HTTP response parser tests passed\n";
|
std::cout << "HTTP response parser tests passed\n";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
tests/test_operation_state.cpp
Normal file
19
tests/test_operation_state.cpp
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#include "operation_state.hpp"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
OperationState operation;
|
||||||
|
assert(!operation.busy());
|
||||||
|
|
||||||
|
operation.begin("Stopping container", "Please wait...");
|
||||||
|
assert(operation.busy());
|
||||||
|
assert(operation.title() == "Stopping container");
|
||||||
|
assert(operation.message() == "Please wait...");
|
||||||
|
|
||||||
|
operation.complete("Container stopped.");
|
||||||
|
assert(!operation.busy());
|
||||||
|
assert(operation.result() == "Container stopped.");
|
||||||
|
std::cout << "Operation state tests passed\n";
|
||||||
|
}
|
||||||
22
tests/test_stop_sequence.cpp
Normal file
22
tests/test_stop_sequence.cpp
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#include "stop_waiter.hpp"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int stop_requests = 0;
|
||||||
|
int probes = 0;
|
||||||
|
const auto first = [&] {
|
||||||
|
++stop_requests;
|
||||||
|
return stop_requests == 1 ? StopProbe::Unknown : StopProbe::Stopped;
|
||||||
|
};
|
||||||
|
const auto state = waitForStopped([&] {
|
||||||
|
++probes;
|
||||||
|
return probes < 2 ? StopProbe::Unknown : StopProbe::Stopped;
|
||||||
|
}, std::chrono::milliseconds(10), std::chrono::milliseconds(1));
|
||||||
|
assert(state == StopProbe::Stopped);
|
||||||
|
assert(first() == StopProbe::Unknown);
|
||||||
|
assert(first() == StopProbe::Stopped);
|
||||||
|
assert(stop_requests == 2);
|
||||||
|
std::cout << "Stop sequence tests passed\n";
|
||||||
|
}
|
||||||
33
tests/test_stop_waiter.cpp
Normal file
33
tests/test_stop_waiter.cpp
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#include "stop_waiter.hpp"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <chrono>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int calls = 0;
|
||||||
|
const auto stopped = waitForStopped([&] {
|
||||||
|
return ++calls < 3 ? StopProbe::Running : StopProbe::Stopped;
|
||||||
|
},
|
||||||
|
std::chrono::milliseconds(100),
|
||||||
|
std::chrono::milliseconds(1));
|
||||||
|
assert(stopped == StopProbe::Stopped);
|
||||||
|
|
||||||
|
const auto timed_out = waitForStopped([] { return StopProbe::Unknown; },
|
||||||
|
std::chrono::milliseconds(5),
|
||||||
|
std::chrono::milliseconds(1));
|
||||||
|
assert(timed_out == StopProbe::Unknown);
|
||||||
|
int transient_calls = 0;
|
||||||
|
const auto transient = waitForStopped([&] {
|
||||||
|
++transient_calls;
|
||||||
|
if (transient_calls < 3) return StopProbe::Unknown;
|
||||||
|
return StopProbe::Stopped;
|
||||||
|
}, std::chrono::milliseconds(100), std::chrono::milliseconds(1));
|
||||||
|
assert(transient == StopProbe::Stopped);
|
||||||
|
// A failed refresh must not be interpreted as a stopped state.
|
||||||
|
const auto preserved = waitForStopped([] { return StopProbe::Unknown; },
|
||||||
|
std::chrono::milliseconds(3),
|
||||||
|
std::chrono::milliseconds(1));
|
||||||
|
assert(preserved == StopProbe::Unknown);
|
||||||
|
std::cout << "Stop waiter tests passed\n";
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue