Concurrency audit: last_error_ was written unlocked at the five startProcess() error sites (while the monitor thread could be in setState() writing it under error_mutex_) and read unlocked in start() — a data race on a std::string. process_pid_ / stdout_fd_ (POSIX) and process_handle_ (Windows) are plain scalars written by startProcess, the stop paths and the monitor (reap) and read by accessors. - Route every startProcess() last_error_ write through a new locked setLastError(); read the start() detail via the already-locked getLastError(). - Make process_pid_ / stdout_fd_ / process_handle_ std::atomic (trivially atomic scalars/handles), so individual loads/stores no longer race. Process start/stop ordering and the shutdown hardening are unchanged — this only removes the data races, not the (already-serialized) sequencing. Synchronization only; no fund-path call touched. Threading fix — verified by a clean build + ctest; the race itself is runtime-only. (The Windows process_handle_ atomic is reasoning/compile-only on this Linux box.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
562 lines
27 KiB
C++
562 lines
27 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#pragma once
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <atomic>
|
|
#include <thread>
|
|
#include <mutex>
|
|
#include <condition_variable>
|
|
#include <chrono>
|
|
#include <set>
|
|
#include <vector>
|
|
|
|
#include "output_window.h"
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#else
|
|
#include <sys/types.h>
|
|
#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<void(State, const std::string&)>;
|
|
|
|
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 (or is already running); false while a stop
|
|
* session (beginStop() .. endStop()) is still in progress
|
|
*/
|
|
static constexpr int kDatadirLockWaitMaxPolls = 3; // ~300ms total, breaks early on exit
|
|
|
|
/** @param lockWaitPolls max 100ms datadir-lock waits (0 = single check, never sleeps; for the UI thread) */
|
|
bool start(const std::string& binary_path = "", int lockWaitPolls = kDatadirLockWaitMaxPolls);
|
|
|
|
/**
|
|
* @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/<pid>/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
|
|
*
|
|
* Synchronous (Linux scans /proc, macOS forks `ps`): UI code uses memoryProbe() through a
|
|
* util::BackgroundSampler instead.
|
|
*/
|
|
double getMemoryUsageMB() const;
|
|
|
|
/**
|
|
* @brief A self-contained RSS probe (MB) for another thread. Snapshots what it needs on the calling
|
|
* thread (the pid; on Windows the value itself: one cheap call on the owned handle); the callable
|
|
* touches no EmbeddedDaemon state. Empty when there is no process.
|
|
*/
|
|
std::function<double()> memoryProbe() 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<std::mutex> lk(error_mutex_); return last_error_; }
|
|
|
|
/**
|
|
* @brief Get dragonxd process output (thread-safe copy)
|
|
*/
|
|
std::string getOutput() const {
|
|
std::lock_guard<std::mutex> 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<std::mutex> 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)
|
|
{
|
|
return outputWindowSince(buffer, total, offset);
|
|
}
|
|
|
|
/** Absolute end of everything ever appended (a valid getOutputSince() offset). */
|
|
size_t getOutputEnd() const {
|
|
std::lock_guard<std::mutex> lk(output_mutex_);
|
|
return output_total_;
|
|
}
|
|
|
|
/**
|
|
* @brief Get current output size (thread-safe, no copy)
|
|
*/
|
|
size_t getOutputSize() const {
|
|
std::lock_guard<std::mutex> lk(output_mutex_);
|
|
return process_output_.size();
|
|
}
|
|
|
|
/**
|
|
* @brief Get last N lines of daemon output (thread-safe snapshot)
|
|
*/
|
|
std::vector<std::string> 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<std::mutex> 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<std::string> getChainParams();
|
|
|
|
/**
|
|
* @brief Set debug logging categories (appended as -debug= flags on next start)
|
|
*/
|
|
void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; }
|
|
const std::set<std::string>& 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=<name> so it loads
|
|
// <datadir>/<name>; 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<std::string> 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 --------------------------------------------------------------------------
|
|
// <datadir>/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/<pid>/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(); // <datadir>/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;
|
|
}
|
|
|
|
/**
|
|
* Drop the wallet's own -dbcache= pin from `args` when the user's DRAGONX.conf sets dbcache (a
|
|
* command-line flag overrides the conf, so the user's own value must win). Pure; no logging.
|
|
*/
|
|
static void dropPinnedDbcacheIfConfSets(std::vector<std::string>& args, const std::string& conf)
|
|
{
|
|
if (!confSetsKey(conf, "dbcache")) return;
|
|
args.erase(std::remove_if(args.begin(), args.end(),
|
|
[](const std::string& a) { return a.rfind("-dbcache=", 0) == 0; }),
|
|
args.end());
|
|
}
|
|
|
|
/** 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; }
|
|
/** Unexpected exits since this object was created; never reset (unlike the crash count), so a
|
|
* reader can tell a new crash from an error it has already reported. */
|
|
std::uint64_t getUnexpectedExitCount() const { return unexpected_exits_.load(); }
|
|
|
|
/** Monitor poll interval: stay `fast` for the first 30 s after the thread starts and for 3 s after any
|
|
* output; otherwise double per idle tick up to `cap`. Any output resets to `fast`. Pure. */
|
|
static std::chrono::milliseconds nextMonitorInterval(std::chrono::milliseconds cur, bool bytesRead,
|
|
std::chrono::milliseconds sinceStart, std::chrono::milliseconds sinceLastData,
|
|
std::chrono::milliseconds fast, std::chrono::milliseconds cap)
|
|
{
|
|
using ms = std::chrono::milliseconds;
|
|
if (bytesRead || sinceStart < ms(30000) || sinceLastData < ms(3000)) return fast;
|
|
const ms next = cur * 2;
|
|
return next > cap ? cap : (next < fast ? fast : next);
|
|
}
|
|
|
|
private:
|
|
void setState(State s, const std::string& message = "");
|
|
void monitorProcess();
|
|
void requestMonitorStop(); // set should_stop_ under monitor_mutex_ and wake the monitor
|
|
size_t outputTotalSnapshot() const; // output_total_ under output_mutex_ (monitor idle detection)
|
|
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<std::string>& args);
|
|
|
|
std::atomic<State> state_{State::Stopped};
|
|
std::atomic<bool> external_daemon_detected_{false};
|
|
std::string last_error_;
|
|
mutable std::mutex error_mutex_; // protects last_error_ (written by main + monitor threads)
|
|
// Locked writer for last_error_ (startProcess() runs on the caller thread while the monitor may call setState()).
|
|
void setLastError(std::string message) { std::lock_guard<std::mutex> lk(error_mutex_); last_error_ = std::move(message); }
|
|
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
|
|
std::atomic<HANDLE> process_handle_{nullptr}; // written by start/stop/monitor threads, read by accessors
|
|
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
|
|
std::atomic<pid_t> process_pid_{0}; // written by start/stop paths and the monitor (reap), read by accessors
|
|
std::atomic<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 <datadir>/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<bool> should_stop_{false};
|
|
std::mutex monitor_mutex_; // pairs with monitor_cv_ so stop wakes the monitor at once
|
|
std::condition_variable monitor_cv_;
|
|
std::atomic<bool> stop_requested_{false}; // a stop was asked for: the next exit is not a crash
|
|
// Serializes start/beginStop/stop/endStop over monitor_thread_, should_stop_ and stop_requested_ (two
|
|
// threads joining one std::thread is undefined). Never taken by the monitor thread or the state callback.
|
|
std::mutex lifecycle_mutex_;
|
|
std::set<std::string> 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=<name> for the active wallet; empty/"wallet.dat" = default
|
|
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
|
std::atomic<std::uint64_t> unexpected_exits_{0}; // all unexpected exits (see getUnexpectedExitCount)
|
|
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
|
|
std::atomic<bool> zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start
|
|
std::atomic<bool> salvage_on_next_start_{false}; // -salvagewallet flag for next start
|
|
std::atomic<bool> 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<std::string> 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
|