fix(net): stop re-running the connect sequence while the wallet is locked
Connected-but-locked fell through to tryConnect() on every Core tick while the lock screen sat idle: new sockets, getinfo and getwalletinfo, a forced address scan, a CoinGecko fetch and a wallet-index rewrite. Its wallet scans also held cs_main ahead of the user's walletpassphrase. - While locked, poll chain state only (refreshCoreData without the balance). That keeps the sync height current and the 3-strike lost-connection check armed, so a node that exits while locked is still restarted. Pool mining keeps the lock screen down, so while it runs the throttled balance, the peers and the lock state (for an unlock done in the Console tab) keep refreshing too. - Warmup polls count refused/reset connections toward the lost-connection check, so a node that died during warmup is restarted instead of leaving the loading or lock screen over it. - The wallet salvage/degraded detectors run at connect and again when warmup ends: for encrypted wallets they relied on the repeated tryConnect. - restartDaemonAfterEncryption records the wallet as encrypted and locked itself (the wizard's deferred path never did), and marks the main client disconnected so the reconnect path picks up the restarted node, aborting any in-flight call first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+19
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
|
||||
+26
-2
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user