From 41e4e73e631908e8c9b45f5af624424955d6b09c Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 15 Jul 2026 21:45:20 -0500 Subject: [PATCH] fix(wallet): wait for IPv6 port + retry start so a switch survives the DB-env race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live logs showed the switch's new node starting on the correct wallet, reaching "Verifying wallet…", then aborting with "Binding RPC on ::1 port 21769 failed" + "Failed to rename wallet-savings.dat" — the OLD node's RPC port (on ::1/IPv6) and Berkeley DB environment weren't fully released yet, so the wallet-verify DB recovery couldn't rename the file. The app then reverted, and the connect loop brought a node up on the DEFAULT wallet. Two causes fixed: - isPortInUse() only probed 127.0.0.1 (IPv4). The daemon also binds ::1 (IPv6), which lingers after IPv4 releases — so the readiness wait returned "free" prematurely. Now probe BOTH families (Windows: IPv4 + ::1 via in6addr_loopback; Linux: /proc/net/tcp + tcp6). mingw-verified. - The datadir/DB-env can still be briefly held right after the old node exits, so the first start can abort. Retry the start (up to 6×, 2s backoff) with the CORRECT -wallet — active_wallet_file isn't reverted until we give up — resetting the crash count and re-arming -rescan each attempt, until one survives. Also: the "Wallet switch failed" modal no longer repeats its title in the warning header — it now shows the actual reason there. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.cpp | 6 +-- src/app_network.cpp | 21 +++++--- src/daemon/embedded_daemon.cpp | 88 ++++++++++++++++++++++------------ 3 files changed, 75 insertions(+), 40 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index 411edb3..ad84834 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -3901,9 +3901,9 @@ void App::renderSwitchStopDaemonDialog() show_switch_stop_daemon_confirm_ = false; } } else if (failed) { - ui::material::DialogWarningHeader(TR("switch_progress_failed_title")); - ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); - ImGui::TextWrapped("%s", wallet_switch_error_.c_str()); + // The title already says "Wallet switch failed" — put the actual reason in the warning header + // rather than repeating the title. + ui::material::DialogWarningHeader(wallet_switch_error_.c_str()); ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd())); if (ui::material::TactileButton(TR("close"), ImVec2(110.0f * dp, 0))) { wallet_switch_dialog_open_.store(false); diff --git a/src/app_network.cpp b/src/app_network.cpp index 3e4bdef..dcaafb2 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -1136,14 +1136,21 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed // as owned (stop/isRunning/exit behave normally for the next switch and app exit). wallet_switch_phase_.store(static_cast(WalletSwitchPhase::Starting)); if (daemon_controller_) daemon_controller_->clearExternalDaemonDetected(); - if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); - ok = shutting_down_ ? true : startEmbeddedDaemon(); - // A missing/corrupt wallet makes dragonxd exit during init — confirm the process survived a - // moment; if not, the wallet is bad. (A valid launch keeps the process alive while it syncs.) - if (ok && !shutting_down_) { - std::this_thread::sleep_for(std::chrono::milliseconds(1500)); - if (!isEmbeddedDaemonRunning()) ok = false; + // Retry the start. Right after the old node exits, its datadir lock can linger for a few + // seconds (the process is gone but the OS/BDB lock isn't released yet), so the first start + // hits "Cannot obtain a lock on data directory" and exits within the grace below. Retry — + // with the CORRECT -wallet, since active_wallet_file isn't reverted until we give up — until + // one survives (valid launch stays alive while it loads the block index/rescans, ~30-60s). + for (int attempt = 0; attempt < 6 && !ok && !shutting_down_; ++attempt) { + if (attempt > 0) std::this_thread::sleep_for(std::chrono::milliseconds(2000)); // let the lock release + if (daemon_controller_) daemon_controller_->resetCrashCount(); // retry lock-crashes aren't real + if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); // one-shot; re-arm each try + if (shutting_down_) { ok = true; break; } + if (!startEmbeddedDaemon()) continue; // couldn't spawn (port busy) — back off + retry + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); // grace: a datadir-lock crash exits within this + ok = shutting_down_ || isEmbeddedDaemonRunning(); } + if (ok && daemon_controller_) daemon_controller_->resetCrashCount(); // clean slate for the survivor // Process is up and staying up — now waiting for RPC to answer (onConnected closes the modal). if (ok) wallet_switch_phase_.store(static_cast(WalletSwitchPhase::Reconnecting)); } diff --git a/src/daemon/embedded_daemon.cpp b/src/daemon/embedded_daemon.cpp index dfc33ce..17f2593 100644 --- a/src/daemon/embedded_daemon.cpp +++ b/src/daemon/embedded_daemon.cpp @@ -386,52 +386,80 @@ static std::string getPortOwnerInfo(int port) #endif } -// Check if a TCP port is already in use (something is LISTENING) +// Check if a TCP port is already in use (something is LISTENING). The daemon binds BOTH 127.0.0.1 (IPv4) +// and ::1 (IPv6); during shutdown one can linger after the other releases (and a "Binding RPC on ::1 … +// failed" is fatal to a fresh start), so we treat the port as in use if EITHER localhost family has it. static bool isPortInUse(int port) { #ifdef _WIN32 WSADATA wsa; if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false; - SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (sock == INVALID_SOCKET) { WSACleanup(); return false; } - struct sockaddr_in addr; - addr.sin_family = AF_INET; - addr.sin_port = htons(static_cast(port)); - addr.sin_addr.s_addr = inet_addr("127.0.0.1"); - int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr)); - closesocket(sock); + bool inUse = false; + { // IPv4 127.0.0.1 + SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (sock != INVALID_SOCKET) { + struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(static_cast(port)); + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) inUse = true; + closesocket(sock); + } + } + if (!inUse) { // IPv6 ::1 + SOCKET sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); + if (sock != INVALID_SOCKET) { + struct sockaddr_in6 addr; memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + addr.sin6_port = htons(static_cast(port)); + addr.sin6_addr = in6addr_loopback; // ::1 — avoids inet_pton's _WIN32_WINNT gating on mingw + if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) inUse = true; + closesocket(sock); + } + } WSACleanup(); - return (result == 0); + return inUse; #else - // On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp to avoid - // creating sockets. Fall back to connect() if /proc is unavailable. - FILE* fp = fopen("/proc/net/tcp", "r"); - if (fp) { - char line[256]; + // On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp{,6} to avoid creating sockets. The + // parse is family-agnostic: %*X skips the local IP (8 hex for v4, 32 for v6), %X grabs the port. + auto scanProc = [port](const char* path) -> bool { + FILE* fp = fopen(path, "r"); + if (!fp) return false; + char line[512]; unsigned int localPort, state; bool found = false; while (fgets(line, sizeof(line), fp)) { if (sscanf(line, " %*d: %*X:%X %*X:%*X %X", &localPort, &state) == 2) { - if (localPort == static_cast(port) && state == 0x0A) { - found = true; - break; - } + if (localPort == static_cast(port) && state == 0x0A) { found = true; break; } } } fclose(fp); return found; + }; + if (FILE* probe = fopen("/proc/net/tcp", "r")) { // /proc available → authoritative LISTEN check + fclose(probe); + return scanProc("/proc/net/tcp") || scanProc("/proc/net/tcp6"); } - // Fallback (macOS): try to connect - int sock = socket(AF_INET, SOCK_STREAM, 0); - if (sock < 0) return false; - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_port = htons(static_cast(port)); - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr)); - close(sock); - return (result == 0); + // Fallback (macOS): connect() probe on both loopback families. + auto connProbe = [port](int family, const char* addr) -> bool { + int sock = socket(family, SOCK_STREAM, 0); + if (sock < 0) return false; + bool ok = false; + if (family == AF_INET) { + struct sockaddr_in a; memset(&a, 0, sizeof(a)); + a.sin_family = AF_INET; a.sin_port = htons(static_cast(port)); + a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + ok = (connect(sock, (struct sockaddr*)&a, sizeof(a)) == 0); + } else { + struct sockaddr_in6 a; memset(&a, 0, sizeof(a)); + a.sin6_family = AF_INET6; a.sin6_port = htons(static_cast(port)); + inet_pton(AF_INET6, addr, &a.sin6_addr); + ok = (connect(sock, (struct sockaddr*)&a, sizeof(a)) == 0); + } + close(sock); + return ok; + }; + return connProbe(AF_INET, "127.0.0.1") || connProbe(AF_INET6, "::1"); #endif }