Files
ObsidianDragon/src/daemon/xmrig_manager.cpp
DanS 8d51f374cd docs: add copilot-instructions.md and file-level comments
- Create .github/copilot-instructions.md with project coding standards,
  architecture overview, threading model, and key rules for AI sessions
- Add module description comments to app.cpp, rpc_client.cpp, rpc_worker.cpp,
  embedded_daemon.cpp, xmrig_manager.cpp, console_tab.cpp, settings.cpp
- Add ASCII connection state diagram to app_network.cpp
- Remove /.github/ from .gitignore so instructions file is tracked
2026-04-04 11:14:31 -05:00

814 lines
25 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// xmrig_manager.cpp — Pool mining process management via xmrig-hac.
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.
#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{};
// Extract pool hostname for stats API queries
{
std::string url = cfg.pool_url;
// Strip protocol prefix if present
auto pos = url.find("://");
if (pos != std::string::npos) url = url.substr(pos + 3);
// Strip port suffix
pos = url.find(':');
if (pos != std::string::npos) url = url.substr(0, pos);
pool_host_ = url;
}
// 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]);
// Detach from controlling terminal's stdin to prevent SIGTTIN/SIGTTOU
// when running in a new process group (setpgid below).
int devnull = open("/dev/null", O_RDONLY);
if (devnull >= 0) {
dup2(devnull, STDIN_FILENO);
close(devnull);
}
// Ignore job-control signals that a background process group may receive
signal(SIGTTIN, SIG_IGN);
signal(SIGTTOU, SIG_IGN);
// 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;
#ifdef __APPLE__
// macOS: use ps to read RSS for xmrig process
char cmd[128];
snprintf(cmd, sizeof(cmd), "ps -o rss= -p %d 2>/dev/null", process_pid_);
FILE* fp = popen(cmd, "r");
if (!fp) return 0.0;
char line[64];
double mb = 0.0;
if (fgets(line, sizeof(line), fp)) {
long rss_kb = atol(line);
if (rss_kb > 0) mb = static_cast<double>(rss_kb) / 1024.0;
}
pclose(fp);
return mb;
#else
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);
#endif
}
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;
int pool_api_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();
}
// Poll pool-side stats every ~30 seconds (300 * 100ms)
if (++pool_api_counter >= 300) {
pool_api_counter = 0;
fetchPoolApiStats();
}
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
}
}
// ============================================================================
// Pool-side stats (hashrate reported by the pool)
// ============================================================================
void XmrigManager::fetchPoolApiStats() {
if (state_ != State::Running || pool_host_.empty()) return;
// Query the pool's public stats API
std::string url = "https://" + pool_host_ + "/api/stats";
std::string responseData;
CURL* curl = curl_easy_init();
if (!curl) return;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 5000L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res != CURLE_OK) return;
try {
json resp = json::parse(responseData);
// Pool stats API format: { "pools": { "<name>": { "hashrate": ... } } }
double poolHR = 0;
if (resp.contains("pools") && resp["pools"].is_object()) {
for (auto& [key, pool] : resp["pools"].items()) {
if (pool.contains("hashrate") && pool["hashrate"].is_number()) {
poolHR = pool["hashrate"].get<double>();
break; // Use the first pool entry
}
}
}
std::lock_guard<std::mutex> lk(stats_mutex_);
stats_.pool_hashrate = poolHR;
} catch (...) {
// Malformed response — ignore
}
}
} // namespace daemon
} // namespace dragonx