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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
2
setup.sh
2
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
|
||||
}
|
||||
|
||||
|
||||
35
src/app.cpp
35
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")) {
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user