// 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 #include #include #include #include #include #include #include #include #include #include #include #include "../util/logger.h" #ifdef _WIN32 #include #include #include #include #include #include #include #else #include #include #include #include #include #include #include #include #include #ifdef __APPLE__ #include #include #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 localPaths = { exe_dir + "\\dragonxd.exe", }; #else std::vector 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 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 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(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((static_cast(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(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 /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 lk(monitor_mutex_); should_stop_ = true; } monitor_cv_.notify_all(); } size_t EmbeddedDaemon::outputTotalSnapshot() const { std::lock_guard 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 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 EmbeddedDaemon::getRecentLines(int maxLines) const { std::vector lines; std::lock_guard 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 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(buf.data()); DWORD ownerPid = 0; for (DWORD i = 0; i < table->dwNumEntries; i++) { auto& row = table->table[i]; int rowPort = ntohs(static_cast(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(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 = ""; 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(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 = ""; 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(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(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(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(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(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 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(in), std::istreambuf_iterator()); 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 /. 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 /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); launch_utc_ = static_cast(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(GetProcessId(process_handle_)) : 0; #else const long pid = static_cast(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& 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 /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(pmc.WorkingSetSize) / (1024.0 * 1024.0); } return 0.0; } std::function 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(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(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(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(std::min(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 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 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::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 life(lifecycle_mutex_); if (monitor_thread_.joinable()) { monitor_thread_.join(); } } setState(State::Stopped, "dragonxd stopped"); return exitedOnItsOwn; } bool EmbeddedDaemon::beginStop() { std::lock_guard 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(ft.dwHighDateTime) << 32) | ft.dwLowDateTime; }; usage.cpuSeconds = static_cast(ticks(kernel) + ticks(user)) / 1e7; // 100ns units } IO_COUNTERS io; if (GetProcessIoCounters(process_handle_, &io)) usage.ioBytes = static_cast(io.ReadTransferCount + io.WriteTransferCount); return usage; } void EmbeddedDaemon::endStop(bool keepWatching) { std::lock_guard 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(now - startTime), std::chrono::duration_cast(now - lastData), fast, cap); std::unique_lock 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(logSize) > debug_log_offset_) ? static_cast(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 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& 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 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("/bin/bash")); argv.push_back(const_cast(binary_path.c_str())); } else { argv.push_back(const_cast(binary_path.c_str())); } for (const auto& arg : args) { argv.push_back(const_cast(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(&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(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(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(rss_kb) / 1024.0; } pclose(fp); return total_rss_mb; #else // Linux: sum VmRSS for every /proc/ 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(name[0]))) continue; // Read process group from /proc//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//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(rss_kb) / 1024.0; break; } } fclose(f); } return total_rss_mb; #endif // __APPLE__ / Linux } } // namespace std::function 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(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(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(debug_log_offset_)); if (n <= 0) break; buffer[n] = '\0'; { std::lock_guard lk(output_mutex_); appendOutput(buffer, static_cast(n)); } VERBOSE_LOGF("[dragonxd] %s", buffer); debug_log_offset_ += static_cast(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 lk(output_mutex_); appendOutput(buffer, static_cast(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 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 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(value); anyIo = true; } } } const long hz = sysconf(_SC_CLK_TCK); if (anyCpu && hz > 0) usage.cpuSeconds = static_cast(ticks) / static_cast(hz); if (anyIo) usage.ioBytes = io; return usage; #endif } void EmbeddedDaemon::endStop(bool keepWatching) { std::lock_guard 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(now - startTime), std::chrono::duration_cast(now - lastData), fast, cap); // Wakes immediately on requestMonitorStop(); idle polling backs off to `cap`. std::unique_lock 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(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(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(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(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(created.dwHighDateTime) << 32) | created.dwLowDateTime; // 100ns since 1601 if (t > 116444736000000000ULL) utc = static_cast((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 " std::string key; long long btime = 0; while (s >> key) { if (key == "btime") { s >> btime; break; } s.ignore(std::numeric_limits::max(), '\n'); } const long hz = sysconf(_SC_CLK_TCK); if (btime <= 0 || hz <= 0) return 0; return static_cast(btime + static_cast(startTicks / static_cast(hz))); #elif defined(__APPLE__) struct proc_bsdinfo info{}; if (proc_pidinfo(static_cast(pid), PROC_PIDTBSDINFO, 0, &info, sizeof(info)) != sizeof(info)) return 0; return static_cast(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(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(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(in), std::istreambuf_iterator()); } 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 pids(static_cast(count) + 64); // room for processes started meanwhile const int n = proc_listallpids(pids.data(), static_cast(pids.size() * sizeof(pid_t))); char path[PROC_PIDPATHINFO_MAXSIZE]; for (int i = 0; i < n && i < static_cast(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