From 4b16a2a2c4f519f01bbe3a4d0e83716c0620cb1d Mon Sep 17 00:00:00 2001 From: dan_s Date: Thu, 5 Mar 2026 05:26:04 -0600 Subject: [PATCH] improve diagnostics, security UX, and network tab refresh Diagnostics & logging: - add verbose logging system (VERBOSE_LOGF) with toggle in Settings - forward app-level log messages to Console tab for in-UI visibility - add detailed connection attempt logging (attempt #, daemon state, config paths, auth failures, port owner identification) - detect HTTP 401 auth failures and show actionable error messages - identify port owner process (PID + name) on both Linux and Windows - demote noisy acrylic/shader traces from DEBUG_LOGF to VERBOSE_LOGF - persist verbose_logging preference in settings.json - link iphlpapi on Windows for GetExtendedTcpTable Security & encryption: - update local encryption state immediately after encryptwallet RPC so Settings reflects the change before daemon restarts - show notifications for encrypt success/failure and PIN skip - use dedicated RPC client for z_importwallet during decrypt flow to avoid blocking main rpc_ curl_mutex (which starved peer/tx refresh) - force full state refresh (addresses, transactions, peers) after successful wallet import Network tab: - redesign peers refresh button as glass-panel with icon + label, matching the mining button style - add spinning arc animation while peer data is loading (peer_refresh_in_progress_ atomic flag set/cleared in refreshPeerInfo) - prevent double-click spam during refresh - add refresh-button size to ui.toml Other: - use fast_rpc_ for rescan polling to avoid blocking on main rpc_ - enable DRAGONX_DEBUG in all build configs (was debug-only) - setup.sh: pull latest xmrig-hac when repo already exists --- CMakeLists.txt | 4 +- res/themes/ui.toml | 1 + setup.sh | 2 + src/app.cpp | 35 +++++++- src/app.h | 4 + src/app_network.cpp | 94 ++++++++++++++++++--- src/app_security.cpp | 53 +++++++++++- src/config/settings.cpp | 2 + src/config/settings.h | 5 ++ src/daemon/embedded_daemon.cpp | 123 ++++++++++++++++++++++++++-- src/rpc/rpc_client.cpp | 4 +- src/rpc/rpc_client.h | 6 ++ src/ui/effects/acrylic.cpp | 8 +- src/ui/effects/scroll_fade_shader.h | 2 +- src/ui/pages/settings_page.cpp | 11 +++ src/ui/windows/mining_tab.cpp | 4 +- src/ui/windows/peers_tab.cpp | 120 +++++++++++++++++++++++---- src/util/logger.cpp | 11 +++ src/util/logger.h | 24 ++++++ 19 files changed, 461 insertions(+), 52 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 073bb16..aa170b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -428,7 +428,7 @@ target_link_libraries(ObsidianDragon PRIVATE # Platform-specific settings if(WIN32) - target_link_libraries(ObsidianDragon PRIVATE ws2_32 winmm imm32 version setupapi dwmapi crypt32 wldap32 psapi d3d11 dxgi d3dcompiler dcomp) + target_link_libraries(ObsidianDragon PRIVATE ws2_32 winmm imm32 version setupapi dwmapi crypt32 wldap32 psapi iphlpapi d3d11 dxgi d3dcompiler dcomp) # Hide console window in release builds if(CMAKE_BUILD_TYPE STREQUAL "Release") set_target_properties(ObsidianDragon PROPERTIES WIN32_EXECUTABLE TRUE) @@ -453,7 +453,7 @@ endif() # Compile definitions target_compile_definitions(ObsidianDragon PRIVATE - $<$:DRAGONX_DEBUG> + DRAGONX_DEBUG ) if(WIN32) target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11) diff --git a/res/themes/ui.toml b/res/themes/ui.toml index e66cd4e..7386959 100644 --- a/res/themes/ui.toml +++ b/res/themes/ui.toml @@ -781,6 +781,7 @@ log-panel-height = { size = 120.0 } log-panel-min = { size = 60.0 } [tabs.peers] +refresh-button = { size = 110.0 } table-min-height = 150.0 table-height-ratio = 0.45 version-column-width = 150.0 diff --git a/setup.sh b/setup.sh index 940614d..d672bbd 100755 --- a/setup.sh +++ b/setup.sh @@ -638,6 +638,8 @@ clone_xmrig_if_needed() { git clone https://git.dragonx.is/dragonx/xmrig-hac.git "$XMRIG_SRC" else ok "xmrig-hac source already present" + info "Pulling latest xmrig-hac..." + (cd "$XMRIG_SRC" && git pull --ff-only 2>/dev/null || true) fi } diff --git a/src/app.cpp b/src/app.cpp index 0e98226..3c2ba6a 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -105,6 +105,9 @@ bool App::init() DEBUG_LOGF("Warning: Could not load settings, using defaults\n"); } + // Apply verbose logging preference from saved settings + util::Logger::instance().setVerbose(settings_->getVerboseLogging()); + // Apply saved user font scale so fonts are correct on first reload { float fs = settings_->getFontScale(); @@ -191,6 +194,31 @@ bool App::init() console_tab_.addLine(msg, color); }); + // Forward all app-level log messages (DEBUG_LOGF, LOGF, etc.) to the + // console tab so they are visible in the UI, not just in the log file. + util::Logger::instance().setCallback( + [this](const std::string& msg) { + // Classify by content: errors in red, warnings in warning color, + // everything else in the default info color. + ImU32 color = ui::ConsoleTab::COLOR_INFO; + if (msg.find("[ERROR]") != std::string::npos || + msg.find("error") != std::string::npos || + msg.find("Error") != std::string::npos || + msg.find("failed") != std::string::npos || + msg.find("Failed") != std::string::npos) { + color = ui::ConsoleTab::COLOR_ERROR; + } else if (msg.find("[WARN]") != std::string::npos || + msg.find("warn") != std::string::npos) { + color = ui::material::Warning(); + } + // Strip trailing newline so console tab lines look clean + std::string trimmed = msg; + while (!trimmed.empty() && (trimmed.back() == '\n' || trimmed.back() == '\r')) + trimmed.pop_back(); + if (!trimmed.empty()) + console_tab_.addLine("[app] " + trimmed, color); + }); + // Check for first-run wizard — also re-run if blockchain data is missing // even when wizard was previously completed (e.g. data dir was deleted) if (isFirstRun()) { @@ -286,10 +314,13 @@ void App::update() refreshMiningInfo(); // Poll getrescaninfo for rescan progress (if rescan flag is set) + // Use fast_rpc_ when available to avoid blocking on rpc_'s + // curl_mutex (which may be held by a long-running import). if (state_.sync.rescanning && fast_worker_) { - fast_worker_->post([this]() -> rpc::RPCWorker::MainCb { + auto* rescanRpc = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(); + fast_worker_->post([this, rescanRpc]() -> rpc::RPCWorker::MainCb { try { - auto info = rpc_->call("getrescaninfo"); + auto info = rescanRpc->call("getrescaninfo"); bool rescanning = info.value("rescanning", false); float progress = 0.0f; if (info.contains("rescan_progress")) { diff --git a/src/app.h b/src/app.h index ecd3399..a8c51a8 100644 --- a/src/app.h +++ b/src/app.h @@ -177,6 +177,7 @@ public: // Peers const std::vector& getPeers() const { return state_.peers; } const std::vector& getBannedPeers() const { return state_.bannedPeers; } + bool isPeerRefreshInProgress() const { return peer_refresh_in_progress_.load(std::memory_order_relaxed); } void banPeer(const std::string& ip, int duration_seconds = 86400); void unbanPeer(const std::string& ip); void clearBans(); @@ -445,6 +446,9 @@ private: // Mining toggle guard (prevents concurrent setgenerate calls) std::atomic mining_toggle_in_progress_{false}; + // Peer refresh guard (visual feedback for refresh button) + std::atomic peer_refresh_in_progress_{false}; + // Auto-shield guard (prevents concurrent auto-shield operations) std::atomic auto_shield_pending_{false}; diff --git a/src/app_network.cpp b/src/app_network.cpp index 7665ee9..b4d19dd 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -32,6 +32,9 @@ void App::tryConnect() { if (connection_in_progress_) return; + static int connect_attempt = 0; + ++connect_attempt; + connection_in_progress_ = true; connection_status_ = "Loading configuration..."; @@ -40,12 +43,15 @@ void App::tryConnect() if (config.rpcuser.empty() || config.rpcpassword.empty()) { connection_in_progress_ = false; - DEBUG_LOGF("Could not find DRAGONX.conf or missing rpcuser/rpcpassword\n"); + std::string confPath = rpc::Connection::getDefaultConfPath(); + VERBOSE_LOGF("[connect #%d] No valid config — DRAGONX.conf missing or no rpcuser/rpcpassword (looked at: %s)\n", + connect_attempt, confPath.c_str()); // If we already know an external daemon is on the port, just wait // for the config file to appear (the daemon creates it on first run). if (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) { connection_status_ = "Waiting for daemon config..."; + VERBOSE_LOGF("[connect #%d] External daemon detected on port, waiting for config file to appear\n", connect_attempt); return; } @@ -56,22 +62,32 @@ void App::tryConnect() connection_status_ = "Starting dragonxd..."; if (startEmbeddedDaemon()) { // Will retry connection after daemon starts - DEBUG_LOGF("Embedded daemon starting, will retry connection...\n"); + VERBOSE_LOGF("[connect #%d] Embedded daemon starting, will retry connection...\n", connect_attempt); } else if (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) { connection_status_ = "Waiting for daemon config..."; - DEBUG_LOGF("External daemon detected but no config yet, will retry...\n"); + VERBOSE_LOGF("[connect #%d] External daemon detected but no config yet, will retry...\n", connect_attempt); + } else { + VERBOSE_LOGF("[connect #%d] startEmbeddedDaemon() failed — lastError: %s, binary: %s\n", + connect_attempt, + embedded_daemon_ ? embedded_daemon_->getLastError().c_str() : "(no daemon object)", + daemon::EmbeddedDaemon::findDaemonBinary().c_str()); } + } else if (!use_embedded_daemon_) { + VERBOSE_LOGF("[connect #%d] Embedded daemon disabled (using external). No config found at %s\n", + connect_attempt, confPath.c_str()); } return; } connection_status_ = "Connecting to dragonxd..."; - DEBUG_LOGF("Connecting to %s:%s\n", config.host.c_str(), config.port.c_str()); + VERBOSE_LOGF("[connect #%d] Connecting to %s:%s (user=%s)\n", + connect_attempt, config.host.c_str(), config.port.c_str(), config.rpcuser.c_str()); // Run the blocking rpc_->connect() on the worker thread so the UI // stays responsive (curl connect timeout can be up to 10 seconds). if (!worker_) { connection_in_progress_ = false; + VERBOSE_LOGF("[connect #%d] No worker thread available!\n", connect_attempt); return; } @@ -80,41 +96,90 @@ void App::tryConnect() (embedded_daemon_->getState() == daemon::EmbeddedDaemon::State::Starting || embedded_daemon_->getState() == daemon::EmbeddedDaemon::State::Running); bool externalDetected = embedded_daemon_ && embedded_daemon_->externalDaemonDetected(); + int attempt = connect_attempt; - worker_->post([this, config, daemonStarting, externalDetected]() -> rpc::RPCWorker::MainCb { + // Log detailed daemon state for diagnostics + if (embedded_daemon_) { + const char* stateStr = "unknown"; + switch (embedded_daemon_->getState()) { + case daemon::EmbeddedDaemon::State::Stopped: stateStr = "Stopped"; break; + case daemon::EmbeddedDaemon::State::Starting: stateStr = "Starting"; break; + case daemon::EmbeddedDaemon::State::Running: stateStr = "Running"; break; + case daemon::EmbeddedDaemon::State::Stopping: stateStr = "Stopping"; break; + case daemon::EmbeddedDaemon::State::Error: stateStr = "Error"; break; + } + VERBOSE_LOGF("[connect #%d] Daemon state: %s, running: %s, external: %s, crashes: %d, lastErr: %s\n", + attempt, stateStr, + embedded_daemon_->isRunning() ? "yes" : "no", + externalDetected ? "yes" : "no", + embedded_daemon_->getCrashCount(), + embedded_daemon_->getLastError().empty() ? "(none)" : embedded_daemon_->getLastError().c_str()); + } else { + VERBOSE_LOGF("[connect #%d] No embedded daemon object (use_embedded=%s)\n", + attempt, use_embedded_daemon_ ? "yes" : "no"); + } + + worker_->post([this, config, daemonStarting, externalDetected, attempt]() -> rpc::RPCWorker::MainCb { bool connected = rpc_->connect(config.host, config.port, config.rpcuser, config.rpcpassword); + std::string connectErr = rpc_->getLastConnectError(); - return [this, config, connected, daemonStarting, externalDetected]() { + return [this, config, connected, daemonStarting, externalDetected, attempt, connectErr]() { if (connected) { + VERBOSE_LOGF("[connect #%d] Connected successfully\n", attempt); saved_config_ = config; // save for fast-lane connection onConnected(); } else { - if (daemonStarting) { + // HTTP 401 = authentication failure. The daemon is running + // but our rpcuser/rpcpassword don't match. Don't retry + // endlessly — tell the user what's wrong. + bool authFailure = (connectErr.find("401") != std::string::npos); + if (authFailure) { + state_.connected = false; + std::string confPath = rpc::Connection::getDefaultConfPath(); + connection_status_ = "Auth failed — check rpcuser/rpcpassword"; + VERBOSE_LOGF("[connect #%d] HTTP 401 — rpcuser/rpcpassword in %s don't match the daemon. " + "Edit the file or restart the daemon to regenerate credentials.\n", + attempt, confPath.c_str()); + ui::Notifications::instance().error( + "RPC authentication failed (HTTP 401). " + "The rpcuser/rpcpassword in DRAGONX.conf don't match the running daemon. " + "Restart the daemon or correct the credentials."); + } else if (daemonStarting) { state_.connected = false; connection_status_ = "Waiting for dragonxd to start..."; - DEBUG_LOGF("Connection attempt failed — daemon still starting, will retry...\n"); + VERBOSE_LOGF("[connect #%d] RPC connection failed — daemon still starting, will retry...\n", attempt); } else if (externalDetected) { state_.connected = false; connection_status_ = "Connecting to daemon..."; - DEBUG_LOGF("External daemon on port but RPC not ready yet, will retry...\n"); + VERBOSE_LOGF("[connect #%d] RPC connection failed — external daemon on port but RPC not ready yet, will retry...\n", attempt); } else { onDisconnected("Connection failed"); + VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt); if (use_embedded_daemon_ && !isEmbeddedDaemonRunning()) { // Prevent infinite crash-restart loop if (embedded_daemon_ && embedded_daemon_->getCrashCount() >= 3) { connection_status_ = "Daemon crashed " + std::to_string(embedded_daemon_->getCrashCount()) + " times"; - DEBUG_LOGF("Daemon crashed %d times — not restarting (use Settings > Restart Daemon to retry)\n", - embedded_daemon_->getCrashCount()); + VERBOSE_LOGF("[connect #%d] Daemon crashed %d times — not restarting (use Settings > Restart Daemon to retry)\n", + attempt, embedded_daemon_->getCrashCount()); } else { connection_status_ = "Starting dragonxd..."; if (startEmbeddedDaemon()) { - DEBUG_LOGF("Embedded daemon starting, will retry connection...\n"); + VERBOSE_LOGF("[connect #%d] Embedded daemon starting, will retry connection...\n", attempt); } else if (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) { connection_status_ = "Connecting to daemon..."; - DEBUG_LOGF("External daemon detected, will connect via RPC...\n"); + VERBOSE_LOGF("[connect #%d] External daemon detected, will connect via RPC...\n", attempt); + } else { + VERBOSE_LOGF("[connect #%d] Failed to start embedded daemon — lastError: %s\n", + attempt, + embedded_daemon_ ? embedded_daemon_->getLastError().c_str() : "(no daemon object)"); } } + } else if (!use_embedded_daemon_) { + VERBOSE_LOGF("[connect #%d] Embedded daemon disabled — external daemon at %s:%s not responding\n", + attempt, config.host.c_str(), config.port.c_str()); + } else { + VERBOSE_LOGF("[connect #%d] Embedded daemon is running but RPC failed — daemon may be initializing\n", attempt); } } } @@ -1033,6 +1098,8 @@ void App::refreshPeerInfo() auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(); if (!w) return; + peer_refresh_in_progress_.store(true, std::memory_order_relaxed); + w->post([this, r]() -> rpc::RPCWorker::MainCb { std::vector peers; std::vector bannedPeers; @@ -1079,6 +1146,7 @@ void App::refreshPeerInfo() state_.peers = std::move(peers); state_.bannedPeers = std::move(bannedPeers); state_.last_peer_update = std::time(nullptr); + peer_refresh_in_progress_.store(false, std::memory_order_relaxed); }; }); } diff --git a/src/app_security.cpp b/src/app_security.cpp index c916612..d4f9a8a 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -8,6 +8,7 @@ #include "app.h" #include "rpc/rpc_client.h" #include "rpc/rpc_worker.h" +#include "rpc/connection.h" #include "config/settings.h" #include "daemon/embedded_daemon.h" #include "ui/notifications.h" @@ -51,12 +52,24 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) { encrypt_status_ = "Wallet encrypted. Restarting daemon..."; DEBUG_LOGF("[App] Wallet encrypted — restarting daemon\n"); + // Immediately update local encryption state so the + // settings page reflects that the wallet is now encrypted + // (the daemon is about to restart, so getwalletinfo won't + // be available for a while). + state_.encrypted = true; + state_.locked = true; + state_.unlocked_until = 0; + state_.encryption_state_known = true; + // Transition settings dialog to PIN setup phase if (show_encrypt_dialog_ && encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) { encrypt_dialog_phase_ = EncryptDialogPhase::PinSetup; } + ui::Notifications::instance().info( + "Wallet encrypted successfully"); + // The daemon shuts itself down after encryptwallet if (isUsingEmbeddedDaemon()) { // Give daemon a moment to shut down, then restart @@ -69,7 +82,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) { }).detach(); } else { ui::Notifications::instance().warning( - "Wallet encrypted. Please restart your daemon."); + "Please restart your daemon for encryption to take effect."); } }; } catch (const std::exception& e) { @@ -79,6 +92,9 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) { encrypt_status_ = "Encryption failed: " + err; DEBUG_LOGF("[App] encryptwallet failed: %s\n", err.c_str()); + ui::Notifications::instance().error( + "Encryption failed: " + err); + // Return to passphrase entry on failure if (show_encrypt_dialog_ && encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) { @@ -908,6 +924,8 @@ void App::renderEncryptWalletDialog() { memset(enc_dlg_pin_buf_, 0, sizeof(enc_dlg_pin_buf_)); memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_)); show_encrypt_dialog_ = false; + ui::Notifications::instance().info( + "PIN skipped. You can set one later in Settings."); } } EndOverlayDialog(); @@ -1173,9 +1191,23 @@ void App::renderDecryptWalletDialog() { } // Step 6: Import wallet in background (use full path) + // Use a SEPARATE RPC client so the main rpc_'s + // curl_mutex isn't held for the entire import duration. + // Blocking rpc_ prevents refreshData/refreshPeerInfo + // from running, which leaves the UI with no peers. + auto importRpc = std::make_unique(); + bool importRpcOk = importRpc->connect( + saved_config_.host, saved_config_.port, + saved_config_.rpcuser, saved_config_.rpcpassword); + if (!importRpcOk) { + // Fall back to main client if temp connect fails + importRpc.reset(); + } + auto* rpcForImport = importRpc ? importRpc.get() : rpc_.get(); + // Use 20-minute timeout — import + rescan can be very slow try { - rpc_->call("z_importwallet", {exportPath}, 1200L); + rpcForImport->call("z_importwallet", {exportPath}, 1200L); } catch (const std::exception& e) { std::string err = e.what(); if (worker_) { @@ -1192,13 +1224,28 @@ void App::renderDecryptWalletDialog() { return; } - // Success + // Disconnect the temporary RPC client + if (importRpc) { + importRpc->disconnect(); + importRpc.reset(); + } + + // Success — force full state refresh so peers, + // balances, and addresses are fetched immediately. if (worker_) { worker_->post([this]() -> rpc::RPCWorker::MainCb { return [this]() { decrypt_import_active_ = false; + // Force address + peer refresh + addresses_dirty_ = true; + transactions_dirty_ = true; + last_tx_block_height_ = -1; + refreshWalletEncryptionState(); + refreshData(); + refreshPeerInfo(); + ui::Notifications::instance().success( "Wallet decrypted successfully! All keys imported.", 8.0f); diff --git a/src/config/settings.cpp b/src/config/settings.cpp index 10a4b79..a265d9e 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -129,6 +129,7 @@ bool Settings::load(const std::string& path) if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get(); if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get(); if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get(); + if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get(); if (j.contains("debug_categories") && j["debug_categories"].is_array()) { debug_categories_.clear(); for (const auto& c : j["debug_categories"]) @@ -203,6 +204,7 @@ bool Settings::save(const std::string& path) j["pin_enabled"] = pin_enabled_; j["keep_daemon_running"] = keep_daemon_running_; j["stop_external_daemon"] = stop_external_daemon_; + j["verbose_logging"] = verbose_logging_; j["debug_categories"] = json::array(); for (const auto& cat : debug_categories_) j["debug_categories"].push_back(cat); diff --git a/src/config/settings.h b/src/config/settings.h index 87ebb67..60141d5 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -163,6 +163,10 @@ public: bool getStopExternalDaemon() const { return stop_external_daemon_; } void setStopExternalDaemon(bool v) { stop_external_daemon_ = v; } + // Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.) + bool getVerboseLogging() const { return verbose_logging_; } + void setVerboseLogging(bool v) { verbose_logging_ = v; } + // Daemon — debug logging categories const std::set& getDebugCategories() const { return debug_categories_; } void setDebugCategories(const std::set& cats) { debug_categories_ = cats; } @@ -243,6 +247,7 @@ private: bool pin_enabled_ = false; bool keep_daemon_running_ = false; bool stop_external_daemon_ = false; + bool verbose_logging_ = false; std::set debug_categories_; bool theme_effects_enabled_ = true; bool low_spec_mode_ = false; diff --git a/src/daemon/embedded_daemon.cpp b/src/daemon/embedded_daemon.cpp index 29870dc..b9eba0e 100644 --- a/src/daemon/embedded_daemon.cpp +++ b/src/daemon/embedded_daemon.cpp @@ -17,12 +17,13 @@ #include "../util/logger.h" #ifdef _WIN32 +#include +#include #include #include #include #include -#include -#include +#include #else #include #include @@ -224,6 +225,113 @@ std::vector EmbeddedDaemon::getRecentLines(int maxLines) const return lines; } +// Identify what process owns a given TCP port. +// Returns a string like "PID 1234 (dragonxd.exe)" or "unknown" on failure. +static std::string getPortOwnerInfo(int port) +{ +#ifdef _WIN32 + DWORD size = 0; + // First call to get required buffer size + GetExtendedTcpTable(nullptr, &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0); + if (size == 0) return "unknown (GetExtendedTcpTable size query failed)"; + std::vector buf(size); + DWORD ret = GetExtendedTcpTable(buf.data(), &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0); + if (ret != NO_ERROR) return "unknown (GetExtendedTcpTable failed, error " + std::to_string(ret) + ")"; + auto* table = reinterpret_cast(buf.data()); + DWORD ownerPid = 0; + for (DWORD i = 0; i < table->dwNumEntries; i++) { + auto& row = table->table[i]; + int rowPort = ntohs(static_cast(row.dwLocalPort)); + // Match port in LISTEN state (MIB_TCP_STATE_LISTEN = 2) + if (rowPort == port && row.dwState == MIB_TCP_STATE_LISTEN) { + ownerPid = row.dwOwningPid; + break; + } + } + if (ownerPid == 0) { + // Maybe it's in ESTABLISHED or another state from our connect probe — try any state + for (DWORD i = 0; i < table->dwNumEntries; i++) { + auto& row = table->table[i]; + int rowPort = ntohs(static_cast(row.dwLocalPort)); + if (rowPort == port && row.dwOwningPid != 0) { + ownerPid = row.dwOwningPid; + break; + } + } + } + if (ownerPid == 0) return "unknown (no PID found for port " + std::to_string(port) + ")"; + // Resolve PID to process name + std::string procName = ""; + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snap != INVALID_HANDLE_VALUE) { + PROCESSENTRY32 pe; + pe.dwSize = sizeof(pe); + if (Process32First(snap, &pe)) { + do { + if (pe.th32ProcessID == ownerPid) { + procName = pe.szExeFile; + break; + } + } while (Process32Next(snap, &pe)); + } + CloseHandle(snap); + } + return "PID " + std::to_string(ownerPid) + " (" + procName + ")"; +#else + // Linux: parse /proc/net/tcp to find the inode, then scan /proc/*/fd + FILE* fp = fopen("/proc/net/tcp", "r"); + if (!fp) return "unknown (cannot read /proc/net/tcp)"; + char line[512]; + unsigned long inode = 0; + while (fgets(line, sizeof(line), fp)) { + unsigned int localPort, state; + unsigned long lineInode; + if (sscanf(line, " %*d: %*X:%X %*X:%*X %X %*X:%*X %*X:%*X %*X %*u %*u %lu", + &localPort, &state, &lineInode) == 3) { + if (static_cast(localPort) == port && state == 0x0A) { // 0x0A = LISTEN + inode = lineInode; + break; + } + } + } + fclose(fp); + if (inode == 0) return "unknown (no listener found for port " + std::to_string(port) + ")"; + // Scan /proc/*/fd/* for the matching inode + for (const auto& entry : fs::directory_iterator("/proc")) { + if (!entry.is_directory()) continue; + std::string pidStr = entry.path().filename().string(); + if (pidStr.empty() || !std::isdigit(pidStr[0])) continue; + std::string fdDir = "/proc/" + pidStr + "/fd"; + try { + for (const auto& fdEntry : fs::directory_iterator(fdDir)) { + char target[512]; + ssize_t len = readlink(fdEntry.path().c_str(), target, sizeof(target) - 1); + if (len > 0) { + target[len] = '\0'; + std::string t(target); + if (t.find("socket:[" + std::to_string(inode) + "]") != std::string::npos) { + // Found the PID, now get the process name + std::string commPath = "/proc/" + pidStr + "/comm"; + FILE* cf = fopen(commPath.c_str(), "r"); + std::string procName = ""; + if (cf) { + char name[256]; + if (fgets(name, sizeof(name), cf)) { + procName = name; + while (!procName.empty() && procName.back() == '\n') procName.pop_back(); + } + fclose(cf); + } + return "PID " + pidStr + " (" + procName + ")"; + } + } + } + } catch (...) { /* permission denied — skip */ } + } + return "unknown (inode " + std::to_string(inode) + " found but no matching PID)"; +#endif +} + // Check if a TCP port is already in use (something is LISTENING) static bool isPortInUse(int port) { @@ -279,10 +387,11 @@ bool EmbeddedDaemon::start(const std::string& binary_path) // Check if something is already listening on the RPC port int rpc_port = std::atoi(DRAGONX_DEFAULT_RPC_PORT); if (isPortInUse(rpc_port)) { - DEBUG_LOGF("[INFO] Port %d is already in use — external daemon detected, will connect to it.\\n", rpc_port); + std::string owner = getPortOwnerInfo(rpc_port); + VERBOSE_LOGF("[INFO] Port %d is already in use by %s — external daemon detected, will connect to it.\\n", rpc_port, owner.c_str()); external_daemon_detected_ = true; // Don't set Error — the wallet will connect to the running daemon. - setState(State::Stopped, "External daemon detected on port " + std::string(DRAGONX_DEFAULT_RPC_PORT)); + setState(State::Stopped, "External daemon detected on port " + std::string(DRAGONX_DEFAULT_RPC_PORT) + " (owned by " + owner + ")"); return false; } external_daemon_detected_ = false; @@ -519,7 +628,7 @@ void EmbeddedDaemon::drainOutput() std::lock_guard lk(output_mutex_); appendOutput(buffer, bytes_read); } - DEBUG_LOGF("[dragonxd] %s", buffer); + VERBOSE_LOGF("[dragonxd] %s", buffer); debug_log_offset_ += bytes_read; } @@ -895,7 +1004,7 @@ void EmbeddedDaemon::drainOutput() std::lock_guard lk(output_mutex_); appendOutput(buffer, static_cast(n)); } - DEBUG_LOGF("[dragonxd] %s", buffer); + VERBOSE_LOGF("[dragonxd] %s", buffer); } } @@ -987,7 +1096,7 @@ void EmbeddedDaemon::monitorProcess() std::lock_guard lk(output_mutex_); appendOutput(buffer, static_cast(bytes_read)); } - DEBUG_LOGF("[dragonxd] %s", buffer); + VERBOSE_LOGF("[dragonxd] %s", buffer); } std::this_thread::sleep_for(std::chrono::milliseconds(100)); diff --git a/src/rpc/rpc_client.cpp b/src/rpc/rpc_client.cpp index dd8674b..17a7896 100644 --- a/src/rpc/rpc_client.cpp +++ b/src/rpc/rpc_client.cpp @@ -68,7 +68,7 @@ bool RPCClient::connect(const std::string& host, const std::string& port, // Build URL - use HTTP for localhost RPC (TLS not always enabled) impl_->url = "http://" + host + ":" + port + "/"; - DEBUG_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str()); + VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str()); // Initialize curl handle impl_->curl = curl_easy_init(); @@ -94,10 +94,12 @@ bool RPCClient::connect(const std::string& host, const std::string& port, json result = call("getinfo"); if (result.contains("version")) { connected_ = true; + last_connect_error_.clear(); DEBUG_LOGF("Connected to dragonxd v%d\n", result["version"].get()); return true; } } catch (const std::exception& e) { + last_connect_error_ = e.what(); DEBUG_LOGF("Connection failed: %s\n", e.what()); } diff --git a/src/rpc/rpc_client.h b/src/rpc/rpc_client.h index eeea3ef..a831110 100644 --- a/src/rpc/rpc_client.h +++ b/src/rpc/rpc_client.h @@ -53,6 +53,11 @@ public: */ bool isConnected() const { return connected_; } + /** + * @brief Get the error message from the last failed connect() attempt. + */ + const std::string& getLastConnectError() const { return last_connect_error_; } + /** * @brief Make a raw RPC call * @param method RPC method name @@ -177,6 +182,7 @@ private: std::string port_; std::string auth_; // Base64 encoded "user:password" bool connected_ = false; + std::string last_connect_error_; mutable std::recursive_mutex curl_mutex_; // serializes all curl handle access // HTTP client (implementation hidden) diff --git a/src/ui/effects/acrylic.cpp b/src/ui/effects/acrylic.cpp index 543d17b..eb5c956 100644 --- a/src/ui/effects/acrylic.cpp +++ b/src/ui/effects/acrylic.cpp @@ -1200,7 +1200,7 @@ void AcrylicMaterial::resize(int width, int height) dx_blurHeight_ = bh; dx_blurCurrent_ = 0; dirtyFrames_ = 2; - DEBUG_LOGF("[Acrylic DX11] Resized %dx%d (blur %dx%d)\n", width, height, bw, bh); + VERBOSE_LOGF("[Acrylic DX11] Resized %dx%d (blur %dx%d)\n", width, height, bw, bh); } void AcrylicMaterial::captureBackground() @@ -1391,7 +1391,7 @@ void AcrylicMaterial::applyBlur(float radius) static bool s_traced = false; if (!s_traced) { s_traced = true; - DEBUG_LOGF("[Acrylic DX11] applyBlur: %d passes, radius=%.1f, blurSize=%dx%d\n", + VERBOSE_LOGF("[Acrylic DX11] applyBlur: %d passes, radius=%.1f, blurSize=%dx%d\n", passes, effectiveRadius, dx_blurWidth_, dx_blurHeight_); } @@ -1469,7 +1469,7 @@ void AcrylicMaterial::drawRect(ImDrawList* drawList, const ImVec2& pMin, const I static bool s_traced = false; if (!s_traced) { s_traced = true; - DEBUG_LOGF("[Acrylic DX11] drawRect first call: fallback=%d enabled=%d quality=%d " + VERBOSE_LOGF("[Acrylic DX11] drawRect first call: fallback=%d enabled=%d quality=%d " "hasCapture=%d blurValid=%d viewport=%dx%d\n", (int)currentFallback_, (int)settings_.enabled, (int)settings_.quality, (int)hasValidCapture_, @@ -1524,7 +1524,7 @@ void AcrylicMaterial::drawRect(ImDrawList* drawList, const ImVec2& pMin, const I static bool s_blurTraced = false; if (!s_blurTraced && blurTex) { s_blurTraced = true; - DEBUG_LOGF("[Acrylic DX11] blur tex=%p UV: (%.3f,%.3f)-(%.3f,%.3f)\n", + VERBOSE_LOGF("[Acrylic DX11] blur tex=%p UV: (%.3f,%.3f)-(%.3f,%.3f)\n", (void*)blurTex, u0, v0, u1, v1); } diff --git a/src/ui/effects/scroll_fade_shader.h b/src/ui/effects/scroll_fade_shader.h index 75045eb..cd7feb8 100644 --- a/src/ui/effects/scroll_fade_shader.h +++ b/src/ui/effects/scroll_fade_shader.h @@ -310,7 +310,7 @@ struct ScrollFadeShader { return false; } - DEBUG_LOGF("ScrollFadeShader: DX11 pixel shader + CB created\n"); + VERBOSE_LOGF("ScrollFadeShader: DX11 pixel shader + CB created\n"); return true; } diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 9f20464..aa08383 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -6,6 +6,7 @@ #include "../../app.h" #include "../../config/version.h" #include "../../config/settings.h" +#include "../../util/logger.h" #include "../windows/balance_tab.h" #include "../windows/console_tab.h" #include "../../util/i18n.h" @@ -103,6 +104,7 @@ static LowSpecSnapshot s_lowSpecSnap; // Daemon — keep running on close static bool sp_keep_daemon_running = false; static bool sp_stop_external_daemon = false; +static bool sp_verbose_logging = false; // Debug logging categories static std::set sp_debug_categories; @@ -163,6 +165,7 @@ static void loadSettingsPageState(config::Settings* settings) { Layout::setUserFontScale(sp_font_scale); // sync with Layout on load sp_keep_daemon_running = settings->getKeepDaemonRunning(); sp_stop_external_daemon = settings->getStopExternalDaemon(); + sp_verbose_logging = settings->getVerboseLogging(); sp_debug_categories = settings->getDebugCategories(); sp_debug_cats_dirty = false; @@ -209,6 +212,7 @@ static void saveSettingsPageState(config::Settings* settings) { settings->setFontScale(sp_font_scale); settings->setKeepDaemonRunning(sp_keep_daemon_running); settings->setStopExternalDaemon(sp_stop_external_daemon); + settings->setVerboseLogging(sp_verbose_logging); settings->setDebugCategories(sp_debug_categories); settings->save(); @@ -1060,6 +1064,13 @@ void RenderSettingsPage(App* app) { } if (ImGui::IsItemHovered()) ImGui::SetTooltip("Applies when connecting to a daemon\nyou started outside this wallet"); + ImGui::SameLine(0, sp); + if (ImGui::Checkbox("Verbose logging", &sp_verbose_logging)) { + dragonx::util::Logger::instance().setVerbose(sp_verbose_logging); + saveSettingsPageState(app->settings()); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Log detailed connection diagnostics,\ndaemon state, and port owner info\nto the Console tab"); if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f); } diff --git a/src/ui/windows/mining_tab.cpp b/src/ui/windows/mining_tab.cpp index 9440342..b2da8bd 100644 --- a/src/ui/windows/mining_tab.cpp +++ b/src/ui/windows/mining_tab.cpp @@ -271,7 +271,9 @@ void RenderMiningTab(App* app) s_pool_mode = true; app->settings()->setPoolMode(true); app->settings()->save(); - app->stopMining(); + // Note: soloMiningActive is already false (checked above), + // so no need to call stopMining() — it would just set the + // toggle-in-progress flag and make the button show "STARTING". } if (poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (poolHov && soloMiningActive && !s_pool_mode) { diff --git a/src/ui/windows/peers_tab.cpp b/src/ui/windows/peers_tab.cpp index f31d2cd..c063422 100644 --- a/src/ui/windows/peers_tab.cpp +++ b/src/ui/windows/peers_tab.cpp @@ -7,6 +7,7 @@ #include "../../data/wallet_state.h" #include "../theme.h" #include "../effects/imgui_acrylic.h" +#include "../effects/low_spec.h" #include "../schema/ui_schema.h" #include "../material/type.h" #include "../material/draw_helpers.h" @@ -505,28 +506,111 @@ void RenderPeersTab(App* app) } if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - // Refresh button — top-right of the toggle header line + // Refresh button — top-right, glass panel style (similar to mining button) { + bool isRefreshing = app->isPeerRefreshInProgress(); + auto refreshBtn = S.drawElement("tabs.peers", "refresh-button"); + float btnW = refreshBtn.size; + float btnH = toggleH - 4.0f * Layout::dpiScale(); + float btnX = ImGui::GetWindowPos().x + availWidth - btnW - Layout::spacingSm(); + float btnY = toggleY + (toggleH - btnH) * 0.5f; + ImVec2 bMin(btnX, btnY); + ImVec2 bMax(btnX + btnW, btnY + btnH); + + bool btnHovered = material::IsRectHovered(bMin, bMax); + bool btnClicked = btnHovered && ImGui::IsMouseClicked(0); + + // Glass panel background + GlassPanelSpec btnGlass; + btnGlass.rounding = Layout::glassRounding(); + if (isRefreshing) { + float pulse = effects::isLowSpecMode() + ? 0.5f + : 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 4.0); + btnGlass.fillAlpha = (int)(15 + 25 * pulse); + } else { + btnGlass.fillAlpha = btnHovered ? 30 : 18; + } + DrawGlassPanel(dl, bMin, bMax, btnGlass); + + // Hover highlight + if (btnHovered && !isRefreshing) { + dl->AddRectFilled(bMin, bMax, WithAlpha(Primary(), 20), btnGlass.rounding); + } + + // Icon: spinner while refreshing, refresh icon otherwise + float cx = bMin.x + btnW * 0.35f; + float cy = bMin.y + btnH * 0.5f; ImFont* iconFont = Type().iconMed(); float iconSz = iconFont->LegacySize; - float btnPad = Layout::spacingSm(); - float btnX = ImGui::GetWindowPos().x + availWidth - iconSz - btnPad; - float btnY = toggleY + (toggleH - iconSz) * 0.5f; - ImGui::SetCursorScreenPos(ImVec2(btnX, btnY)); - ImGui::PushID("##peersRefresh"); - if (ImGui::InvisibleButton("##btn", ImVec2(iconSz + btnPad, iconSz + btnPad))) { - app->refreshPeerInfo(); - app->refreshNow(); + + if (isRefreshing) { + // Spinning arc spinner (same style as mining toggle) + float spinnerR = iconSz * 0.5f; + float thickness = std::max(1.5f, spinnerR * 0.18f); + float time = (float)ImGui::GetTime(); + + // Track circle (faint) + dl->AddCircle(ImVec2(cx, cy), spinnerR, WithAlpha(Primary(), 40), 0, thickness); + + // Animated arc + float rotation = fmodf(time * 2.0f * IM_PI / 1.4f, IM_PI * 2.0f); + float cycleTime = fmodf(time, 1.333f); + float arcLength = (cycleTime < 0.666f) + ? (cycleTime / 0.666f) * 0.75f + 0.1f + : ((1.333f - cycleTime) / 0.666f) * 0.75f + 0.1f; + float startAngle = rotation - IM_PI * 0.5f; + float endAngle = startAngle + IM_PI * 2.0f * arcLength; + int segments = (int)(32 * arcLength) + 1; + float angleStep = (endAngle - startAngle) / segments; + ImU32 arcCol = Primary(); + for (int si = 0; si < segments; si++) { + float a1 = startAngle + angleStep * si; + float a2 = startAngle + angleStep * (si + 1); + ImVec2 p1(cx + cosf(a1) * spinnerR, cy + sinf(a1) * spinnerR); + ImVec2 p2(cx + cosf(a2) * spinnerR, cy + sinf(a2) * spinnerR); + dl->AddLine(p1, p2, arcCol, thickness); + } + } else { + // Static refresh icon + ImU32 iconCol = btnHovered ? OnSurface() : OnSurfaceMedium(); + ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_REFRESH); + dl->AddText(iconFont, iconFont->LegacySize, + ImVec2(cx - iSz.x * 0.5f, cy - iSz.y * 0.5f), + iconCol, ICON_MD_REFRESH); } - bool hovered = ImGui::IsItemHovered(); - if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - ImU32 iconCol = hovered ? OnSurface() : OnSurfaceMedium(); - // Centre icon within the invisible button - float drawX = btnX + (iconSz + btnPad - iconSz) * 0.5f; - float drawY = btnY + (iconSz + btnPad - iconSz) * 0.5f; - dl->AddText(iconFont, iconSz, ImVec2(drawX, drawY), iconCol, ICON_MD_REFRESH); - if (hovered) { - ImGui::SetTooltip("Refresh peers & blockchain"); + + // Label to the right of icon + { + const char* label = isRefreshing ? "REFRESHING" : "REFRESH"; + ImU32 lblCol; + if (isRefreshing) { + float pulse = effects::isLowSpecMode() + ? 0.7f + : 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 3.0); + lblCol = WithAlpha(Primary(), (int)(120 + 135 * pulse)); + } else { + lblCol = btnHovered ? OnSurface() : WithAlpha(OnSurface(), 160); + } + ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label); + float lblX = cx + iconSz * 0.5f + Layout::spacingXs(); + float lblY = cy - lblSz.y * 0.5f; + dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lblX, lblY), lblCol, label); + } + + // Invisible button for click handling + ImGui::SetCursorScreenPos(bMin); + ImGui::PushID("##peersRefresh"); + if (ImGui::InvisibleButton("##btn", ImVec2(btnW, btnH))) { + if (!isRefreshing) { + app->refreshPeerInfo(); + app->refreshNow(); + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if (!isRefreshing) + ImGui::SetTooltip("Refresh peers & blockchain"); } ImGui::PopID(); } diff --git a/src/util/logger.cpp b/src/util/logger.cpp index 17e4aa3..36de00a 100644 --- a/src/util/logger.cpp +++ b/src/util/logger.cpp @@ -73,6 +73,11 @@ void Logger::write(const std::string& message) #ifdef DRAGONX_DEBUG printf("%s\n", line.c_str()); #endif + + // Forward to callback (e.g. ConsoleTab) with the raw message + if (callback_) { + callback_(message); + } } void Logger::writef(const char* format, ...) @@ -87,5 +92,11 @@ void Logger::writef(const char* format, ...) write(buffer); } +void Logger::setCallback(std::function cb) +{ + std::lock_guard lock(mutex_); + callback_ = std::move(cb); +} + } // namespace util } // namespace dragonx diff --git a/src/util/logger.h b/src/util/logger.h index c4a5460..76d1ef6 100644 --- a/src/util/logger.h +++ b/src/util/logger.h @@ -6,6 +6,7 @@ #include #include +#include #include namespace dragonx { @@ -43,10 +44,26 @@ public: */ static Logger& instance(); + /** + * @brief Set a callback invoked for every log message. + * The callback receives the raw (unformatted) message string. + * Thread-safe — the callback is invoked under the logger lock. + */ + void setCallback(std::function cb); + + /** + * @brief Enable/disable verbose (diagnostic) logging. + * When disabled, VERBOSE_LOGF calls are suppressed. + */ + void setVerbose(bool v) { verbose_ = v; } + bool isVerbose() const { return verbose_; } + private: std::ofstream file_; std::mutex mutex_; bool initialized_ = false; + bool verbose_ = false; + std::function callback_; }; // Convenience macros @@ -61,5 +78,12 @@ private: #define DEBUG_LOGF(...) #endif +// Verbose logging — only emitted when Logger::isVerbose() is true. +// Used for detailed diagnostic output (connection attempts, daemon state, etc.) +#define VERBOSE_LOGF(...) do { \ + if (dragonx::util::Logger::instance().isVerbose()) \ + dragonx::util::Logger::instance().writef(__VA_ARGS__); \ +} while (0) + } // namespace util } // namespace dragonx