From a6b7b966b0f868492d17df1121ea553cec090b92 Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 14 Oct 2026 06:26:13 -0500 Subject: [PATCH] fix(daemon): reset the console offset to the watermark on restart When the daemon was restarted, the output-parse offset was reset to 0. The daemon's output buffer is retained across runs, so the next poll re-read the whole retained tail of the previous run and fed it to the busy tracker as if it were fresh. A witness rebuild or rescan line left over from the old run could then be mistaken for activity in the new one. - On restart the offset is now reset to the absolute end of the output at that moment, captured on the lifecycle thread after the stop and before the start, so the UI resumes parsing only the new run's output rather than replaying the old tail. - The capture is handed to the UI thread through a pending flag and applied there before the next read, both on the normal poll and when skipping the backlog, so the offset is only ever moved on the thread that owns it. Tests cover reading from the captured watermark (only the new run is seen) versus the old reset-to-0 behaviour (the previous run replays). Co-Authored-By: Claude Opus 4.8 --- src/app.cpp | 13 ++++++++++++- src/app.h | 3 +++ src/daemon/daemon_controller.cpp | 2 ++ src/daemon/daemon_controller.h | 1 + src/daemon/embedded_daemon.h | 6 ++++++ tests/test_phase4.cpp | 19 +++++++++++++++++++ 6 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/app.cpp b/src/app.cpp index 56685d9..b7a607f 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -333,7 +333,10 @@ public: bool startDaemon() override { return app_.startEmbeddedDaemon(); } void resetOutputOffset() override { - app_.daemon_output_offset_ = 0; + // Capture the absolute end NOW (lifecycle thread, after stop / before start) so the UI resumes + // parsing only this run's output, not the retained tail of the previous run. + app_.daemon_output_reset_to_.store(app_.daemon_controller_ ? app_.daemon_controller_->outputEnd() : 0); + app_.daemon_output_reset_pending_.store(true); app_.resetDaemonBusy(); } @@ -1280,6 +1283,7 @@ void App::update() // Check daemon output for rescan progress (offloaded to worker) if (daemon_controller_ && daemon_controller_->isRunning()) { + applyPendingDaemonOutputReset(); std::string newOutput = daemon_controller_->outputSince(daemon_output_offset_); if (!newOutput.empty()) { feedDaemonBusy(newOutput); // witness rebuild / startup replay, for the quit guard + stop loop @@ -5741,10 +5745,17 @@ void App::skipDaemonOutputBacklog() if (daemon_controller_ && daemon_controller_->isRunning()) { // Advances daemon_output_offset_ to the end. The busy tracker still sees it: whether the node is // mid-rebuild must not depend on whether the window was minimized while the rebuild started. + applyPendingDaemonOutputReset(); feedDaemonBusy(daemon_controller_->outputSince(daemon_output_offset_)); } } +void App::applyPendingDaemonOutputReset() +{ + if (daemon_output_reset_pending_.exchange(false)) + daemon_output_offset_ = daemon_output_reset_to_.load(); +} + void App::handlePaymentURI(const std::string& uri) { auto payment = util::parsePaymentURI(uri); diff --git a/src/app.h b/src/app.h index 236425d..cd36d57 100644 --- a/src/app.h +++ b/src/app.h @@ -569,6 +569,7 @@ public: // output parser skips while the wallet is locked), so a background witness rebuild that started and // finished meanwhile isn't parsed in one batch and mistaken for a completed rescan (spurious toast). void skipDaemonOutputBacklog(); + void applyPendingDaemonOutputReset(); // A real user input event (key, text, mouse, touch, pen, gamepad button) was ingested. Main thread only. // Called from main.cpp's noteEvent; do NOT call from an SDL event watch/filter (resizeWatcher). void noteUserInput() { idle_.noteActivity(); } @@ -1452,6 +1453,8 @@ private: void setDaemonStatus(std::string s); mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection) + std::atomic daemon_output_reset_pending_{false}; // lifecycle thread -> UI thread + std::atomic daemon_output_reset_to_{0}; // What the node's own output says it is doing (witness rebuild / startup replay). Fed on the UI thread // from the captured output; node_rebuilding_ is its snapshot for the shutdown thread's stop loop. daemon::DaemonBusyTracker daemon_busy_; diff --git a/src/daemon/daemon_controller.cpp b/src/daemon/daemon_controller.cpp index 518e045..ccabd7f 100644 --- a/src/daemon/daemon_controller.cpp +++ b/src/daemon/daemon_controller.cpp @@ -109,6 +109,8 @@ std::vector DaemonController::recentLines(std::size_t count) const return daemon_ ? daemon_->getRecentLines(count) : std::vector{}; } +std::size_t DaemonController::outputEnd() const { return daemon_ ? daemon_->getOutputEnd() : 0; } + std::string DaemonController::outputSince(std::size_t& offset) const { return daemon_ ? daemon_->getOutputSince(offset) : std::string{}; diff --git a/src/daemon/daemon_controller.h b/src/daemon/daemon_controller.h index 78ef21f..6bbf219 100644 --- a/src/daemon/daemon_controller.h +++ b/src/daemon/daemon_controller.h @@ -110,6 +110,7 @@ public: std::function memoryProbe() const; // EmbeddedDaemon::memoryProbe(); empty without a daemon std::vector recentLines(std::size_t count) const; std::string outputSince(std::size_t& offset) const; + std::size_t outputEnd() const; void resetCrashCount(); void setRescanOnNextStart(bool enabled); diff --git a/src/daemon/embedded_daemon.h b/src/daemon/embedded_daemon.h index 386d76b..d341b72 100644 --- a/src/daemon/embedded_daemon.h +++ b/src/daemon/embedded_daemon.h @@ -176,6 +176,12 @@ public: return outputWindowSince(buffer, total, offset); } + /** Absolute end of everything ever appended (a valid getOutputSince() offset). */ + size_t getOutputEnd() const { + std::lock_guard lk(output_mutex_); + return output_total_; + } + /** * @brief Get current output size (thread-safe, no copy) */ diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 579875b..34597ef 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -6105,6 +6105,24 @@ void testOutputWindowSaturation() EXPECT_EQ(offset, total); } +void testOutputEndWatermarkReplay() +{ + using dragonx::daemon::appendBounded; + using dragonx::daemon::outputWindowSince; + const size_t cap = 1024 * 1024; + std::string buf; + size_t total = 0; + const std::string run1 = "run1 rescanning\nrun1 rescan complete\n"; + appendBounded(buf, total, run1.data(), run1.size(), cap); + const size_t end = total; // what getOutputEnd() reports at the pre-start boundary + const std::string run2 = "run2 start\n"; + appendBounded(buf, total, run2.data(), run2.size(), cap); + size_t off = end; + EXPECT_EQ(outputWindowSince(buf, total, off), run2); // only the new run, none of run1 + size_t zero = 0; + EXPECT_EQ(outputWindowSince(buf, total, zero), run1 + run2); // the old reset-to-0 replayed run1 +} + // --------------------------------------------------------------------------------------------------- // S6: the busy gate (don't flood a busy node's RPC queue) and the connect backoff. // --------------------------------------------------------------------------------------------------- @@ -18656,6 +18674,7 @@ int main() testShouldConfirmStop(); testOutputSinceAcrossTrim(); testOutputWindowSaturation(); + testOutputEndWatermarkReplay(); testChooseDaemonStatus(); testDaemonStopStringsTranslated(); testMiningAddressStringsTranslated();