// 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 #include #include #include #include #include #include #include #include #include #include #include "../util/logger.h" #ifdef _WIN32 #include #include #include #include #else #include #include #include #include #include #include #include #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 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 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(userdata); s->append(static_cast(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 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(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(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 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(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(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(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 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 XmrigManager::getRecentLines(int maxLines) const { std::lock_guard lk(output_mutex_); std::vector 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 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(); stats_.hashrate_60s = total[1].is_null() ? 0.0 : total[1].get(); stats_.hashrate_15m = total[2].is_null() ? 0.0 : total[2].get(); } } 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(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": { "": { "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(); break; // Use the first pool entry } } } std::lock_guard lk(stats_mutex_); stats_.pool_hashrate = poolHR; } catch (...) { // Malformed response — ignore } } } // namespace daemon } // namespace dragonx