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>
1252 lines
44 KiB
C++
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
|