Files
ObsidianDragon/src/daemon/embedded_daemon.cpp
DanS e8a8ce68b2 feat(wallet): wallet-files list + switching (P2)
Second phase of multi-wallet: list wallet files and switch the active one.

- Daemon plumbing: EmbeddedDaemon::setWalletFile -> start() passes -wallet=<name>
  (non-default only; skipped during the isolated seed-migration start), and
  DaemonController::syncSettings pushes active_wallet_file on each start.
- App::switchToWallet: persist the new active wallet, then stop + restart the
  node on -wallet=<name> (rescan only if it was never synced in this datadir).
  Reuses the seed-adopt restart coordination; WalletState clears on disconnect
  and the P1 identity-scoped caches re-key, so no previous-wallet data leaks.
  Guarded: full-node, embedded daemon, not mid-restart, no pending send.
- Wallets dialog (Settings -> Backup & Data -> "Wallets…"): a table of wallet
  files (datadir wallet*.dat + user-added folders' *.dat) with size (disk) and
  cached balance / address count / last-opened (wallet index), a current/Active
  badge, Open (switch), and Add folder. Out-of-datadir wallets show "import to
  open" (P3). Added as a sweep surface.
- Mining payout safety: non-destructive warning if the pool-mode worker looks
  like a DragonX address not in the current wallet (stale after a switch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 16:20:41 -05:00

1252 lines
44 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// embedded_daemon.cpp — Manages the dragonxd child process lifecycle:
// binary discovery, process spawn, stdout/stderr monitoring, crash recovery.
#include "embedded_daemon.h"
#include "../config/version.h"
#include "../resources/embedded_resources.h"
#include "../util/platform.h"
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <chrono>
#include <cstring>
#include <algorithm>
#include <vector>
#include "../util/logger.h"
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <psapi.h>
#include <tlhelp32.h>
#include <shlobj.h>
#include <iphlpapi.h>
#else
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#ifdef __APPLE__
#include <sys/sysctl.h>
#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<std::string> localPaths = {
exe_dir + "\\dragonxd.exe",
};
#else
std::vector<std::string> localPaths = {
exe_dir + "/dragonxd",
};
#endif
for (const auto& path : localPaths) {
if (fs::exists(path)) {
DEBUG_LOGF("[INFO] Found daemon in wallet directory: %s\n", path.c_str());
return path;
}
}
}
// ---------------------------------------------------------------
// 2. Check if we have an embedded daemon that needs extraction
// ---------------------------------------------------------------
std::string embeddedPath = resources::getDaemonPath();
if (!embeddedPath.empty() && fs::exists(embeddedPath)) {
DEBUG_LOGF("[INFO] Using extracted daemon at: %s\n", embeddedPath.c_str());
return embeddedPath;
}
// ---------------------------------------------------------------
// 3. Search additional well-known locations
// ---------------------------------------------------------------
// IMPORTANT: Always prefer dragonxd.exe directly over .bat wrappers.
// Using .bat files causes issues because cmd.exe exits immediately
// while dragonxd.exe continues running, making process monitoring fail.
std::vector<std::string> search_paths;
#ifdef _WIN32
// Parent directory
if (!exe_dir.empty()) {
search_paths.push_back(exe_dir + "\\..\\dragonxd.exe");
}
search_paths.push_back("C:\\Program Files\\DragonX\\dragonxd.exe");
#else
if (!exe_dir.empty()) {
search_paths.push_back(exe_dir + "/../dragonxd");
}
// Standard Linux locations
search_paths.push_back("/usr/local/bin/dragonxd");
search_paths.push_back("/usr/bin/dragonxd");
// Home directory
const char* home = getenv("HOME");
if (home) {
search_paths.push_back(std::string(home) + "/dragonx/src/dragonxd");
search_paths.push_back(std::string(home) + "/bin/dragonxd");
}
#ifdef __APPLE__
// macOS app bundle
search_paths.push_back("/Applications/DragonX.app/Contents/MacOS/dragonxd");
#endif
#endif
// Check each path
for (const auto& path : search_paths) {
if (fs::exists(path)) {
DEBUG_LOGF("[INFO] Found daemon launcher at: %s\n", path.c_str());
return path;
}
}
DEBUG_LOGF("[ERROR] Daemon launcher not found in any standard location\n");
return "";
}
std::vector<std::string> EmbeddedDaemon::getChainParams()
{
// DragonX chain parameters.
// On Windows, omit -printtoconsole: we tail debug.log instead of piping stdout.
// On Linux, -printtoconsole is used for pipe-based output capture.
// Auto-detect a reasonable -dbcache based on available physical RAM.
// Default LevelDB cache is small (~450MB); larger caches improve sync
// performance and reduce disk I/O — especially on macOS with APFS.
std::string dbcache_arg = "-dbcache=450";
{
#ifdef __APPLE__
// sysctl hw.memsize returns total physical RAM in bytes
int64_t memsize = 0;
size_t len = sizeof(memsize);
if (sysctlbyname("hw.memsize", &memsize, &len, nullptr, 0) == 0 && memsize > 0) {
int totalMB = static_cast<int>(memsize / (1024 * 1024));
// Use ~12.5% of RAM for dbcache, clamped to [450, 4096]
int cache = std::max(450, std::min(4096, totalMB / 8));
dbcache_arg = "-dbcache=" + std::to_string(cache);
}
#elif defined(__linux__)
long pages = sysconf(_SC_PHYS_PAGES);
long page_size = sysconf(_SC_PAGE_SIZE);
if (pages > 0 && page_size > 0) {
int totalMB = static_cast<int>((static_cast<int64_t>(pages) * page_size) / (1024 * 1024));
int cache = std::max(450, std::min(4096, totalMB / 8));
dbcache_arg = "-dbcache=" + std::to_string(cache);
}
#elif defined(_WIN32)
MEMORYSTATUSEX memInfo;
memInfo.dwLength = sizeof(memInfo);
if (GlobalMemoryStatusEx(&memInfo)) {
int totalMB = static_cast<int>(memInfo.ullTotalPhys / (1024 * 1024));
int cache = std::max(450, std::min(4096, totalMB / 8));
dbcache_arg = "-dbcache=" + std::to_string(cache);
}
#endif
DEBUG_LOGF("[INFO] Using %s\n", dbcache_arg.c_str());
}
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<std::string> EmbeddedDaemon::getRecentLines(int maxLines) const
{
std::vector<std::string> lines;
std::lock_guard<std::mutex> lk(output_mutex_);
const std::string& out = process_output_;
if (out.empty()) return lines;
// Walk backwards collecting up to maxLines newline-delimited lines
size_t end = out.size();
// Skip trailing newline
if (end > 0 && out[end - 1] == '\n') --end;
for (int i = 0; i < maxLines && end > 0; ++i) {
size_t nl = out.rfind('\n', end - 1);
size_t start = (nl == std::string::npos) ? 0 : nl + 1;
std::string line = out.substr(start, end - start);
if (!line.empty()) lines.push_back(std::move(line));
end = (nl == std::string::npos) ? 0 : nl;
}
// Reverse so oldest is first
std::reverse(lines.begin(), lines.end());
return lines;
}
// Identify what process owns a given TCP port.
// Returns a string like "PID 1234 (dragonxd.exe)" or "unknown" on failure.
static std::string getPortOwnerInfo(int port)
{
#ifdef _WIN32
DWORD size = 0;
// First call to get required buffer size
GetExtendedTcpTable(nullptr, &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0);
if (size == 0) return "unknown (GetExtendedTcpTable size query failed)";
std::vector<BYTE> buf(size);
DWORD ret = GetExtendedTcpTable(buf.data(), &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0);
if (ret != NO_ERROR) return "unknown (GetExtendedTcpTable failed, error " + std::to_string(ret) + ")";
auto* table = reinterpret_cast<MIB_TCPTABLE_OWNER_PID*>(buf.data());
DWORD ownerPid = 0;
for (DWORD i = 0; i < table->dwNumEntries; i++) {
auto& row = table->table[i];
int rowPort = ntohs(static_cast<u_short>(row.dwLocalPort));
// Match port in LISTEN state (MIB_TCP_STATE_LISTEN = 2)
if (rowPort == port && row.dwState == MIB_TCP_STATE_LISTEN) {
ownerPid = row.dwOwningPid;
break;
}
}
if (ownerPid == 0) {
// Maybe it's in ESTABLISHED or another state from our connect probe — try any state
for (DWORD i = 0; i < table->dwNumEntries; i++) {
auto& row = table->table[i];
int rowPort = ntohs(static_cast<u_short>(row.dwLocalPort));
if (rowPort == port && row.dwOwningPid != 0) {
ownerPid = row.dwOwningPid;
break;
}
}
}
if (ownerPid == 0) return "unknown (no PID found for port " + std::to_string(port) + ")";
// Resolve PID to process name
std::string procName = "<unknown>";
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap != INVALID_HANDLE_VALUE) {
PROCESSENTRY32 pe;
pe.dwSize = sizeof(pe);
if (Process32First(snap, &pe)) {
do {
if (pe.th32ProcessID == ownerPid) {
procName = pe.szExeFile;
break;
}
} while (Process32Next(snap, &pe));
}
CloseHandle(snap);
}
return "PID " + std::to_string(ownerPid) + " (" + procName + ")";
#else
// Linux: parse /proc/net/tcp to find the inode, then scan /proc/*/fd
FILE* fp = fopen("/proc/net/tcp", "r");
if (!fp) return "unknown (cannot read /proc/net/tcp)";
char line[512];
unsigned long inode = 0;
while (fgets(line, sizeof(line), fp)) {
unsigned int localPort, state;
unsigned long lineInode;
if (sscanf(line, " %*d: %*X:%X %*X:%*X %X %*X:%*X %*X:%*X %*X %*u %*u %lu",
&localPort, &state, &lineInode) == 3) {
if (static_cast<int>(localPort) == port && state == 0x0A) { // 0x0A = LISTEN
inode = lineInode;
break;
}
}
}
fclose(fp);
if (inode == 0) return "unknown (no listener found for port " + std::to_string(port) + ")";
// Scan /proc/*/fd/* for the matching inode
for (const auto& entry : fs::directory_iterator("/proc")) {
if (!entry.is_directory()) continue;
std::string pidStr = entry.path().filename().string();
if (pidStr.empty() || !std::isdigit(pidStr[0])) continue;
std::string fdDir = "/proc/" + pidStr + "/fd";
try {
for (const auto& fdEntry : fs::directory_iterator(fdDir)) {
char target[512];
ssize_t len = readlink(fdEntry.path().c_str(), target, sizeof(target) - 1);
if (len > 0) {
target[len] = '\0';
std::string t(target);
if (t.find("socket:[" + std::to_string(inode) + "]") != std::string::npos) {
// Found the PID, now get the process name
std::string commPath = "/proc/" + pidStr + "/comm";
FILE* cf = fopen(commPath.c_str(), "r");
std::string procName = "<unknown>";
if (cf) {
char name[256];
if (fgets(name, sizeof(name), cf)) {
procName = name;
while (!procName.empty() && procName.back() == '\n') procName.pop_back();
}
fclose(cf);
}
return "PID " + pidStr + " (" + procName + ")";
}
}
}
} catch (...) { /* permission denied — skip */ }
}
return "unknown (inode " + std::to_string(inode) + " found but no matching PID)";
#endif
}
// Check if a TCP port is already in use (something is LISTENING)
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<u_short>(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<unsigned int>(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<uint16_t>(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 <datadir>/<name>. Only pass it for a
// non-default name so the common case's command line is unchanged; skip during an isolated
// start (seed migration manages its own throwaway wallet).
if (!wallet_file_.empty() && wallet_file_ != "wallet.dat" && override_datadir_.empty()) {
DEBUG_LOGF("[INFO] Loading wallet file: %s\n", wallet_file_.c_str());
args.push_back("-wallet=" + wallet_file_);
}
// Add wallet-repair flag if requested (one-shot). -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 <datadir>/DRAGONX.conf automatically, so
// no -conf is passed (an explicit -conf confuses the Komodo/Hush path resolution).
if (!override_datadir_.empty()) {
DEBUG_LOGF("[INFO] Isolated start override: -datadir=%s\n", override_datadir_.c_str());
args.push_back("-datadir=" + override_datadir_);
}
for (const auto& a : override_extra_args_) args.push_back(a);
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<std::string>& 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<size_t>(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 <exe_dir>/dragonx/ to avoid conflicts with lock files and data.
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
ZeroMemory(&pi, sizeof(pi));
char* cmd_line = _strdup(cmd.c_str());
BOOL success = CreateProcessA(
NULL,
cmd_line,
NULL,
NULL,
FALSE,
CREATE_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<double>(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<size_t>(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<LONGLONG>(debug_log_offset_);
SetFilePointerEx(hFile, seekPos, NULL, FILE_BEGIN);
// Read new content in chunks
char buffer[4096];
DWORD bytes_read;
while (debug_log_offset_ < currentSize) {
DWORD toRead = static_cast<DWORD>(std::min<size_t>(sizeof(buffer) - 1, currentSize - debug_log_offset_));
if (!ReadFile(hFile, buffer, toRead, &bytes_read, NULL) || bytes_read == 0) break;
buffer[bytes_read] = '\0';
{
std::lock_guard<std::mutex> lk(output_mutex_);
appendOutput(buffer, bytes_read);
}
VERBOSE_LOGF("[dragonxd] %s", buffer);
debug_log_offset_ += bytes_read;
}
CloseHandle(hFile);
}
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::milliseconds>(
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<size_t>(logSize) > debug_log_offset_)
? static_cast<size_t>(logSize) - debug_log_offset_ : 0;
char logBuf[64];
snprintf(logBuf, sizeof(logBuf), "%zu", newBytes);
error_msg += "\n debug.log new bytes: " + std::string(logBuf);
}
}
// Include daemon output from debug.log
{
std::lock_guard<std::mutex> lk(output_mutex_);
if (!process_output_.empty()) {
// Get last ~500 chars of output for context
std::string last_output = process_output_;
if (last_output.size() > 500) {
last_output = "..." + last_output.substr(last_output.size() - 500);
}
error_msg += "\n\nDaemon output:\n" + last_output;
}
}
setState(State::Error, error_msg);
}
}
#else // Linux/macOS
bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vector<std::string>& args)
{
// Create pipe for stdout/stderr
int pipefd[2];
if (pipe(pipefd) == -1) {
last_error_ = "Failed to create pipe: " + std::string(strerror(errno));
return false;
}
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<char*> argv;
// Check if this is a shell script
bool is_script = false;
if (binary_path.size() >= 3) {
std::string ext = binary_path.substr(binary_path.size() - 3);
if (ext == ".sh" || binary_path.find("dragonxd") != std::string::npos) {
// Check if it's a script by looking at first bytes
FILE* f = fopen(binary_path.c_str(), "r");
if (f) {
char buf[2] = {0};
if (fread(buf, 1, 2, f) == 2 && buf[0] == '#' && buf[1] == '!') {
is_script = true;
}
fclose(f);
}
}
}
if (is_script) {
// Run shell scripts through bash
argv.push_back(const_cast<char*>("/bin/bash"));
argv.push_back(const_cast<char*>(binary_path.c_str()));
} else {
argv.push_back(const_cast<char*>(binary_path.c_str()));
}
for (const auto& arg : args) {
argv.push_back(const_cast<char*>(arg.c_str()));
}
argv.push_back(nullptr);
// Execute using execvp for PATH resolution
if (is_script) {
execv("/bin/bash", argv.data());
} else {
execv(binary_path.c_str(), argv.data());
}
// If we get here, 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<double>(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/<pid>/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/<pid>/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/<pid>/stat are space-separated, but field 2 (comm)
// is wrapped in parens and may contain spaces. Skip past ')'.
const char* p = strrchr(statLine, ')');
if (!p) continue;
p++; // skip ')'
// After ')' the fields are: state(3), ppid(4), pgrp(5), ...
int pgrp = 0;
char state;
int ppid;
if (sscanf(p, " %c %d %d", &state, &ppid, &pgrp) != 3) continue;
if (pgrp != 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<double>(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<std::mutex> lk(output_mutex_);
appendOutput(buffer, static_cast<size_t>(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::milliseconds>(
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::milliseconds>(
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<std::mutex> lk(output_mutex_);
appendOutput(buffer, static_cast<size_t>(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