Fixes all 22 confirmed findings from the mining-tab audit (10 Medium, 12 Low; 0 Critical/High), adversarially reviewed (6 follow-ups found + fixed, incl. the review-caught idle-auto-start bypass and a wrong benchmark-restore condition). Crash-safety & lifecycle: - M-04: join a stale/finished monitor thread in XmrigManager::start() and ~XmrigManager so an xmrig crash-then-restart (or quit) no longer std::terminate()s the wallet. - L-03/L-10: surface an unexpected miner exit once and clear the stale running flag. UI never blocks (M-03/L-06/L-08/L-09/L-13): pool start/stop now run on a dedicated serialized FIFO mining-control thread (joined before teardown), so the ~13 call sites don't block the render thread on stop()'s SIGTERM->SIGKILL->join; the spawn result marshals back to the UI. Miner-process / pool trust boundary: - M-01: validate the payout address (util::isValidRecipientAddress) at EVERY start path — the UI gate AND App::startPoolMining() (idle auto-start / thread scaling) — so a stale/wrong-chain address can't silently lose rewards. - M-09: SSRF guard skips the background pool-stats GET for loopback/private/link-local/single-label hosts. - M-02/L-02: cap the pool-stats + xmrig-API HTTP response bodies. - L-01: write the xmrig config 0600 at creation (POSIX open with mode) — no world/group-readable window. - M-10: reject shell-metacharacter binary paths before the version popen (excluding '()' so Program Files (x86) still works). Solo mining: M-06/M-08 clamp thread count to [1, cores] at the setgenerate/xmrig boundary; M-07 notify + don't lie on stop failure. Correctness: L-05 block-time constant 75->150s (chainparams); M-05 discloses pool-mode "Est. Daily" as a rough solo-equivalent; L-04/L-11/L-12 benchmark lifecycle (cancel on nav-away / mode-switch with restore, skip rebalance mid-benchmark); L-07 honor cancel mid-extract in both the xmrig and daemon updaters. Two new i18n keys back-filled across all 8 languages; CJK subset font rebuilt for the new glyphs. Verified across full-node, lite, and Windows builds; tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
962 lines
33 KiB
C++
962 lines
33 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
//
|
|
// xmrig_manager.cpp — Pool mining process management via drg-xmrig.
|
|
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.
|
|
|
|
#include "xmrig_manager.h"
|
|
#include "../resources/embedded_resources.h"
|
|
|
|
#include <cctype>
|
|
#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"
|
|
#include "../util/pool_registry.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);
|
|
const size_t add = sz * n;
|
|
// Stats JSON (local xmrig HTTP API + pool API) is tiny; refuse an unbounded body from a hostile or
|
|
// MITM'd endpoint so it can't grow this string until OOM. Returning < add aborts the transfer. (L-02)
|
|
constexpr size_t kMaxStatsBytes = 1u << 20; // 1 MiB
|
|
if (s->size() + add > kMaxStatsBytes) return 0;
|
|
s->append(static_cast<char*>(ptr), add);
|
|
return add;
|
|
}
|
|
|
|
// True if `host` (already stripped of scheme+port) is a loopback/private/link-local/single-label target
|
|
// that a public mining pool would never be — used to refuse a background stats GET to it (M-09).
|
|
static bool hostLooksInternal(const std::string& host) {
|
|
if (host.empty() || host == "localhost") return true;
|
|
if (host.rfind("127.", 0) == 0 || host.rfind("10.", 0) == 0 ||
|
|
host.rfind("192.168.", 0) == 0 || host.rfind("169.254.", 0) == 0) return true;
|
|
if (host.rfind("172.", 0) == 0) { // 172.16.0.0 - 172.31.255.255
|
|
const int second = std::atoi(host.c_str() + 4);
|
|
if (second >= 16 && second <= 31) return true;
|
|
}
|
|
if (host.find(':') != std::string::npos) { // IPv6 literal: loopback / ULA / link-local
|
|
if (host == "::1" || host.rfind("fc", 0) == 0 || host.rfind("fd", 0) == 0 ||
|
|
host.rfind("fe80", 0) == 0) return true;
|
|
}
|
|
if (host.size() >= 6 && host.compare(host.size() - 6, 6, ".local") == 0) return true;
|
|
if (host.find('.') == std::string::npos) return true; // bare single-label name = LAN/hosts, not a pool
|
|
return false;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Lifecycle
|
|
// ============================================================================
|
|
|
|
XmrigManager::XmrigManager() = default;
|
|
|
|
XmrigManager::~XmrigManager() {
|
|
should_stop_ = true;
|
|
if (isRunning()) {
|
|
stop(3000);
|
|
}
|
|
// Join a monitor thread left joinable by an unexpected xmrig exit (State::Error, so isRunning() is
|
|
// false and stop() above was skipped) — std::thread's destructor would otherwise std::terminate(). (M-04)
|
|
if (monitor_thread_.joinable())
|
|
monitor_thread_.join();
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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());
|
|
const std::string dumped = j.dump(4);
|
|
#ifndef _WIN32
|
|
// Create the config 0600 AT CREATION (open with mode) so the API token + wallet address are never
|
|
// in a world/group-readable file — even for a local attacker who opened it in the old
|
|
// create-then-chmod window and held the fd open across the chmod. (L-01)
|
|
int fd = ::open(outPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
|
if (fd < 0) {
|
|
setLastError("Cannot write xmrig config: " + outPath);
|
|
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
|
return false;
|
|
}
|
|
size_t off = 0;
|
|
bool wrote = true;
|
|
while (off < dumped.size()) {
|
|
ssize_t nw = ::write(fd, dumped.data() + off, dumped.size() - off);
|
|
if (nw <= 0) { wrote = false; break; }
|
|
off += static_cast<size_t>(nw);
|
|
}
|
|
::close(fd);
|
|
if (!wrote) {
|
|
setLastError("Cannot write xmrig config: " + outPath);
|
|
return false;
|
|
}
|
|
return true;
|
|
#else
|
|
std::ofstream ofs(outPath, std::ios::trunc);
|
|
if (!ofs.is_open()) {
|
|
setLastError("Cannot write xmrig config: " + outPath);
|
|
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
|
return false;
|
|
}
|
|
ofs << dumped;
|
|
ofs.close();
|
|
return true;
|
|
#endif
|
|
} catch (const std::exception& e) {
|
|
setLastError(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) {
|
|
setLastError("Already running");
|
|
DEBUG_LOGF("[WARN] XmrigManager: %s\n", last_error_.c_str());
|
|
return false;
|
|
}
|
|
|
|
state_ = State::Starting;
|
|
should_stop_ = false;
|
|
setLastError(std::string());
|
|
{
|
|
std::lock_guard<std::mutex> lk(output_mutex_);
|
|
process_output_.clear();
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> lk(stats_mutex_);
|
|
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()) {
|
|
setLastError("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;
|
|
}
|
|
|
|
// Join a prior monitor thread before move-assigning: if xmrig exited unexpectedly, monitorProcess set
|
|
// State::Error and returned, leaving monitor_thread_ joinable — move-assigning over a joinable
|
|
// std::thread calls std::terminate() and aborts the whole wallet. (M-04)
|
|
if (monitor_thread_.joinable())
|
|
monitor_thread_.join();
|
|
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)) {
|
|
setLastError("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);
|
|
setLastError("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) {
|
|
setLastError("pipe() failed");
|
|
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
|
return false;
|
|
}
|
|
|
|
pid_t pid = fork();
|
|
if (pid < 0) {
|
|
setLastError("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;
|
|
setLastError("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;
|
|
setLastError("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_);
|
|
|
|
// Miner version (top-level in /2/summary) — lets the UI show the actually
|
|
// running miner's version even when no release tag was persisted (bundled miner).
|
|
if (resp.contains("version") && resp["version"].is_string())
|
|
stats_.version = resp["version"].get<std::string>();
|
|
|
|
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;
|
|
|
|
// Resolve the stats endpoint + JSON schema for this pool. Known pools carry their
|
|
// own API shape (pool.dragonx.is = custom /api/stats; pool.dragonx.cc = Miningcore
|
|
// /api/pools); unknown/custom hosts fall back to the .is convention.
|
|
const util::KnownPool* known = util::findKnownPoolByUrl(pool_host_);
|
|
// SSRF guard: for an UNKNOWN (user-typed) pool host, don't let the wallet issue a background GET to a
|
|
// loopback/private/link-local/single-label target — those aren't public mining pools, and a
|
|
// paste-a-pool-config lure could otherwise point us at an internal host. Known pools use their trusted
|
|
// registry statsUrl and are exempt. (M-09)
|
|
if (!known && hostLooksInternal(pool_host_)) return;
|
|
const std::string url = known ? known->statsUrl
|
|
: ("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);
|
|
// pool.dragonx.cc sits behind Cloudflare and 403s odd User-Agents.
|
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; ObsidianDragon)");
|
|
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
|
|
|
|
CURLcode res = curl_easy_perform(curl);
|
|
curl_easy_cleanup(curl);
|
|
if (res != CURLE_OK) return;
|
|
|
|
bool ok = false;
|
|
const double poolHR = util::parsePoolHashrate(
|
|
known ? known->schema : util::PoolStatsSchema::DragonXIs,
|
|
responseData, known ? known->miningcorePoolId : std::string{}, ok);
|
|
if (!ok) return;
|
|
|
|
std::lock_guard<std::mutex> lk(stats_mutex_);
|
|
stats_.pool_hashrate = poolHR;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Installed-miner version detection (`<binary> --version`, cached)
|
|
// ============================================================================
|
|
|
|
namespace {
|
|
std::mutex g_installed_ver_mutex;
|
|
std::string g_installed_ver;
|
|
std::atomic<bool> g_ver_detect_started{false};
|
|
|
|
// Extract the first "D.D[.D...]" version token from `--version` output (skips the
|
|
// build date, which uses '-' separators). Returns e.g. "6.21.0", or "" if none.
|
|
std::string parseMinerVersion(const std::string& out)
|
|
{
|
|
for (size_t i = 0; i < out.size(); ++i) {
|
|
if (std::isdigit(static_cast<unsigned char>(out[i]))) {
|
|
size_t j = i;
|
|
int dots = 0;
|
|
while (j < out.size() &&
|
|
(std::isdigit(static_cast<unsigned char>(out[j])) || out[j] == '.')) {
|
|
if (out[j] == '.') ++dots;
|
|
++j;
|
|
}
|
|
if (dots >= 1 && (j - i) >= 3) {
|
|
// Include a trailing build suffix like "-hac" / "-drg1" (e.g. "6.25.1-hac"),
|
|
// matching what the running miner's API reports.
|
|
size_t end = j;
|
|
if (end < out.size() && out[end] == '-') {
|
|
size_t k = end + 1;
|
|
while (k < out.size() && std::isalnum(static_cast<unsigned char>(out[k]))) ++k;
|
|
if (k > end + 1) end = k;
|
|
}
|
|
return out.substr(i, end - i);
|
|
}
|
|
i = j;
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
} // namespace
|
|
|
|
void XmrigManager::startVersionDetection()
|
|
{
|
|
if (g_ver_detect_started.exchange(true)) return; // one-shot
|
|
std::thread([]() {
|
|
const std::string bin = findXmrigBinary();
|
|
std::string ver;
|
|
// Don't hand a path containing shell/cmd metacharacters to popen()'s shell — bin is normally an
|
|
// app-controlled path, but this closes command injection if it ever isn't. (M-10)
|
|
// Reject only chars that stay shell-special INSIDE the double-quotes we wrap bin in ("\"" + bin + "\"")
|
|
// on cmd.exe or /bin/sh. Parens are inert when quoted, so they're excluded — otherwise common Windows
|
|
// paths like "C:\Program Files (x86)\..." would be rejected and version detection would silently fail. (M-10)
|
|
const bool binShellSafe =
|
|
!bin.empty() && bin.find_first_of("\"'`$;&|<>^%\n\r") == std::string::npos;
|
|
if (binShellSafe) {
|
|
const std::string cmd = "\"" + bin + "\" --version 2>&1";
|
|
#ifdef _WIN32
|
|
FILE* fp = _popen(cmd.c_str(), "r");
|
|
#else
|
|
FILE* fp = popen(cmd.c_str(), "r");
|
|
#endif
|
|
if (fp) {
|
|
std::string out;
|
|
char buf[256];
|
|
size_t n;
|
|
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) out.append(buf, n);
|
|
#ifdef _WIN32
|
|
_pclose(fp);
|
|
#else
|
|
pclose(fp);
|
|
#endif
|
|
ver = parseMinerVersion(out);
|
|
}
|
|
}
|
|
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
|
|
g_installed_ver = ver;
|
|
}).detach();
|
|
}
|
|
|
|
std::string XmrigManager::installedVersion()
|
|
{
|
|
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
|
|
return g_installed_ver;
|
|
}
|
|
|
|
} // namespace daemon
|
|
} // namespace dragonx
|