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
This commit is contained in:
dan_s
2026-03-05 05:26:04 -06:00
parent c51d3dafff
commit 4b16a2a2c4
19 changed files with 461 additions and 52 deletions

View File

@@ -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
$<$<CONFIG:Debug>:DRAGONX_DEBUG>
DRAGONX_DEBUG
)
if(WIN32)
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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")) {

View File

@@ -177,6 +177,7 @@ public:
// Peers
const std::vector<PeerInfo>& getPeers() const { return state_.peers; }
const std::vector<BannedPeer>& 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<bool> mining_toggle_in_progress_{false};
// Peer refresh guard (visual feedback for refresh button)
std::atomic<bool> peer_refresh_in_progress_{false};
// Auto-shield guard (prevents concurrent auto-shield operations)
std::atomic<bool> auto_shield_pending_{false};

View File

@@ -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<PeerInfo> peers;
std::vector<BannedPeer> 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);
};
});
}

View File

@@ -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<rpc::RPCClient>();
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);

View File

@@ -129,6 +129,7 @@ bool Settings::load(const std::string& path)
if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get<bool>();
if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get<bool>();
if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get<bool>();
if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>();
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);

View File

@@ -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<std::string>& getDebugCategories() const { return debug_categories_; }
void setDebugCategories(const std::set<std::string>& 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<std::string> debug_categories_;
bool theme_effects_enabled_ = true;
bool low_spec_mode_ = false;

View File

@@ -17,12 +17,13 @@
#include "../util/logger.h"
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <psapi.h>
#include <tlhelp32.h>
#include <shlobj.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#else
#include <unistd.h>
#include <signal.h>
@@ -224,6 +225,113 @@ std::vector<std::string> 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<BYTE> 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<MIB_TCPTABLE_OWNER_PID*>(buf.data());
DWORD ownerPid = 0;
for (DWORD i = 0; i < table->dwNumEntries; i++) {
auto& row = table->table[i];
int rowPort = ntohs(static_cast<u_short>(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<u_short>(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 = "<unknown>";
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<int>(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 = "<unknown>";
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<std::mutex> 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<std::mutex> lk(output_mutex_);
appendOutput(buffer, static_cast<size_t>(n));
}
DEBUG_LOGF("[dragonxd] %s", buffer);
VERBOSE_LOGF("[dragonxd] %s", buffer);
}
}
@@ -987,7 +1096,7 @@ void EmbeddedDaemon::monitorProcess()
std::lock_guard<std::mutex> lk(output_mutex_);
appendOutput(buffer, static_cast<size_t>(bytes_read));
}
DEBUG_LOGF("[dragonxd] %s", buffer);
VERBOSE_LOGF("[dragonxd] %s", buffer);
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));

View File

@@ -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<int>());
return true;
}
} catch (const std::exception& e) {
last_connect_error_ = e.what();
DEBUG_LOGF("Connection failed: %s\n", e.what());
}

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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<std::string> 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);
}

View File

@@ -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) {

View File

@@ -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();
}

View File

@@ -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<void(const std::string&)> cb)
{
std::lock_guard<std::mutex> lock(mutex_);
callback_ = std::move(cb);
}
} // namespace util
} // namespace dragonx

View File

@@ -6,6 +6,7 @@
#include <string>
#include <fstream>
#include <functional>
#include <mutex>
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<void(const std::string&)> 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<void(const std::string&)> 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