diff --git a/src/app.cpp b/src/app.cpp index b99cbe4..be2c4e7 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1441,6 +1441,25 @@ void App::update() if (network_refresh_.consumeDue(RefreshTimer::Peers)) { refreshPeerInfo(); } + } else if (rpcConnected && state_.isLocked()) { + // Connected but locked is still connected. This used to fall through to tryConnect(), which + // re-ran the whole connect sequence every Core tick while the lock screen sat idle: new + // sockets, getinfo + getwalletinfo, a forced address scan, a CoinGecko fetch and a wallet-index + // rewrite. Poll chain state only: that keeps the sync height current and refreshCoreData's + // 3-strike lost-connection check armed, so a node that exits while locked is still noticed and + // restarted. Wallet scans wait for the unlock (they would also hold cs_main ahead of the user's + // walletpassphrase), except the throttled balance while pool mining keeps the lock screen down. + const bool poolMiningActive = xmrig_manager_ && xmrig_manager_->isRunning(); + if (!rpcBusy && !runtime_rescan_active_ && network_refresh_.consumeDue(RefreshTimer::Core)) { + refreshCoreData(/*allowBalance=*/poolMiningActive); + } + // Pool mining keeps the lock screen down, so the tabs stay usable while locked: keep peers + // current, and re-read the lock state at the slow peers cadence so an unlock done another way + // (walletpassphrase in the Console tab) is noticed. + if (poolMiningActive && !rpcBusy && network_refresh_.consumeDue(RefreshTimer::Peers)) { + refreshPeerInfo(); + refreshEncryptionState(); + } } else if (!rpcBusy && network_refresh_.consumeDue(RefreshTimer::Core)) { if (!connection_in_progress_ && wizard_phase_ == WizardPhase::None && diff --git a/src/app.h b/src/app.h index a211ce1..d4781d9 100644 --- a/src/app.h +++ b/src/app.h @@ -1657,7 +1657,7 @@ private: // Private methods - data refresh void refreshData(); // Orchestrator: dispatches per-category refreshes - void refreshCoreData(); // Balance + blockchain info (can use fast_worker_) + void refreshCoreData(bool allowBalance = true); // Balance + blockchain info (can use fast_worker_) void refreshAddressData(); // Address lists + balances void refreshTransactionData(); // Transaction list + z_viewtransaction enrichment void refreshRecentTransactionData(); // Lightweight recent/unconfirmed tx poll diff --git a/src/app_network.cpp b/src/app_network.cpp index c3992a1..5f70b19 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -709,6 +709,7 @@ void App::onConnected() connection_status_ = TR("connected"); detectWalletAutoRecovery(); // also runs every tryConnect tick — catches a salvage even if we never connect + detectWalletDegraded(); // Re-arm the empty-wallet-with-funded-sibling check for this (possibly switched) wallet: re-evaluate the // on-disk state once it finishes loading + syncing. Deliberately DON'T touch empty_wallet_scan_in_flight_ @@ -1786,7 +1787,7 @@ void App::refreshData() // Uses fast_worker_ when on Overview tab for lower latency. // ============================================================================ -void App::refreshCoreData() +void App::refreshCoreData(bool allowBalance) { if (!state_.connected) return; @@ -1800,6 +1801,15 @@ void App::refreshCoreData() return [this, result = std::move(result)]() { if (result.ready) { // Warmup finished — daemon is fully ready + consecutive_core_failures_ = 0; + // The salvage / degraded-wallet lines are logged during wallet load, inside warmup, so + // a connect that landed in warmup scanned too early. Look again now (both latch). The + // locked-wallet poll no longer re-runs tryConnect, which used to catch them for + // encrypted wallets. + if (!daemon_restarting_) { + detectWalletAutoRecovery(); + detectWalletDegraded(); + } state_.warming_up = false; state_.warmup_status.clear(); state_.warmup_description.clear(); @@ -1831,6 +1841,18 @@ void App::refreshCoreData() // Trigger full data refresh now that daemon is ready refreshData(); } else { + // A node that died during warmup answers with a refused/reset connection, not -28. + // Count those like the Core poll does, so a dead node is torn down and restarted + // instead of leaving the loading or lock screen over it indefinitely. + if (rpc::rpcKindMeansLost(result.errorKind)) { + if (++consecutive_core_failures_ >= 3 && state_.connected) { + consecutive_core_failures_ = 0; + handleLostConnection("Lost connection to daemon"); + return; + } + } else { + consecutive_core_failures_ = 0; // -28 warmup / busy: the node is there + } // Still warming up — update status applyWarmupText(result.errorMessage); VERBOSE_LOGF("[warmup] Still warming up: %s\n", result.errorMessage.c_str()); @@ -1855,7 +1877,9 @@ void App::refreshCoreData() // large shielded wallet backs off automatically instead of re-scanning every couple of seconds. // A wallet mutation (send/shield) forces the next poll through so the user's own action updates the // balance immediately. Captured on the main thread to avoid reading state_ off the worker thread. - const bool includeBalance = !effectivelySyncing() && + // allowBalance=false (the locked wallet's chain-state poll) skips it and leaves a forced refresh + // pending for after the unlock. + const bool includeBalance = allowBalance && !effectivelySyncing() && (force_balance_refresh_ || balanceRefreshDue()); if (includeBalance) force_balance_refresh_ = false; diff --git a/src/app_security.cpp b/src/app_security.cpp index 732a164..5fabe73 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -274,6 +274,14 @@ void App::sweepStaleDecryptExports() } void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestartStatus) { + // Both callers have just had encryptwallet succeed. Record it here: the first-run wizard's + // deferred path never did, and the reconnect after the restart usually lands in RPC warmup, where + // the connect-time getwalletinfo fails and can't correct it. + state_.encrypted = true; + state_.locked = true; + state_.unlocked_until = 0; + state_.encryption_state_known = true; + if (isUsingEmbeddedDaemon()) { if (announceRestartStatus) { // Update connection_status_ so the loading overlay explains why @@ -283,6 +291,17 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar // Gate the main-loop reconnect while the daemon is down (so tryConnect can't hit the stopped // node or start a duplicate) and block a concurrent wallet switch/rescan, which check this flag. daemon_restarting_ = true; + // Mark the main client disconnected so the update loop's reconnect path (tryConnect, held back + // by daemon_restarting_) picks the restarted node up. Connected-but-locked no longer re-runs + // the connect sequence on its own. Not onDisconnected(): its state_.clear() would drop the + // encrypted/locked flags just set for the lock screen. Abort any in-flight call first: + // disconnect() takes the curl lock, which a worker call (say a balance scan queued behind + // encryptwallet) holds for its whole transfer, up to the 30 s timeout. open() clears the abort + // when tryConnect reconnects. + if (rpc_) { + rpc_->requestAbort(); + rpc_->disconnect(); + } // Give daemon a moment to shut down, then restart // (do this off the main thread to avoid stalling the UI) async_tasks_.submit(taskName, [this](const util::AsyncTaskManager::Token& token) { diff --git a/src/services/network_refresh_service.cpp b/src/services/network_refresh_service.cpp index 69f572b..532e0af 100644 --- a/src/services/network_refresh_service.cpp +++ b/src/services/network_refresh_service.cpp @@ -234,6 +234,7 @@ NetworkRefreshService::WarmupPollResult NetworkRefreshService::collectWarmupPoll result.info = parseConnectionInfoResult(info); } catch (const std::exception& e) { result.errorMessage = e.what(); + result.errorKind = rpc::rpcFailureKindOf(e); } return result; } diff --git a/src/services/network_refresh_service.h b/src/services/network_refresh_service.h index e22b340..4f14e14 100644 --- a/src/services/network_refresh_service.h +++ b/src/services/network_refresh_service.h @@ -90,6 +90,9 @@ public: bool ready = false; ConnectionInfoResult info; std::string errorMessage; + // Why the poll failed: a -28 warmup reply means the node is there; a refused/reset connection + // means it's gone (the caller counts those toward its lost-connection check). + rpc::RpcFailureKind errorKind = rpc::RpcFailureKind::None; }; struct ConnectionInitResult {