Fixed the duplicate portmappings caused by Docker's /containers/json API returning two Ports entries for every bound port

This commit is contained in:
mrkmntal 2026-08-29 21:25:58 -04:00
commit c3419524e5
5 changed files with 24 additions and 5 deletions

View file

@ -1,5 +1,16 @@
# Tux-Dock Development Log
## 0.4.1-beta
### TUI stability
- Fixed duplicate port mappings shown in the container list TUI. Docker's `/containers/json` API returns separate `Ports` entries for IPv4 (`0.0.0.0`) and IPv6 (`::`) bindings of the same host port; the container parser now deduplicates these into a single `public:private` display string.
- Added a container parser regression test covering IPv4/IPv6 duplicate port bindings.
### Version
- Bumped version to `0.4.1-beta`.
## 0.4-beta
### ARM64 / tuxreaperd fixes

View file

@ -251,7 +251,7 @@ ctest --test-dir build --output-on-failure
## About / Version
- Version: `0.4-beta`
- Version: `0.4.1-beta`
- Created by: `markmental`
- GitHub: https://github.com/MARKMENTAL/tuxdock
- Forgejo: https://mentalnet.xyz/forgejo-v2/markmental/tuxdock

View file

@ -717,7 +717,7 @@ void TuxDockApp::ActionExecDetachedCommand() {
}
void TuxDockApp::ActionAbout() {
OpenMessage("About Tux-Dock", "Tux-Dock 0.4-beta | Created by markmental");
OpenMessage("About Tux-Dock", "Tux-Dock 0.4.1-beta | Created by markmental");
}
void TuxDockApp::ExecuteSelectedAction() {

View file

@ -3,6 +3,7 @@
#include <nlohmann/json.hpp>
#include <sstream>
#include <unordered_set>
using json = nlohmann::json;
@ -17,6 +18,7 @@ std::string portsFor(const json& item) {
const auto it = item.find("Ports");
if (it == item.end() || !it->is_array()) return {};
std::ostringstream result;
std::unordered_set<std::string> seen;
bool first = true;
for (const auto& port : *it) {
if (!port.is_object()) continue;
@ -24,8 +26,11 @@ std::string portsFor(const json& item) {
const auto private_it = port.find("PrivatePort");
if (public_it == port.end() || private_it == port.end() ||
!public_it->is_number() || !private_it->is_number()) continue;
const std::string mapping = std::to_string(public_it->get<int>()) + ":" +
std::to_string(private_it->get<int>());
if (!seen.insert(mapping).second) continue;
if (!first) result << ", ";
result << *public_it << ":" << *private_it;
result << mapping;
first = false;
}
return result.str();

View file

@ -7,15 +7,18 @@ int main() {
const auto result = parseContainerList(R"([
{"Id":"running-id","Names":["/web"],"State":"running","Status":"Up 2 minutes","Ports":[]},
{"Id":"exited-id","Names":["/old"],"State":"exited","Status":"Exited (0) 1 minute ago","Ports":[{"PrivatePort":80,"Type":"tcp"}]},
{"Id":"odd-id","Names":["/odd"],"State":"exited","Ports":"unexpected"}
{"Id":"odd-id","Names":["/odd"],"State":"exited","Ports":"unexpected"},
{"Id":"mapped-id","Names":["/mapped"],"State":"running","Status":"Up 1 minute","Ports":[{"IP":"0.0.0.0","PrivatePort":80,"PublicPort":8080,"Type":"tcp"},{"IP":"::","PrivatePort":80,"PublicPort":8080,"Type":"tcp"}]}
])");
assert(result.ok());
assert(result.items.size() == 3);
assert(result.items.size() == 4);
assert(result.items[0].running);
assert(!result.items[1].running);
assert(result.items[1].name == "old");
assert(result.items[1].ports.empty());
assert(result.items[2].name == "odd");
assert(result.items[3].name == "mapped");
assert(result.items[3].ports == "8080:80");
const auto empty = parseContainerList("[]");
assert(empty.ok());