// 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 "../config/version.h" #include "../resources/embedded_resources.h" #include "../util/platform.h" #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 #ifdef __APPLE__ #include #endif #endif namespace fs = std::filesystem; namespace dragonx { namespace daemon { EmbeddedDaemon::EmbeddedDaemon() = default; EmbeddedDaemon::~EmbeddedDaemon() { stop(3000); // Wait up to 3 seconds for clean shutdown } 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()); } return { "-tls=only", #ifndef _WIN32 "-printtoconsole", #endif "-clientname=ObsidianDragon", "-ac_name=DRAGONX", "-ac_algo=randomx", "-ac_halving=3500000", "-ac_reward=300000000", "-ac_blocktime=36", "-ac_private=1", "-addnode=node.dragonx.is", "-addnode=node1.dragonx.is", "-addnode=node2.dragonx.is", "-addnode=node3.dragonx.is", "-addnode=node4.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::setState(State s, const std::string& message) { state_ = s; if (!message.empty()) { if (s == State::Error) { last_error_ = message; } } 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_ process_output_.append(data, len); if (process_output_.size() > MAX_OUTPUT_BYTES + MAX_OUTPUT_BYTES / 4) { // Trim to MAX_OUTPUT_BYTES, cutting at a newline boundary size_t trim_to = process_output_.size() - MAX_OUTPUT_BYTES; size_t nl = process_output_.find('\n', trim_to); if (nl != std::string::npos && nl < process_output_.size() - 1) { process_output_.erase(0, nl + 1); } else { process_output_.erase(0, trim_to); } } } 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) static bool isPortInUse(int port) { #ifdef _WIN32 WSADATA wsa; if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false; SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock == INVALID_SOCKET) { WSACleanup(); return false; } struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(static_cast(port)); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr)); closesocket(sock); WSACleanup(); return (result == 0); #else // On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp to avoid // creating sockets. Fall back to connect() if /proc is unavailable. FILE* fp = fopen("/proc/net/tcp", "r"); if (fp) { char line[256]; 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; } // Fallback (macOS): try to connect int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) return false; 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 = htonl(INADDR_LOOPBACK); int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr)); close(sock); return (result == 0); #endif } bool EmbeddedDaemon::start(const std::string& binary_path) { // 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; } // 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; 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_)); } // 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). -zapwallettxes=2 wipes all wallet tx/note // records and rebuilds them from the chain; it implies -rescan, so don't also pass -rescan. if (zap_on_next_start_.exchange(false)) { 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"); } // 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); override_datadir_.clear(); override_extra_args_.clear(); if (!startProcess(daemon_path, args)) { DEBUG_LOGF("[ERROR] Failed to start dragonxd process: %s\\n", last_error_.c_str()); setState(State::Error, "Failed to start dragonxd process"); return false; } setState(State::Running, "dragonxd started"); DEBUG_LOGF("[INFO] dragonxd process started successfully\\n"); // Start monitor thread (if not already running) should_stop_ = false; if (!monitor_thread_.joinable()) { monitor_thread_ = std::thread(&EmbeddedDaemon::monitorProcess, this); } return true; } #ifdef _WIN32 // Forward declaration — defined after startProcess static DWORD findProcessByName(const char* name); bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vector& args) { // Build command line std::string cmd = "\"" + binary_path + "\""; for (const auto& arg : args) { cmd += " " + 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); } } // Determine debug.log path for output tailing. // The daemon writes to %APPDATA%\Hush\DRAGONX\debug.log when -printtoconsole is NOT used. { char appdata[MAX_PATH]; if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata))) { debug_log_path_ = std::string(appdata) + "\\Hush\\DRAGONX\\debug.log"; } else { const char* home = getenv("APPDATA"); if (home) { debug_log_path_ = std::string(home) + "\\Hush\\DRAGONX\\debug.log"; } } // Record current file size so we only read NEW output from this launch if (!debug_log_path_.empty() && fs::exists(debug_log_path_)) { debug_log_offset_ = static_cast(fs::file_size(debug_log_path_)); } else { debug_log_offset_ = 0; } DEBUG_LOGF("[INFO] Will tail daemon output from: %s (offset %zu)\n", debug_log_path_.c_str(), debug_log_offset_); } // Launch daemon with CREATE_NEW_CONSOLE (hidden via SW_HIDE). // 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_NEW_CONSOLE, 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; PROCESSENTRY32 entry; entry.dwSize = sizeof(entry); DWORD pid = 0; if (Process32First(snap, &entry)) { do { if (_stricmp(entry.szExeFile, name) == 0) { pid = entry.th32ProcessID; break; } } while (Process32Next(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; } 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); 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); } void EmbeddedDaemon::stop(int wait_ms) { should_stop_ = true; 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 } }; 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); } drainOutput(); CloseHandle(process_handle_); process_handle_ = nullptr; } else { // Tracked handle is dead (batch file case: cmd.exe already exited). // The real dragonxd.exe may still be running as an orphan. if (process_handle_ != nullptr) { CloseHandle(process_handle_); process_handle_ = nullptr; } // Find the real dragonxd.exe process by name DWORD dragonxd_pid = findProcessByName("dragonxd.exe"); if (dragonxd_pid != 0) { DEBUG_LOGF("Found orphaned dragonxd.exe (PID %lu) — waiting for RPC stop to take effect...\n", dragonxd_pid); HANDLE hProc = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, dragonxd_pid); if (hProc) { // RPC stop was already sent — wait for graceful exit if (!pollWait(hProc, wait_ms)) { DEBUG_LOGF("Timeout — forcing dragonxd.exe (PID %lu) termination...\n", dragonxd_pid); TerminateProcess(hProc, 1); WaitForSingleObject(hProc, 2000); } drainOutput(); CloseHandle(hProc); DEBUG_LOGF("dragonxd.exe stopped\n"); } else { DEBUG_LOGF("Could not open dragonxd.exe process (PID %lu), error %lu\n", dragonxd_pid, GetLastError()); } } else { DEBUG_LOGF("No dragonxd.exe process found — daemon may have already exited\n"); } } if (stdout_read_ != nullptr) { CloseHandle(stdout_read_); stdout_read_ = nullptr; } // Final drain of debug.log drainOutput(); if (monitor_thread_.joinable()) { monitor_thread_.join(); } setState(State::Stopped, "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() { while (!should_stop_ && isRunning()) { // Tail debug.log for new output drainOutput(); std::this_thread::sleep_for(std::chrono::milliseconds(250)); } // Read any remaining output after process exits drainOutput(); // 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_++; // 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; } pid_t pid = fork(); if (pid == -1) { last_error_ = "Fork failed: " + std::string(strerror(errno)); close(pipefd[0]); close(pipefd[1]); return false; } if (pid == 0) { // Child process close(pipefd[0]); // Close read end // 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, exec failed fprintf(stderr, "execv failed: %s\n", strerror(errno)); _exit(127); } // Parent process close(pipefd[1]); // Close write end stdout_fd_ = pipefd[0]; // Also set process group from parent side (race with child's setpgid) setpgid(pid, pid); // Set non-blocking int flags = fcntl(stdout_fd_, F_GETFL, 0); fcntl(stdout_fd_, F_SETFL, flags | O_NONBLOCK); process_pid_ = pid; return true; } double EmbeddedDaemon::getMemoryUsageMB() const { if (process_pid_ <= 0) return 0.0; #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", process_pid_); 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: The tracked PID is often a bash wrapper script; the real daemon // (dragonxd) is a child in the same process group. Sum VmRSS for every // process whose PGID matches our tracked PID. double total_rss_mb = 0.0; // Iterate /proc//stat for all numeric entries for (const auto& entry : fs::directory_iterator("/proc")) { if (!entry.is_directory()) continue; const std::string name = entry.path().filename().string(); if (name.empty() || !std::isdigit(name[0])) continue; // Read process group from /proc//stat (field 5 is pgrp) std::string statPath = entry.path().string() + "/stat"; FILE* sf = fopen(statPath.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 != process_pid_) continue; // This process belongs to our group — read its VmRSS std::string statusPath = entry.path().string() + "/status"; FILE* f = fopen(statusPath.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 } bool EmbeddedDaemon::isRunning() const { if (process_pid_ <= 0) return false; int status; pid_t result = waitpid(process_pid_, &status, WNOHANG); if (result == 0) { // Still running return true; } return false; } void EmbeddedDaemon::drainOutput() { if (stdout_fd_ < 0) return; 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); } } void EmbeddedDaemon::stop(int wait_ms) { should_stop_ = true; if (process_pid_ > 0) { setState(State::Stopping, "Stopping dragonxd..."); // 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); auto start = std::chrono::steady_clock::now(); while (isRunning()) { drainOutput(); auto elapsed = std::chrono::duration_cast( std::chrono::steady_clock::now() - start).count(); if (elapsed >= wait_ms) { break; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } // Phase 2: If still running, send SIGTERM and wait a further 10s. if (isRunning()) { DEBUG_LOGF("Daemon did not exit gracefully — sending SIGTERM to process group -%d\n", process_pid_); kill(-process_pid_, SIGTERM); auto sigterm_start = std::chrono::steady_clock::now(); while (isRunning()) { drainOutput(); auto elapsed = std::chrono::duration_cast( std::chrono::steady_clock::now() - sigterm_start).count(); if (elapsed >= 10000) { // Phase 3: Force kill DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_); kill(-process_pid_, SIGKILL); break; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } else { DEBUG_LOGF("Daemon exited cleanly after RPC stop\n"); } drainOutput(); // read any remaining output // Reap the child process int status; waitpid(process_pid_, &status, 0); process_pid_ = 0; } if (stdout_fd_ >= 0) { close(stdout_fd_); stdout_fd_ = -1; } if (monitor_thread_.joinable()) { monitor_thread_.join(); } setState(State::Stopped, "dragonxd stopped"); } void EmbeddedDaemon::monitorProcess() { char buffer[4096]; 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(); crash_count_++; std::string exitInfo; if (WIFEXITED(status)) { exitInfo = "exit code: " + std::to_string(WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { exitInfo = "killed by signal " + std::to_string(WTERMSIG(status)); } else { exitInfo = "unknown exit status"; } process_pid_ = 0; setState(State::Error, "dragonxd exited unexpectedly (" + exitInfo + ")"); return; } } // Read process output (non-blocking) ssize_t bytes_read = read(stdout_fd_, buffer, sizeof(buffer) - 1); if (bytes_read > 0) { buffer[bytes_read] = '\0'; { std::lock_guard lk(output_mutex_); appendOutput(buffer, static_cast(bytes_read)); } VERBOSE_LOGF("[dragonxd] %s", buffer); } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } #endif // _WIN32 bool EmbeddedDaemon::isRpcPortInUse() { int port = std::atoi(DRAGONX_DEFAULT_RPC_PORT); return isPortInUse(port); } bool EmbeddedDaemon::tcpPortInUse(int port) { return isPortInUse(port); } } // namespace daemon } // namespace dragonx