// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #else #include #endif namespace dragonx { namespace daemon { /** * @brief Manages the embedded dragonxd process * * Handles starting, stopping, and monitoring the dragonxd daemon process. * Ports functionality from Qt version's connection.cpp. */ class EmbeddedDaemon { public: enum class State { Stopped, Starting, Running, Stopping, Error }; using StateCallback = std::function; EmbeddedDaemon(); ~EmbeddedDaemon(); // Non-copyable EmbeddedDaemon(const EmbeddedDaemon&) = delete; EmbeddedDaemon& operator=(const EmbeddedDaemon&) = delete; /** * @brief Start the embedded dragonxd daemon * @param binary_path Path to dragonxd binary (auto-detect if empty) * @return true if process started successfully */ bool start(const std::string& binary_path = ""); /** * @brief Stop the daemon process this object launched (never any other dragonxd) * @param wait_ms Maximum milliseconds to wait for graceful shutdown * @return true if it exited on its own (or there was nothing to stop), false if it had to be * force-killed — then its wallet/chain files may not have been closed cleanly */ bool stop(int wait_ms = 5000); /** * @brief Record that we are about to ask the daemon to stop (e.g. over RPC) * * Call it before sending the stop. If the daemon then exits before stop() runs, the monitor * records a clean stop instead of a crash (crash_count_ drives auto-restart). stop() sets it * too; start() clears it. */ void markStopRequested() { stop_requested_ = true; } // --- A stop session, driven by DaemonController::executeDaemonStop through the app's StopRuntime --- /** Marks the stop as requested and stops the monitor, so the session is the only reaper. * @return true if a process this object launched is still alive; then finish with endStop(). */ bool beginStop(); /** Polls our own process (reaping it on POSIX) and drains its output; false once it has exited. */ bool pollOwnedProcess(); /** POSIX: SIGTERM our process group (dragonxd treats it like RPC stop). Windows has no such channel. */ bool signalGracefulStop(); /** Ends our own process by force (SIGKILL / TerminateProcess) and reaps it. Explicit user choice only. */ void forceKillOwnedProcess(); struct ResourceUsage { double cpuSeconds = -1.0; // cumulative; < 0 if unknown double ioBytes = -1.0; // cumulative storage I/O; < 0 if unknown }; /** CPU and I/O used so far by our process (its whole process group on Linux). */ ResourceUsage ownedResourceUsage() const; /** Finishes a stop session. The process is gone: release everything. Still running: keepWatching * resumes the monitor (its exit is still seen, and booked as a clean stop); otherwise let it go — * the user chose to leave it running. */ void endStop(bool keepWatching); /** * @brief Parse a /proc//stat line: process group (field 5) and utime+stime ticks (fields 14+15) * * comm (field 2) is parenthesised and may itself contain spaces and ')', so fields are read from * after the LAST ')'. */ static bool parseProcStatLine(const std::string& line, long& pgrp, unsigned long long& cpuTicks) { const size_t close = line.rfind(')'); if (close == std::string::npos) return false; unsigned long long utime = 0, stime = 0; // state ppid pgrp session tty_nr tpgid flags minflt cminflt majflt cmajflt utime stime if (std::sscanf(line.c_str() + close + 1, " %*c %*d %ld %*d %*d %*d %*u %*u %*u %*u %*u %llu %llu", &pgrp, &utime, &stime) != 3) return false; cpuTicks = utime + stime; return true; } /** * @brief Check if daemon process is running */ bool isRunning() const; /** * @brief Get current daemon state */ State getState() const { return state_; } /** * @brief Get daemon process memory usage (RSS) in MB * @return Memory usage in MB, or 0 if unavailable */ double getMemoryUsageMB() const; /** * @brief Get last error message */ // Copy under lock: last_error_ is written from the monitor thread (setState on an unexpected exit) // while the UI thread reads it — a reference would be a torn-read / use-after-free race (M-04). std::string getLastError() const { std::lock_guard lk(error_mutex_); return last_error_; } /** * @brief Get dragonxd process output (thread-safe copy) */ std::string getOutput() const { std::lock_guard lk(output_mutex_); return process_output_; } /** * @brief Get new output since a given offset (thread-safe). * Returns the new text and advances offset. Offsets are absolute positions in everything ever * appended (start from 0), so trimming old output never shifts them: a reader that falls behind a * trim only loses the trimmed part, not output that is still buffered. */ std::string getOutputSince(size_t& offset) const { std::lock_guard lk(output_mutex_); return outputSince(process_output_, output_total_, offset); } /** getOutputSince() on a buffer holding the LAST buffer.size() of `total` bytes ever appended. */ static std::string outputSince(const std::string& buffer, size_t total, size_t& offset) { const size_t base = total - buffer.size(); // absolute position of buffer[0] if (offset < base) offset = base; // trimmed before it was read: resume at what's left if (offset >= total) { offset = total; return {}; } std::string result = buffer.substr(offset - base); offset = total; return result; } /** * @brief Get current output size (thread-safe, no copy) */ size_t getOutputSize() const { std::lock_guard lk(output_mutex_); return process_output_.size(); } /** * @brief Get last N lines of daemon output (thread-safe snapshot) */ std::vector getRecentLines(int maxLines = 8) const; /** * @brief Extract the latest block height from daemon output (thread-safe). * Parses the last "height=N" from UpdateTip lines without copying * the entire output buffer. Returns -1 if no UpdateTip found. */ int getLastBlockHeight() const { std::lock_guard lk(output_mutex_); // Search backwards from the end for "height=" size_t pos = process_output_.rfind("height="); if (pos == std::string::npos) return -1; pos += 7; // skip "height=" int h = 0; for (size_t i = pos; i < process_output_.size(); ++i) { char c = process_output_[i]; if (c >= '0' && c <= '9') h = h * 10 + (c - '0'); else break; } return h > 0 ? h : -1; } /** * @brief Whether start() detected an existing daemon on the RPC port. * When true the wallet should connect to it instead of showing an error. */ bool externalDaemonDetected() const { return external_daemon_detected_; } // Clear the adopted-external latch before relaunching our OWN process (e.g. a wallet switch that // stopped the adopted daemon), so the freshly spawned daemon is treated as owned. start() also // clears it on the port-free fall-through, but only if not short-circuited by an early guard. void clearExternalDaemonDetected() { external_daemon_detected_ = false; } /** * @brief Set callback for state changes */ void setStateCallback(StateCallback cb) { state_callback_ = cb; } /** * @brief Find dragonxd binary in standard locations * @return Path to binary, or empty if not found */ static std::string findDaemonBinary(); /** * @brief Check whether anything is listening on the default RPC port. * Useful for detecting an externally-started daemon. */ static bool isRpcPortInUse(); /** * @brief Get the chain parameters for dragonxd * @return Command line arguments for dragonx chain */ static std::vector getChainParams(); /** * @brief Set debug logging categories (appended as -debug= flags on next start) */ void setDebugCategories(const std::set& cats) { debug_categories_ = cats; } const std::set& getDebugCategories() const { return debug_categories_; } /** * @brief Set maximum peer connections (0 = use daemon default) */ void setMaxConnections(int v) { max_connections_ = v; } void setStratumHosting(bool enabled, const std::string& allowIp) { stratum_enabled_ = enabled; stratum_allowip_ = allowIp; } /** * @brief Request a blockchain rescan on the next daemon start */ void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; } bool rescanOnNextStart() const { return rescan_on_next_start_.load(); } // Active wallet file (multi-wallet). Passed to the daemon as -wallet= so it loads // /; empty or "wallet.dat" keeps the daemon default (no arg). Must be a plain // filename in the datadir — the daemon rejects paths. void setWalletFile(const std::string& v) { wallet_file_ = v; } std::string walletFile() const { return wallet_file_; } /** * @brief Request a wallet repair (-zapwallettxes=2) on the next daemon start. This deletes all * wallet transaction/note records and rebuilds them from the chain (keys are kept); the * daemon implicitly rescans afterwards. One-shot, like the rescan flag. */ void setZapOnNextStart(bool v) { zap_on_next_start_ = v; } bool zapOnNextStart() const { return zap_on_next_start_.load(); } // -salvagewallet: attempt to recover keys from a corrupt wallet.dat on startup (implies -rescan in // the daemon). One-shot, consumed on the next start. Used to repair a wallet a switch flagged corrupt. void setSalvageOnNextStart(bool v) { salvage_on_next_start_ = v; } bool salvageOnNextStart() const { return salvage_on_next_start_.load(); } // -reindex: rebuild the block index + chainstate from the raw blocks (blk*.dat) on startup. One-shot, // consumed on the next start. Offered when the node aborts on an unreadable block database (a // daemon-vs-chaindata format mismatch after an update, or a corrupt index). It implies a wallet // rescan, so it's the block-DB analogue of -salvagewallet and coexists with the wallet-repair flags. void setReindexOnNextStart(bool v) { reindex_on_next_start_ = v; } bool reindexOnNextStart() const { return reindex_on_next_start_.load(); } /** * @brief One-shot isolated-datadir override for the NEXT start(): run the daemon against a * different datadir (with its own DRAGONX.conf) plus the given extra args. Used by the * "migrate to a seed wallet" flow to mint a fresh mnemonic wallet in a throwaway datadir * without touching the real one. Consumed on the next start(); later starts are normal. * The caller must serialize this with start() (no concurrent starts). */ void setNextStartOverride(const std::string& datadir, std::vector extraArgs) { override_datadir_ = datadir; override_extra_args_ = std::move(extraArgs); } void clearNextStartOverride() { override_datadir_.clear(); override_extra_args_.clear(); } /** * @brief Skip the "default RPC port already in use → external daemon" bail in start(). * Set true only for an isolated instance running on its OWN (non-default) port * alongside the main daemon (migrate-to-seed flow). */ void setSkipPortCheck(bool v) { skip_port_check_ = v; } /** * @brief True while ANY dragonxd process is running (by process name), regardless of who started it. * Unlike isRpcPortInUse()/isRunning(), this reflects the actual PROCESS still being alive — a * graceful shutdown stops accepting RPC (port reads "free") but keeps the datadir lock until the * process exits, which can take up to ~90s. Use this to know a stopped node has FULLY released * the datadir before starting a replacement. Matches the daemon binary name on all platforms. */ static bool isDaemonProcessRunning(); /** * @brief Does a /proc entry (its comm + its exe link's basename) belong to a dragonxd? (Linux) * * comm alone misses a node in its final shutdown phase: Shutdown() renames the main thread to * "hush-shutoff" while the process still holds the datadir lock. The exe link keeps the binary * name, with " (deleted)" appended once the binary was replaced underneath it (reinstall/update). */ static bool isDaemonProcessIdentity(const std::string& comm, std::string exeName) { static const std::string kDeleted = " (deleted)"; if (exeName.size() > kDeleted.size() && exeName.compare(exeName.size() - kDeleted.size(), kDeleted.size(), kDeleted) == 0) exeName.erase(exeName.size() - kDeleted.size()); return comm == "dragonxd" || exeName == "dragonxd"; } // ---- Ownership record -------------------------------------------------------------------------- // /obsidiandragon-dragonxd.pid names the node this wallet launched, so a later session that // finds it still running (after "Leave running") knows it is ours: it may stop it gracefully and wait // for it, never kill it. The start token (when that process started) defeats PID reuse and makes a // record from before a reboot stale. struct OwnershipRecord { long pid = 0; std::string startToken; std::string exePath; }; static std::string formatOwnershipRecord(const OwnershipRecord& r) { return "pid=" + std::to_string(r.pid) + "\nstart=" + r.startToken + "\nexe=" + r.exePath + "\n"; } static bool parseOwnershipRecord(const std::string& text, OwnershipRecord& out) { OwnershipRecord r; size_t pos = 0; while (pos < text.size()) { size_t end = text.find('\n', pos); if (end == std::string::npos) end = text.size(); std::string line = text.substr(pos, end - pos); pos = end + 1; if (!line.empty() && line.back() == '\r') line.pop_back(); if (line.rfind("pid=", 0) == 0) r.pid = std::strtol(line.c_str() + 4, nullptr, 10); else if (line.rfind("start=", 0) == 0) r.startToken = line.substr(6); else if (line.rfind("exe=", 0) == 0) r.exePath = line.substr(4); } if (r.pid <= 0 || r.startToken.empty()) return false; out = r; return true; } /** Linux /proc//stat: the process state and its start time (clock ticks after boot, field 22). */ static bool parseProcStatStart(const std::string& line, char& state, unsigned long long& startTicks) { const size_t close = line.rfind(')'); if (close == std::string::npos) return false; // state ppid pgrp session tty_nr tpgid flags minflt cminflt majflt cmajflt utime stime cutime cstime // priority nice num_threads itrealvalue starttime return std::sscanf(line.c_str() + close + 1, " %c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %*u %*u %*d %*d %*d %*d %*d %*d %llu", &state, &startTicks) == 2; } /** * dragonxd shrinks a large debug.log at startup (it keeps the tail), so the file can become smaller * than where we were reading. Resume at THIS run's startup banner — the first banner line stamped at or * after `launchUtc` (UTC) — not at 0, which would replay the previous run's lines (old rebuild markers, * old corruption errors) as if they were new. No such banner yet: skip to the end (it follows). */ static size_t shrinkResumeOffset(const std::string& content, std::int64_t launchUtc); /** When process `pid` started, as an opaque token; "" if it isn't running (or is a zombie). */ static std::string processStartToken(long pid); /** When process `pid` started, in UTC seconds; 0 if unknown. */ static std::int64_t processStartUtc(long pid); /** Is process `pid` a dragonxd (by name / executable)? */ static bool processIsDaemon(long pid); static std::string ownershipRecordPath(); // /obsidiandragon-dragonxd.pid static void writeOwnershipRecord(const std::string& path, const OwnershipRecord& r); /** The record at `path`, if its process is still running; a stale or unreadable record is deleted. */ static bool liveOwnershipRecord(const std::string& path, OwnershipRecord* out = nullptr); /** * Does this DRAGONX.conf text set `key` on an active line ("key=value", spaces allowed around '='; * '#' starts a comment)? Used before adding a flag of our own: a command-line flag overrides the conf, * so the user's own setting must win. */ static bool confSetsKey(const std::string& confText, const std::string& key) { size_t pos = 0; while (pos < confText.size()) { size_t end = confText.find('\n', pos); if (end == std::string::npos) end = confText.size(); std::string line = confText.substr(pos, end - pos); pos = end + 1; const size_t hash = line.find('#'); if (hash != std::string::npos) line.erase(hash); const size_t eq = line.find('='); if (eq == std::string::npos) continue; size_t b = line.find_first_not_of(" \t\r"); size_t e = line.find_last_not_of(" \t\r", eq == 0 ? 0 : eq - 1); if (b == std::string::npos || b >= eq || e == std::string::npos || e < b) continue; if (line.compare(b, e - b + 1, key) == 0) return true; } return false; } /** Decision returned by evaluateDatadirLockGate(): whether start() may spawn now. */ struct StartLockGateDecision { bool proceed = true; // false => bail before spawning const char* errorMessage = ""; // set (a string literal) when proceed == false }; /** * @brief Pure decision for start(): bail because a previous dragonxd still holds the * shared datadir lock? Isolated instances (skip_port_check_ / an active -datadir * override) are exempt — they run their own throwaway datadir+port and can coexist * with the main daemon. Does no process/fs I/O itself (the caller does the probing), * so it is directly unit-testable; defined inline so tests need only this header. */ static StartLockGateDecision evaluateDatadirLockGate(bool skipPortCheck, bool isolatedOverride, bool stillRunningAfterWait) { if (skipPortCheck || isolatedOverride) return {true, ""}; if (stillRunningAfterWait) { return {false, "A previous dragonxd is still shutting down and holding the data " "directory lock. Retrying shortly…"}; } return {true, ""}; } /** @brief Is an arbitrary TCP port currently in use on localhost? (used to pick a free port) */ static bool tcpPortInUse(int port); /** Get number of consecutive daemon crashes (resets on successful start or manual reset) */ int getCrashCount() const { return crash_count_.load(); } /** Reset crash counter (call on successful connection or manual restart) */ void resetCrashCount() { crash_count_ = 0; } private: void setState(State s, const std::string& message = ""); void monitorProcess(); void drainOutput(); // read any pending stdout into process_output_ void appendOutput(const char* data, size_t len); // append + trim (caller holds output_mutex_) bool startProcess(const std::string& binary_path, const std::vector& args); std::atomic state_{State::Stopped}; std::atomic external_daemon_detected_{false}; std::string last_error_; mutable std::mutex error_mutex_; // protects last_error_ (written by main + monitor threads) mutable std::mutex output_mutex_; // protects process_output_ std::string process_output_; size_t output_total_ = 0; // bytes ever appended (the absolute end of process_output_) StateCallback state_callback_; // Process handle #ifdef _WIN32 HANDLE process_handle_ = nullptr; HANDLE stdout_read_ = nullptr; // pipe handle (Linux-style pipe, unused when tailing debug.log) std::string launch_cmd_; // full command line used to launch daemon (for diagnostics) std::string launch_binary_; // binary path used (for diagnostics) std::string launch_workdir_; // working directory used (for diagnostics) #else pid_t process_pid_ = 0; int stdout_fd_ = -1; // the child's stdout+stderr (crash diagnostics; the log itself is debug.log) void tailDebugLog(); // Waits up to timeout_ms for `pid` to exit, draining its output; true once reaped. Only valid // after monitor_thread_ is joined, so that stop() is the sole reaper. bool reapWithin(pid_t pid, int timeout_ms, int& status); #endif // The node's output is its /debug.log (timestamped; it outlives us if the node is left running). std::string debug_log_path_; size_t debug_log_offset_ = 0; // read offset into debug.log std::int64_t launch_utc_ = 0; // when we launched it (to find this run's banner after a log shrink) std::thread monitor_thread_; std::atomic should_stop_{false}; std::atomic stop_requested_{false}; // a stop was asked for: the next exit is not a crash std::set debug_categories_; int max_connections_ = 0; // 0 = daemon default bool stratum_enabled_ = false; // -stratum: host a RandomX pool (v1.3.0+; older daemons ignore it) std::string stratum_allowip_; // -stratumallowip subnet (empty = daemon default: loopback only) std::string wallet_file_; // -wallet= for the active wallet; empty/"wallet.dat" = default std::atomic crash_count_{0}; // consecutive crash counter std::atomic rescan_on_next_start_{false}; // -rescan flag for next start std::atomic zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start std::atomic salvage_on_next_start_{false}; // -salvagewallet flag for next start std::atomic reindex_on_next_start_{false}; // -reindex flag for next start (rebuild block DB) std::string override_datadir_; // one-shot: -datadir for the next start std::vector override_extra_args_; // one-shot: extra args for the next start bool skip_port_check_ = false; // isolated instance on a non-default port }; } // namespace daemon } // namespace dragonx