ObsidianDragon - DragonX ImGui Wallet
Full-node GUI wallet for DragonX cryptocurrency. Built with Dear ImGui, SDL3, and OpenGL3/DX11. Features: - Send/receive shielded and transparent transactions - Autoshield with merged transaction display - Built-in CPU mining (xmrig) - Peer management and network monitoring - Wallet encryption with PIN lock - QR code generation for receive addresses - Transaction history with pagination - Console for direct RPC commands - Cross-platform (Linux, Windows)
This commit is contained in:
1011
src/daemon/embedded_daemon.cpp
Normal file
1011
src/daemon/embedded_daemon.cpp
Normal file
File diff suppressed because it is too large
Load Diff
195
src/daemon/embedded_daemon.h
Normal file
195
src/daemon/embedded_daemon.h
Normal file
@@ -0,0 +1,195 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
|
||||
/**
|
||||
* @brief Manages the embedded dragonxd process
|
||||
*
|
||||
* Handles starting, stopping, and monitoring the dragonxd daemon process.
|
||||
* Ports functionality from Qt version's connection.cpp.
|
||||
*/
|
||||
class EmbeddedDaemon {
|
||||
public:
|
||||
enum class State {
|
||||
Stopped,
|
||||
Starting,
|
||||
Running,
|
||||
Stopping,
|
||||
Error
|
||||
};
|
||||
|
||||
using StateCallback = std::function<void(State, const std::string&)>;
|
||||
|
||||
EmbeddedDaemon();
|
||||
~EmbeddedDaemon();
|
||||
|
||||
// Non-copyable
|
||||
EmbeddedDaemon(const EmbeddedDaemon&) = delete;
|
||||
EmbeddedDaemon& operator=(const EmbeddedDaemon&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Start the embedded dragonxd daemon
|
||||
* @param binary_path Path to dragonxd binary (auto-detect if empty)
|
||||
* @return true if process started successfully
|
||||
*/
|
||||
bool start(const std::string& binary_path = "");
|
||||
|
||||
/**
|
||||
* @brief Stop the running daemon
|
||||
* @param wait_ms Maximum milliseconds to wait for graceful shutdown
|
||||
*/
|
||||
void stop(int wait_ms = 5000);
|
||||
|
||||
/**
|
||||
* @brief Check if daemon process is running
|
||||
*/
|
||||
bool isRunning() const;
|
||||
|
||||
/**
|
||||
* @brief Get current daemon state
|
||||
*/
|
||||
State getState() const { return state_; }
|
||||
|
||||
/**
|
||||
* @brief Get daemon process memory usage (RSS) in MB
|
||||
* @return Memory usage in MB, or 0 if unavailable
|
||||
*/
|
||||
double getMemoryUsageMB() const;
|
||||
|
||||
/**
|
||||
* @brief Get last error message
|
||||
*/
|
||||
const std::string& getLastError() const { return last_error_; }
|
||||
|
||||
/**
|
||||
* @brief Get dragonxd process output (thread-safe copy)
|
||||
*/
|
||||
std::string getOutput() const {
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
return process_output_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get new output since a given offset (thread-safe).
|
||||
* Returns the new text and updates offset to the current size.
|
||||
*/
|
||||
std::string getOutputSince(size_t& offset) const {
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
if (offset >= process_output_.size()) {
|
||||
offset = process_output_.size();
|
||||
return {};
|
||||
}
|
||||
std::string result = process_output_.substr(offset);
|
||||
offset = process_output_.size();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get current output size (thread-safe, no copy)
|
||||
*/
|
||||
size_t getOutputSize() const {
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
return process_output_.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get last N lines of daemon output (thread-safe snapshot)
|
||||
*/
|
||||
std::vector<std::string> getRecentLines(int maxLines = 8) const;
|
||||
|
||||
/**
|
||||
* @brief Whether start() detected an existing daemon on the RPC port.
|
||||
* When true the wallet should connect to it instead of showing an error.
|
||||
*/
|
||||
bool externalDaemonDetected() const { return external_daemon_detected_; }
|
||||
|
||||
/**
|
||||
* @brief Set callback for state changes
|
||||
*/
|
||||
void setStateCallback(StateCallback cb) { state_callback_ = cb; }
|
||||
|
||||
/**
|
||||
* @brief Find dragonxd binary in standard locations
|
||||
* @return Path to binary, or empty if not found
|
||||
*/
|
||||
static std::string findDaemonBinary();
|
||||
|
||||
/**
|
||||
* @brief Check whether anything is listening on the default RPC port.
|
||||
* Useful for detecting an externally-started daemon.
|
||||
*/
|
||||
static bool isRpcPortInUse();
|
||||
|
||||
/**
|
||||
* @brief Get the chain parameters for dragonxd
|
||||
* @return Command line arguments for dragonx chain
|
||||
*/
|
||||
static std::vector<std::string> getChainParams();
|
||||
|
||||
/**
|
||||
* @brief Set debug logging categories (appended as -debug= flags on next start)
|
||||
*/
|
||||
void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; }
|
||||
const std::set<std::string>& getDebugCategories() const { return debug_categories_; }
|
||||
|
||||
/** Get number of consecutive daemon crashes (resets on successful start or manual reset) */
|
||||
int getCrashCount() const { return crash_count_.load(); }
|
||||
/** Reset crash counter (call on successful connection or manual restart) */
|
||||
void resetCrashCount() { crash_count_ = 0; }
|
||||
|
||||
private:
|
||||
void setState(State s, const std::string& message = "");
|
||||
void monitorProcess();
|
||||
void drainOutput(); // read any pending stdout into process_output_
|
||||
void appendOutput(const char* data, size_t len); // append + trim (caller holds output_mutex_)
|
||||
bool startProcess(const std::string& binary_path, const std::vector<std::string>& args);
|
||||
|
||||
std::atomic<State> state_{State::Stopped};
|
||||
std::atomic<bool> external_daemon_detected_{false};
|
||||
std::string last_error_;
|
||||
mutable std::mutex output_mutex_; // protects process_output_
|
||||
std::string process_output_;
|
||||
StateCallback state_callback_;
|
||||
|
||||
// Process handle
|
||||
#ifdef _WIN32
|
||||
HANDLE process_handle_ = nullptr;
|
||||
HANDLE stdout_read_ = nullptr; // pipe handle (Linux-style pipe, unused when tailing debug.log)
|
||||
std::string debug_log_path_; // path to daemon's debug.log for output tailing
|
||||
size_t debug_log_offset_ = 0; // read offset into debug.log
|
||||
std::string launch_cmd_; // full command line used to launch daemon (for diagnostics)
|
||||
std::string launch_binary_; // binary path used (for diagnostics)
|
||||
std::string launch_workdir_; // working directory used (for diagnostics)
|
||||
#else
|
||||
pid_t process_pid_ = 0;
|
||||
int stdout_fd_ = -1;
|
||||
#endif
|
||||
|
||||
std::thread monitor_thread_;
|
||||
std::atomic<bool> should_stop_{false};
|
||||
std::set<std::string> debug_categories_;
|
||||
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
||||
};
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
715
src/daemon/xmrig_manager.cpp
Normal file
715
src/daemon/xmrig_manager.cpp
Normal file
@@ -0,0 +1,715 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "xmrig_manager.h"
|
||||
#include "../resources/embedded_resources.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include "../util/logger.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#include <shlobj.h>
|
||||
#include <psapi.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <pwd.h>
|
||||
#include <errno.h>
|
||||
#endif
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
static std::string randomHexToken(int bytes = 16) {
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<int> dist(0, 15);
|
||||
std::string out;
|
||||
out.reserve(bytes * 2);
|
||||
for (int i = 0; i < bytes * 2; ++i)
|
||||
out.push_back(hex[dist(gen)]);
|
||||
return out;
|
||||
}
|
||||
|
||||
static int randomPort() {
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<int> dist(18000, 18999);
|
||||
return dist(gen);
|
||||
}
|
||||
|
||||
static std::string getConfigDir() {
|
||||
#ifdef _WIN32
|
||||
char path[MAX_PATH];
|
||||
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
|
||||
return std::string(path) + "\\ObsidianDragon";
|
||||
}
|
||||
return ".";
|
||||
#else
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
home = pw ? pw->pw_dir : "/tmp";
|
||||
}
|
||||
return std::string(home) + "/.config/ObsidianDragon";
|
||||
#endif
|
||||
}
|
||||
|
||||
// libcurl write callback
|
||||
static size_t curlWriteCb(void* ptr, size_t sz, size_t n, void* userdata) {
|
||||
auto* s = static_cast<std::string*>(userdata);
|
||||
s->append(static_cast<char*>(ptr), sz * n);
|
||||
return sz * n;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Lifecycle
|
||||
// ============================================================================
|
||||
|
||||
XmrigManager::XmrigManager() = default;
|
||||
|
||||
XmrigManager::~XmrigManager() {
|
||||
if (isRunning()) {
|
||||
stop(3000);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Binary discovery
|
||||
// ============================================================================
|
||||
|
||||
std::string XmrigManager::findXmrigBinary() {
|
||||
// Use the embedded_resources system (same pattern as daemon)
|
||||
std::string path = resources::getXmrigPath();
|
||||
if (!path.empty() && fs::exists(path)) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Fallback: system PATH
|
||||
#ifdef _WIN32
|
||||
FILE* f = _popen("where xmrig.exe 2>nul", "r");
|
||||
#else
|
||||
FILE* f = popen("which xmrig 2>/dev/null", "r");
|
||||
#endif
|
||||
if (f) {
|
||||
char line[512];
|
||||
if (fgets(line, sizeof(line), f)) {
|
||||
std::string s(line);
|
||||
while (!s.empty() && (s.back() == '\n' || s.back() == '\r'))
|
||||
s.pop_back();
|
||||
if (!s.empty() && fs::exists(s)) {
|
||||
#ifdef _WIN32
|
||||
_pclose(f);
|
||||
#else
|
||||
pclose(f);
|
||||
#endif
|
||||
return s;
|
||||
}
|
||||
}
|
||||
#ifdef _WIN32
|
||||
_pclose(f);
|
||||
#else
|
||||
pclose(f);
|
||||
#endif
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Config generation
|
||||
// ============================================================================
|
||||
|
||||
bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath) {
|
||||
api_port_ = randomPort();
|
||||
api_token_ = randomHexToken(16);
|
||||
|
||||
int hw = (int)std::thread::hardware_concurrency();
|
||||
if (hw < 1) hw = 1;
|
||||
// Use explicit thread count (not just a hint)
|
||||
threads_ = (cfg.threads > 0) ? cfg.threads : std::max(1, hw / 2);
|
||||
if (threads_ > hw) threads_ = hw;
|
||||
|
||||
json j;
|
||||
j["autosave"] = false;
|
||||
j["background"] = false;
|
||||
j["colors"] = false;
|
||||
|
||||
j["http"] = {
|
||||
{"enabled", true},
|
||||
{"host", "127.0.0.1"},
|
||||
{"port", api_port_},
|
||||
{"access-token", api_token_},
|
||||
{"restricted", true}
|
||||
};
|
||||
|
||||
j["randomx"] = {
|
||||
{"init", -1},
|
||||
{"mode", "auto"},
|
||||
{"1gb-pages", false},
|
||||
{"numa", true},
|
||||
{"scratchpad_prefetch_mode", 1}
|
||||
};
|
||||
|
||||
j["cpu"] = {
|
||||
{"enabled", true},
|
||||
{"huge-pages", cfg.hugepages},
|
||||
{"max-threads-hint", 100}, // Use 100% of allotted threads
|
||||
{"priority", 0}, // Idle priority (lowest) - prevents UI lag
|
||||
{"yield", true} // Yield to other processes
|
||||
};
|
||||
|
||||
j["pools"] = json::array({
|
||||
{
|
||||
{"algo", cfg.algo},
|
||||
{"url", cfg.pool_url},
|
||||
{"user", cfg.wallet_address},
|
||||
{"pass", cfg.worker_name},
|
||||
{"keepalive", true},
|
||||
{"tls", cfg.tls}
|
||||
}
|
||||
});
|
||||
|
||||
j["donate-level"] = 0;
|
||||
j["print-time"] = 10;
|
||||
j["retries"] = 5;
|
||||
j["retry-pause"] = 5;
|
||||
|
||||
try {
|
||||
fs::create_directories(fs::path(outPath).parent_path());
|
||||
std::ofstream ofs(outPath);
|
||||
if (!ofs.is_open()) {
|
||||
last_error_ = "Cannot write xmrig config: " + outPath;
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
ofs << j.dump(4);
|
||||
ofs.close();
|
||||
|
||||
#ifndef _WIN32
|
||||
// 0600 permissions — only owner can read/write
|
||||
chmod(outPath.c_str(), 0600);
|
||||
#endif
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
last_error_ = std::string("Config write error: ") + e.what();
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// start / stop
|
||||
// ============================================================================
|
||||
|
||||
bool XmrigManager::start(const Config& cfg) {
|
||||
if (state_ == State::Running || state_ == State::Starting) {
|
||||
last_error_ = "Already running";
|
||||
DEBUG_LOGF("[WARN] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
state_ = State::Starting;
|
||||
should_stop_ = false;
|
||||
last_error_.clear();
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
process_output_.clear();
|
||||
}
|
||||
stats_ = PoolStats{};
|
||||
|
||||
// Find binary
|
||||
std::string binary = findXmrigBinary();
|
||||
if (binary.empty()) {
|
||||
last_error_ = "xmrig binary not found";
|
||||
state_ = State::Error;
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: xmrig binary not found\n");
|
||||
return false;
|
||||
}
|
||||
DEBUG_LOGF("[INFO] XmrigManager: found binary at %s\n", binary.c_str());
|
||||
|
||||
// Generate config
|
||||
std::string cfgDir = getConfigDir();
|
||||
#ifdef _WIN32
|
||||
std::string cfgPath = cfgDir + "\\xmrig-pool.json";
|
||||
#else
|
||||
std::string cfgPath = cfgDir + "/xmrig-pool.json";
|
||||
#endif
|
||||
if (!generateConfig(cfg, cfgPath)) {
|
||||
state_ = State::Error;
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: failed to generate config\n");
|
||||
return false;
|
||||
}
|
||||
DEBUG_LOGF("[INFO] XmrigManager: config written to %s (API port %d, threads %d)\n",
|
||||
cfgPath.c_str(), api_port_, threads_);
|
||||
|
||||
// Spawn process
|
||||
if (!startProcess(binary, cfgPath, threads_)) {
|
||||
state_ = State::Error;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start monitor thread
|
||||
monitor_thread_ = std::thread(&XmrigManager::monitorProcess, this);
|
||||
state_ = State::Running;
|
||||
DEBUG_LOGF("[INFO] XmrigManager: started\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
void XmrigManager::stop(int wait_ms) {
|
||||
if (state_ == State::Stopped || state_ == State::Stopping)
|
||||
return;
|
||||
|
||||
state_ = State::Stopping;
|
||||
should_stop_ = true;
|
||||
|
||||
#ifdef _WIN32
|
||||
if (process_handle_) {
|
||||
// Try graceful termination first
|
||||
TerminateProcess(process_handle_, 0);
|
||||
WaitForSingleObject(process_handle_, wait_ms);
|
||||
CloseHandle(process_handle_);
|
||||
process_handle_ = nullptr;
|
||||
}
|
||||
if (stdout_read_) {
|
||||
CloseHandle(stdout_read_);
|
||||
stdout_read_ = nullptr;
|
||||
}
|
||||
#else
|
||||
if (process_pid_ > 0) {
|
||||
// Send SIGTERM to process group
|
||||
kill(-process_pid_, SIGTERM);
|
||||
|
||||
// Poll-wait with drain
|
||||
auto deadline = std::chrono::steady_clock::now()
|
||||
+ std::chrono::milliseconds(wait_ms);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
drainOutput();
|
||||
int status = 0;
|
||||
pid_t ret = waitpid(process_pid_, &status, WNOHANG);
|
||||
if (ret == process_pid_ || ret < 0) break;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
}
|
||||
|
||||
// If still alive, SIGKILL
|
||||
if (kill(process_pid_, 0) == 0) {
|
||||
kill(-process_pid_, SIGKILL);
|
||||
waitpid(process_pid_, nullptr, 0);
|
||||
}
|
||||
process_pid_ = 0;
|
||||
}
|
||||
if (stdout_fd_ >= 0) {
|
||||
close(stdout_fd_);
|
||||
stdout_fd_ = -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (monitor_thread_.joinable())
|
||||
monitor_thread_.join();
|
||||
|
||||
state_ = State::Stopped;
|
||||
DEBUG_LOGF("[INFO] XmrigManager: stopped\n");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Process spawning — platform-specific
|
||||
// ============================================================================
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads) {
|
||||
SECURITY_ATTRIBUTES sa{};
|
||||
sa.nLength = sizeof(sa);
|
||||
sa.bInheritHandle = TRUE;
|
||||
|
||||
HANDLE hRead = nullptr, hWrite = nullptr;
|
||||
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) {
|
||||
last_error_ = "CreatePipe failed";
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0);
|
||||
|
||||
// Use explicit --threads to enforce exact thread count (not just a hint)
|
||||
std::string cmdLine = "\"" + xmrigPath + "\" --config=\"" + cfgPath + "\" --threads=" + std::to_string(threads);
|
||||
|
||||
STARTUPINFOA si{};
|
||||
si.cb = sizeof(si);
|
||||
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
|
||||
si.hStdOutput = hWrite;
|
||||
si.hStdError = hWrite;
|
||||
si.wShowWindow = SW_HIDE;
|
||||
|
||||
PROCESS_INFORMATION pi{};
|
||||
BOOL ok = CreateProcessA(
|
||||
nullptr, const_cast<char*>(cmdLine.c_str()),
|
||||
nullptr, nullptr, TRUE,
|
||||
CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP | IDLE_PRIORITY_CLASS,
|
||||
nullptr, nullptr, &si, &pi
|
||||
);
|
||||
|
||||
CloseHandle(hWrite);
|
||||
|
||||
if (!ok) {
|
||||
CloseHandle(hRead);
|
||||
DWORD err = GetLastError();
|
||||
char errBuf[256];
|
||||
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
NULL, err, 0, errBuf, sizeof(errBuf), NULL);
|
||||
last_error_ = "CreateProcess failed for xmrig (error " + std::to_string(err) + "): " + errBuf;
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\nCommand: %s\n", last_error_.c_str(), cmdLine.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
process_handle_ = pi.hProcess;
|
||||
stdout_read_ = hRead;
|
||||
CloseHandle(pi.hThread);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XmrigManager::isRunning() const {
|
||||
if (!process_handle_) return false;
|
||||
DWORD exit_code;
|
||||
GetExitCodeProcess(process_handle_, &exit_code);
|
||||
return exit_code == STILL_ACTIVE;
|
||||
}
|
||||
|
||||
double XmrigManager::getMemoryUsageMB() const {
|
||||
if (!process_handle_) return 0.0;
|
||||
PROCESS_MEMORY_COUNTERS pmc;
|
||||
ZeroMemory(&pmc, sizeof(pmc));
|
||||
pmc.cb = sizeof(pmc);
|
||||
if (GetProcessMemoryInfo(process_handle_, &pmc, sizeof(pmc))) {
|
||||
return static_cast<double>(pmc.WorkingSetSize) / (1024.0 * 1024.0);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
void XmrigManager::drainOutput() {
|
||||
if (!stdout_read_) return;
|
||||
char buf[4096];
|
||||
DWORD avail = 0;
|
||||
while (PeekNamedPipe(stdout_read_, nullptr, 0, nullptr, &avail, nullptr) && avail > 0) {
|
||||
DWORD nread = 0;
|
||||
DWORD toRead = std::min(avail, (DWORD)sizeof(buf));
|
||||
if (ReadFile(stdout_read_, buf, toRead, &nread, nullptr) && nread > 0) {
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
appendOutput(buf, nread);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#else // ---- POSIX ----
|
||||
|
||||
bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads) {
|
||||
int pipefd[2];
|
||||
if (pipe(pipefd) != 0) {
|
||||
last_error_ = "pipe() failed";
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
last_error_ = "fork() failed";
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
// Child
|
||||
close(pipefd[0]);
|
||||
dup2(pipefd[1], STDOUT_FILENO);
|
||||
dup2(pipefd[1], STDERR_FILENO);
|
||||
close(pipefd[1]);
|
||||
|
||||
// New process group so we can kill the whole group
|
||||
setpgid(0, 0);
|
||||
|
||||
// Lowest priority to reduce UI lag (nice value 19 = minimum priority)
|
||||
if (nice(19) == -1 && errno != 0) { /* ignore failure */ }
|
||||
|
||||
std::string cfgArg = "--config=" + cfgPath;
|
||||
std::string threadsArg = "--threads=" + std::to_string(threads);
|
||||
const char* argv[] = { xmrigPath.c_str(), cfgArg.c_str(), threadsArg.c_str(), nullptr };
|
||||
execv(xmrigPath.c_str(), const_cast<char* const*>(argv));
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
// Parent
|
||||
close(pipefd[1]);
|
||||
process_pid_ = pid;
|
||||
stdout_fd_ = pipefd[0];
|
||||
|
||||
// Non-blocking reads
|
||||
int flags = fcntl(stdout_fd_, F_GETFL, 0);
|
||||
fcntl(stdout_fd_, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XmrigManager::isRunning() const {
|
||||
// Use state_ instead of waitpid() to avoid races with moitorProcess
|
||||
// which also calls waitpid. state_ is atomic and always correct.
|
||||
State s = state_.load(std::memory_order_relaxed);
|
||||
return (s == State::Running || s == State::Starting);
|
||||
}
|
||||
|
||||
double XmrigManager::getMemoryUsageMB() const {
|
||||
if (process_pid_ <= 0) return 0.0;
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "/proc/%d/statm", process_pid_);
|
||||
FILE* fp = fopen(path, "r");
|
||||
if (!fp) return 0.0;
|
||||
long dummy = 0, pages = 0;
|
||||
// statm: size resident shared text lib data dt
|
||||
// We want resident (2nd field)
|
||||
if (fscanf(fp, "%ld %ld", &dummy, &pages) != 2) pages = 0;
|
||||
fclose(fp);
|
||||
long pageSize = sysconf(_SC_PAGESIZE);
|
||||
return static_cast<double>(pages * pageSize) / (1024.0 * 1024.0);
|
||||
}
|
||||
|
||||
void XmrigManager::drainOutput() {
|
||||
if (stdout_fd_ < 0) return;
|
||||
char buf[4096];
|
||||
while (true) {
|
||||
ssize_t n = read(stdout_fd_, buf, sizeof(buf));
|
||||
if (n > 0) {
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
appendOutput(buf, (size_t)n);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // platform
|
||||
|
||||
// ============================================================================
|
||||
// Output management
|
||||
// ============================================================================
|
||||
|
||||
void XmrigManager::appendOutput(const char* data, size_t len) {
|
||||
// Caller must hold output_mutex_
|
||||
static constexpr size_t MAX_OUTPUT = 1024 * 1024; // 1 MB cap
|
||||
process_output_.append(data, len);
|
||||
if (process_output_.size() > MAX_OUTPUT) {
|
||||
// Trim from the front at a newline boundary
|
||||
size_t cut = process_output_.size() - MAX_OUTPUT;
|
||||
auto pos = process_output_.find('\n', cut);
|
||||
if (pos != std::string::npos)
|
||||
process_output_.erase(0, pos + 1);
|
||||
else
|
||||
process_output_.erase(0, cut);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> XmrigManager::getRecentLines(int maxLines) const {
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
std::vector<std::string> lines;
|
||||
if (process_output_.empty()) return lines;
|
||||
|
||||
// Walk backwards collecting lines
|
||||
size_t end = process_output_.size();
|
||||
while ((int)lines.size() < maxLines && end > 0) {
|
||||
size_t nl = process_output_.rfind('\n', end - 1);
|
||||
if (nl == std::string::npos) {
|
||||
lines.push_back(process_output_.substr(0, end));
|
||||
break;
|
||||
}
|
||||
if (nl + 1 < end)
|
||||
lines.push_back(process_output_.substr(nl + 1, end - nl - 1));
|
||||
end = nl;
|
||||
}
|
||||
std::reverse(lines.begin(), lines.end());
|
||||
// Remove empty trailing line
|
||||
while (!lines.empty() && lines.back().empty())
|
||||
lines.pop_back();
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Monitor thread
|
||||
// ============================================================================
|
||||
|
||||
void XmrigManager::monitorProcess() {
|
||||
// Wait a few seconds for xmrig HTTP API to start up before first poll
|
||||
for (int i = 0; i < 30 && !should_stop_; i++) {
|
||||
drainOutput();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
int poll_counter = 0;
|
||||
while (!should_stop_) {
|
||||
drainOutput();
|
||||
|
||||
// Check if the child process is still alive (monitor thread only)
|
||||
#ifdef _WIN32
|
||||
if (process_handle_) {
|
||||
DWORD exitCode = 0;
|
||||
if (GetExitCodeProcess(process_handle_, &exitCode) && exitCode != STILL_ACTIVE) {
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: process exited (code %lu)\n", exitCode);
|
||||
state_ = State::Error;
|
||||
last_error_ = "xmrig process exited unexpectedly";
|
||||
break;
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (process_pid_ > 0) {
|
||||
int status = 0;
|
||||
pid_t ret = waitpid(process_pid_, &status, WNOHANG);
|
||||
if (ret == process_pid_ || ret < 0) {
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: process exited (waitpid=%d)\n", ret);
|
||||
state_ = State::Error;
|
||||
last_error_ = "xmrig process exited unexpectedly";
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Poll HTTP stats every ~2 seconds (20 * 100ms)
|
||||
if (++poll_counter >= 20) {
|
||||
poll_counter = 0;
|
||||
fetchStatsHttp();
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
drainOutput(); // Final drain
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Stats polling via HTTP API
|
||||
// ============================================================================
|
||||
|
||||
void XmrigManager::pollStats() {
|
||||
// No-op on UI thread — stats are fetched by the monitor thread.
|
||||
// Just drain stdout so log lines stay fresh.
|
||||
drainOutput();
|
||||
}
|
||||
|
||||
void XmrigManager::fetchStatsHttp() {
|
||||
if (state_ != State::Running) return;
|
||||
|
||||
// Drain stdout while we're at it
|
||||
drainOutput();
|
||||
|
||||
// Build URL
|
||||
char url[256];
|
||||
snprintf(url, sizeof(url), "http://127.0.0.1:%d/2/summary", api_port_);
|
||||
|
||||
std::string responseData;
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
DEBUG_LOGF("[WARN] XmrigManager::pollStats: curl_easy_init failed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up auth header
|
||||
std::string authHeader = "Authorization: Bearer " + api_token_;
|
||||
struct curl_slist* headers = nullptr;
|
||||
headers = curl_slist_append(headers, authHeader.c_str());
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 2000L); // 2s timeout — generous for loaded system
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 1000L); // 1s connect timeout
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); // Avoid signal issues in threads
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
static int s_fail_count = 0;
|
||||
if (++s_fail_count <= 5 || s_fail_count % 30 == 0) {
|
||||
DEBUG_LOGF("[WARN] XmrigManager::pollStats: curl failed (%d): %s url=%s\n",
|
||||
s_fail_count, curl_easy_strerror(res), url);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
json resp = json::parse(responseData);
|
||||
|
||||
std::lock_guard<std::mutex> lk(stats_mutex_);
|
||||
|
||||
if (resp.contains("hashrate") && resp["hashrate"].contains("total")) {
|
||||
auto& total = resp["hashrate"]["total"];
|
||||
if (total.is_array() && total.size() >= 3) {
|
||||
stats_.hashrate_10s = total[0].is_null() ? 0.0 : total[0].get<double>();
|
||||
stats_.hashrate_60s = total[1].is_null() ? 0.0 : total[1].get<double>();
|
||||
stats_.hashrate_15m = total[2].is_null() ? 0.0 : total[2].get<double>();
|
||||
}
|
||||
}
|
||||
|
||||
if (resp.contains("connection")) {
|
||||
auto& conn = resp["connection"];
|
||||
stats_.accepted = conn.value("accepted", (int64_t)0);
|
||||
stats_.rejected = conn.value("rejected", (int64_t)0);
|
||||
stats_.uptime_sec = conn.value("uptime", (int64_t)0);
|
||||
stats_.pool_diff = conn.value("diff", 0.0);
|
||||
stats_.pool_url = conn.value("pool", std::string{});
|
||||
stats_.algo = conn.value("algo", std::string{});
|
||||
stats_.connected = (stats_.uptime_sec > 0);
|
||||
}
|
||||
|
||||
// Parse memory usage from "resources" section
|
||||
if (resp.contains("resources") && resp["resources"].contains("memory")) {
|
||||
auto& mem = resp["resources"]["memory"];
|
||||
stats_.memory_free = mem.value("free", (int64_t)0);
|
||||
stats_.memory_total = mem.value("total", (int64_t)0);
|
||||
stats_.memory_used = mem.value("resident_set_memory", (int64_t)0);
|
||||
}
|
||||
|
||||
// Parse active thread count from hashrate.threads array
|
||||
if (resp.contains("hashrate") && resp["hashrate"].contains("threads")) {
|
||||
auto& threads = resp["hashrate"]["threads"];
|
||||
if (threads.is_array()) {
|
||||
stats_.threads_active = static_cast<int>(threads.size());
|
||||
}
|
||||
} else if (resp.contains("cpu") && resp["cpu"].contains("threads")) {
|
||||
// Fallback: get from cpu section
|
||||
stats_.threads_active = resp["cpu"].value("threads", 0);
|
||||
}
|
||||
} catch (...) {
|
||||
// Malformed JSON — ignore, retry next poll
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
169
src/daemon/xmrig_manager.h
Normal file
169
src/daemon/xmrig_manager.h
Normal file
@@ -0,0 +1,169 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
|
||||
/**
|
||||
* @brief Manages the xmrig pool mining process
|
||||
*
|
||||
* Handles starting/stopping xmrig for pool mining, polling stats via its
|
||||
* HTTP API, and capturing stdout for the log panel. Modelled on
|
||||
* EmbeddedDaemon (same fork/pipe/monitor pattern, same #ifdef split).
|
||||
*/
|
||||
class XmrigManager {
|
||||
public:
|
||||
enum class State { Stopped, Starting, Running, Stopping, Error };
|
||||
|
||||
/// Live stats polled from xmrig HTTP API (/2/summary)
|
||||
struct PoolStats {
|
||||
double hashrate_10s = 0;
|
||||
double hashrate_60s = 0;
|
||||
double hashrate_15m = 0;
|
||||
int64_t accepted = 0;
|
||||
int64_t rejected = 0;
|
||||
int64_t uptime_sec = 0;
|
||||
double pool_diff = 0;
|
||||
std::string pool_url;
|
||||
std::string algo;
|
||||
bool connected = false;
|
||||
// Memory usage
|
||||
int64_t memory_free = 0; // bytes
|
||||
int64_t memory_total = 0; // bytes
|
||||
int64_t memory_used = 0; // bytes (resident set size)
|
||||
int threads_active = 0; // actual mining threads
|
||||
};
|
||||
|
||||
/// User-facing config (maps 1:1 to UI fields / Settings)
|
||||
struct Config {
|
||||
std::string pool_url = "pool.dragonx.is";
|
||||
std::string wallet_address;
|
||||
std::string worker_name = "x";
|
||||
std::string algo = "rx/hush";
|
||||
int threads = 0; // 0 = xmrig auto
|
||||
bool tls = false;
|
||||
bool hugepages = true;
|
||||
};
|
||||
|
||||
XmrigManager();
|
||||
~XmrigManager();
|
||||
|
||||
// Non-copyable
|
||||
XmrigManager(const XmrigManager&) = delete;
|
||||
XmrigManager& operator=(const XmrigManager&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Start xmrig with the given config.
|
||||
* Generates a JSON config file, finds the binary, and spawns the process.
|
||||
*/
|
||||
bool start(const Config& cfg);
|
||||
|
||||
/**
|
||||
* @brief Stop xmrig gracefully (SIGTERM → wait → SIGKILL).
|
||||
*/
|
||||
void stop(int wait_ms = 5000);
|
||||
|
||||
bool isRunning() const;
|
||||
State getState() const { return state_.load(std::memory_order_relaxed); }
|
||||
|
||||
const PoolStats& getStats() const { return stats_; }
|
||||
const std::string& getLastError() const { return last_error_; }
|
||||
|
||||
/**
|
||||
* @brief Get last N lines of xmrig stdout (thread-safe snapshot).
|
||||
*/
|
||||
std::vector<std::string> getRecentLines(int maxLines = 30) const;
|
||||
|
||||
/**
|
||||
* @brief Get new output since a given offset (thread-safe).
|
||||
* Returns the new text and updates offset to the current size.
|
||||
*/
|
||||
std::string getOutputSince(size_t& offset) const {
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
if (offset >= process_output_.size()) {
|
||||
offset = process_output_.size();
|
||||
return {};
|
||||
}
|
||||
std::string result = process_output_.substr(offset);
|
||||
offset = process_output_.size();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get current output size (thread-safe, no copy)
|
||||
*/
|
||||
size_t getOutputSize() const {
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
return process_output_.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Poll the xmrig HTTP API for live stats.
|
||||
* Lightweight — reads cached stats updated by the monitor thread.
|
||||
* Called from App::update() every ~2 s while running.
|
||||
*/
|
||||
void pollStats();
|
||||
|
||||
/**
|
||||
* @brief Get xmrig process memory usage in MB (from OS, not API).
|
||||
*/
|
||||
double getMemoryUsageMB() const;
|
||||
|
||||
/**
|
||||
* @brief Find xmrig binary in standard locations.
|
||||
*/
|
||||
static std::string findXmrigBinary();
|
||||
|
||||
private:
|
||||
bool generateConfig(const Config& cfg, const std::string& outPath);
|
||||
bool startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads);
|
||||
void monitorProcess();
|
||||
void drainOutput();
|
||||
void appendOutput(const char* data, size_t len);
|
||||
void fetchStatsHttp(); // Blocking HTTP call — runs on monitor thread only
|
||||
|
||||
std::atomic<State> state_{State::Stopped};
|
||||
std::string last_error_;
|
||||
|
||||
mutable std::mutex output_mutex_;
|
||||
std::string process_output_;
|
||||
|
||||
// xmrig HTTP API credentials (random per session)
|
||||
int api_port_ = 0;
|
||||
std::string api_token_;
|
||||
int threads_ = 0; // Thread count for mining
|
||||
PoolStats stats_;
|
||||
mutable std::mutex stats_mutex_;
|
||||
|
||||
// Process handles
|
||||
#ifdef _WIN32
|
||||
HANDLE process_handle_ = nullptr;
|
||||
HANDLE stdout_read_ = nullptr;
|
||||
#else
|
||||
pid_t process_pid_ = 0;
|
||||
int stdout_fd_ = -1;
|
||||
#endif
|
||||
|
||||
std::thread monitor_thread_;
|
||||
std::atomic<bool> should_stop_{false};
|
||||
};
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
Reference in New Issue
Block a user