fix(rpc): detect mid-session disconnects and stop blocking the UI thread
The connection state machine never tore down on a lost connection: refresh-loop RPC errors were swallowed, rpc_->isConnected() stayed true after a daemon crash/restart/socket drop, and the UI showed stale balances with no reconnect. Several operations also ran synchronous curl straight from ImGui handlers. - Add handleLostConnection(): after N consecutive cycles where BOTH core RPCs fail (warmup excluded, so no reconnect loop), disconnect so update()'s reconnect branch re-enters tryConnect(). - Move banPeer/unbanPeer/clearBans and key export/import onto the worker thread (import requests a rescan that could freeze the UI for the curl timeout). - Run the block-info dialog's two chained RPCs on the worker thread (+ guard the getblockhash result type). - Detect daemon warmup via the JSON-RPC -28 code (new RpcError carrying the code; message text preserved so 401/warmup string-matching is unaffected), and widen CONNECTTIMEOUT to 10s for remote/TLS hosts (2s localhost). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -493,6 +493,12 @@ void App::onDisconnected(const std::string& reason)
|
||||
last_tx_block_height_ = -1;
|
||||
pending_opids_.clear();
|
||||
pending_send_info_.clear();
|
||||
// Resolve any deferred send callbacks so their UI doesn't spin forever on disconnect.
|
||||
for (auto& entry : pending_send_callbacks_) {
|
||||
if (entry.second) entry.second(false, reason);
|
||||
}
|
||||
pending_send_callbacks_.clear();
|
||||
consecutive_core_failures_ = 0;
|
||||
send_progress_active_ = false;
|
||||
send_submissions_in_flight_ = 0;
|
||||
network_refresh_.resetJobs();
|
||||
@@ -510,6 +516,15 @@ void App::onDisconnected(const std::string& reason)
|
||||
}
|
||||
}
|
||||
|
||||
void App::handleLostConnection(const std::string& reason)
|
||||
{
|
||||
DEBUG_LOGF("[Connection] %s — tearing down for reconnect\n", reason.c_str());
|
||||
// Flip the main client's connected_ flag so update()'s else-branch re-enters
|
||||
// tryConnect(). onDisconnected() alone only tears down the fast lane.
|
||||
if (rpc_) rpc_->disconnect();
|
||||
onDisconnected(reason);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Data Refresh — Tab-Aware Prioritized System
|
||||
//
|
||||
@@ -1034,6 +1049,25 @@ void App::refreshCoreData()
|
||||
try {
|
||||
NetworkRefreshService::applyCoreRefreshResult(state_, result, std::time(nullptr));
|
||||
applyPendingSendBalanceDeltas(true);
|
||||
|
||||
// Mid-session connection-loss detection. During normal operation, both core
|
||||
// RPCs failing together means the daemon connection is dead (a busy daemon
|
||||
// fails them individually, not both at once). Warmup is excluded — both fail
|
||||
// with -28 there legitimately, and counting it would cause a reconnect loop.
|
||||
constexpr int kCoreFailuresBeforeDisconnect = 3;
|
||||
if (!state_.warming_up) {
|
||||
if (!result.balanceOk && !result.blockchainOk) {
|
||||
if (++consecutive_core_failures_ >= kCoreFailuresBeforeDisconnect &&
|
||||
state_.connected) {
|
||||
consecutive_core_failures_ = 0;
|
||||
handleLostConnection("Lost connection to daemon");
|
||||
return; // state torn down — skip the rest of this callback
|
||||
}
|
||||
} else {
|
||||
consecutive_core_failures_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-shield transparent funds if enabled
|
||||
if (result.balanceOk && settings_ && settings_->getAutoShield() &&
|
||||
state_.transparent_balance > 0.0001 && !state_.sync.syncing &&
|
||||
@@ -1573,28 +1607,58 @@ void App::stopPoolMining()
|
||||
|
||||
void App::banPeer(const std::string& ip, int duration_seconds)
|
||||
{
|
||||
if (!state_.connected || !rpc_) return;
|
||||
|
||||
rpc_->setBan(ip, "add", [this](const json&) {
|
||||
refreshPeerInfo();
|
||||
}, nullptr, duration_seconds);
|
||||
if (!state_.connected || !rpc_ || !worker_) return;
|
||||
// Run on the worker thread — these are called straight from the Peers tab's ImGui
|
||||
// handlers, and rpc_->call() blocks on synchronous curl under curl_mutex_.
|
||||
worker_->post([this, ip, duration_seconds]() -> rpc::RPCWorker::MainCb {
|
||||
std::string err;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Peers / Ban");
|
||||
rpc_->call("setban", {ip, "add", duration_seconds});
|
||||
} catch (const std::exception& e) {
|
||||
err = e.what();
|
||||
}
|
||||
return [this, err]() {
|
||||
if (!err.empty()) ui::Notifications::instance().error("Ban failed: " + err);
|
||||
else refreshPeerInfo();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
void App::unbanPeer(const std::string& ip)
|
||||
{
|
||||
if (!state_.connected || !rpc_) return;
|
||||
|
||||
rpc_->setBan(ip, "remove", [this](const json&) {
|
||||
refreshPeerInfo();
|
||||
if (!state_.connected || !rpc_ || !worker_) return;
|
||||
worker_->post([this, ip]() -> rpc::RPCWorker::MainCb {
|
||||
std::string err;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Peers / Unban");
|
||||
rpc_->call("setban", {ip, "remove"});
|
||||
} catch (const std::exception& e) {
|
||||
err = e.what();
|
||||
}
|
||||
return [this, err]() {
|
||||
if (!err.empty()) ui::Notifications::instance().error("Unban failed: " + err);
|
||||
else refreshPeerInfo();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
void App::clearBans()
|
||||
{
|
||||
if (!state_.connected || !rpc_) return;
|
||||
|
||||
rpc_->clearBanned([this](const json&) {
|
||||
state_.banned_peers.clear();
|
||||
if (!state_.connected || !rpc_ || !worker_) return;
|
||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||
std::string err;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Peers / Clear bans");
|
||||
rpc_->call("clearbanned", nlohmann::json::array());
|
||||
} catch (const std::exception& e) {
|
||||
err = e.what();
|
||||
}
|
||||
return [this, err]() {
|
||||
if (!err.empty()) { ui::Notifications::instance().error("Clear bans failed: " + err); return; }
|
||||
state_.banned_peers.clear();
|
||||
refreshPeerInfo();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1881,33 +1945,35 @@ void App::invalidateAddressValidationCache()
|
||||
|
||||
void App::exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback)
|
||||
{
|
||||
if (!state_.connected || !rpc_) {
|
||||
if (!state_.connected || !rpc_ || !worker_) {
|
||||
if (callback) callback("");
|
||||
return;
|
||||
}
|
||||
|
||||
auto keyKind = services::WalletSecurityController::classifyAddress(address);
|
||||
if (keyKind == services::WalletSecurityController::KeyKind::Shielded) {
|
||||
// Z-address: use z_exportkey
|
||||
rpc::RPCClient::TraceScope trace("Settings / Export private key");
|
||||
rpc_->z_exportKey(address, [callback](const json& result) {
|
||||
if (callback) callback(result.get<std::string>());
|
||||
}, [callback](const std::string& error) {
|
||||
DEBUG_LOGF("Export z-key error: %s\n", error.c_str());
|
||||
ui::Notifications::instance().error("Key export failed: " + error);
|
||||
if (callback) callback("");
|
||||
});
|
||||
} else {
|
||||
// T-address: use dumpprivkey
|
||||
rpc::RPCClient::TraceScope trace("Settings / Export private key");
|
||||
rpc_->dumpPrivKey(address, [callback](const json& result) {
|
||||
if (callback) callback(result.get<std::string>());
|
||||
}, [callback](const std::string& error) {
|
||||
DEBUG_LOGF("Export t-key error: %s\n", error.c_str());
|
||||
ui::Notifications::instance().error("Key export failed: " + error);
|
||||
if (callback) callback("");
|
||||
});
|
||||
}
|
||||
|
||||
const bool shielded = services::WalletSecurityController::classifyAddress(address)
|
||||
== services::WalletSecurityController::KeyKind::Shielded;
|
||||
const char* method = shielded ? "z_exportkey" : "dumpprivkey";
|
||||
// Run on the worker thread — z_exportkey/dumpprivkey block on synchronous curl and
|
||||
// are invoked straight from the export dialog (UI thread).
|
||||
worker_->post([this, method, address, callback]() -> rpc::RPCWorker::MainCb {
|
||||
std::string key;
|
||||
std::string err;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Export private key");
|
||||
key = rpc_->call(method, {address}).get<std::string>();
|
||||
} catch (const std::exception& e) {
|
||||
err = e.what();
|
||||
}
|
||||
return [callback, key, err]() {
|
||||
if (!err.empty()) {
|
||||
DEBUG_LOGF("Export key error: %s\n", err.c_str());
|
||||
ui::Notifications::instance().error("Key export failed: " + err);
|
||||
if (callback) callback("");
|
||||
} else if (callback) {
|
||||
callback(key);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
void App::exportAllKeys(std::function<void(const std::string&)> callback)
|
||||
@@ -1961,34 +2027,36 @@ void App::exportAllKeys(std::function<void(const std::string&)> callback)
|
||||
|
||||
void App::importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> callback)
|
||||
{
|
||||
if (!state_.connected || !rpc_) {
|
||||
if (!state_.connected || !rpc_ || !worker_) {
|
||||
if (callback) callback(false, "Not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
auto keyKind = services::WalletSecurityController::classifyPrivateKey(key);
|
||||
|
||||
if (keyKind == services::WalletSecurityController::KeyKind::Shielded) {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Import private key");
|
||||
rpc_->z_importKey(key, true, [this, callback](const json& result) {
|
||||
|
||||
const bool shielded = services::WalletSecurityController::classifyPrivateKey(key)
|
||||
== services::WalletSecurityController::KeyKind::Shielded;
|
||||
// Run on the worker thread — import requests a full rescan (rescan=true), so the
|
||||
// synchronous curl call can take many seconds; never block the UI thread on it.
|
||||
worker_->post([this, key, shielded, callback]() -> rpc::RPCWorker::MainCb {
|
||||
std::string err;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Import private key");
|
||||
if (shielded) rpc_->call("z_importkey", {key, "yes"}); // rescan
|
||||
else rpc_->call("importprivkey", {key, "", true}); // label "", rescan
|
||||
} catch (const std::exception& e) {
|
||||
err = e.what();
|
||||
}
|
||||
return [this, shielded, err, callback]() {
|
||||
if (!err.empty()) {
|
||||
if (callback) callback(false, err);
|
||||
return;
|
||||
}
|
||||
invalidateAddressValidationCache();
|
||||
refreshAddresses();
|
||||
if (callback) callback(true, services::WalletSecurityController::importSuccessMessage(
|
||||
services::WalletSecurityController::KeyKind::Shielded));
|
||||
}, [callback](const std::string& error) {
|
||||
if (callback) callback(false, error);
|
||||
});
|
||||
} else {
|
||||
rpc::RPCClient::TraceScope trace("Settings / Import private key");
|
||||
rpc_->importPrivKey(key, true, [this, callback](const json& result) {
|
||||
invalidateAddressValidationCache();
|
||||
refreshAddresses();
|
||||
if (callback) callback(true, services::WalletSecurityController::importSuccessMessage(
|
||||
services::WalletSecurityController::KeyKind::Transparent));
|
||||
}, [callback](const std::string& error) {
|
||||
if (callback) callback(false, error);
|
||||
});
|
||||
}
|
||||
shielded ? services::WalletSecurityController::KeyKind::Shielded
|
||||
: services::WalletSecurityController::KeyKind::Transparent));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
void App::backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback)
|
||||
|
||||
Reference in New Issue
Block a user