From 8fe20f638563ccfe545528ee53dcefb355aad269 Mon Sep 17 00:00:00 2001 From: DanS Date: Sun, 18 Oct 2026 12:09:49 -0500 Subject: [PATCH] feat(mining): persistent start/stop error caption + timeout-aware pool status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A mining start/stop/pool failure now sets a persistent App::mining_last_error_ (cleared on the next attempt), rendered as a red caption with "Press Mine to try again" below the Mine button (plus a Dismiss, and a Windows-Defender "How to fix" on an AV block), so a crashed/blocked/failed miner is no longer just a button that "did nothing" once its toast faded. Two more hardcoded English strings (pool-payout-invalid, AV-blocked) moved to i18n. - Pool connection status is now a persistent "Status" row in the pool stats card (PoolMiningState.running_since / last_error): it flips from "Connecting…" to "Can't reach pool — check the URL" after a 30s timeout instead of sitting on red forever, and no longer vanishes once logs arrive. The old transient banner in mining_tab.cpp was removed. Co-Authored-By: Claude Opus 4.8 --- src/app.cpp | 23 +++++++++++++++++++++-- src/app.h | 9 +++++++++ src/app_network.cpp | 27 +++++++++++++++++++-------- src/data/wallet_state.h | 2 ++ src/ui/windows/mining_controls.cpp | 24 ++++++++++++++++++++++++ src/ui/windows/mining_stats.cpp | 17 ++++++++++++++++- src/ui/windows/mining_tab.cpp | 21 +++------------------ src/util/i18n.cpp | 10 ++++++++++ 8 files changed, 104 insertions(+), 29 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index e19d08a..b604882 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1233,14 +1233,18 @@ void App::update() if (xmrig_manager_ && state_.pool_mining.xmrig_running && xmrig_manager_->getState() == daemon::XmrigManager::State::Error) { state_.pool_mining.xmrig_running = false; + state_.pool_mining.running_since = 0; + state_.pool_mining.last_error.clear(); state_.pool_mining.hashrate_10s = 0.0; state_.pool_mining.hashrate_60s = 0.0; state_.pool_mining.hashrate_15m = 0.0; pool_starting_.store(false, std::memory_order_relaxed); pool_threads_target_.store(0, std::memory_order_relaxed); // that miner is gone const std::string err = xmrig_manager_->getLastError(); - ui::Notifications::instance().error(err.empty() ? TR("appx_miner_stopped_unexpectedly") - : (std::string(TR("appx_miner_stopped_prefix")) + err)); + const std::string mmsg = err.empty() ? std::string(TR("appx_miner_stopped_unexpectedly")) + : (std::string(TR("appx_miner_stopped_prefix")) + err); + setMiningError(mmsg); // persist: a miner that crashed/was killed otherwise just "did nothing" + ui::Notifications::instance().error(mmsg); } // Poll xmrig stats every ~2 seconds (use a simple toggle) @@ -1273,6 +1277,19 @@ void App::update() ps.threads_active = xs.threads_active; ps.pool_hashrate = xs.pool_hashrate; ps.log_lines = xmrig_manager_->getRecentLines(30); + if (ps.running_since == 0) ps.running_since = std::time(nullptr); + // While not yet connected, surface the newest error-looking log line so "Connecting" can flip + // to a real "can't reach pool" after a timeout instead of sitting on red forever. + ps.last_error.clear(); + if (!ps.connected) { + for (auto it = ps.log_lines.rbegin(); it != ps.log_lines.rend(); ++it) { + std::string l = *it; + std::transform(l.begin(), l.end(), l.begin(), [](unsigned char c){ return (char)std::tolower(c); }); + if (l.find("error") != std::string::npos || l.find("refused") != std::string::npos || + l.find("timed out") != std::string::npos || l.find("unreachable") != std::string::npos || + l.find("dns") != std::string::npos) { ps.last_error = *it; break; } + } + } // Record hashrate sample for the chart ps.hashrate_history.push_back(ps.hashrate_10s); @@ -1283,6 +1300,8 @@ void App::update() } } else if (xmrig_manager_ && !xmrig_manager_->isRunning()) { state_.pool_mining.xmrig_running = false; + state_.pool_mining.running_since = 0; + state_.pool_mining.last_error.clear(); pool_starting_.store(false, std::memory_order_relaxed); // stopped / never came up } diff --git a/src/app.h b/src/app.h index 031d9fc..8000ec2 100644 --- a/src/app.h +++ b/src/app.h @@ -389,6 +389,11 @@ public: void startMining(int threads); void stopMining(); bool isMiningToggleInProgress() const { return mining_toggle_in_progress_.load(std::memory_order_relaxed); } + // Persistent mining error (last start/stop/pool failure) so a failed miner isn't just a fading toast. + const std::string& miningLastError() const { return mining_last_error_; } + bool miningErrorIsAntivirus() const { return mining_error_av_; } + void clearMiningError() { mining_last_error_.clear(); mining_error_av_ = false; } + void showAntivirusHelp() { pending_antivirus_dialog_ = true; } // Windows-only dialog; inert elsewhere // Pool mining (xmrig) void startPoolMining(int threads); @@ -1745,6 +1750,10 @@ private: // Mining toggle guard (prevents concurrent setgenerate calls) std::atomic mining_toggle_in_progress_{false}; + // Mining error state (main-thread only — every writer is in a MainCb or render). + std::string mining_last_error_; + bool mining_error_av_ = false; + void setMiningError(std::string msg, bool av = false) { mining_last_error_ = std::move(msg); mining_error_av_ = av; } // 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). diff --git a/src/app_network.cpp b/src/app_network.cpp index e0c4930..b533e14 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -3529,6 +3529,7 @@ void App::startMining(int threads) const int maxThreads = std::max(1, (int)std::thread::hardware_concurrency()); threads = std::clamp(threads, 1, maxThreads); if (mining_toggle_in_progress_.exchange(true)) return; // already in progress + clearMiningError(); // a fresh attempt wipes any stale error worker_->post([this, threads]() -> rpc::RPCWorker::MainCb { bool ok = false; @@ -3548,7 +3549,9 @@ void App::startMining(int threads) DEBUG_LOGF("Mining started with %d threads\n", threads); } else { DEBUG_LOGF("Failed to start mining: %s\n", errMsg.c_str()); - ui::Notifications::instance().error(std::string(TR("toast_mining_failed")) + util::friendlyRpcError(errMsg)); + const std::string merr = std::string(TR("toast_mining_failed")) + util::friendlyRpcError(errMsg); + setMiningError(merr); + ui::Notifications::instance().error(merr); } }; }); @@ -3559,6 +3562,7 @@ void App::stopMining() if (!supportsSoloMining()) return; if (!state_.connected || !rpc_ || !worker_) return; if (mining_toggle_in_progress_.exchange(true)) return; // already in progress + clearMiningError(); worker_->post([this]() -> rpc::RPCWorker::MainCb { bool ok = false; @@ -3580,7 +3584,9 @@ void App::stopMining() } else { // Don't silently leave generate=true as if it worked: tell the user and let the next // getmininginfo refresh reconcile the true daemon state. (M-07) - ui::Notifications::instance().error(std::string(TR("toast_stop_mining_failed")) + util::friendlyRpcError(errMsg)); + const std::string merr = std::string(TR("toast_stop_mining_failed")) + util::friendlyRpcError(errMsg); + setMiningError(merr); + ui::Notifications::instance().error(merr); } }; }); @@ -3634,9 +3640,11 @@ void App::postPoolMinerJob(int threads, bool retargetOnly) // A pool pays with ordinary transactions, so only a shielded address can be paid (private chain). if (!cfg.worker_name.empty() && cfg.worker_name != "x" && !util::isValidRecipientAddress(cfg.worker_name)) { - ui::Notifications::instance().error(util::isTransparentAddress(cfg.worker_name) - ? TR("mining_payout_needs_shielded") - : "Pool payout address is not a valid DragonX address — mining not started."); + const std::string merr = util::isTransparentAddress(cfg.worker_name) + ? std::string(TR("mining_payout_needs_shielded")) + : std::string(TR("toast_pool_payout_invalid")); + setMiningError(merr); + ui::Notifications::instance().error(merr); if (retargetOnly) stopPoolMining(); // never keep mining on a config that no longer validates return; } @@ -3667,6 +3675,7 @@ void App::postPoolMinerJob(int threads, bool retargetOnly) if (cfg.wallet_address.empty()) { DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n"); + setMiningError(TR("toast_no_address_generate_z")); ui::Notifications::instance().error(TR("toast_no_address_generate_z")); if (retargetOnly) stopPoolMining(); return; @@ -3715,13 +3724,15 @@ void App::postPoolMinerJob(int threads, bool retargetOnly) DEBUG_LOGF("[ERROR] Pool mining: %s\n", err.c_str()); // Windows Defender blocking (error 225 = ERROR_VIRUS_INFECTED) if (err.find("error 225") != std::string::npos || err.find("virus") != std::string::npos) { - ui::Notifications::instance().error( - "Windows Defender blocked xmrig. Add exclusion for %APPDATA%\\ObsidianDragon"); + setMiningError(TR("toast_av_blocked_xmrig"), /*av=*/true); + ui::Notifications::instance().error(TR("toast_av_blocked_xmrig")); #ifdef _WIN32 pending_antivirus_dialog_ = true; #endif } else { - ui::Notifications::instance().error(std::string(TR("toast_pool_start_failed")) + util::friendlyRpcError(err)); + const std::string merr = std::string(TR("toast_pool_start_failed")) + util::friendlyRpcError(err); + setMiningError(merr); + ui::Notifications::instance().error(merr); } } }; diff --git a/src/data/wallet_state.h b/src/data/wallet_state.h index 4f87016..743955f 100644 --- a/src/data/wallet_state.h +++ b/src/data/wallet_state.h @@ -254,6 +254,8 @@ struct PoolMiningState { int64_t uptime_sec = 0; double pool_diff = 0; bool connected = false; + std::time_t running_since = 0; // wall-clock the miner was first seen running this session (0 = not running) + std::string last_error; // newest xmrig log line that looks like a connection error ("" = none) // Memory/thread usage (bytes for memory) int64_t memory_used = 0; diff --git a/src/ui/windows/mining_controls.cpp b/src/ui/windows/mining_controls.cpp index a960a45..f5ad952 100644 --- a/src/ui/windows/mining_controls.cpp +++ b/src/ui/windows/mining_controls.cpp @@ -991,6 +991,30 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); + + // Persistent error caption (a crashed/AV-blocked/failed miner otherwise leaves a button that + // "did nothing" once its toast faded). Normal ImGui flow, OUTSIDE the hand-drawn button card. + const std::string& merr = app->miningLastError(); + if (!merr.empty() && !app->isMiningToggleInProgress()) { + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + ImGui::PushTextWrapPos(cardMin.x + availWidth); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Error())); + ImGui::TextWrapped("%s", merr.c_str()); + ImGui::PopStyleColor(); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium())); + ImGui::TextUnformatted(TR("mining_error_retry_hint")); + ImGui::PopStyleColor(); + ImGui::PopTextWrapPos(); +#ifdef _WIN32 + if (app->miningErrorIsAntivirus()) { + ImGui::SameLine(0, Layout::spacingSm()); + if (ImGui::SmallButton(TR("mining_error_av_help"))) app->showAntivirusHelp(); + } +#endif + ImGui::SameLine(0, Layout::spacingSm()); + if (ImGui::SmallButton(TR("mining_error_dismiss"))) app->clearMiningError(); + } + ImGui::Dummy(ImVec2(0, gap)); } diff --git a/src/ui/windows/mining_stats.cpp b/src/ui/windows/mining_stats.cpp index 1987a55..704578b 100644 --- a/src/ui/windows/mining_stats.cpp +++ b/src/ui/windows/mining_stats.cpp @@ -248,6 +248,21 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d else snprintf(uptBuf, sizeof(uptBuf), "%lldh %lldm", (long long)(up / 3600), (long long)((up % 3600) / 60)); metricRow(TR("mining_uptime"), uptBuf, OnSurface()); } + // Connection status — replaces the old transient "Connecting" banner, with a timeout so a bad URL / + // blocked port flips from "Connecting" to "Can't reach pool" instead of spinning on red forever. + { + static constexpr int kPoolConnectTimeoutSec = 30; + const bool shares = pm.accepted + pm.rejected > 0; + const bool ok = pm.connected || shares || pm.hashrate_10s > 0; + const bool failed = pm.xmrig_running && !ok && pm.running_since > 0 && + (std::time(nullptr) - pm.running_since) >= kPoolConnectTimeoutSec; + const char* stateText; ImU32 col; + if (ok) { stateText = TR("mining_status_connected"); col = Success(); } + else if (failed) { stateText = TR("mining_pool_unreachable"); col = Error(); } + else if (pm.xmrig_running) { stateText = TR("mining_status_connecting"); col = Warning(); } + else { stateText = TR("mining_status_stopped"); col = OnSurfaceDisabled(); } + metricRow(TR("mining_status_label"), stateText, col); + } y += gap * 0.5f; // The pool list = official pools ∪ user-saved favorites ∪ the current custom pool. @@ -397,7 +412,7 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining, const float mRowH = sub1->LegacySize + 7 * dp; const float listMinH = (capFont->LegacySize + 10 * dp) * 2.6f; // ~2–3 rows before scroll float leftContentH = pad * 0.75f + toggleH + gap - + mRowH * 4.0f + gap * 0.5f + + mRowH * 5.0f + gap * 0.5f // 4 metric rows + the connection-status row + (ovFont->LegacySize + 4 * dp) + listMinH + pad * 0.75f; float cardH = std::max(leftContentH, statRowH + chartBudgetH + pad); diff --git a/src/ui/windows/mining_tab.cpp b/src/ui/windows/mining_tab.cpp index 45a0838..9a559d6 100644 --- a/src/ui/windows/mining_tab.cpp +++ b/src/ui/windows/mining_tab.cpp @@ -335,24 +335,9 @@ static void RenderMiningTabContent(App* app) isMiningActive, s_pool_mode, availWidth, glassSpec, sHdr, gapOver); // ================================================================ - // POOL CONNECTION STATUS — inline indicator (pool mode, no log) - // ================================================================ - if (s_pool_mode && state.pool_mining.log_lines.empty() && state.pool_mining.xmrig_running) { - ImFont* iconFont = Type().iconSmall(); - const char* dotIcon = ICON_MD_CIRCLE; - ImU32 dotCol = state.pool_mining.connected ? WithAlpha(Success(), 200) : WithAlpha(Error(), 200); - const char* statusText = state.pool_mining.connected - ? (state.pool_mining.pool_url.empty() ? TR("mining_connected") : state.pool_mining.pool_url.c_str()) - : TR("mining_connecting"); - - ImVec2 pos = ImGui::GetCursorScreenPos(); - ImVec2 dotSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, dotIcon); - dl->AddText(iconFont, iconFont->LegacySize, pos, dotCol, dotIcon); - dl->AddText(capFont, capFont->LegacySize, - ImVec2(pos.x + dotSz.x + 4 * dp, pos.y + (dotSz.y - capFont->LegacySize) * 0.5f), - OnSurfaceMedium(), statusText); - ImGui::Dummy(ImVec2(availWidth, capFont->LegacySize + 4 * dp)); - } + // (Pool connection status now lives as a persistent, timeout-aware row in RenderLeftPoolCard + // (mining_stats.cpp) — the old transient "Connecting" banner that vanished once logs arrived and + // sat on red forever on failure was removed.) } } // namespace ui diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 53eb565..3325fae 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1957,6 +1957,11 @@ void I18n::loadBuiltinEnglish() strings_["toast_pool_connecting"] = "Starting pool miner — connecting to the pool…"; strings_["toast_not_connected_daemon"] = "Not connected to the node."; strings_["toast_mining_failed"] = "Couldn't start mining: "; + strings_["toast_pool_payout_invalid"] = "Pool payout address is not a valid DragonX address — mining not started."; + strings_["toast_av_blocked_xmrig"] = "Windows Defender blocked the miner. Add an exclusion for %APPDATA%\\ObsidianDragon."; + strings_["mining_error_retry_hint"] = "Press Mine to try again."; + strings_["mining_error_av_help"] = "How to fix"; + strings_["mining_error_dismiss"] = "Dismiss"; strings_["toast_stop_mining_failed"] = "Couldn't stop mining: "; strings_["toast_pool_start_failed"] = "Couldn't start the pool miner: "; strings_["toast_ban_failed"] = "Couldn't ban the peer: "; @@ -2258,6 +2263,11 @@ void I18n::loadBuiltinEnglish() strings_["mining_click_copy_difficulty"] = "Click to copy difficulty"; strings_["mining_connected"] = "Connected"; strings_["mining_connecting"] = "Connecting..."; + strings_["mining_status_label"] = "Status"; + strings_["mining_status_connected"] = "Connected"; + strings_["mining_status_connecting"] = "Connecting…"; + strings_["mining_status_stopped"] = "Stopped"; + strings_["mining_pool_unreachable"] = "Can't reach pool — check the URL"; strings_["mining_difficulty_copied"] = "Difficulty copied"; strings_["mining_est_block"] = "Est. Block"; strings_["mining_est_daily"] = "Est. Daily";