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();