fix: large-wallet sync starvation + shutdown/console-flash UX (Windows full node) #2
107
src/app.cpp
107
src/app.cpp
@@ -2224,6 +2224,7 @@ void App::render()
|
|||||||
renderDecryptWalletDialog();
|
renderDecryptWalletDialog();
|
||||||
renderPinDialogs();
|
renderPinDialogs();
|
||||||
renderSwitchStopDaemonDialog();
|
renderSwitchStopDaemonDialog();
|
||||||
|
renderDaemonStopConfirm();
|
||||||
renderBlockDbReindexDialog();
|
renderBlockDbReindexDialog();
|
||||||
renderWalletRecoveredDialog();
|
renderWalletRecoveredDialog();
|
||||||
renderEmptyWalletWarningDialog();
|
renderEmptyWalletWarningDialog();
|
||||||
@@ -5652,6 +5653,16 @@ void App::beginShutdown()
|
|||||||
{
|
{
|
||||||
// Only start shutdown once
|
// Only start shutdown once
|
||||||
if (shutting_down_) return;
|
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;
|
shutting_down_ = true;
|
||||||
quit_requested_ = true;
|
quit_requested_ = true;
|
||||||
shutdown_timer_ = 0.0f;
|
shutdown_timer_ = 0.0f;
|
||||||
@@ -5712,7 +5723,7 @@ void App::beginShutdown()
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto shutdownDecision = daemon_controller_->shutdownDecision(
|
auto shutdownDecision = daemon_controller_->shutdownDecision(
|
||||||
settings_ && settings_->getKeepDaemonRunning(),
|
(settings_ && settings_->getKeepDaemonRunning()) || shutdown_keep_daemon_override_,
|
||||||
settings_ && settings_->getStopExternalDaemon());
|
settings_ && settings_->getStopExternalDaemon());
|
||||||
if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) {
|
if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) {
|
||||||
DEBUG_LOGF("beginShutdown: %s, skipping daemon stop\n", shutdownDecision.logReason);
|
DEBUG_LOGF("beginShutdown: %s, skipping daemon stop\n", shutdownDecision.logReason);
|
||||||
@@ -5775,6 +5786,100 @@ std::vector<std::string> App::tailDaemonDebugLog(int maxLines) const
|
|||||||
return lines;
|
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()
|
void App::renderShutdownScreen()
|
||||||
{
|
{
|
||||||
using namespace ui::material;
|
using namespace ui::material;
|
||||||
|
|||||||
13
src/app.h
13
src/app.h
@@ -182,6 +182,14 @@ public:
|
|||||||
*/
|
*/
|
||||||
std::vector<std::string> tailDaemonDebugLog(int maxLines) const;
|
std::vector<std::string> 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
|
* @brief Render loading overlay in content area while daemon is starting/syncing
|
||||||
* @param contentH Height of the content area child window
|
* @param contentH Height of the content area child window
|
||||||
@@ -911,6 +919,11 @@ private:
|
|||||||
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
|
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
|
||||||
GuardedStatus shutdown_status_; // thread-safe: written by the shutdown thread, read by the UI (M-05)
|
GuardedStatus shutdown_status_; // thread-safe: written by the shutdown thread, read by the UI (M-05)
|
||||||
std::thread shutdown_thread_;
|
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;
|
float shutdown_timer_ = 0.0f;
|
||||||
bool force_quit_confirm_ = false;
|
bool force_quit_confirm_ = false;
|
||||||
std::chrono::steady_clock::time_point shutdown_start_time_;
|
std::chrono::steady_clock::time_point shutdown_start_time_;
|
||||||
|
|||||||
Reference in New Issue
Block a user