From e9042f091ef8ee02f1a15f605156cbcb7a867157 Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 14 Oct 2026 06:26:13 -0500 Subject: [PATCH] refactor(daemon): share the output ring-window helper The embedded daemon and the xmrig manager each kept their own copy of the console output ring: append new bytes, trim the front to a cap at a newline boundary, and hand a reader everything since its offset. The two copies had drifted. The daemon tracked an absolute byte total so a reader's offset stayed valid across a front-trim, while the miner keyed its reader off the live buffer size, so a trim could silently rewind the console feed. - The append/trim and the since-offset logic now live in one small header, outputWindowSince and appendBounded, included by both. The trim keeps the newline-boundary cut and the guard that stops a lone trailing newline from erasing the whole buffer. - The xmrig manager now carries the same absolute byte total, so its reader resolves against the window the same way and survives a trim (and a process restart that clears the buffer but keeps the total). Tests cover feeding the ring well past the trim point while a reader polls each chunk (the feed is continuous and never dies at the trim), the lone-trailing-newline guard, and a cleared buffer with a retained total. Co-Authored-By: Claude Opus 4.8 --- src/daemon/embedded_daemon.cpp | 13 +---------- src/daemon/embedded_daemon.h | 12 +++------- src/daemon/output_window.h | 41 ++++++++++++++++++++++++++++++++++ src/daemon/xmrig_manager.cpp | 12 +--------- src/daemon/xmrig_manager.h | 11 ++++----- tests/test_phase4.cpp | 35 +++++++++++++++++++++++++++++ 6 files changed, 85 insertions(+), 39 deletions(-) create mode 100644 src/daemon/output_window.h diff --git a/src/daemon/embedded_daemon.cpp b/src/daemon/embedded_daemon.cpp index 5a1f189..68c97cf 100644 --- a/src/daemon/embedded_daemon.cpp +++ b/src/daemon/embedded_daemon.cpp @@ -276,18 +276,7 @@ static constexpr size_t MAX_OUTPUT_BYTES = 1024 * 1024; // 1 MB void EmbeddedDaemon::appendOutput(const char* data, size_t len) { // Caller must hold output_mutex_ - process_output_.append(data, len); - output_total_ += len; - if (process_output_.size() > MAX_OUTPUT_BYTES + MAX_OUTPUT_BYTES / 4) { - // Trim to MAX_OUTPUT_BYTES, cutting at a newline boundary - size_t trim_to = process_output_.size() - MAX_OUTPUT_BYTES; - size_t nl = process_output_.find('\n', trim_to); - if (nl != std::string::npos && nl < process_output_.size() - 1) { - process_output_.erase(0, nl + 1); - } else { - process_output_.erase(0, trim_to); - } - } + appendBounded(process_output_, output_total_, data, len, MAX_OUTPUT_BYTES); } std::vector EmbeddedDaemon::getRecentLines(int maxLines) const diff --git a/src/daemon/embedded_daemon.h b/src/daemon/embedded_daemon.h index 6b65194..386d76b 100644 --- a/src/daemon/embedded_daemon.h +++ b/src/daemon/embedded_daemon.h @@ -16,6 +16,8 @@ #include #include +#include "output_window.h" + #ifdef _WIN32 #include #else @@ -171,15 +173,7 @@ public: /** getOutputSince() on a buffer holding the LAST buffer.size() of `total` bytes ever appended. */ static std::string outputSince(const std::string& buffer, size_t total, size_t& offset) { - const size_t base = total - buffer.size(); // absolute position of buffer[0] - if (offset < base) offset = base; // trimmed before it was read: resume at what's left - if (offset >= total) { - offset = total; - return {}; - } - std::string result = buffer.substr(offset - base); - offset = total; - return result; + return outputWindowSince(buffer, total, offset); } /** diff --git a/src/daemon/output_window.h b/src/daemon/output_window.h new file mode 100644 index 0000000..8745d0d --- /dev/null +++ b/src/daemon/output_window.h @@ -0,0 +1,41 @@ +// DragonX Wallet - ImGui Edition +// Shared absolute-offset output ring helpers (daemon debug.log feed, xmrig console feed). + +#pragma once + +#include +#include + +namespace dragonx { +namespace daemon { + +// getOutputSince() over a buffer holding the LAST buffer.size() of `total` bytes ever appended. +inline std::string outputWindowSince(const std::string& buffer, std::size_t total, std::size_t& offset) +{ + const std::size_t base = total - buffer.size(); // absolute position of buffer[0] + if (offset < base) offset = base; // trimmed before read: resume at what's left + if (offset >= total) { + offset = total; + return {}; + } + std::string result = buffer.substr(offset - base); + offset = total; + return result; +} + +// Append `n` bytes, advance absolute `total`, and trim to ~cap at a newline once size exceeds 1.25x cap. +// The `nl < size-1` guard keeps a lone trailing newline from erasing the whole buffer. +inline void appendBounded(std::string& buffer, std::size_t& total, const char* data, std::size_t n, std::size_t cap) +{ + buffer.append(data, n); + total += n; + if (buffer.size() > cap + cap / 4) { + const std::size_t trim_to = buffer.size() - cap; + const std::size_t nl = buffer.find('\n', trim_to); + if (nl != std::string::npos && nl < buffer.size() - 1) buffer.erase(0, nl + 1); + else buffer.erase(0, trim_to); + } +} + +} // namespace daemon +} // namespace dragonx diff --git a/src/daemon/xmrig_manager.cpp b/src/daemon/xmrig_manager.cpp index 03ab12d..0ef57c7 100644 --- a/src/daemon/xmrig_manager.cpp +++ b/src/daemon/xmrig_manager.cpp @@ -613,17 +613,7 @@ void XmrigManager::drainOutput() { void XmrigManager::appendOutput(const char* data, size_t len) { // Caller must hold output_mutex_ - static constexpr size_t MAX_OUTPUT = 1024 * 1024; // 1 MB cap - process_output_.append(data, len); - if (process_output_.size() > MAX_OUTPUT) { - // Trim from the front at a newline boundary - size_t cut = process_output_.size() - MAX_OUTPUT; - auto pos = process_output_.find('\n', cut); - if (pos != std::string::npos) - process_output_.erase(0, pos + 1); - else - process_output_.erase(0, cut); - } + appendBounded(process_output_, output_total_, data, len, 1024 * 1024); // 1 MB cap (1.25x hysteresis) } std::vector XmrigManager::getRecentLines(int maxLines) const { diff --git a/src/daemon/xmrig_manager.h b/src/daemon/xmrig_manager.h index 3a7bf8d..03c3014 100644 --- a/src/daemon/xmrig_manager.h +++ b/src/daemon/xmrig_manager.h @@ -11,6 +11,8 @@ #include #include +#include "output_window.h" + #ifdef _WIN32 #include #include @@ -106,13 +108,7 @@ public: */ std::string getOutputSince(size_t& offset) const { std::lock_guard lk(output_mutex_); - if (offset >= process_output_.size()) { - offset = process_output_.size(); - return {}; - } - std::string result = process_output_.substr(offset); - offset = process_output_.size(); - return result; + return outputWindowSince(process_output_, output_total_, offset); } /** @@ -169,6 +165,7 @@ private: mutable std::mutex output_mutex_; std::string process_output_; + std::size_t output_total_ = 0; // bytes ever appended (absolute end; guarded by output_mutex_, kept across restarts) // xmrig HTTP API credentials (random per session) int api_port_ = 0; diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index ef75788..579875b 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -6071,6 +6071,40 @@ void testOutputSinceAcrossTrim() EXPECT_TRUE(got.empty()); } +void testOutputWindowSaturation() +{ + using dragonx::daemon::appendBounded; + using dragonx::daemon::outputWindowSince; + const size_t cap = 1024 * 1024; + std::string buf, seen, expected; + size_t total = 0, offset = 0; + int n = 0; + // Feed well past the 1.25x trim point in 4 KB chunks of whole lines; the reader polls every chunk. + while (total < cap * 2) { + std::string chunk; + while (chunk.size() < 4096) chunk += "line " + std::to_string(n++) + "\n"; + appendBounded(buf, total, chunk.data(), chunk.size(), cap); + expected += chunk; + seen += outputWindowSince(buf, total, offset); + } + EXPECT_TRUE(buf.size() <= cap + cap / 4); + EXPECT_TRUE(buf.size() > cap / 2); // trimmed, but not wiped + EXPECT_EQ(total, expected.size()); + EXPECT_EQ(seen, expected); // the feed never dies at the trim + EXPECT_EQ(offset, total); + // A lone trailing newline must not erase the whole buffer. + std::string b2(cap + cap / 4 - 1, 'x'); + size_t t2 = b2.size(); + appendBounded(b2, t2, "\n", 1, cap); + EXPECT_TRUE(b2.size() >= cap); + // clear()-then-keep-total (process restart) keeps absolute offsets valid. + buf.clear(); + const std::string fresh = "new run\n"; + appendBounded(buf, total, fresh.data(), fresh.size(), cap); + EXPECT_EQ(outputWindowSince(buf, total, offset), fresh); + EXPECT_EQ(offset, total); +} + // --------------------------------------------------------------------------------------------------- // S6: the busy gate (don't flood a busy node's RPC queue) and the connect backoff. // --------------------------------------------------------------------------------------------------- @@ -18621,6 +18655,7 @@ int main() testDaemonBusyTracker(); testShouldConfirmStop(); testOutputSinceAcrossTrim(); + testOutputWindowSaturation(); testChooseDaemonStatus(); testDaemonStopStringsTranslated(); testMiningAddressStringsTranslated();