Starting the daemon polls for an already-running node holding the datadir, sleeping up to three 100 ms intervals to let a shutting-down node clear out. When a start runs on the UI thread, that is up to 300 ms of blocking sleep on the render thread. - The lock-wait poll count is now a parameter. A start invoked on the UI thread passes 0, which does a single check and never sleeps; every other caller keeps the three-poll wait. The default is unchanged, so background starts behave exactly as before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2014 lines
81 KiB
C++
2014 lines
81 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
//
|
|
// embedded_daemon.cpp — Manages the dragonxd child process lifecycle:
|
|
// binary discovery, process spawn, stdout/stderr monitoring, crash recovery.
|
|
|
|
#include "embedded_daemon.h"
|
|
#include "daemon_busy_detect.h"
|
|
#include "../config/version.h"
|
|
#include "../resources/embedded_resources.h"
|
|
#include "../util/platform.h"
|
|
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <chrono>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <algorithm>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <vector>
|
|
|
|
#include "../util/logger.h"
|
|
|
|
#ifdef _WIN32
|
|
#include <winsock2.h>
|
|
#include <ws2tcpip.h>
|
|
#include <windows.h>
|
|
#include <psapi.h>
|
|
#include <tlhelp32.h>
|
|
#include <shlobj.h>
|
|
#include <iphlpapi.h>
|
|
#else
|
|
#include <unistd.h>
|
|
#include <signal.h>
|
|
#include <sys/wait.h>
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <errno.h>
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#ifdef __APPLE__
|
|
#include <libproc.h>
|
|
#include <sys/sysctl.h>
|
|
#endif
|
|
#endif
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace dragonx {
|
|
namespace daemon {
|
|
|
|
EmbeddedDaemon::EmbeddedDaemon() = default;
|
|
|
|
EmbeddedDaemon::~EmbeddedDaemon()
|
|
{
|
|
// Never stop() from here: a destructor must not kill a node. On Windows stop() also used to fall
|
|
// back to killing ANY dragonxd.exe by name, so destroying an object that had already stopped its own
|
|
// process (seed migration's isolated node) could take down the user's main node. Owners call stop()
|
|
// explicitly (the app's own daemon is never destroyed: main() ends in _Exit). Just stop watching and
|
|
// release what we hold; a process still running is left to finish on its own.
|
|
requestMonitorStop();
|
|
if (monitor_thread_.joinable()) {
|
|
monitor_thread_.join();
|
|
}
|
|
#ifdef _WIN32
|
|
if (process_handle_ != nullptr) {
|
|
CloseHandle(process_handle_);
|
|
process_handle_ = nullptr;
|
|
}
|
|
if (stdout_read_ != nullptr) {
|
|
CloseHandle(stdout_read_);
|
|
stdout_read_ = nullptr;
|
|
}
|
|
#else
|
|
if (stdout_fd_ >= 0) {
|
|
close(stdout_fd_);
|
|
stdout_fd_ = -1;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
std::string EmbeddedDaemon::findDaemonBinary()
|
|
{
|
|
// Get the directory where the wallet binary is located
|
|
std::string exe_dir;
|
|
#ifdef _WIN32
|
|
char exe_path[MAX_PATH];
|
|
GetModuleFileNameA(NULL, exe_path, MAX_PATH);
|
|
exe_dir = fs::path(exe_path).parent_path().string();
|
|
#elif defined(__APPLE__)
|
|
exe_dir = util::Platform::getExecutableDirectory();
|
|
#else
|
|
char exe_path[4096];
|
|
ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
|
|
if (len != -1) {
|
|
exe_path[len] = '\0';
|
|
exe_dir = fs::path(exe_path).parent_path().string();
|
|
}
|
|
#endif
|
|
|
|
// ---------------------------------------------------------------
|
|
// 1. Check wallet's own directory first — allows bundling binaries
|
|
// alongside the wallet executable (highest priority).
|
|
// ---------------------------------------------------------------
|
|
if (!exe_dir.empty()) {
|
|
#ifdef _WIN32
|
|
// Check wallet's own directory for manually placed binaries
|
|
std::vector<std::string> localPaths = {
|
|
exe_dir + "\\dragonxd.exe",
|
|
};
|
|
#else
|
|
std::vector<std::string> localPaths = {
|
|
exe_dir + "/dragonxd",
|
|
};
|
|
#endif
|
|
for (const auto& path : localPaths) {
|
|
if (fs::exists(path)) {
|
|
DEBUG_LOGF("[INFO] Found daemon in wallet directory: %s\n", path.c_str());
|
|
return path;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// 2. Check if we have an embedded daemon that needs extraction
|
|
// ---------------------------------------------------------------
|
|
std::string embeddedPath = resources::getDaemonPath();
|
|
if (!embeddedPath.empty() && fs::exists(embeddedPath)) {
|
|
DEBUG_LOGF("[INFO] Using extracted daemon at: %s\n", embeddedPath.c_str());
|
|
return embeddedPath;
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// 3. Search additional well-known locations
|
|
// ---------------------------------------------------------------
|
|
// IMPORTANT: Always prefer dragonxd.exe directly over .bat wrappers.
|
|
// Using .bat files causes issues because cmd.exe exits immediately
|
|
// while dragonxd.exe continues running, making process monitoring fail.
|
|
std::vector<std::string> search_paths;
|
|
|
|
#ifdef _WIN32
|
|
// Parent directory
|
|
if (!exe_dir.empty()) {
|
|
search_paths.push_back(exe_dir + "\\..\\dragonxd.exe");
|
|
}
|
|
search_paths.push_back("C:\\Program Files\\DragonX\\dragonxd.exe");
|
|
#else
|
|
if (!exe_dir.empty()) {
|
|
search_paths.push_back(exe_dir + "/../dragonxd");
|
|
}
|
|
|
|
// Standard Linux locations
|
|
search_paths.push_back("/usr/local/bin/dragonxd");
|
|
search_paths.push_back("/usr/bin/dragonxd");
|
|
|
|
// Home directory
|
|
const char* home = getenv("HOME");
|
|
if (home) {
|
|
search_paths.push_back(std::string(home) + "/dragonx/src/dragonxd");
|
|
search_paths.push_back(std::string(home) + "/bin/dragonxd");
|
|
}
|
|
|
|
#ifdef __APPLE__
|
|
// macOS app bundle
|
|
search_paths.push_back("/Applications/DragonX.app/Contents/MacOS/dragonxd");
|
|
#endif
|
|
#endif
|
|
|
|
// Check each path
|
|
for (const auto& path : search_paths) {
|
|
if (fs::exists(path)) {
|
|
DEBUG_LOGF("[INFO] Found daemon launcher at: %s\n", path.c_str());
|
|
return path;
|
|
}
|
|
}
|
|
|
|
DEBUG_LOGF("[ERROR] Daemon launcher not found in any standard location\n");
|
|
return "";
|
|
}
|
|
|
|
std::vector<std::string> EmbeddedDaemon::getChainParams()
|
|
{
|
|
// DragonX chain parameters.
|
|
// On Windows, omit -printtoconsole: we tail debug.log instead of piping stdout.
|
|
// On Linux, -printtoconsole is used for pipe-based output capture.
|
|
// Auto-detect a reasonable -dbcache based on available physical RAM.
|
|
// Default LevelDB cache is small (~450MB); larger caches improve sync
|
|
// performance and reduce disk I/O — especially on macOS with APFS.
|
|
std::string dbcache_arg = "-dbcache=450";
|
|
{
|
|
#ifdef __APPLE__
|
|
// sysctl hw.memsize returns total physical RAM in bytes
|
|
int64_t memsize = 0;
|
|
size_t len = sizeof(memsize);
|
|
if (sysctlbyname("hw.memsize", &memsize, &len, nullptr, 0) == 0 && memsize > 0) {
|
|
int totalMB = static_cast<int>(memsize / (1024 * 1024));
|
|
// Use ~12.5% of RAM for dbcache, clamped to [450, 4096]
|
|
int cache = std::max(450, std::min(4096, totalMB / 8));
|
|
dbcache_arg = "-dbcache=" + std::to_string(cache);
|
|
}
|
|
#elif defined(__linux__)
|
|
long pages = sysconf(_SC_PHYS_PAGES);
|
|
long page_size = sysconf(_SC_PAGE_SIZE);
|
|
if (pages > 0 && page_size > 0) {
|
|
int totalMB = static_cast<int>((static_cast<int64_t>(pages) * page_size) / (1024 * 1024));
|
|
int cache = std::max(450, std::min(4096, totalMB / 8));
|
|
dbcache_arg = "-dbcache=" + std::to_string(cache);
|
|
}
|
|
#elif defined(_WIN32)
|
|
MEMORYSTATUSEX memInfo;
|
|
memInfo.dwLength = sizeof(memInfo);
|
|
if (GlobalMemoryStatusEx(&memInfo)) {
|
|
int totalMB = static_cast<int>(memInfo.ullTotalPhys / (1024 * 1024));
|
|
int cache = std::max(450, std::min(4096, totalMB / 8));
|
|
dbcache_arg = "-dbcache=" + std::to_string(cache);
|
|
}
|
|
#endif
|
|
DEBUG_LOGF("[INFO] Using %s\n", dbcache_arg.c_str());
|
|
}
|
|
|
|
// No -printtoconsole on any platform: the node writes its timestamped <datadir>/debug.log, which we
|
|
// tail (and which keeps going if the node is left running after we quit); its stdout/stderr stay piped
|
|
// on POSIX for crash diagnostics.
|
|
return {
|
|
"-tls=only",
|
|
"-clientname=ObsidianDragon",
|
|
"-ac_name=DRAGONX",
|
|
"-ac_algo=randomx",
|
|
"-ac_halving=3500000",
|
|
"-ac_reward=300000000",
|
|
"-ac_blocktime=36",
|
|
"-ac_private=1",
|
|
// Seeds: seed.dragonx.is is a round-robin A record over the live seed set (self-updates without
|
|
// a wallet release), with node1/node5 as static fallbacks — mirrors the daemon's own vSeeds.
|
|
// Plain -addnode hostname resolution works on EVERY daemon version, and is load-bearing for
|
|
// pre-1.3.0 daemons whose built-in peer discovery was broken (they rely on these to find peers).
|
|
"-addnode=seed.dragonx.is",
|
|
"-addnode=node1.dragonx.is",
|
|
"-addnode=node5.dragonx.is",
|
|
"-experimentalfeatures",
|
|
"-developerencryptwallet",
|
|
// Create fresh wallets from a BIP39 mnemonic so their 24-word phrase can be
|
|
// exported (z_exportmnemonic) and is portable to SDXLite/ObsidianDragonLite.
|
|
// The daemon reads this ONLY inside GenerateNewSeed() when a wallet has no seed
|
|
// yet, so it is inert on existing wallets — safe to pass unconditionally.
|
|
"-usemnemonic=1",
|
|
dbcache_arg
|
|
};
|
|
}
|
|
|
|
void EmbeddedDaemon::requestMonitorStop()
|
|
{
|
|
// Take monitor_mutex_ so the flag cannot flip between the monitor's predicate check and its wait
|
|
// (lost wakeup). Lock order lifecycle_mutex_ -> monitor_mutex_ is deadlock-free: the monitor thread
|
|
// never takes lifecycle_mutex_.
|
|
{
|
|
std::lock_guard<std::mutex> lk(monitor_mutex_);
|
|
should_stop_ = true;
|
|
}
|
|
monitor_cv_.notify_all();
|
|
}
|
|
|
|
size_t EmbeddedDaemon::outputTotalSnapshot() const
|
|
{
|
|
std::lock_guard<std::mutex> lk(output_mutex_);
|
|
return output_total_;
|
|
}
|
|
|
|
void EmbeddedDaemon::setState(State s, const std::string& message)
|
|
{
|
|
// Message first, state second: a reader that sees Error must also see that error's message, not the
|
|
// previous one (the console prints the message when it sees the state change).
|
|
if (!message.empty() && s == State::Error) {
|
|
std::lock_guard<std::mutex> lk(error_mutex_); // dedicated mutex — never taken with output_mutex_ held
|
|
last_error_ = message;
|
|
}
|
|
state_ = s;
|
|
|
|
if (state_callback_) {
|
|
state_callback_(s, message);
|
|
}
|
|
}
|
|
|
|
// Cap process_output_ to prevent unbounded memory growth.
|
|
// Keeps the most recent MAX_OUTPUT_BYTES, trimming at a newline boundary.
|
|
static constexpr size_t MAX_OUTPUT_BYTES = 1024 * 1024; // 1 MB
|
|
|
|
void EmbeddedDaemon::appendOutput(const char* data, size_t len)
|
|
{
|
|
// Caller must hold output_mutex_
|
|
appendBounded(process_output_, output_total_, data, len, MAX_OUTPUT_BYTES);
|
|
}
|
|
|
|
std::vector<std::string> EmbeddedDaemon::getRecentLines(int maxLines) const
|
|
{
|
|
std::vector<std::string> lines;
|
|
std::lock_guard<std::mutex> lk(output_mutex_);
|
|
const std::string& out = process_output_;
|
|
if (out.empty()) return lines;
|
|
|
|
// Walk backwards collecting up to maxLines newline-delimited lines
|
|
size_t end = out.size();
|
|
// Skip trailing newline
|
|
if (end > 0 && out[end - 1] == '\n') --end;
|
|
|
|
for (int i = 0; i < maxLines && end > 0; ++i) {
|
|
size_t nl = out.rfind('\n', end - 1);
|
|
size_t start = (nl == std::string::npos) ? 0 : nl + 1;
|
|
std::string line = out.substr(start, end - start);
|
|
if (!line.empty()) lines.push_back(std::move(line));
|
|
end = (nl == std::string::npos) ? 0 : nl;
|
|
}
|
|
// Reverse so oldest is first
|
|
std::reverse(lines.begin(), lines.end());
|
|
return lines;
|
|
}
|
|
|
|
// Identify what process owns a given TCP port.
|
|
// Returns a string like "PID 1234 (dragonxd.exe)" or "unknown" on failure.
|
|
static std::string getPortOwnerInfo(int port)
|
|
{
|
|
#ifdef _WIN32
|
|
DWORD size = 0;
|
|
// First call to get required buffer size
|
|
GetExtendedTcpTable(nullptr, &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0);
|
|
if (size == 0) return "unknown (GetExtendedTcpTable size query failed)";
|
|
std::vector<BYTE> buf(size);
|
|
DWORD ret = GetExtendedTcpTable(buf.data(), &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0);
|
|
if (ret != NO_ERROR) return "unknown (GetExtendedTcpTable failed, error " + std::to_string(ret) + ")";
|
|
auto* table = reinterpret_cast<MIB_TCPTABLE_OWNER_PID*>(buf.data());
|
|
DWORD ownerPid = 0;
|
|
for (DWORD i = 0; i < table->dwNumEntries; i++) {
|
|
auto& row = table->table[i];
|
|
int rowPort = ntohs(static_cast<u_short>(row.dwLocalPort));
|
|
// Match port in LISTEN state (MIB_TCP_STATE_LISTEN = 2)
|
|
if (rowPort == port && row.dwState == MIB_TCP_STATE_LISTEN) {
|
|
ownerPid = row.dwOwningPid;
|
|
break;
|
|
}
|
|
}
|
|
if (ownerPid == 0) {
|
|
// Maybe it's in ESTABLISHED or another state from our connect probe — try any state
|
|
for (DWORD i = 0; i < table->dwNumEntries; i++) {
|
|
auto& row = table->table[i];
|
|
int rowPort = ntohs(static_cast<u_short>(row.dwLocalPort));
|
|
if (rowPort == port && row.dwOwningPid != 0) {
|
|
ownerPid = row.dwOwningPid;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (ownerPid == 0) return "unknown (no PID found for port " + std::to_string(port) + ")";
|
|
// Resolve PID to process name
|
|
std::string procName = "<unknown>";
|
|
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
|
if (snap != INVALID_HANDLE_VALUE) {
|
|
PROCESSENTRY32 pe;
|
|
pe.dwSize = sizeof(pe);
|
|
if (Process32First(snap, &pe)) {
|
|
do {
|
|
if (pe.th32ProcessID == ownerPid) {
|
|
procName = pe.szExeFile;
|
|
break;
|
|
}
|
|
} while (Process32Next(snap, &pe));
|
|
}
|
|
CloseHandle(snap);
|
|
}
|
|
return "PID " + std::to_string(ownerPid) + " (" + procName + ")";
|
|
#else
|
|
// Linux: parse /proc/net/tcp to find the inode, then scan /proc/*/fd
|
|
FILE* fp = fopen("/proc/net/tcp", "r");
|
|
if (!fp) return "unknown (cannot read /proc/net/tcp)";
|
|
char line[512];
|
|
unsigned long inode = 0;
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
unsigned int localPort, state;
|
|
unsigned long lineInode;
|
|
if (sscanf(line, " %*d: %*X:%X %*X:%*X %X %*X:%*X %*X:%*X %*X %*u %*u %lu",
|
|
&localPort, &state, &lineInode) == 3) {
|
|
if (static_cast<int>(localPort) == port && state == 0x0A) { // 0x0A = LISTEN
|
|
inode = lineInode;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
fclose(fp);
|
|
if (inode == 0) return "unknown (no listener found for port " + std::to_string(port) + ")";
|
|
// Scan /proc/*/fd/* for the matching inode
|
|
for (const auto& entry : fs::directory_iterator("/proc")) {
|
|
if (!entry.is_directory()) continue;
|
|
std::string pidStr = entry.path().filename().string();
|
|
if (pidStr.empty() || !std::isdigit(pidStr[0])) continue;
|
|
std::string fdDir = "/proc/" + pidStr + "/fd";
|
|
try {
|
|
for (const auto& fdEntry : fs::directory_iterator(fdDir)) {
|
|
char target[512];
|
|
ssize_t len = readlink(fdEntry.path().c_str(), target, sizeof(target) - 1);
|
|
if (len > 0) {
|
|
target[len] = '\0';
|
|
std::string t(target);
|
|
if (t.find("socket:[" + std::to_string(inode) + "]") != std::string::npos) {
|
|
// Found the PID, now get the process name
|
|
std::string commPath = "/proc/" + pidStr + "/comm";
|
|
FILE* cf = fopen(commPath.c_str(), "r");
|
|
std::string procName = "<unknown>";
|
|
if (cf) {
|
|
char name[256];
|
|
if (fgets(name, sizeof(name), cf)) {
|
|
procName = name;
|
|
while (!procName.empty() && procName.back() == '\n') procName.pop_back();
|
|
}
|
|
fclose(cf);
|
|
}
|
|
return "PID " + pidStr + " (" + procName + ")";
|
|
}
|
|
}
|
|
}
|
|
} catch (...) { /* permission denied — skip */ }
|
|
}
|
|
return "unknown (inode " + std::to_string(inode) + " found but no matching PID)";
|
|
#endif
|
|
}
|
|
|
|
// Check if a TCP port is already in use (something is LISTENING). The daemon binds BOTH 127.0.0.1 (IPv4)
|
|
// and ::1 (IPv6); during shutdown one can linger after the other releases (and a "Binding RPC on ::1 …
|
|
// failed" is fatal to a fresh start), so we treat the port as in use if EITHER localhost family has it.
|
|
static bool isPortInUse(int port)
|
|
{
|
|
#ifdef _WIN32
|
|
WSADATA wsa;
|
|
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false;
|
|
bool inUse = false;
|
|
{ // IPv4 127.0.0.1
|
|
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
if (sock != INVALID_SOCKET) {
|
|
struct sockaddr_in addr; memset(&addr, 0, sizeof(addr));
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons(static_cast<u_short>(port));
|
|
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
|
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) inUse = true;
|
|
closesocket(sock);
|
|
}
|
|
}
|
|
if (!inUse) { // IPv6 ::1
|
|
SOCKET sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
|
|
if (sock != INVALID_SOCKET) {
|
|
struct sockaddr_in6 addr; memset(&addr, 0, sizeof(addr));
|
|
addr.sin6_family = AF_INET6;
|
|
addr.sin6_port = htons(static_cast<u_short>(port));
|
|
addr.sin6_addr = in6addr_loopback; // ::1 — avoids inet_pton's _WIN32_WINNT gating on mingw
|
|
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) inUse = true;
|
|
closesocket(sock);
|
|
}
|
|
}
|
|
WSACleanup();
|
|
return inUse;
|
|
#else
|
|
// On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp{,6} to avoid creating sockets. The
|
|
// parse is family-agnostic: %*X skips the local IP (8 hex for v4, 32 for v6), %X grabs the port.
|
|
auto scanProc = [port](const char* path) -> bool {
|
|
FILE* fp = fopen(path, "r");
|
|
if (!fp) return false;
|
|
char line[512];
|
|
unsigned int localPort, state;
|
|
bool found = false;
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
if (sscanf(line, " %*d: %*X:%X %*X:%*X %X", &localPort, &state) == 2) {
|
|
if (localPort == static_cast<unsigned int>(port) && state == 0x0A) { found = true; break; }
|
|
}
|
|
}
|
|
fclose(fp);
|
|
return found;
|
|
};
|
|
if (FILE* probe = fopen("/proc/net/tcp", "r")) { // /proc available → authoritative LISTEN check
|
|
fclose(probe);
|
|
return scanProc("/proc/net/tcp") || scanProc("/proc/net/tcp6");
|
|
}
|
|
// Fallback (macOS): connect() probe on both loopback families.
|
|
auto connProbe = [port](int family, const char* addr) -> bool {
|
|
int sock = socket(family, SOCK_STREAM, 0);
|
|
if (sock < 0) return false;
|
|
bool ok = false;
|
|
if (family == AF_INET) {
|
|
struct sockaddr_in a; memset(&a, 0, sizeof(a));
|
|
a.sin_family = AF_INET; a.sin_port = htons(static_cast<uint16_t>(port));
|
|
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
ok = (connect(sock, (struct sockaddr*)&a, sizeof(a)) == 0);
|
|
} else {
|
|
struct sockaddr_in6 a; memset(&a, 0, sizeof(a));
|
|
a.sin6_family = AF_INET6; a.sin6_port = htons(static_cast<uint16_t>(port));
|
|
inet_pton(AF_INET6, addr, &a.sin6_addr);
|
|
ok = (connect(sock, (struct sockaddr*)&a, sizeof(a)) == 0);
|
|
}
|
|
close(sock);
|
|
return ok;
|
|
};
|
|
return connProbe(AF_INET, "127.0.0.1") || connProbe(AF_INET6, "::1");
|
|
#endif
|
|
}
|
|
|
|
bool EmbeddedDaemon::start(const std::string& binary_path, int lockWaitPolls)
|
|
{
|
|
std::lock_guard<std::mutex> life(lifecycle_mutex_);
|
|
|
|
// Don't start if already running or starting
|
|
if (state_ == State::Starting || state_ == State::Running) {
|
|
DEBUG_LOGF("[INFO] Daemon already %s\n", state_ == State::Starting ? "starting" : "running");
|
|
return true;
|
|
}
|
|
|
|
if (isRunning()) {
|
|
DEBUG_LOGF("[INFO] Daemon process already running\n");
|
|
return true;
|
|
}
|
|
|
|
// A stop session owns this object until endStop() sets Stopped; never spawn under it (its endStop would
|
|
// adopt and abandon our new pid, and it still uses stdout_fd_/process_handle_).
|
|
if (state_ == State::Stopping) {
|
|
DEBUG_LOGF("[INFO] start(): a stop is still in progress, not starting\n");
|
|
return false;
|
|
}
|
|
// A monitor that returned on its own (unexpected exit, or a requested stop that landed before stop()) is
|
|
// still joinable. Join it, then release what its run left behind. Only in this case: if a stop session
|
|
// already joined it, the fd/handle belong to that session and endStop() closes them.
|
|
if (monitor_thread_.joinable()) {
|
|
requestMonitorStop(); // a monitor somehow still looping wakes immediately via monitor_cv_
|
|
monitor_thread_.join(); // normally immediate (it already returned)
|
|
#ifdef _WIN32
|
|
if (process_handle_ != nullptr) { CloseHandle(process_handle_); process_handle_ = nullptr; }
|
|
if (stdout_read_ != nullptr) { CloseHandle(stdout_read_); stdout_read_ = nullptr; }
|
|
#else
|
|
if (stdout_fd_ >= 0) { close(stdout_fd_); stdout_fd_ = -1; } // also stops it leaking into the next fork
|
|
#endif
|
|
}
|
|
should_stop_ = false;
|
|
|
|
// Check if something is already listening on the RPC port. An isolated instance (migrate-to-
|
|
// seed) runs on its own non-default port alongside the main daemon, so it skips this bail.
|
|
int rpc_port = std::atoi(DRAGONX_DEFAULT_RPC_PORT);
|
|
if (!skip_port_check_ && isPortInUse(rpc_port)) {
|
|
std::string owner = getPortOwnerInfo(rpc_port);
|
|
VERBOSE_LOGF("[INFO] Port %d is already in use by %s — external daemon detected, will connect to it.\\n", rpc_port, owner.c_str());
|
|
external_daemon_detected_ = true;
|
|
// Don't set Error — the wallet will connect to the running daemon.
|
|
setState(State::Stopped, "External daemon detected on port " + std::string(DRAGONX_DEFAULT_RPC_PORT) + " (owned by " + owner + ")");
|
|
return false;
|
|
}
|
|
external_daemon_detected_ = false;
|
|
|
|
// A previous dragonxd can release the RPC port well before it releases the datadir
|
|
// .lock — a graceful shutdown can take up to ~90s (see isDaemonProcessRunning). Starting
|
|
// into a still-held lock spawns a process that dies instantly with "Cannot obtain a lock
|
|
// on data directory"; the crash monitor reports that generically and, three times in
|
|
// ~12s, that is enough to trip the 3-strike restart cap before the lock's ~90s life
|
|
// elapses. Gate on the process actually still being alive, with a SHORT bounded wait
|
|
// (not the full ~90s — start() runs on the UI thread). Isolated starts (migrate-to-seed:
|
|
// skip_port_check_ / -datadir override) are exempt; they run their own datadir+port.
|
|
{
|
|
constexpr int kDatadirLockWaitPollMs = 100;
|
|
bool stillRunning = false;
|
|
if (!skip_port_check_ && override_datadir_.empty()) {
|
|
stillRunning = isDaemonProcessRunning();
|
|
for (int i = 0; stillRunning && i < lockWaitPolls; ++i) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(kDatadirLockWaitPollMs));
|
|
stillRunning = isDaemonProcessRunning();
|
|
}
|
|
}
|
|
const StartLockGateDecision gate =
|
|
evaluateDatadirLockGate(skip_port_check_, !override_datadir_.empty(), stillRunning);
|
|
if (!gate.proceed) {
|
|
VERBOSE_LOGF("[INFO] %s\n", gate.errorMessage);
|
|
setState(State::Error, gate.errorMessage);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
setState(State::Starting, "Looking for dragonxd binary...");
|
|
|
|
std::string daemon_path = binary_path;
|
|
if (daemon_path.empty()) {
|
|
daemon_path = findDaemonBinary();
|
|
}
|
|
|
|
if (daemon_path.empty() || !fs::exists(daemon_path)) {
|
|
DEBUG_LOGF("[ERROR] dragonxd binary not found\\n");
|
|
setState(State::Error, "dragonxd binary not found.\n\nTo use embedded daemon, place dragonxd.exe (or dragonxd.bat) in the same directory as the wallet.\n\nAlternatively, start dragonxd manually and the wallet will connect to it.");
|
|
return false;
|
|
}
|
|
|
|
DEBUG_LOGF("[INFO] Starting dragonxd from: %s\\n", daemon_path.c_str());
|
|
|
|
auto args = getChainParams();
|
|
|
|
// Append debug logging flags from user settings
|
|
for (const auto& cat : debug_categories_) {
|
|
args.push_back("-debug=" + cat);
|
|
}
|
|
|
|
// Append max connections if configured (0 = daemon default)
|
|
if (max_connections_ > 0) {
|
|
args.push_back("-maxconnections=" + std::to_string(max_connections_));
|
|
}
|
|
|
|
// The user's own DRAGONX.conf wins over our flags (a command-line flag would override it): read the
|
|
// conf once, for a normal start only (an isolated start has its own datadir + conf and keeps the pin).
|
|
std::string conf;
|
|
if (override_datadir_.empty()) {
|
|
std::ifstream in((fs::path(util::Platform::getDragonXDataDir()) / "DRAGONX.conf").string(), std::ios::binary);
|
|
if (in) conf.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
|
if (confSetsKey(conf, "dbcache")) {
|
|
DEBUG_LOGF("[INFO] DRAGONX.conf sets dbcache; not pinning -dbcache\n");
|
|
}
|
|
dropPinnedDbcacheIfConfSets(args, conf);
|
|
}
|
|
|
|
// More RPC workers for a node we launch: dragonxd's default 8 workers + 16-deep queue fill up with
|
|
// requests waiting behind a long cs_main holder (a witness rebuild), after which even "stop" is refused.
|
|
// The GUI itself no longer floods it; this is headroom for dragonx-cli and other clients — a delay,
|
|
// not a fix. Not for an isolated start, and DRAGONX.conf wins (a flag would override the user's own).
|
|
if (override_datadir_.empty()) {
|
|
if (!confSetsKey(conf, "rpcthreads")) args.push_back("-rpcthreads=16");
|
|
}
|
|
|
|
// Host a RandomX stratum pool from this node (-stratum). Only v1.3.0+ daemons implement it; older
|
|
// ones ignore the unknown flag (no fatal arg check), and the Settings toggle is gated on daemon
|
|
// version, so this is only enabled against a daemon that supports it. Without -stratumallowip the
|
|
// daemon serves loopback only (safe default); a subnet opens it to that LAN.
|
|
if (stratum_enabled_) {
|
|
args.push_back("-stratum");
|
|
if (!stratum_allowip_.empty()) args.push_back("-stratumallowip=" + stratum_allowip_);
|
|
}
|
|
|
|
// Active wallet file (multi-wallet). The daemon loads <datadir>/<name>. Only pass it for a
|
|
// non-default name so the common case's command line is unchanged; skip during an isolated
|
|
// start (seed migration manages its own throwaway wallet).
|
|
if (!wallet_file_.empty() && wallet_file_ != "wallet.dat" && override_datadir_.empty()) {
|
|
DEBUG_LOGF("[INFO] Loading wallet file: %s\n", wallet_file_.c_str());
|
|
args.push_back("-wallet=" + wallet_file_);
|
|
}
|
|
|
|
// Add wallet-repair flag if requested (one-shot). Precedence: salvage > zap > rescan; each implies a
|
|
// rescan in the daemon, so we don't stack them.
|
|
if (salvage_on_next_start_.exchange(false)) {
|
|
// -salvagewallet recovers readable keypairs from a corrupt wallet.dat; the daemon then implies -rescan.
|
|
DEBUG_LOGF("[INFO] Adding -salvagewallet flag to recover a corrupt wallet\n");
|
|
args.push_back("-salvagewallet");
|
|
zap_on_next_start_.store(false);
|
|
rescan_on_next_start_.store(false);
|
|
} else if (zap_on_next_start_.exchange(false)) {
|
|
// -zapwallettxes=2 wipes all wallet tx/note records and rebuilds them from the chain (implies -rescan).
|
|
DEBUG_LOGF("[INFO] Adding -zapwallettxes=2 flag for wallet repair (zap & rebuild)\n");
|
|
args.push_back("-zapwallettxes=2");
|
|
rescan_on_next_start_.store(false); // implied by zap; avoid redundant -rescan
|
|
} else if (rescan_on_next_start_.exchange(false)) {
|
|
// Add -rescan flag if requested (one-shot)
|
|
DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");
|
|
args.push_back("-rescan");
|
|
}
|
|
|
|
// -reindex rebuilds the block index + chainstate from the raw blocks (fixes an unreadable/format-
|
|
// mismatched block DB). It's about the CHAIN, not the wallet, so it's independent of the wallet-repair
|
|
// chain above (and implies its own wallet rescan). One-shot, consumed here.
|
|
if (reindex_on_next_start_.exchange(false)) {
|
|
DEBUG_LOGF("[INFO] Adding -reindex flag to rebuild the block database from raw blocks\n");
|
|
args.push_back("-reindex");
|
|
}
|
|
|
|
// One-shot isolated-datadir override (migrate-to-seed flow): run this start against a
|
|
// throwaway datadir, plus any extra args (e.g. -connect=0). Consumed here so later starts
|
|
// revert to the normal datadir. The datadir's basename MUST be the assetchain name (DRAGONX)
|
|
// or the daemon mis-resolves its conf/port; it reads <datadir>/DRAGONX.conf automatically, so
|
|
// no -conf is passed (an explicit -conf confuses the Komodo/Hush path resolution).
|
|
if (!override_datadir_.empty()) {
|
|
DEBUG_LOGF("[INFO] Isolated start override: -datadir=%s\n", override_datadir_.c_str());
|
|
args.push_back("-datadir=" + override_datadir_);
|
|
}
|
|
for (const auto& a : override_extra_args_) args.push_back(a);
|
|
const bool isolated = !override_datadir_.empty();
|
|
// Tail this node's debug.log — in the isolated datadir for an isolated start — from its current end.
|
|
{
|
|
debug_log_path_ = (fs::path(isolated ? override_datadir_ : util::Platform::getDragonXDataDir()) /
|
|
"debug.log").string();
|
|
std::error_code ec;
|
|
const auto size = fs::file_size(debug_log_path_, ec);
|
|
debug_log_offset_ = ec ? 0 : static_cast<size_t>(size);
|
|
launch_utc_ = static_cast<std::int64_t>(std::time(nullptr));
|
|
DEBUG_LOGF("[INFO] Will tail daemon output from: %s (offset %zu)\n", debug_log_path_.c_str(),
|
|
debug_log_offset_);
|
|
}
|
|
override_datadir_.clear();
|
|
override_extra_args_.clear();
|
|
|
|
stop_requested_ = false; // a new process: its exits are crashes until a stop is asked for
|
|
if (!startProcess(daemon_path, args)) {
|
|
// startProcess() sets a precise last_error_ (e.g. "dragonxd could not be executed:
|
|
// ... not executable or wrong architecture"). Surface THAT via setState — which also
|
|
// stores the Error message into last_error_ — instead of clobbering it with a generic
|
|
// string that would then be all getLastError()/the UI ever sees.
|
|
std::string detail = last_error_.empty() ? std::string("Failed to start dragonxd process")
|
|
: last_error_;
|
|
DEBUG_LOGF("[ERROR] %s\n", detail.c_str());
|
|
setState(State::Error, detail);
|
|
return false;
|
|
}
|
|
|
|
setState(State::Running, "dragonxd started");
|
|
DEBUG_LOGF("[INFO] dragonxd process started successfully\\n");
|
|
|
|
// Record it as ours, so a later session recognises it if it is left running (main datadir only).
|
|
if (!isolated) {
|
|
#ifdef _WIN32
|
|
const long pid = process_handle_ ? static_cast<long>(GetProcessId(process_handle_)) : 0;
|
|
#else
|
|
const long pid = static_cast<long>(process_pid_);
|
|
#endif
|
|
OwnershipRecord rec;
|
|
rec.pid = pid;
|
|
rec.startToken = processStartToken(pid);
|
|
rec.exePath = daemon_path;
|
|
if (pid > 0 && !rec.startToken.empty()) writeOwnershipRecord(ownershipRecordPath(), rec);
|
|
}
|
|
|
|
// Supervise the process just launched. A monitor left over from an earlier run was joined above.
|
|
should_stop_ = false;
|
|
if (monitor_thread_.joinable()) { // unreachable under lifecycle_mutex_; never join a live monitor unsignalled
|
|
requestMonitorStop(); monitor_thread_.join(); should_stop_ = false;
|
|
}
|
|
monitor_thread_ = std::thread(&EmbeddedDaemon::monitorProcess, this);
|
|
|
|
return true;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
|
|
// Forward declaration — defined after startProcess
|
|
static DWORD findProcessByName(const char* name);
|
|
|
|
// Quote a single argument per the CommandLineToArgvW rules (MSDN) so a value containing a space or a
|
|
// quote is delivered as ONE argv token to the daemon instead of splitting/corrupting argv (L-02).
|
|
static std::string quoteWinArg(const std::string& arg) {
|
|
if (!arg.empty() && arg.find_first_of(" \t\n\v\"") == std::string::npos) return arg;
|
|
std::string out = "\"";
|
|
for (size_t i = 0; ; ++i) {
|
|
size_t nbs = 0;
|
|
while (i < arg.size() && arg[i] == '\\') { ++nbs; ++i; }
|
|
if (i == arg.size()) { out.append(nbs * 2, '\\'); break; }
|
|
if (arg[i] == '"') { out.append(nbs * 2 + 1, '\\'); out.push_back('"'); }
|
|
else { out.append(nbs, '\\'); out.push_back(arg[i]); }
|
|
}
|
|
out.push_back('"');
|
|
return out;
|
|
}
|
|
|
|
bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vector<std::string>& args)
|
|
{
|
|
// Build command line (binary path always quoted; each arg quoted/escaped per Windows rules — L-02)
|
|
std::string cmd = "\"" + binary_path + "\"";
|
|
for (const auto& arg : args) {
|
|
cmd += " " + quoteWinArg(arg);
|
|
}
|
|
DEBUG_LOGF("[INFO] Starting daemon: %s\n", cmd.c_str());
|
|
|
|
// Set working directory to the daemon binary's directory
|
|
std::string work_dir = fs::path(binary_path).parent_path().string();
|
|
if (work_dir.empty()) {
|
|
work_dir = ".";
|
|
}
|
|
DEBUG_LOGF("[INFO] Working directory: %s\n", work_dir.c_str());
|
|
|
|
// Store launch info for diagnostics (shown in error messages if crash occurs)
|
|
launch_cmd_ = cmd;
|
|
launch_binary_ = binary_path;
|
|
launch_workdir_ = work_dir;
|
|
|
|
// Log binary file size for corruption detection
|
|
{
|
|
std::error_code ec;
|
|
auto fsize = fs::file_size(binary_path, ec);
|
|
if (!ec) {
|
|
DEBUG_LOGF("[INFO] Daemon binary size: %llu bytes\n", (unsigned long long)fsize);
|
|
}
|
|
}
|
|
|
|
// Launch daemon windowless. Use CREATE_NO_WINDOW (NOT CREATE_NEW_CONSOLE): CREATE_NEW_CONSOLE
|
|
// allocates a console window that briefly flashes on screen before SW_HIDE can hide it, which is
|
|
// visible as a console-window flash on wallet launch. CREATE_NO_WINDOW gives the console child no
|
|
// window at all (same approach as the xmrig launcher). The daemon logs to debug.log, not a console.
|
|
// The daemon binary must NOT be in the data directory (%APPDATA%\Hush\DRAGONX)
|
|
// — it must be in <exe_dir>/dragonx/ to avoid conflicts with lock files and data.
|
|
STARTUPINFOA si;
|
|
PROCESS_INFORMATION pi;
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
si.dwFlags = STARTF_USESHOWWINDOW;
|
|
si.wShowWindow = SW_HIDE;
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
|
|
char* cmd_line = _strdup(cmd.c_str());
|
|
BOOL success = CreateProcessA(
|
|
NULL,
|
|
cmd_line,
|
|
NULL,
|
|
NULL,
|
|
FALSE,
|
|
CREATE_NO_WINDOW,
|
|
NULL,
|
|
work_dir.c_str(),
|
|
&si,
|
|
&pi
|
|
);
|
|
free(cmd_line);
|
|
|
|
if (!success) {
|
|
DWORD err = GetLastError();
|
|
char errBuf[256] = {0};
|
|
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
NULL, err, 0, errBuf, sizeof(errBuf), NULL);
|
|
last_error_ = "CreateProcess failed with error " + std::to_string(err) + ": " + errBuf;
|
|
DEBUG_LOGF("[ERROR] CreateProcess failed: error %lu - %s\nCommand: %s\n", err, errBuf, cmd.c_str());
|
|
return false;
|
|
}
|
|
|
|
process_handle_ = pi.hProcess;
|
|
CloseHandle(pi.hThread);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool EmbeddedDaemon::isRunning() const
|
|
{
|
|
if (process_handle_ == nullptr) return false;
|
|
|
|
DWORD exit_code;
|
|
if (GetExitCodeProcess(process_handle_, &exit_code)) {
|
|
return exit_code == STILL_ACTIVE;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Find a running process by executable name, return its PID (0 if not found)
|
|
static DWORD findProcessByName(const char* name)
|
|
{
|
|
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
|
if (snap == INVALID_HANDLE_VALUE) return 0;
|
|
|
|
// Use the explicit WIDE Toolhelp API + a wide compare so this is correct regardless of the UNICODE
|
|
// macro. (The non-suffixed PROCESSENTRY32/Process32First map to the wide variants when UNICODE is
|
|
// defined, in which case szExeFile is WCHAR[] and an ANSI _stricmp would compare garbage and NEVER
|
|
// match — silently making findProcessByName a no-op that returns 0 for a running process.)
|
|
wchar_t wname[MAX_PATH];
|
|
if (MultiByteToWideChar(CP_ACP, 0, name, -1, wname, MAX_PATH) == 0) { CloseHandle(snap); return 0; }
|
|
|
|
PROCESSENTRY32W entry;
|
|
entry.dwSize = sizeof(entry);
|
|
|
|
DWORD pid = 0;
|
|
if (Process32FirstW(snap, &entry)) {
|
|
do {
|
|
if (lstrcmpiW(entry.szExeFile, wname) == 0) { // Win32 case-insensitive wide compare
|
|
pid = entry.th32ProcessID;
|
|
break;
|
|
}
|
|
} while (Process32NextW(snap, &entry));
|
|
}
|
|
CloseHandle(snap);
|
|
return pid;
|
|
}
|
|
|
|
double EmbeddedDaemon::getMemoryUsageMB() const
|
|
{
|
|
if (process_handle_ == nullptr) 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;
|
|
}
|
|
|
|
std::function<double()> EmbeddedDaemon::memoryProbe() const
|
|
{
|
|
if (process_handle_ == nullptr) return {};
|
|
// GetProcessMemoryInfo on the handle we own is one cheap call: read it here and hand back the value, so the
|
|
// callable never touches process_handle_ (the monitor can close it). The expensive part on Windows, the
|
|
// Toolhelp process scan App falls back to when this reads 0, runs on the sampler thread.
|
|
const double mb = getMemoryUsageMB();
|
|
return [mb]() { return mb; };
|
|
}
|
|
|
|
void EmbeddedDaemon::drainOutput()
|
|
{
|
|
// Read new content from debug.log (tail-follow approach)
|
|
if (debug_log_path_.empty()) return;
|
|
|
|
HANDLE hFile = CreateFileA(debug_log_path_.c_str(), GENERIC_READ,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
|
if (hFile == INVALID_HANDLE_VALUE) return;
|
|
|
|
// Get current file size
|
|
LARGE_INTEGER fileSize;
|
|
if (!GetFileSizeEx(hFile, &fileSize)) {
|
|
CloseHandle(hFile);
|
|
return;
|
|
}
|
|
|
|
size_t currentSize = static_cast<size_t>(fileSize.QuadPart);
|
|
// Truncation / rotation detection. dragonxd shrinks debug.log on startup (ShrinkDebugFile keeps
|
|
// only the tail once it has grown large), so the file can become SMALLER than where we left off.
|
|
// Our offset was set to the PRE-shrink size at spawn, so without this reset it stays stranded ahead
|
|
// of the freshly-truncated file and we read NOTHING for the whole session — no block height (status
|
|
// bar shows "Block: 0") and, worse, no witness-rebuild progress, since the "Setting Initial Sapling
|
|
// Witness …" lines the warmup progress bar parses only exist in debug.log on Windows. On a shrink,
|
|
// restart from the new beginning; the parser is monotonic and converges as it reaches current output.
|
|
if (currentSize < debug_log_offset_) {
|
|
std::string content(currentSize, '\0');
|
|
DWORD got = 0;
|
|
LARGE_INTEGER zero{};
|
|
SetFilePointerEx(hFile, zero, NULL, FILE_BEGIN);
|
|
if (currentSize > 0 && ReadFile(hFile, &content[0], static_cast<DWORD>(currentSize), &got, NULL))
|
|
content.resize(got);
|
|
debug_log_offset_ = shrinkResumeOffset(content, launch_utc_);
|
|
}
|
|
if (currentSize <= debug_log_offset_) {
|
|
CloseHandle(hFile);
|
|
return; // No new data
|
|
}
|
|
|
|
// Seek to where we left off
|
|
LARGE_INTEGER seekPos;
|
|
seekPos.QuadPart = static_cast<LONGLONG>(debug_log_offset_);
|
|
SetFilePointerEx(hFile, seekPos, NULL, FILE_BEGIN);
|
|
|
|
// Read new content in chunks
|
|
char buffer[4096];
|
|
DWORD bytes_read;
|
|
while (debug_log_offset_ < currentSize) {
|
|
DWORD toRead = static_cast<DWORD>(std::min<size_t>(sizeof(buffer) - 1, currentSize - debug_log_offset_));
|
|
if (!ReadFile(hFile, buffer, toRead, &bytes_read, NULL) || bytes_read == 0) break;
|
|
|
|
buffer[bytes_read] = '\0';
|
|
{
|
|
std::lock_guard<std::mutex> lk(output_mutex_);
|
|
appendOutput(buffer, bytes_read);
|
|
}
|
|
VERBOSE_LOGF("[dragonxd] %s", buffer);
|
|
debug_log_offset_ += bytes_read;
|
|
}
|
|
|
|
CloseHandle(hFile);
|
|
}
|
|
|
|
bool EmbeddedDaemon::stop(int wait_ms)
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> life(lifecycle_mutex_);
|
|
stop_requested_ = true;
|
|
requestMonitorStop();
|
|
}
|
|
setState(State::Stopping, "Stopping dragonxd...");
|
|
|
|
// Check if our tracked process handle is still alive
|
|
bool tracked_alive = false;
|
|
if (process_handle_ != nullptr) {
|
|
DWORD exit_code;
|
|
if (GetExitCodeProcess(process_handle_, &exit_code) && exit_code == STILL_ACTIVE) {
|
|
tracked_alive = true;
|
|
}
|
|
}
|
|
|
|
// Helper: poll-wait for a process handle, draining stdout each iteration
|
|
auto pollWait = [&](HANDLE hProc, int ms) -> bool {
|
|
auto start = std::chrono::steady_clock::now();
|
|
while (true) {
|
|
drainOutput();
|
|
DWORD res = WaitForSingleObject(hProc, 200);
|
|
if (res == WAIT_OBJECT_0) return true; // exited
|
|
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - start).count();
|
|
if (elapsed >= ms) return false; // timeout
|
|
}
|
|
};
|
|
|
|
bool exitedOnItsOwn = true;
|
|
if (tracked_alive) {
|
|
// Our tracked process (dragonxd.exe launched directly) is still alive.
|
|
// The RPC "stop" was already sent by the caller — wait for it to exit.
|
|
DEBUG_LOGF("Waiting up to %d ms for tracked daemon process to exit...\n", wait_ms);
|
|
if (!pollWait(process_handle_, wait_ms)) {
|
|
DEBUG_LOGF("Timeout — forcing daemon termination...\n");
|
|
TerminateProcess(process_handle_, 1);
|
|
WaitForSingleObject(process_handle_, 2000);
|
|
exitedOnItsOwn = false;
|
|
}
|
|
drainOutput();
|
|
CloseHandle(process_handle_);
|
|
process_handle_ = nullptr;
|
|
} else {
|
|
// No live process of OUR OWN to stop. Never go looking for one by name: findProcessByName returns
|
|
// the first dragonxd.exe of any kind (an old node still shutting down, a node on another datadir,
|
|
// an isolated seed-migration node), and killing it is how an in-progress shutdown got
|
|
// TerminateProcess'd. Nodes we didn't launch only ever get an RPC stop from the caller.
|
|
if (process_handle_ != nullptr) {
|
|
CloseHandle(process_handle_);
|
|
process_handle_ = nullptr;
|
|
}
|
|
DEBUG_LOGF("No tracked daemon process to stop (any other dragonxd.exe is not ours and is left alone)\n");
|
|
}
|
|
|
|
if (stdout_read_ != nullptr) {
|
|
CloseHandle(stdout_read_);
|
|
stdout_read_ = nullptr;
|
|
}
|
|
// Final drain of debug.log
|
|
drainOutput();
|
|
|
|
{
|
|
std::lock_guard<std::mutex> life(lifecycle_mutex_);
|
|
if (monitor_thread_.joinable()) {
|
|
monitor_thread_.join();
|
|
}
|
|
}
|
|
|
|
setState(State::Stopped, "dragonxd stopped");
|
|
return exitedOnItsOwn;
|
|
}
|
|
|
|
bool EmbeddedDaemon::beginStop()
|
|
{
|
|
std::lock_guard<std::mutex> life(lifecycle_mutex_); // before the flags: a racing start() would reset should_stop_
|
|
stop_requested_ = true;
|
|
requestMonitorStop();
|
|
if (monitor_thread_.joinable()) {
|
|
monitor_thread_.join();
|
|
}
|
|
if (!isRunning()) return false; // handle-based: our own process only
|
|
setState(State::Stopping, "Stopping dragonxd...");
|
|
return true;
|
|
}
|
|
|
|
bool EmbeddedDaemon::pollOwnedProcess()
|
|
{
|
|
drainOutput();
|
|
return isRunning();
|
|
}
|
|
|
|
bool EmbeddedDaemon::signalGracefulStop()
|
|
{
|
|
return false; // no graceful channel on Windows: the GUI launches dragonxd without a console
|
|
}
|
|
|
|
void EmbeddedDaemon::forceKillOwnedProcess()
|
|
{
|
|
if (process_handle_ == nullptr || !isRunning()) return;
|
|
DEBUG_LOGF("Killing dragonxd at the user's request (TerminateProcess on our own handle)\n");
|
|
TerminateProcess(process_handle_, 1);
|
|
WaitForSingleObject(process_handle_, 5000);
|
|
drainOutput();
|
|
}
|
|
|
|
EmbeddedDaemon::ResourceUsage EmbeddedDaemon::ownedResourceUsage() const
|
|
{
|
|
ResourceUsage usage;
|
|
if (process_handle_ == nullptr) return usage;
|
|
FILETIME created, exited, kernel, user;
|
|
if (GetProcessTimes(process_handle_, &created, &exited, &kernel, &user)) {
|
|
auto ticks = [](const FILETIME& ft) {
|
|
return (static_cast<unsigned long long>(ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
|
|
};
|
|
usage.cpuSeconds = static_cast<double>(ticks(kernel) + ticks(user)) / 1e7; // 100ns units
|
|
}
|
|
IO_COUNTERS io;
|
|
if (GetProcessIoCounters(process_handle_, &io))
|
|
usage.ioBytes = static_cast<double>(io.ReadTransferCount + io.WriteTransferCount);
|
|
return usage;
|
|
}
|
|
|
|
void EmbeddedDaemon::endStop(bool keepWatching)
|
|
{
|
|
std::lock_guard<std::mutex> life(lifecycle_mutex_);
|
|
if (keepWatching && isRunning()) {
|
|
// Still running and still ours: watch it again. Its exit is booked as a clean stop.
|
|
if (monitor_thread_.joinable()) { // unreachable after beginStop()'s join; never join a live monitor unsignalled
|
|
requestMonitorStop(); monitor_thread_.join();
|
|
}
|
|
should_stop_ = false;
|
|
monitor_thread_ = std::thread(&EmbeddedDaemon::monitorProcess, this);
|
|
return;
|
|
}
|
|
const bool leftRunning = isRunning();
|
|
if (leftRunning)
|
|
DEBUG_LOGF("Leaving dragonxd running; it will finish shutting down on its own\n");
|
|
if (process_handle_ != nullptr) {
|
|
CloseHandle(process_handle_); // only our handle: the process carries on
|
|
process_handle_ = nullptr;
|
|
}
|
|
if (stdout_read_ != nullptr) {
|
|
CloseHandle(stdout_read_);
|
|
stdout_read_ = nullptr;
|
|
}
|
|
drainOutput();
|
|
setState(State::Stopped, leftRunning ? "dragonxd left running" : "dragonxd stopped");
|
|
}
|
|
|
|
// Translate Windows NTSTATUS / exit codes to human-readable descriptions
|
|
static std::string translateWindowsExitCode(DWORD code) {
|
|
switch (code) {
|
|
case 0x80000003: return "STATUS_BREAKPOINT (0x80000003) — possible missing DLL or binary crash";
|
|
case 0xC0000005: return "STATUS_ACCESS_VIOLATION (0xC0000005) — memory access violation";
|
|
case 0xC0000135: return "STATUS_DLL_NOT_FOUND (0xC0000135) — required DLL not found";
|
|
case 0xC000007B: return "STATUS_INVALID_IMAGE_FORMAT (0xC000007B) — wrong architecture or corrupt binary";
|
|
case 0xC0000142: return "STATUS_DLL_INIT_FAILED (0xC0000142) — DLL initialization failed";
|
|
case 0xC0000409: return "STATUS_STACK_BUFFER_OVERRUN (0xC0000409) — stack buffer overflow";
|
|
case 0xC0000374: return "STATUS_HEAP_CORRUPTION (0xC0000374) — heap corruption detected";
|
|
default:
|
|
if (code > 0x80000000) {
|
|
char buf[64];
|
|
snprintf(buf, sizeof(buf), "exit code: 0x%08lX", code);
|
|
return std::string(buf);
|
|
}
|
|
return "exit code: " + std::to_string(code);
|
|
}
|
|
}
|
|
|
|
void EmbeddedDaemon::monitorProcess()
|
|
{
|
|
const auto fast = std::chrono::milliseconds(250);
|
|
const auto cap = std::chrono::milliseconds(500);
|
|
const auto startTime = std::chrono::steady_clock::now();
|
|
auto lastData = startTime;
|
|
auto interval = fast;
|
|
while (!should_stop_ && isRunning()) {
|
|
// Tail debug.log for new output
|
|
const size_t before = outputTotalSnapshot();
|
|
drainOutput();
|
|
const auto now = std::chrono::steady_clock::now();
|
|
const bool got = outputTotalSnapshot() != before;
|
|
if (got) lastData = now;
|
|
interval = nextMonitorInterval(interval, got,
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(now - startTime),
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(now - lastData), fast, cap);
|
|
std::unique_lock<std::mutex> lk(monitor_mutex_);
|
|
monitor_cv_.wait_for(lk, interval, [this] { return should_stop_.load(); });
|
|
}
|
|
|
|
// Read any remaining output after process exits
|
|
drainOutput();
|
|
|
|
// An exit after markStopRequested() is the stop we asked for (it can land before stop() runs).
|
|
if (!should_stop_ && !isRunning() && stop_requested_) {
|
|
DEBUG_LOGF("dragonxd exited after a requested stop\n");
|
|
setState(State::Stopped, "dragonxd stopped");
|
|
return;
|
|
}
|
|
|
|
// Check if process exited unexpectedly
|
|
if (!should_stop_ && !isRunning()) {
|
|
// Get exit code for diagnostics
|
|
DWORD exit_code = 0;
|
|
if (process_handle_) {
|
|
GetExitCodeProcess(process_handle_, &exit_code);
|
|
}
|
|
|
|
crash_count_++;
|
|
unexpected_exits_++; // before setState(Error): a reader that sees Error sees the new count
|
|
|
|
// Build informative error message with full diagnostic details
|
|
std::string error_msg = "dragonxd exited unexpectedly (" + translateWindowsExitCode(exit_code) + ")";
|
|
|
|
// Add launch diagnostics
|
|
error_msg += "\n\nLaunch details:";
|
|
error_msg += "\n Binary: " + launch_binary_;
|
|
error_msg += "\n Working dir: " + launch_workdir_;
|
|
error_msg += "\n Command: " + launch_cmd_;
|
|
|
|
// Check binary file size
|
|
{
|
|
std::error_code ec;
|
|
auto fsize = fs::file_size(launch_binary_, ec);
|
|
if (!ec) {
|
|
char sizeBuf[32];
|
|
snprintf(sizeBuf, sizeof(sizeBuf), "%llu", (unsigned long long)fsize);
|
|
error_msg += "\n Binary size: " + std::string(sizeBuf) + " bytes";
|
|
} else {
|
|
error_msg += "\n Binary size: could not read (" + ec.message() + ")";
|
|
}
|
|
}
|
|
|
|
// Check if debug.log has any content from this launch
|
|
if (!debug_log_path_.empty()) {
|
|
std::error_code ec;
|
|
auto logSize = fs::file_size(debug_log_path_, ec);
|
|
if (!ec) {
|
|
size_t newBytes = (static_cast<size_t>(logSize) > debug_log_offset_)
|
|
? static_cast<size_t>(logSize) - debug_log_offset_ : 0;
|
|
char logBuf[64];
|
|
snprintf(logBuf, sizeof(logBuf), "%zu", newBytes);
|
|
error_msg += "\n debug.log new bytes: " + std::string(logBuf);
|
|
}
|
|
}
|
|
|
|
// Include daemon output from debug.log
|
|
{
|
|
std::lock_guard<std::mutex> lk(output_mutex_);
|
|
if (!process_output_.empty()) {
|
|
// Get last ~500 chars of output for context
|
|
std::string last_output = process_output_;
|
|
if (last_output.size() > 500) {
|
|
last_output = "..." + last_output.substr(last_output.size() - 500);
|
|
}
|
|
error_msg += "\n\nDaemon output:\n" + last_output;
|
|
}
|
|
}
|
|
|
|
setState(State::Error, error_msg);
|
|
}
|
|
}
|
|
|
|
#else // Linux/macOS
|
|
|
|
bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vector<std::string>& args)
|
|
{
|
|
// Create pipe for stdout/stderr
|
|
int pipefd[2];
|
|
if (pipe(pipefd) == -1) {
|
|
last_error_ = "Failed to create pipe: " + std::string(strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
// Self-pipe used purely as an exec-success/failure handshake, separate from
|
|
// the stdout pipe above. Both ends are close-on-exec, so a successful execv()
|
|
// closes the write end for free (parent reads EOF); on execv() failure the
|
|
// child writes errno here, so the parent learns synchronously instead of
|
|
// reporting State::Running for a child that never became dragonxd. We use
|
|
// pipe()+FD_CLOEXEC (not pipe2) because this POSIX branch is shared with
|
|
// macOS, which has no pipe2().
|
|
int execpipe[2];
|
|
if (pipe(execpipe) == -1) {
|
|
last_error_ = "Failed to create exec-status pipe: " + std::string(strerror(errno));
|
|
close(pipefd[0]);
|
|
close(pipefd[1]);
|
|
return false;
|
|
}
|
|
fcntl(execpipe[0], F_SETFD, FD_CLOEXEC);
|
|
fcntl(execpipe[1], F_SETFD, FD_CLOEXEC);
|
|
|
|
pid_t pid = fork();
|
|
if (pid == -1) {
|
|
last_error_ = "Fork failed: " + std::string(strerror(errno));
|
|
close(pipefd[0]);
|
|
close(pipefd[1]);
|
|
close(execpipe[0]);
|
|
close(execpipe[1]);
|
|
return false;
|
|
}
|
|
|
|
if (pid == 0) {
|
|
// Child process
|
|
close(pipefd[0]); // Close read end of the stdout pipe
|
|
close(execpipe[0]); // Child only writes the exec-status pipe
|
|
|
|
// Put child in its own process group so we can kill the entire
|
|
// group later (including dragonxd spawned by a wrapper script).
|
|
// Without this, SIGTERM only kills the shell, leaving dragonxd orphaned.
|
|
setpgid(0, 0);
|
|
|
|
// Change to the daemon binary's directory so dragonxd can find
|
|
// sapling params via its PWD search path (same as CreateProcessA
|
|
// lpCurrentDirectory on Windows).
|
|
{
|
|
std::string daemon_dir = fs::path(binary_path).parent_path().string();
|
|
if (!daemon_dir.empty()) {
|
|
if (chdir(daemon_dir.c_str()) != 0) {
|
|
fprintf(stderr, "chdir(%s) failed: %s\n", daemon_dir.c_str(), strerror(errno));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Redirect stdout and stderr to pipe
|
|
dup2(pipefd[1], STDOUT_FILENO);
|
|
dup2(pipefd[1], STDERR_FILENO);
|
|
close(pipefd[1]);
|
|
|
|
// Disable output buffering
|
|
setenv("PYTHONUNBUFFERED", "1", 1);
|
|
|
|
// Build argv
|
|
std::vector<char*> argv;
|
|
|
|
// Check if this is a shell script
|
|
bool is_script = false;
|
|
if (binary_path.size() >= 3) {
|
|
std::string ext = binary_path.substr(binary_path.size() - 3);
|
|
if (ext == ".sh" || binary_path.find("dragonxd") != std::string::npos) {
|
|
// Check if it's a script by looking at first bytes
|
|
FILE* f = fopen(binary_path.c_str(), "r");
|
|
if (f) {
|
|
char buf[2] = {0};
|
|
if (fread(buf, 1, 2, f) == 2 && buf[0] == '#' && buf[1] == '!') {
|
|
is_script = true;
|
|
}
|
|
fclose(f);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (is_script) {
|
|
// Run shell scripts through bash
|
|
argv.push_back(const_cast<char*>("/bin/bash"));
|
|
argv.push_back(const_cast<char*>(binary_path.c_str()));
|
|
} else {
|
|
argv.push_back(const_cast<char*>(binary_path.c_str()));
|
|
}
|
|
|
|
for (const auto& arg : args) {
|
|
argv.push_back(const_cast<char*>(arg.c_str()));
|
|
}
|
|
argv.push_back(nullptr);
|
|
|
|
// Execute using execvp for PATH resolution
|
|
if (is_script) {
|
|
execv("/bin/bash", argv.data());
|
|
} else {
|
|
execv(binary_path.c_str(), argv.data());
|
|
}
|
|
|
|
// If we get here, execv() failed — the child never became dragonxd.
|
|
// Capture errno before fprintf/strerror can clobber it, report it to
|
|
// the parent over the exec-status pipe (EINTR-safe), then exit.
|
|
int exec_errno = errno;
|
|
fprintf(stderr, "execv failed: %s\n", strerror(exec_errno));
|
|
ssize_t w;
|
|
do {
|
|
w = write(execpipe[1], &exec_errno, sizeof(exec_errno));
|
|
} while (w < 0 && errno == EINTR);
|
|
_exit(127);
|
|
}
|
|
|
|
// Parent process
|
|
close(pipefd[1]); // Close our copy of the stdout write end
|
|
close(execpipe[1]); // Must close our copy, or the read() below never sees EOF
|
|
|
|
// Exec-status handshake: EOF => execv() succeeded (its write end was closed
|
|
// on exec); a full sizeof(int) => execv() failed and the child sent errno.
|
|
int child_errno = 0;
|
|
size_t got = 0;
|
|
char* ep = reinterpret_cast<char*>(&child_errno);
|
|
for (;;) {
|
|
ssize_t n = read(execpipe[0], ep + got, sizeof(child_errno) - got);
|
|
if (n == 0) break; // EOF: exec succeeded
|
|
if (n < 0) { if (errno == EINTR) continue; break; } // other error: assume success
|
|
got += static_cast<size_t>(n);
|
|
if (got >= sizeof(child_errno)) break; // full errno: exec failed
|
|
}
|
|
close(execpipe[0]);
|
|
|
|
if (got >= sizeof(child_errno)) {
|
|
// execv() never replaced the child; it fprintf'd and _exit(127)'d. Reap
|
|
// the already-dead zombie here — monitorProcess() is only started after
|
|
// this function returns true, so there is no competing reaper.
|
|
close(pipefd[0]);
|
|
int status;
|
|
waitpid(pid, &status, 0);
|
|
last_error_ = "dragonxd could not be executed: " + std::string(strerror(child_errno)) +
|
|
" — not executable or wrong architecture";
|
|
return false;
|
|
}
|
|
|
|
stdout_fd_ = pipefd[0];
|
|
|
|
// Best-effort: the child already calls setpgid(0, 0); this parent-side call
|
|
// just closes the fork/exec race window. A failure here is not fatal to
|
|
// startup, so we log rather than abort.
|
|
if (setpgid(pid, pid) != 0) {
|
|
DEBUG_LOGF("[WARN] setpgid(%d) from parent failed: %s\n", (int)pid, strerror(errno));
|
|
}
|
|
|
|
// Set non-blocking
|
|
int flags = fcntl(stdout_fd_, F_GETFL, 0);
|
|
fcntl(stdout_fd_, F_SETFL, flags | O_NONBLOCK);
|
|
|
|
process_pid_ = pid;
|
|
return true;
|
|
}
|
|
|
|
namespace {
|
|
// Sum of the RSS (MB) of every process in process group `pgid`. The tracked pid is often a bash wrapper and the
|
|
// real dragonxd a child in the same group (setpgid in startProcess). Runs on a sampler thread
|
|
// (util::BackgroundSampler), so the /proc scan / `ps` fork no longer costs the UI anything.
|
|
double sampleProcessGroupRssMB(pid_t pgid)
|
|
{
|
|
#ifdef __APPLE__
|
|
// macOS: use ps to read RSS for the daemon process and its children
|
|
// in the same process group.
|
|
char cmd[128];
|
|
snprintf(cmd, sizeof(cmd), "ps -o rss= -g %d 2>/dev/null", static_cast<int>(pgid));
|
|
FILE* fp = popen(cmd, "r");
|
|
if (!fp) return 0.0;
|
|
double total_rss_mb = 0.0;
|
|
char line[64];
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
long rss_kb = atol(line);
|
|
if (rss_kb > 0) total_rss_mb += static_cast<double>(rss_kb) / 1024.0;
|
|
}
|
|
pclose(fp);
|
|
return total_rss_mb;
|
|
#else
|
|
// Linux: sum VmRSS for every /proc/<pid> whose process group is pgid. The error_code overloads: an
|
|
// unreadable /proc ends the scan instead of throwing (the old range-for constructor threw).
|
|
double total_rss_mb = 0.0;
|
|
std::error_code ec;
|
|
for (fs::directory_iterator it("/proc", ec), end; !ec && it != end; it.increment(ec)) {
|
|
const std::string name = it->path().filename().string();
|
|
if (name.empty() || !std::isdigit(static_cast<unsigned char>(name[0]))) continue;
|
|
|
|
// Read process group from /proc/<pid>/stat (field 5 is pgrp)
|
|
const std::string base = it->path().string();
|
|
FILE* sf = fopen((base + "/stat").c_str(), "r");
|
|
if (!sf) continue;
|
|
|
|
char statLine[512];
|
|
if (!fgets(statLine, sizeof(statLine), sf)) { fclose(sf); continue; }
|
|
fclose(sf);
|
|
|
|
// Fields in /proc/<pid>/stat are space-separated, but field 2 (comm)
|
|
// is wrapped in parens and may contain spaces. Skip past ')'.
|
|
const char* p = strrchr(statLine, ')');
|
|
if (!p) continue;
|
|
p++; // skip ')'
|
|
|
|
// After ')' the fields are: state(3), ppid(4), pgrp(5), ...
|
|
int pgrp = 0;
|
|
char state;
|
|
int ppid;
|
|
if (sscanf(p, " %c %d %d", &state, &ppid, &pgrp) != 3) continue;
|
|
if (pgrp != pgid) continue;
|
|
|
|
// This process belongs to our group — read its VmRSS
|
|
FILE* f = fopen((base + "/status").c_str(), "r");
|
|
if (!f) continue;
|
|
char line[256];
|
|
while (fgets(line, sizeof(line), f)) {
|
|
if (strncmp(line, "VmRSS:", 6) == 0) {
|
|
long rss_kb = 0;
|
|
if (sscanf(line + 6, "%ld", &rss_kb) == 1)
|
|
total_rss_mb += static_cast<double>(rss_kb) / 1024.0;
|
|
break;
|
|
}
|
|
}
|
|
fclose(f);
|
|
}
|
|
return total_rss_mb;
|
|
#endif // __APPLE__ / Linux
|
|
}
|
|
} // namespace
|
|
|
|
std::function<double()> EmbeddedDaemon::memoryProbe() const
|
|
{
|
|
const pid_t pid = process_pid_; // snapshot on the calling thread; the callable carries only this
|
|
if (pid <= 0) return {};
|
|
return [pid]() { return sampleProcessGroupRssMB(pid); };
|
|
}
|
|
|
|
double EmbeddedDaemon::getMemoryUsageMB() const
|
|
{
|
|
const auto probe = memoryProbe();
|
|
return probe ? probe() : 0.0;
|
|
}
|
|
|
|
bool EmbeddedDaemon::isRunning() const
|
|
{
|
|
// Read the atomic state_ instead of calling waitpid() here. monitorProcess()
|
|
// is the sole thread allowed to waitpid() process_pid_ during normal operation.
|
|
// Calling waitpid() from this method too (as it used to, and this is invoked
|
|
// from the UI thread nearly every frame) meant whichever thread reaped the
|
|
// child's exit first consumed the status; if isRunning() won that race,
|
|
// monitorProcess() never saw the exit, so crash_count_ / the decoded exit
|
|
// code / the State::Error transition were all silently lost. Mirrors the
|
|
// fix already in XmrigManager::isRunning().
|
|
if (process_pid_ <= 0) return false;
|
|
|
|
const State s = state_.load(std::memory_order_relaxed);
|
|
// State::Stopping is included: stop()'s graceful/SIGTERM wait loops poll
|
|
// isRunning() while state_ == Stopping — before the process has actually
|
|
// terminated — and must keep seeing "alive" to wait/escalate correctly.
|
|
return (s == State::Running || s == State::Stopping);
|
|
}
|
|
|
|
void EmbeddedDaemon::tailDebugLog()
|
|
{
|
|
if (debug_log_path_.empty()) return;
|
|
const int fd = open(debug_log_path_.c_str(), O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0) return; // not created yet
|
|
struct stat st{};
|
|
if (fstat(fd, &st) != 0) { close(fd); return; }
|
|
const size_t size = static_cast<size_t>(st.st_size);
|
|
if (size < debug_log_offset_) { // shrunk at startup: resume at this run's banner (see shrinkResumeOffset)
|
|
std::string content(size, '\0');
|
|
const ssize_t got = size ? pread(fd, &content[0], size, 0) : 0;
|
|
content.resize(got > 0 ? static_cast<size_t>(got) : 0);
|
|
debug_log_offset_ = shrinkResumeOffset(content, launch_utc_);
|
|
}
|
|
char buffer[4096];
|
|
while (debug_log_offset_ < size) {
|
|
const size_t want = std::min(sizeof(buffer) - 1, size - debug_log_offset_);
|
|
const ssize_t n = pread(fd, buffer, want, static_cast<off_t>(debug_log_offset_));
|
|
if (n <= 0) break;
|
|
buffer[n] = '\0';
|
|
{
|
|
std::lock_guard<std::mutex> lk(output_mutex_);
|
|
appendOutput(buffer, static_cast<size_t>(n));
|
|
}
|
|
VERBOSE_LOGF("[dragonxd] %s", buffer);
|
|
debug_log_offset_ += static_cast<size_t>(n);
|
|
}
|
|
close(fd);
|
|
}
|
|
|
|
void EmbeddedDaemon::drainOutput()
|
|
{
|
|
if (stdout_fd_ >= 0) {
|
|
char buffer[4096];
|
|
for (;;) {
|
|
ssize_t n = read(stdout_fd_, buffer, sizeof(buffer) - 1);
|
|
if (n <= 0) break;
|
|
buffer[n] = '\0';
|
|
{
|
|
std::lock_guard<std::mutex> lk(output_mutex_);
|
|
appendOutput(buffer, static_cast<size_t>(n));
|
|
}
|
|
VERBOSE_LOGF("[dragonxd] %s", buffer);
|
|
}
|
|
}
|
|
tailDebugLog();
|
|
}
|
|
|
|
static std::string describeExitStatus(int status)
|
|
{
|
|
if (WIFEXITED(status)) return "exit code: " + std::to_string(WEXITSTATUS(status));
|
|
if (WIFSIGNALED(status)) return "killed by signal " + std::to_string(WTERMSIG(status));
|
|
return "unknown exit status";
|
|
}
|
|
|
|
bool EmbeddedDaemon::reapWithin(pid_t pid, int timeout_ms, int& status)
|
|
{
|
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
|
|
for (;;) {
|
|
drainOutput();
|
|
const pid_t r = waitpid(pid, &status, WNOHANG);
|
|
if (r == pid) return true;
|
|
if (r < 0 && errno != EINTR) return true; // ECHILD: nothing left to reap
|
|
if (std::chrono::steady_clock::now() >= deadline) return false;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
}
|
|
}
|
|
|
|
bool EmbeddedDaemon::stop(int wait_ms)
|
|
{
|
|
bool exitedOnItsOwn = true;
|
|
|
|
// Join the monitor FIRST (it wakes immediately via monitor_cv_) so that from here on this is
|
|
// the only reaper. isRunning() is state-based and stays true until we set Stopped below, so it can't
|
|
// tell us the child exited: poll waitpid() on a cached pid instead. If the child exited inside that
|
|
// tick, the monitor already reaped it and, seeing stop_requested_, recorded a clean stop (pid now 0).
|
|
// lifecycle_mutex_ covers the flags and the join only, never the reap waits below.
|
|
{
|
|
std::lock_guard<std::mutex> life(lifecycle_mutex_);
|
|
stop_requested_ = true;
|
|
requestMonitorStop();
|
|
if (monitor_thread_.joinable()) {
|
|
monitor_thread_.join();
|
|
}
|
|
}
|
|
|
|
const pid_t pid = process_pid_;
|
|
if (pid > 0) {
|
|
setState(State::Stopping, "Stopping dragonxd...");
|
|
int status = 0;
|
|
|
|
// Phase 1: Wait for the daemon to exit naturally.
|
|
// The caller (stopEmbeddedDaemon) already sent an RPC "stop" which
|
|
// tells the daemon to flush LevelDB, close sockets, and exit cleanly.
|
|
// On macOS/APFS the LevelDB flush can take several seconds — we must
|
|
// NOT send SIGTERM until the daemon has had enough time to finish.
|
|
DEBUG_LOGF("Waiting up to %d ms for daemon to exit after RPC stop...\n", wait_ms);
|
|
if (reapWithin(pid, wait_ms, status)) {
|
|
DEBUG_LOGF("Daemon exited cleanly after RPC stop (%s)\n", describeExitStatus(status).c_str());
|
|
} else {
|
|
// Phase 2: still running — SIGTERM (dragonxd treats it like RPC stop) and a further 10s.
|
|
DEBUG_LOGF("Daemon did not exit gracefully — sending SIGTERM to process group -%d\n", pid);
|
|
kill(-pid, SIGTERM);
|
|
if (!reapWithin(pid, 10000, status)) {
|
|
// Phase 3: Force kill
|
|
DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", pid);
|
|
kill(-pid, SIGKILL);
|
|
while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {}
|
|
exitedOnItsOwn = false;
|
|
}
|
|
DEBUG_LOGF("dragonxd exited (%s)\n", describeExitStatus(status).c_str());
|
|
}
|
|
drainOutput(); // read any remaining output
|
|
process_pid_ = 0;
|
|
}
|
|
|
|
if (stdout_fd_ >= 0) {
|
|
close(stdout_fd_);
|
|
stdout_fd_ = -1;
|
|
}
|
|
|
|
setState(State::Stopped, "dragonxd stopped");
|
|
return exitedOnItsOwn;
|
|
}
|
|
|
|
bool EmbeddedDaemon::beginStop()
|
|
{
|
|
std::lock_guard<std::mutex> life(lifecycle_mutex_); // before the flags: a racing start() would reset should_stop_
|
|
stop_requested_ = true;
|
|
requestMonitorStop();
|
|
// The monitor wakes immediately via monitor_cv_. If the child exited in that window the
|
|
// monitor reaped it (as a clean stop: stop_requested_ is set) and the pid is now 0.
|
|
if (monitor_thread_.joinable()) {
|
|
monitor_thread_.join();
|
|
}
|
|
if (process_pid_ <= 0) return false;
|
|
setState(State::Stopping, "Stopping dragonxd...");
|
|
return true;
|
|
}
|
|
|
|
bool EmbeddedDaemon::pollOwnedProcess()
|
|
{
|
|
const pid_t pid = process_pid_;
|
|
if (pid <= 0) return false;
|
|
drainOutput();
|
|
int status = 0;
|
|
const pid_t r = waitpid(pid, &status, WNOHANG);
|
|
if (r == 0 || (r < 0 && errno == EINTR)) return true;
|
|
if (r == pid) DEBUG_LOGF("dragonxd exited (%s)\n", describeExitStatus(status).c_str());
|
|
drainOutput();
|
|
process_pid_ = 0;
|
|
return false;
|
|
}
|
|
|
|
bool EmbeddedDaemon::signalGracefulStop()
|
|
{
|
|
const pid_t pid = process_pid_;
|
|
if (pid <= 0) return false;
|
|
// The whole group: with a bash wrapper (embedded_daemon.cpp startProcess) a SIGTERM to the wrapper
|
|
// alone would orphan dragonxd.
|
|
DEBUG_LOGF("Sending SIGTERM to dragonxd's process group -%d (graceful stop)\n", pid);
|
|
return kill(-pid, SIGTERM) == 0;
|
|
}
|
|
|
|
void EmbeddedDaemon::forceKillOwnedProcess()
|
|
{
|
|
const pid_t pid = process_pid_;
|
|
if (pid <= 0) return;
|
|
DEBUG_LOGF("Killing dragonxd (group -%d) at the user's request\n", pid);
|
|
kill(-pid, SIGKILL);
|
|
int status = 0;
|
|
while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {}
|
|
drainOutput();
|
|
process_pid_ = 0;
|
|
}
|
|
|
|
EmbeddedDaemon::ResourceUsage EmbeddedDaemon::ownedResourceUsage() const
|
|
{
|
|
ResourceUsage usage;
|
|
#ifdef __APPLE__
|
|
return usage; // no sampler yet (would need proc_pid_rusage): never "idle", so Kill is never offered
|
|
#else
|
|
const pid_t pgid = process_pid_;
|
|
if (pgid <= 0) return usage;
|
|
// The tracked pid may be a bash wrapper that itself uses ~0 CPU: sum the whole process group.
|
|
unsigned long long ticks = 0;
|
|
double io = 0.0;
|
|
bool anyCpu = false, anyIo = false;
|
|
std::error_code ec;
|
|
fs::directory_iterator it("/proc", ec), end;
|
|
for (; !ec && it != end; it.increment(ec)) {
|
|
const std::string name = it->path().filename().string();
|
|
if (name.empty() || name[0] < '0' || name[0] > '9') continue;
|
|
std::ifstream statFile((it->path() / "stat").string());
|
|
std::string line;
|
|
long pgrp = 0;
|
|
unsigned long long t = 0;
|
|
if (!statFile || !std::getline(statFile, line) || !parseProcStatLine(line, pgrp, t) || pgrp != pgid)
|
|
continue;
|
|
ticks += t;
|
|
anyCpu = true;
|
|
std::ifstream ioFile((it->path() / "io").string());
|
|
std::string key;
|
|
unsigned long long value = 0;
|
|
while (ioFile >> key >> value) {
|
|
if (key == "read_bytes:" || key == "write_bytes:") {
|
|
io += static_cast<double>(value);
|
|
anyIo = true;
|
|
}
|
|
}
|
|
}
|
|
const long hz = sysconf(_SC_CLK_TCK);
|
|
if (anyCpu && hz > 0) usage.cpuSeconds = static_cast<double>(ticks) / static_cast<double>(hz);
|
|
if (anyIo) usage.ioBytes = io;
|
|
return usage;
|
|
#endif
|
|
}
|
|
|
|
void EmbeddedDaemon::endStop(bool keepWatching)
|
|
{
|
|
std::lock_guard<std::mutex> life(lifecycle_mutex_);
|
|
const pid_t pid = process_pid_;
|
|
if (pid > 0 && keepWatching) {
|
|
// Still running and still ours: watch it again. Its exit is booked as a clean stop.
|
|
if (monitor_thread_.joinable()) { // unreachable after beginStop()'s join; never join a live monitor unsignalled
|
|
requestMonitorStop(); monitor_thread_.join();
|
|
}
|
|
should_stop_ = false;
|
|
monitor_thread_ = std::thread(&EmbeddedDaemon::monitorProcess, this);
|
|
return;
|
|
}
|
|
if (pid > 0) {
|
|
// Leave it running. Close our end of its output pipe: its further writes fail with EPIPE, which
|
|
// dragonxd ignores (it ignores SIGPIPE), instead of blocking once a pipe nobody drains is full.
|
|
// A detached waiter reaps it when it exits, if this process is still around then.
|
|
DEBUG_LOGF("Leaving dragonxd (pid %d) running; it will finish shutting down on its own\n", pid);
|
|
std::thread([pid] {
|
|
int st = 0;
|
|
while (waitpid(pid, &st, 0) < 0 && errno == EINTR) {}
|
|
}).detach();
|
|
process_pid_ = 0;
|
|
}
|
|
if (stdout_fd_ >= 0) {
|
|
close(stdout_fd_);
|
|
stdout_fd_ = -1;
|
|
}
|
|
setState(State::Stopped, pid > 0 ? "dragonxd left running" : "dragonxd stopped");
|
|
}
|
|
|
|
void EmbeddedDaemon::monitorProcess()
|
|
{
|
|
const auto fast = std::chrono::milliseconds(100);
|
|
const auto cap = std::chrono::milliseconds(500);
|
|
const auto startTime = std::chrono::steady_clock::now();
|
|
auto lastData = startTime;
|
|
auto interval = fast;
|
|
while (!should_stop_) {
|
|
// Check if process exited (non-blocking waitpid captures exit status)
|
|
if (process_pid_ > 0) {
|
|
int status = 0;
|
|
pid_t result = waitpid(process_pid_, &status, WNOHANG);
|
|
if (result == process_pid_) {
|
|
// Process exited — drain remaining output
|
|
drainOutput();
|
|
|
|
const std::string exitInfo = describeExitStatus(status);
|
|
process_pid_ = 0;
|
|
if (stop_requested_) {
|
|
// The stop we asked for (markStopRequested() / stop()) landed before stop() could
|
|
// reap it: a clean stop, not a crash. A crash recorded earlier keeps its count.
|
|
DEBUG_LOGF("dragonxd exited after a requested stop (%s)\n", exitInfo.c_str());
|
|
setState(State::Stopped, "dragonxd stopped");
|
|
return;
|
|
}
|
|
crash_count_++;
|
|
unexpected_exits_++; // before setState(Error): a reader that sees Error sees the new count
|
|
setState(State::Error, "dragonxd exited unexpectedly (" + exitInfo + ")");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Read process output (non-blocking): the stdout/stderr pipe and the node's debug.log
|
|
const size_t before = outputTotalSnapshot();
|
|
drainOutput();
|
|
const auto now = std::chrono::steady_clock::now();
|
|
const bool got = outputTotalSnapshot() != before;
|
|
if (got) lastData = now;
|
|
interval = nextMonitorInterval(interval, got,
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(now - startTime),
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(now - lastData), fast, cap);
|
|
|
|
// Wakes immediately on requestMonitorStop(); idle polling backs off to `cap`.
|
|
std::unique_lock<std::mutex> lk(monitor_mutex_);
|
|
monitor_cv_.wait_for(lk, interval, [this] { return should_stop_.load(); });
|
|
}
|
|
}
|
|
|
|
#endif // _WIN32
|
|
|
|
std::string EmbeddedDaemon::processStartToken(long pid)
|
|
{
|
|
if (pid <= 0) return {};
|
|
#ifdef _WIN32
|
|
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, static_cast<DWORD>(pid));
|
|
if (!h) return {};
|
|
std::string token;
|
|
DWORD code = 0;
|
|
FILETIME created{}, exited{}, kernel{}, user{};
|
|
if (GetExitCodeProcess(h, &code) && code == STILL_ACTIVE &&
|
|
GetProcessTimes(h, &created, &exited, &kernel, &user)) {
|
|
token = std::to_string((static_cast<unsigned long long>(created.dwHighDateTime) << 32) |
|
|
created.dwLowDateTime);
|
|
}
|
|
CloseHandle(h);
|
|
return token;
|
|
#elif defined(__linux__)
|
|
std::ifstream f("/proc/" + std::to_string(pid) + "/stat");
|
|
std::string line;
|
|
if (!f || !std::getline(f, line)) return {};
|
|
char state = 0;
|
|
unsigned long long startTicks = 0;
|
|
if (!parseProcStatStart(line, state, startTicks) || state == 'Z' || state == 'X') return {};
|
|
std::string bootId; // start ticks count from boot: tie them to this boot
|
|
std::ifstream b("/proc/sys/kernel/random/boot_id");
|
|
if (b) std::getline(b, bootId);
|
|
return bootId + "/" + std::to_string(startTicks);
|
|
#elif defined(__APPLE__)
|
|
struct proc_bsdinfo info{};
|
|
if (proc_pidinfo(static_cast<int>(pid), PROC_PIDTBSDINFO, 0, &info, sizeof(info)) != sizeof(info)) return {};
|
|
if (info.pbi_status == 5) return {}; // SZOMB
|
|
return std::to_string(info.pbi_start_tvsec) + "." + std::to_string(info.pbi_start_tvusec);
|
|
#else
|
|
return {};
|
|
#endif
|
|
}
|
|
|
|
std::int64_t EmbeddedDaemon::processStartUtc(long pid)
|
|
{
|
|
if (pid <= 0) return 0;
|
|
#ifdef _WIN32
|
|
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, static_cast<DWORD>(pid));
|
|
if (!h) return 0;
|
|
std::int64_t utc = 0;
|
|
FILETIME created{}, exited{}, kernel{}, user{};
|
|
if (GetProcessTimes(h, &created, &exited, &kernel, &user)) {
|
|
const unsigned long long t = (static_cast<unsigned long long>(created.dwHighDateTime) << 32) |
|
|
created.dwLowDateTime; // 100ns since 1601
|
|
if (t > 116444736000000000ULL) utc = static_cast<std::int64_t>((t - 116444736000000000ULL) / 10000000ULL);
|
|
}
|
|
CloseHandle(h);
|
|
return utc;
|
|
#elif defined(__linux__)
|
|
std::ifstream f("/proc/" + std::to_string(pid) + "/stat");
|
|
std::string line;
|
|
char state = 0;
|
|
unsigned long long startTicks = 0;
|
|
if (!f || !std::getline(f, line) || !parseProcStatStart(line, state, startTicks)) return 0;
|
|
std::ifstream s("/proc/stat"); // "btime <boot time, UTC seconds>"
|
|
std::string key;
|
|
long long btime = 0;
|
|
while (s >> key) {
|
|
if (key == "btime") { s >> btime; break; }
|
|
s.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
|
|
}
|
|
const long hz = sysconf(_SC_CLK_TCK);
|
|
if (btime <= 0 || hz <= 0) return 0;
|
|
return static_cast<std::int64_t>(btime + static_cast<long long>(startTicks / static_cast<unsigned long long>(hz)));
|
|
#elif defined(__APPLE__)
|
|
struct proc_bsdinfo info{};
|
|
if (proc_pidinfo(static_cast<int>(pid), PROC_PIDTBSDINFO, 0, &info, sizeof(info)) != sizeof(info)) return 0;
|
|
return static_cast<std::int64_t>(info.pbi_start_tvsec);
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
bool EmbeddedDaemon::processIsDaemon(long pid)
|
|
{
|
|
if (pid <= 0) return false;
|
|
#ifdef _WIN32
|
|
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, static_cast<DWORD>(pid));
|
|
if (!h) return false;
|
|
char path[MAX_PATH];
|
|
bool is = false;
|
|
// A device path (\Device\HarddiskVolume3\...\dragonxd.exe); only its file name matters.
|
|
const DWORD len = GetProcessImageFileNameA(h, path, MAX_PATH);
|
|
if (len > 0) {
|
|
std::string name(path, len);
|
|
const size_t slash = name.find_last_of("\\/");
|
|
if (slash != std::string::npos) name.erase(0, slash + 1);
|
|
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); });
|
|
is = name == "dragonxd.exe";
|
|
}
|
|
CloseHandle(h);
|
|
return is;
|
|
#elif defined(__linux__)
|
|
const fs::path proc = fs::path("/proc") / std::to_string(pid);
|
|
std::ifstream f((proc / "comm").string());
|
|
std::string comm;
|
|
if (f) std::getline(f, comm);
|
|
std::error_code ec;
|
|
return isDaemonProcessIdentity(comm, fs::read_symlink(proc / "exe", ec).filename().string());
|
|
#elif defined(__APPLE__)
|
|
char path[PROC_PIDPATHINFO_MAXSIZE];
|
|
if (proc_pidpath(static_cast<int>(pid), path, sizeof(path)) <= 0) return false;
|
|
return isDaemonProcessIdentity("", fs::path(path).filename().string());
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
std::string EmbeddedDaemon::ownershipRecordPath()
|
|
{
|
|
return (fs::path(util::Platform::getDragonXDataDir()) / "obsidiandragon-dragonxd.pid").string();
|
|
}
|
|
|
|
size_t EmbeddedDaemon::shrinkResumeOffset(const std::string& content, std::int64_t launchUtc)
|
|
{
|
|
size_t pos = 0;
|
|
while (pos < content.size()) {
|
|
size_t end = content.find('\n', pos);
|
|
if (end == std::string::npos) end = content.size();
|
|
const std::string line = content.substr(pos, end - pos);
|
|
if ((line.find("DragonX version ") != std::string::npos || line.find("Hush version ") != std::string::npos)) {
|
|
const std::int64_t at = parseDaemonLogUtc(line);
|
|
if (at > 0 && at >= launchUtc - 5) return pos; // 5s: the log's second vs our clock read
|
|
}
|
|
pos = end + 1;
|
|
}
|
|
return content.size();
|
|
}
|
|
|
|
void EmbeddedDaemon::writeOwnershipRecord(const std::string& path, const OwnershipRecord& r)
|
|
{
|
|
const std::string tmp = path + ".tmp";
|
|
{
|
|
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
|
|
if (!out) return;
|
|
out << formatOwnershipRecord(r);
|
|
if (!out) return;
|
|
}
|
|
std::error_code ec;
|
|
fs::rename(tmp, path, ec);
|
|
if (ec) fs::remove(tmp, ec);
|
|
else DEBUG_LOGF("[INFO] Recorded our dragonxd (pid %ld) in %s\n", r.pid, path.c_str());
|
|
}
|
|
|
|
bool EmbeddedDaemon::liveOwnershipRecord(const std::string& path, OwnershipRecord* out)
|
|
{
|
|
std::string text;
|
|
{
|
|
std::ifstream in(path, std::ios::binary);
|
|
if (!in) return false;
|
|
text.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
|
}
|
|
OwnershipRecord r;
|
|
if (parseOwnershipRecord(text, r) && processStartToken(r.pid) == r.startToken) {
|
|
if (out) *out = r;
|
|
return true;
|
|
}
|
|
std::error_code ec; // that process is gone (or the pid now belongs to another): the record is stale
|
|
if (fs::remove(path, ec)) DEBUG_LOGF("[INFO] Removed stale dragonxd record (pid %ld)\n", r.pid);
|
|
return false;
|
|
}
|
|
|
|
bool EmbeddedDaemon::isRpcPortInUse()
|
|
{
|
|
int port = std::atoi(DRAGONX_DEFAULT_RPC_PORT);
|
|
return isPortInUse(port);
|
|
}
|
|
|
|
bool EmbeddedDaemon::tcpPortInUse(int port)
|
|
{
|
|
return isPortInUse(port);
|
|
}
|
|
|
|
bool EmbeddedDaemon::isDaemonProcessRunning()
|
|
{
|
|
#ifdef _WIN32
|
|
return findProcessByName("dragonxd.exe") != 0;
|
|
#elif defined(__linux__)
|
|
// Scan /proc for a dragonxd process (by comm or by executable: see isDaemonProcessIdentity). Iterate
|
|
// with an error_code so a proc entry vanishing mid-scan (a process exiting) can't throw.
|
|
std::error_code ec;
|
|
fs::directory_iterator it("/proc", ec), end;
|
|
for (; !ec && it != end; it.increment(ec)) {
|
|
const std::string pid = it->path().filename().string();
|
|
if (pid.empty() || pid[0] < '0' || pid[0] > '9') continue; // numeric pid dirs only
|
|
std::ifstream f((it->path() / "comm").string());
|
|
std::string comm;
|
|
if (f) std::getline(f, comm);
|
|
std::error_code linkEc; // another user's process: EACCES, leaving the name empty
|
|
const std::string exe = fs::read_symlink(it->path() / "exe", linkEc).filename().string();
|
|
if (isDaemonProcessIdentity(comm, exe)) return true;
|
|
}
|
|
return false;
|
|
#elif defined(__APPLE__)
|
|
// Match executables named dragonxd. Not the RPC port: dragonxd frees it early in its shutdown, long
|
|
// before the process exits and lets go of wallet.dat and the datadir lock.
|
|
const int count = proc_listallpids(nullptr, 0);
|
|
if (count <= 0) return isPortInUse(std::atoi(DRAGONX_DEFAULT_RPC_PORT)); // can't list: best effort
|
|
std::vector<pid_t> pids(static_cast<size_t>(count) + 64); // room for processes started meanwhile
|
|
const int n = proc_listallpids(pids.data(), static_cast<int>(pids.size() * sizeof(pid_t)));
|
|
char path[PROC_PIDPATHINFO_MAXSIZE];
|
|
for (int i = 0; i < n && i < static_cast<int>(pids.size()); ++i) {
|
|
if (pids[i] <= 0 || proc_pidpath(pids[i], path, sizeof(path)) <= 0) continue;
|
|
if (isDaemonProcessIdentity("", fs::path(path).filename().string())) return true;
|
|
}
|
|
return false;
|
|
#else
|
|
return isPortInUse(std::atoi(DRAGONX_DEFAULT_RPC_PORT));
|
|
#endif
|
|
}
|
|
|
|
} // namespace daemon
|
|
} // namespace dragonx
|