fix: Tier-2 remaining mediums — maintenance/mining feedback + force-quit hang detection
The last four (more involved) robustness items from the audit:
- deleteBlockchainData: post the deleted-item count to the main thread (via an
atomic, since Notifications isn't thread-safe) and show a completion toast
("Blockchain data deleted (N items). Daemon restarting…") — previously the
result was only logged.
- Pool mining: pool start has a connect delay with no feedback; announce
"Starting pool miner — connecting…" on a successful start and "Pool miner
connected and hashing." once the poll confirms it (contained pool_starting_
flag; the shared mining-toggle state machine is untouched).
- Force Quit (shutdown screen): gate it on the status text having STALLED (a
genuine hang) rather than a bare 10s timer — with a 20s hard-ceiling backstop —
and show a state-aware caution naming the stuck step (force-quitting mid daemon
flush risks the chainstate).
- Benchmark: require a confirming second click that first builds candidates to
estimate the duration ("Benchmark takes ~Ns and interrupts mining"), instead
of interrupting mining immediately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
48
src/app.cpp
48
src/app.cpp
@@ -822,6 +822,11 @@ void App::update()
|
|||||||
ps.algo = xs.algo;
|
ps.algo = xs.algo;
|
||||||
ps.version = xs.version;
|
ps.version = xs.version;
|
||||||
ps.connected = xs.connected;
|
ps.connected = xs.connected;
|
||||||
|
// Pool mining has a connect delay — announce once it's actually connected/hashing.
|
||||||
|
if (pool_starting_.load(std::memory_order_relaxed) && (ps.connected || ps.hashrate_10s > 0.0)) {
|
||||||
|
pool_starting_.store(false, std::memory_order_relaxed);
|
||||||
|
ui::Notifications::instance().success("Pool miner connected and hashing.");
|
||||||
|
}
|
||||||
// Get memory directly from OS (more reliable than API)
|
// Get memory directly from OS (more reliable than API)
|
||||||
double memMB = xmrig_manager_->getMemoryUsageMB();
|
double memMB = xmrig_manager_->getMemoryUsageMB();
|
||||||
ps.memory_used = static_cast<int64_t>(memMB * 1024.0 * 1024.0);
|
ps.memory_used = static_cast<int64_t>(memMB * 1024.0 * 1024.0);
|
||||||
@@ -836,6 +841,7 @@ void App::update()
|
|||||||
}
|
}
|
||||||
} else if (xmrig_manager_ && !xmrig_manager_->isRunning()) {
|
} else if (xmrig_manager_ && !xmrig_manager_->isRunning()) {
|
||||||
state_.pool_mining.xmrig_running = false;
|
state_.pool_mining.xmrig_running = false;
|
||||||
|
pool_starting_.store(false, std::memory_order_relaxed); // stopped / never came up
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-balance: periodically re-pick the pool by hashrate (self-throttled).
|
// Auto-balance: periodically re-pick the pool by hashrate (self-throttled).
|
||||||
@@ -1243,6 +1249,15 @@ void App::render()
|
|||||||
// Process deferred encryption from wizard (runs in background)
|
// Process deferred encryption from wizard (runs in background)
|
||||||
processDeferredEncryption();
|
processDeferredEncryption();
|
||||||
|
|
||||||
|
// Surface the blockchain-delete completion on the main thread (the worker can't touch Notifications).
|
||||||
|
{
|
||||||
|
int deletedN = pending_delete_result_.exchange(-1, std::memory_order_relaxed);
|
||||||
|
if (deletedN >= 0) {
|
||||||
|
ui::Notifications::instance().success("Blockchain data deleted (" + std::to_string(deletedN) +
|
||||||
|
" items). The daemon is restarting to re-sync from the network.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Debug screenshot sweep — pins the current (skin,page) and arms capture once settled. Must run
|
// Debug screenshot sweep — pins the current (skin,page) and arms capture once settled. Must run
|
||||||
// before the sidebar reads current_page_ (below) so the forced page is reflected.
|
// before the sidebar reads current_page_ (below) so the forced page is reflected.
|
||||||
updateScreenshotSweep();
|
updateScreenshotSweep();
|
||||||
@@ -3344,6 +3359,8 @@ void App::deleteBlockchainData()
|
|||||||
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
daemon::AsyncLifecycleTaskContext context(token, shutting_down_);
|
||||||
auto result = daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
auto result = daemon_controller_->executeLifecycleOperation(decision, runtime, context);
|
||||||
DEBUG_LOGF("[App] Blockchain data deleted (%d items removed), restarting daemon...\n", result.deletedItems);
|
DEBUG_LOGF("[App] Blockchain data deleted (%d items removed), restarting daemon...\n", result.deletedItems);
|
||||||
|
// Hand the count to the main thread for a completion toast (Notifications isn't thread-safe).
|
||||||
|
pending_delete_result_.store(result.deletedItems, std::memory_order_relaxed);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3480,6 +3497,17 @@ void App::renderShutdownScreen()
|
|||||||
};
|
};
|
||||||
shutdown_timer_ += ImGui::GetIO().DeltaTime;
|
shutdown_timer_ += ImGui::GetIO().DeltaTime;
|
||||||
|
|
||||||
|
// Track how long the status text has been unchanged. A normal shutdown keeps updating the status
|
||||||
|
// ("Stopping pool miner…" -> "Flushing…" -> "Cleaning up…"); a genuine hang stalls it. Offer Force
|
||||||
|
// Quit only once the status has STALLED (likely hung), not on a bare timer, with a hard ceiling so
|
||||||
|
// it's never impossible to escape.
|
||||||
|
static std::string s_lastShutStatus;
|
||||||
|
static float s_shutStallTimer = 0.0f;
|
||||||
|
if (shutdown_status_ != s_lastShutStatus) { s_lastShutStatus = shutdown_status_; s_shutStallTimer = 0.0f; }
|
||||||
|
else s_shutStallTimer += ImGui::GetIO().DeltaTime;
|
||||||
|
const bool shutdownStalled = s_shutStallTimer >= 8.0f;
|
||||||
|
const bool allowForceQuit = shutdownStalled || shutdown_timer_ >= 20.0f;
|
||||||
|
|
||||||
// Use the main viewport so the overlay covers the primary window
|
// Use the main viewport so the overlay covers the primary window
|
||||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||||
ImVec2 vp_pos = vp->Pos;
|
ImVec2 vp_pos = vp->Pos;
|
||||||
@@ -3499,8 +3527,9 @@ void App::renderShutdownScreen()
|
|||||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
||||||
ImGuiWindowFlags_NoSavedSettings;
|
ImGuiWindowFlags_NoSavedSettings;
|
||||||
// Allow input after 10s so Force Quit button is clickable
|
// Allow input only once Force Quit is offered (status stalled / hard ceiling), so the screen stays
|
||||||
if (shutdown_timer_ < 10.0f)
|
// click-through while a normal shutdown is progressing.
|
||||||
|
if (!allowForceQuit)
|
||||||
shutdownFlags |= ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav;
|
shutdownFlags |= ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav;
|
||||||
ImGui::Begin("##ShutdownOverlay", nullptr, shutdownFlags);
|
ImGui::Begin("##ShutdownOverlay", nullptr, shutdownFlags);
|
||||||
|
|
||||||
@@ -3601,11 +3630,22 @@ void App::renderShutdownScreen()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// 4b. Force Quit button — appears after 10 seconds
|
// 4b. Force Quit button — appears once the shutdown appears stalled (or a hard-ceiling backstop)
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
if (shutdown_timer_ >= 10.0f) {
|
if (allowForceQuit) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
// State-aware caution: while the status is a daemon flush/exit step, force-quitting risks the
|
||||||
|
// chainstate; say so instead of a bare button.
|
||||||
|
if (shutdownStalled && !shutdown_status_.empty()) {
|
||||||
|
std::string stalledMsg = "Still \"" + shutdown_status_ + "\" — force quitting now may corrupt chain data.";
|
||||||
|
ImVec2 ms = ImGui::CalcTextSize(stalledMsg.c_str());
|
||||||
|
ImGui::SetCursorPosX(cx - ms.x * 0.5f);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
||||||
|
ImGui::TextUnformatted(stalledMsg.c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::Spacing();
|
||||||
|
}
|
||||||
const char* forceLabel = TR("force_quit");
|
const char* forceLabel = TR("force_quit");
|
||||||
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0);
|
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0);
|
||||||
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
|
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
|
||||||
|
|||||||
@@ -601,6 +601,10 @@ private:
|
|||||||
// Daemon restart (e.g. after changing debug log categories)
|
// Daemon restart (e.g. after changing debug log categories)
|
||||||
std::atomic<bool> daemon_restarting_{false};
|
std::atomic<bool> daemon_restarting_{false};
|
||||||
|
|
||||||
|
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
|
||||||
|
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
|
||||||
|
std::atomic<int> pending_delete_result_{-1};
|
||||||
|
|
||||||
// Encryption state check timeout
|
// Encryption state check timeout
|
||||||
float encryption_check_timer_ = 0.0f;
|
float encryption_check_timer_ = 0.0f;
|
||||||
|
|
||||||
@@ -702,6 +706,10 @@ private:
|
|||||||
// Mining toggle guard (prevents concurrent setgenerate calls)
|
// Mining toggle guard (prevents concurrent setgenerate calls)
|
||||||
std::atomic<bool> mining_toggle_in_progress_{false};
|
std::atomic<bool> mining_toggle_in_progress_{false};
|
||||||
|
|
||||||
|
// True from a successful startPoolMining() until the miner is confirmed connected/hashing in the
|
||||||
|
// poll — drives the "connecting…" → "connected" feedback for pool mining (which has a connect delay).
|
||||||
|
std::atomic<bool> pool_starting_{false};
|
||||||
|
|
||||||
// Auto-shield guard (prevents concurrent auto-shield operations)
|
// Auto-shield guard (prevents concurrent auto-shield operations)
|
||||||
std::atomic<bool> auto_shield_pending_{false};
|
std::atomic<bool> auto_shield_pending_{false};
|
||||||
|
|
||||||
|
|||||||
@@ -1949,6 +1949,10 @@ void App::startPoolMining(int threads)
|
|||||||
} else {
|
} else {
|
||||||
ui::Notifications::instance().error("Failed to start pool miner: " + err);
|
ui::Notifications::instance().error("Failed to start pool miner: " + err);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Miner spawned — it still needs a few seconds to connect to the pool and start hashing.
|
||||||
|
pool_starting_.store(true, std::memory_order_relaxed);
|
||||||
|
ui::Notifications::instance().info("Starting pool miner — connecting to the pool…");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -453,14 +453,27 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
// Require a wallet address for pool mining
|
// Require a wallet address for pool mining
|
||||||
std::string worker(s_pool_worker);
|
std::string worker(s_pool_worker);
|
||||||
if (!worker.empty()) {
|
if (!worker.empty()) {
|
||||||
s_benchmark.reset();
|
static bool s_benchConfirm = false;
|
||||||
s_benchmark.was_pool_running = state.pool_mining.xmrig_running;
|
if (!s_benchConfirm) {
|
||||||
s_benchmark.prev_threads = s_selected_threads;
|
// First click: build candidates so we can estimate the total duration, then
|
||||||
s_benchmark.buildCandidates(max_threads);
|
// require a confirming click (the benchmark interrupts mining and runs a while).
|
||||||
s_benchmark.phase = ThreadBenchmark::Phase::Starting;
|
s_benchmark.reset();
|
||||||
// Stop any active solo mining first
|
s_benchmark.buildCandidates(max_threads);
|
||||||
if (mining.generate)
|
s_benchConfirm = true;
|
||||||
app->stopMining();
|
char msg[128];
|
||||||
|
snprintf(msg, sizeof(msg),
|
||||||
|
"Benchmark takes ~%ds and interrupts mining. Click again to start.",
|
||||||
|
(int)(s_benchmark.totalEstimatedSecs() + 0.5f));
|
||||||
|
Notifications::instance().warning(msg);
|
||||||
|
} else {
|
||||||
|
// Second click: start (candidates already built above).
|
||||||
|
s_benchConfirm = false;
|
||||||
|
s_benchmark.was_pool_running = state.pool_mining.xmrig_running;
|
||||||
|
s_benchmark.prev_threads = s_selected_threads;
|
||||||
|
s_benchmark.phase = ThreadBenchmark::Phase::Starting;
|
||||||
|
if (mining.generate)
|
||||||
|
app->stopMining();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user