fix(wallet): wait for IPv6 port + retry start so a switch survives the DB-env race

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) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 21:45:20 -05:00
parent 88091bd77c
commit 41e4e73e63
3 changed files with 75 additions and 40 deletions

View File

@@ -3901,9 +3901,9 @@ void App::renderSwitchStopDaemonDialog()
show_switch_stop_daemon_confirm_ = false; show_switch_stop_daemon_confirm_ = false;
} }
} else if (failed) { } else if (failed) {
ui::material::DialogWarningHeader(TR("switch_progress_failed_title")); // The title already says "Wallet switch failed" — put the actual reason in the warning header
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); // rather than repeating the title.
ImGui::TextWrapped("%s", wallet_switch_error_.c_str()); ui::material::DialogWarningHeader(wallet_switch_error_.c_str());
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd())); ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
if (ui::material::TactileButton(TR("close"), ImVec2(110.0f * dp, 0))) { if (ui::material::TactileButton(TR("close"), ImVec2(110.0f * dp, 0))) {
wallet_switch_dialog_open_.store(false); wallet_switch_dialog_open_.store(false);

View File

@@ -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). // as owned (stop/isRunning/exit behave normally for the next switch and app exit).
wallet_switch_phase_.store(static_cast<int>(WalletSwitchPhase::Starting)); wallet_switch_phase_.store(static_cast<int>(WalletSwitchPhase::Starting));
if (daemon_controller_) daemon_controller_->clearExternalDaemonDetected(); if (daemon_controller_) daemon_controller_->clearExternalDaemonDetected();
if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); // Retry the start. Right after the old node exits, its datadir lock can linger for a few
ok = shutting_down_ ? true : startEmbeddedDaemon(); // seconds (the process is gone but the OS/BDB lock isn't released yet), so the first start
// A missing/corrupt wallet makes dragonxd exit during init — confirm the process survived a // hits "Cannot obtain a lock on data directory" and exits within the grace below. Retry —
// moment; if not, the wallet is bad. (A valid launch keeps the process alive while it syncs.) // with the CORRECT -wallet, since active_wallet_file isn't reverted until we give up — until
if (ok && !shutting_down_) { // one survives (valid launch stays alive while it loads the block index/rescans, ~30-60s).
std::this_thread::sleep_for(std::chrono::milliseconds(1500)); for (int attempt = 0; attempt < 6 && !ok && !shutting_down_; ++attempt) {
if (!isEmbeddedDaemonRunning()) ok = false; 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). // Process is up and staying up — now waiting for RPC to answer (onConnected closes the modal).
if (ok) wallet_switch_phase_.store(static_cast<int>(WalletSwitchPhase::Reconnecting)); if (ok) wallet_switch_phase_.store(static_cast<int>(WalletSwitchPhase::Reconnecting));
} }

View File

@@ -386,52 +386,80 @@ static std::string getPortOwnerInfo(int port)
#endif #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) static bool isPortInUse(int port)
{ {
#ifdef _WIN32 #ifdef _WIN32
WSADATA wsa; WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false; if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false;
bool inUse = false;
{ // IPv4 127.0.0.1
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) { WSACleanup(); return false; } if (sock != INVALID_SOCKET) {
struct sockaddr_in addr; struct sockaddr_in addr; memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET; addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<u_short>(port)); addr.sin_port = htons(static_cast<u_short>(port));
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr)); if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) inUse = true;
closesocket(sock); 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<u_short>(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(); WSACleanup();
return (result == 0); return inUse;
#else #else
// On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp to avoid // On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp{,6} to avoid creating sockets. The
// creating sockets. Fall back to connect() if /proc is unavailable. // parse is family-agnostic: %*X skips the local IP (8 hex for v4, 32 for v6), %X grabs the port.
FILE* fp = fopen("/proc/net/tcp", "r"); auto scanProc = [port](const char* path) -> bool {
if (fp) { FILE* fp = fopen(path, "r");
char line[256]; if (!fp) return false;
char line[512];
unsigned int localPort, state; unsigned int localPort, state;
bool found = false; bool found = false;
while (fgets(line, sizeof(line), fp)) { while (fgets(line, sizeof(line), fp)) {
if (sscanf(line, " %*d: %*X:%X %*X:%*X %X", &localPort, &state) == 2) { if (sscanf(line, " %*d: %*X:%X %*X:%*X %X", &localPort, &state) == 2) {
if (localPort == static_cast<unsigned int>(port) && state == 0x0A) { if (localPort == static_cast<unsigned int>(port) && state == 0x0A) { found = true; break; }
found = true;
break;
}
} }
} }
fclose(fp); fclose(fp);
return found; 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 // Fallback (macOS): connect() probe on both loopback families.
int sock = socket(AF_INET, SOCK_STREAM, 0); auto connProbe = [port](int family, const char* addr) -> bool {
int sock = socket(family, SOCK_STREAM, 0);
if (sock < 0) return false; if (sock < 0) return false;
struct sockaddr_in addr; bool ok = false;
memset(&addr, 0, sizeof(addr)); if (family == AF_INET) {
addr.sin_family = AF_INET; struct sockaddr_in a; memset(&a, 0, sizeof(a));
addr.sin_port = htons(static_cast<uint16_t>(port)); a.sin_family = AF_INET; a.sin_port = htons(static_cast<uint16_t>(port));
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr)); 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<uint16_t>(port));
inet_pton(AF_INET6, addr, &a.sin6_addr);
ok = (connect(sock, (struct sockaddr*)&a, sizeof(a)) == 0);
}
close(sock); close(sock);
return (result == 0); return ok;
};
return connProbe(AF_INET, "127.0.0.1") || connProbe(AF_INET6, "::1");
#endif #endif
} }