diff --git a/src/app.cpp b/src/app.cpp index 304d60b..4d25ca5 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -2224,6 +2224,7 @@ void App::render() renderDecryptWalletDialog(); renderPinDialogs(); renderSwitchStopDaemonDialog(); + renderDaemonStopConfirm(); renderBlockDbReindexDialog(); renderWalletRecoveredDialog(); renderEmptyWalletWarningDialog(); @@ -5652,6 +5653,16 @@ void App::beginShutdown() { // Only start shutdown once if (shutting_down_) return; + + // Guard: don't silently discard an in-progress witness-cache rebuild. If we're about to stop the + // daemon while it's rebuilding (stopping now forces a multi-minute rebuild on the next launch), + // defer shutdown and let render() show the confirm modal. The user's choice re-enters beginShutdown() + // with shutdown_confirmed_ set (and, for "keep node running", shutdown_keep_daemon_override_). + if (!shutdown_confirmed_ && shouldConfirmDaemonStop()) { + pending_shutdown_confirm_ = true; + return; // NOT shutting down yet — the normal UI + modal keep rendering + } + shutting_down_ = true; quit_requested_ = true; shutdown_timer_ = 0.0f; @@ -5712,7 +5723,7 @@ void App::beginShutdown() } auto shutdownDecision = daemon_controller_->shutdownDecision( - settings_ && settings_->getKeepDaemonRunning(), + (settings_ && settings_->getKeepDaemonRunning()) || shutdown_keep_daemon_override_, settings_ && settings_->getStopExternalDaemon()); if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) { DEBUG_LOGF("beginShutdown: %s, skipping daemon stop\n", shutdownDecision.logReason); @@ -5775,6 +5786,100 @@ std::vector App::tailDaemonDebugLog(int maxLines) const return lines; } +bool App::daemonWitnessRebuildActive() const +{ + // Scan the debug.log tail for the daemon's witness-rebuild markers (wallet.cpp): "Cleared witness + // data from" (start), "Setting Initial Sapling Witness" / "Reading blocks for witness rebuild" + // (progress), vs. "rebuilt N note witness cache(s)" / "aborting…" (finished). Active iff the most + // recent relevant line is a start/progress line, not a completion. + const auto lines = tailDaemonDebugLog(80); + int state = 0; // 0 none, 1 active, 2 finished/aborted + for (const auto& l : lines) { + if (l.find("note witness cache(s) to height") != std::string::npos || + l.find("aborting witness rebuild") != std::string::npos || + l.find("aborted during witness rebuild") != std::string::npos) { + state = 2; + } else if (l.find("Reading blocks for witness rebuild") != std::string::npos || + l.find("Setting Initial Sapling Witness") != std::string::npos || + l.find("Cleared witness data from") != std::string::npos) { + state = 1; + } + } + return state == 1; +} + +bool App::shouldConfirmDaemonStop() const +{ + if (!daemon_controller_) return false; + // Only relevant when this shutdown would actually STOP the daemon (embedded, or external with + // stop-on-exit) — a DisconnectOnly shutdown leaves it running and loses nothing. + const auto decision = daemon_controller_->shutdownDecision( + settings_ && settings_->getKeepDaemonRunning(), + settings_ && settings_->getStopExternalDaemon()); + if (decision.action != daemon::DaemonController::ShutdownAction::StopDaemon) return false; + return daemonWitnessRebuildActive(); +} + +void App::renderDaemonStopConfirm() +{ + using namespace ui::material; + if (pending_shutdown_confirm_) { + ImGui::OpenPopup("##DaemonStopConfirm"); + pending_shutdown_confirm_ = false; + daemon_stop_confirm_open_ = true; + } + if (!daemon_stop_confirm_open_) return; + + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + bool proceed = false; + if (ImGui::BeginPopupModal("##DaemonStopConfirm", nullptr, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) { + if (Type().subtitle1()) ImGui::PushFont(Type().subtitle1()); + ImGui::TextUnformatted("Node is rebuilding its witness cache"); + if (Type().subtitle1()) ImGui::PopFont(); + ImGui::Spacing(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f); + ImGui::TextUnformatted( + "Stopping the node now discards the in-progress rebuild and restarts it (several minutes) " + "the next time you open the wallet. You can keep the node running instead."); + ImGui::PopTextWrapPos(); + ImGui::Spacing(); + ImGui::Spacing(); + + if (TactileButton("Keep node running & quit", ImVec2(0, 0))) { + shutdown_keep_daemon_override_ = true; + shutdown_confirmed_ = true; + daemon_stop_confirm_open_ = false; + proceed = true; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 210))); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Error())); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 160))); + const bool stopAnyway = TactileButton("Stop anyway & quit", ImVec2(0, 0)); + ImGui::PopStyleColor(3); + if (stopAnyway) { + shutdown_confirmed_ = true; + daemon_stop_confirm_open_ = false; + proceed = true; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (TactileButton("Cancel", ImVec2(0, 0))) { + daemon_stop_confirm_open_ = false; // abort the quit; stay open + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } else { + daemon_stop_confirm_open_ = false; // dismissed via Esc / click-away = Cancel + } + + // Re-enter shutdown outside the popup scope now that the user has chosen (shutdown_confirmed_ set). + if (proceed) beginShutdown(); +} + void App::renderShutdownScreen() { using namespace ui::material; diff --git a/src/app.h b/src/app.h index ba1f41c..0bcd80d 100644 --- a/src/app.h +++ b/src/app.h @@ -182,6 +182,14 @@ public: */ std::vector tailDaemonDebugLog(int maxLines) const; + // True when the daemon's debug.log shows an in-progress Sapling witness-cache rebuild (best-effort + // heuristic). Stopping the daemon during one discards it and forces a multi-minute redo next launch. + bool daemonWitnessRebuildActive() const; + // Whether beginShutdown() should pause and confirm before stopping the daemon (rebuild in progress). + bool shouldConfirmDaemonStop() const; + // The "node is rebuilding — stop anyway / keep running / cancel" modal, rendered from render(). + void renderDaemonStopConfirm(); + /** * @brief Render loading overlay in content area while daemon is starting/syncing * @param contentH Height of the content area child window @@ -911,6 +919,11 @@ private: bool address_list_dirty_ = false; // P8: dedup rebuildAddressList GuardedStatus shutdown_status_; // thread-safe: written by the shutdown thread, read by the UI (M-05) std::thread shutdown_thread_; + // Confirm-before-stopping-daemon-mid-witness-rebuild guard (see beginShutdown / renderDaemonStopConfirm) + bool pending_shutdown_confirm_ = false; // a quit is deferred, waiting to open the confirm modal + bool daemon_stop_confirm_open_ = false; // the confirm modal is currently showing + bool shutdown_confirmed_ = false; // user chose to proceed — bypass the guard on re-entry + bool shutdown_keep_daemon_override_ = false; // user chose "keep node running" for this shutdown only float shutdown_timer_ = 0.0f; bool force_quit_confirm_ = false; std::chrono::steady_clock::time_point shutdown_start_time_;