feat(daemon): recognise our detached node, tail POSIX debug.log, more RPC threads
S12 - our node across sessions: - The node the wallet launches is recorded in <datadir>/obsidiandragon- dragonxd.pid (pid + process start time + exe). A later session that finds it still running (after "Leave running" / keep_daemon_running) stops it gracefully on quit and waits for that exact process (no time limit, "Leave running" offered); never signals or kills it. A reused pid or a reboot never matches; stale records are deleted. - stop_external_daemon is now honoured when this session only connected (no controller); it was ignored. S13 - the node's own log on every platform: - No -printtoconsole on Linux/macOS: the node writes its timestamped debug.log (which outlives the GUI) and we tail it next to the stderr pipe. Tail path from the datadir or the isolated override (Windows had %APPDATA% hard-coded, wrong for migrate-to-seed starts). - After the node shrinks debug.log at startup, resume at this run's banner instead of re-reading the old run's tail. - A node we didn't launch (left running, or found running) feeds the busy tracker from its debug.log, tied to the live process (our record, or POSIX hushd.pid) and only from lines it wrote. A GUI attaching to a node mid-rebuild now says "Node busy: rebuilding note witnesses" and asks before stopping it. - A start that only finds a running node no longer resets the tracker; a found busy node shows the busy overlay at once. S14: launch our node with -rpcthreads=16 unless DRAGONX.conf sets it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+261
-44
@@ -6,16 +6,22 @@
|
||||
// binary discovery, process spawn, stdout/stderr monitoring, crash recovery.
|
||||
|
||||
#include "embedded_daemon.h"
|
||||
#include "daemon_busy_detect.h"
|
||||
#include "../config/version.h"
|
||||
#include "../resources/embedded_resources.h"
|
||||
#include "../util/platform.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
#include "../util/logger.h"
|
||||
@@ -33,6 +39,7 @@
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
@@ -217,11 +224,11 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
|
||||
DEBUG_LOGF("[INFO] Using %s\n", dbcache_arg.c_str());
|
||||
}
|
||||
|
||||
// No -printtoconsole on any platform: the node writes its timestamped <datadir>/debug.log, which we
|
||||
// tail (and which keeps going if the node is left running after we quit); its stdout/stderr stay piped
|
||||
// on POSIX for crash diagnostics.
|
||||
return {
|
||||
"-tls=only",
|
||||
#ifndef _WIN32
|
||||
"-printtoconsole",
|
||||
#endif
|
||||
"-clientname=ObsidianDragon",
|
||||
"-ac_name=DRAGONX",
|
||||
"-ac_algo=randomx",
|
||||
@@ -570,6 +577,19 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
||||
args.push_back("-maxconnections=" + std::to_string(max_connections_));
|
||||
}
|
||||
|
||||
// More RPC workers for a node we launch: dragonxd's default 8 workers + 16-deep queue fill up with
|
||||
// requests waiting behind a long cs_main holder (a witness rebuild), after which even "stop" is refused.
|
||||
// The GUI itself no longer floods it; this is headroom for dragonx-cli and other clients — a delay,
|
||||
// not a fix. Not for an isolated start, and DRAGONX.conf wins (a flag would override the user's own).
|
||||
if (override_datadir_.empty()) {
|
||||
std::string conf;
|
||||
{
|
||||
std::ifstream in((fs::path(util::Platform::getDragonXDataDir()) / "DRAGONX.conf").string(), std::ios::binary);
|
||||
if (in) conf.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
||||
}
|
||||
if (!confSetsKey(conf, "rpcthreads")) args.push_back("-rpcthreads=16");
|
||||
}
|
||||
|
||||
// Host a RandomX stratum pool from this node (-stratum). Only v1.3.0+ daemons implement it; older
|
||||
// ones ignore the unknown flag (no fatal arg check), and the Settings toggle is gated on daemon
|
||||
// version, so this is only enabled against a daemon that supports it. Without -stratumallowip the
|
||||
@@ -624,6 +644,18 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
||||
args.push_back("-datadir=" + override_datadir_);
|
||||
}
|
||||
for (const auto& a : override_extra_args_) args.push_back(a);
|
||||
const bool isolated = !override_datadir_.empty();
|
||||
// Tail this node's debug.log — in the isolated datadir for an isolated start — from its current end.
|
||||
{
|
||||
debug_log_path_ = (fs::path(isolated ? override_datadir_ : util::Platform::getDragonXDataDir()) /
|
||||
"debug.log").string();
|
||||
std::error_code ec;
|
||||
const auto size = fs::file_size(debug_log_path_, ec);
|
||||
debug_log_offset_ = ec ? 0 : static_cast<size_t>(size);
|
||||
launch_utc_ = static_cast<std::int64_t>(std::time(nullptr));
|
||||
DEBUG_LOGF("[INFO] Will tail daemon output from: %s (offset %zu)\n", debug_log_path_.c_str(),
|
||||
debug_log_offset_);
|
||||
}
|
||||
override_datadir_.clear();
|
||||
override_extra_args_.clear();
|
||||
|
||||
@@ -642,6 +674,20 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
||||
|
||||
setState(State::Running, "dragonxd started");
|
||||
DEBUG_LOGF("[INFO] dragonxd process started successfully\\n");
|
||||
|
||||
// Record it as ours, so a later session recognises it if it is left running (main datadir only).
|
||||
if (!isolated) {
|
||||
#ifdef _WIN32
|
||||
const long pid = process_handle_ ? static_cast<long>(GetProcessId(process_handle_)) : 0;
|
||||
#else
|
||||
const long pid = static_cast<long>(process_pid_);
|
||||
#endif
|
||||
OwnershipRecord rec;
|
||||
rec.pid = pid;
|
||||
rec.startToken = processStartToken(pid);
|
||||
rec.exePath = daemon_path;
|
||||
if (pid > 0 && !rec.startToken.empty()) writeOwnershipRecord(ownershipRecordPath(), rec);
|
||||
}
|
||||
|
||||
// Start monitor thread (if not already running)
|
||||
should_stop_ = false;
|
||||
@@ -703,29 +749,6 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
||||
}
|
||||
}
|
||||
|
||||
// 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 windowless. Use CREATE_NO_WINDOW (NOT CREATE_NEW_CONSOLE): CREATE_NEW_CONSOLE
|
||||
// allocates a console window that briefly flashes on screen before SW_HIDE can hide it, which is
|
||||
// visible as a console-window flash on wallet launch. CREATE_NO_WINDOW gives the console child no
|
||||
@@ -850,7 +873,13 @@ void EmbeddedDaemon::drainOutput()
|
||||
// Witness …" lines the warmup progress bar parses only exist in debug.log on Windows. On a shrink,
|
||||
// restart from the new beginning; the parser is monotonic and converges as it reaches current output.
|
||||
if (currentSize < debug_log_offset_) {
|
||||
debug_log_offset_ = 0;
|
||||
std::string content(currentSize, '\0');
|
||||
DWORD got = 0;
|
||||
LARGE_INTEGER zero{};
|
||||
SetFilePointerEx(hFile, zero, NULL, FILE_BEGIN);
|
||||
if (currentSize > 0 && ReadFile(hFile, &content[0], static_cast<DWORD>(currentSize), &got, NULL))
|
||||
content.resize(got);
|
||||
debug_log_offset_ = shrinkResumeOffset(content, launch_utc_);
|
||||
}
|
||||
if (currentSize <= debug_log_offset_) {
|
||||
CloseHandle(hFile);
|
||||
@@ -1381,12 +1410,24 @@ bool EmbeddedDaemon::isRunning() const
|
||||
return (s == State::Running || s == State::Stopping);
|
||||
}
|
||||
|
||||
void EmbeddedDaemon::drainOutput()
|
||||
void EmbeddedDaemon::tailDebugLog()
|
||||
{
|
||||
if (stdout_fd_ < 0) return;
|
||||
if (debug_log_path_.empty()) return;
|
||||
const int fd = open(debug_log_path_.c_str(), O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) return; // not created yet
|
||||
struct stat st{};
|
||||
if (fstat(fd, &st) != 0) { close(fd); return; }
|
||||
const size_t size = static_cast<size_t>(st.st_size);
|
||||
if (size < debug_log_offset_) { // shrunk at startup: resume at this run's banner (see shrinkResumeOffset)
|
||||
std::string content(size, '\0');
|
||||
const ssize_t got = size ? pread(fd, &content[0], size, 0) : 0;
|
||||
content.resize(got > 0 ? static_cast<size_t>(got) : 0);
|
||||
debug_log_offset_ = shrinkResumeOffset(content, launch_utc_);
|
||||
}
|
||||
char buffer[4096];
|
||||
for (;;) {
|
||||
ssize_t n = read(stdout_fd_, buffer, sizeof(buffer) - 1);
|
||||
while (debug_log_offset_ < size) {
|
||||
const size_t want = std::min(sizeof(buffer) - 1, size - debug_log_offset_);
|
||||
const ssize_t n = pread(fd, buffer, want, static_cast<off_t>(debug_log_offset_));
|
||||
if (n <= 0) break;
|
||||
buffer[n] = '\0';
|
||||
{
|
||||
@@ -1394,7 +1435,27 @@ void EmbeddedDaemon::drainOutput()
|
||||
appendOutput(buffer, static_cast<size_t>(n));
|
||||
}
|
||||
VERBOSE_LOGF("[dragonxd] %s", buffer);
|
||||
debug_log_offset_ += static_cast<size_t>(n);
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
void EmbeddedDaemon::drainOutput()
|
||||
{
|
||||
if (stdout_fd_ >= 0) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
tailDebugLog();
|
||||
}
|
||||
|
||||
static std::string describeExitStatus(int status)
|
||||
@@ -1591,8 +1652,6 @@ void EmbeddedDaemon::endStop(bool keepWatching)
|
||||
|
||||
void EmbeddedDaemon::monitorProcess()
|
||||
{
|
||||
char buffer[4096];
|
||||
|
||||
while (!should_stop_) {
|
||||
// Check if process exited (non-blocking waitpid captures exit status)
|
||||
if (process_pid_ > 0) {
|
||||
@@ -1617,23 +1676,181 @@ void EmbeddedDaemon::monitorProcess()
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Read process output (non-blocking): the stdout/stderr pipe and the node's debug.log
|
||||
drainOutput();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
}
|
||||
|
||||
#endif // _WIN32
|
||||
|
||||
std::string EmbeddedDaemon::processStartToken(long pid)
|
||||
{
|
||||
if (pid <= 0) return {};
|
||||
#ifdef _WIN32
|
||||
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, static_cast<DWORD>(pid));
|
||||
if (!h) return {};
|
||||
std::string token;
|
||||
DWORD code = 0;
|
||||
FILETIME created{}, exited{}, kernel{}, user{};
|
||||
if (GetExitCodeProcess(h, &code) && code == STILL_ACTIVE &&
|
||||
GetProcessTimes(h, &created, &exited, &kernel, &user)) {
|
||||
token = std::to_string((static_cast<unsigned long long>(created.dwHighDateTime) << 32) |
|
||||
created.dwLowDateTime);
|
||||
}
|
||||
CloseHandle(h);
|
||||
return token;
|
||||
#elif defined(__linux__)
|
||||
std::ifstream f("/proc/" + std::to_string(pid) + "/stat");
|
||||
std::string line;
|
||||
if (!f || !std::getline(f, line)) return {};
|
||||
char state = 0;
|
||||
unsigned long long startTicks = 0;
|
||||
if (!parseProcStatStart(line, state, startTicks) || state == 'Z' || state == 'X') return {};
|
||||
std::string bootId; // start ticks count from boot: tie them to this boot
|
||||
std::ifstream b("/proc/sys/kernel/random/boot_id");
|
||||
if (b) std::getline(b, bootId);
|
||||
return bootId + "/" + std::to_string(startTicks);
|
||||
#elif defined(__APPLE__)
|
||||
struct proc_bsdinfo info{};
|
||||
if (proc_pidinfo(static_cast<int>(pid), PROC_PIDTBSDINFO, 0, &info, sizeof(info)) != sizeof(info)) return {};
|
||||
if (info.pbi_status == 5) return {}; // SZOMB
|
||||
return std::to_string(info.pbi_start_tvsec) + "." + std::to_string(info.pbi_start_tvusec);
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
std::int64_t EmbeddedDaemon::processStartUtc(long pid)
|
||||
{
|
||||
if (pid <= 0) return 0;
|
||||
#ifdef _WIN32
|
||||
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, static_cast<DWORD>(pid));
|
||||
if (!h) return 0;
|
||||
std::int64_t utc = 0;
|
||||
FILETIME created{}, exited{}, kernel{}, user{};
|
||||
if (GetProcessTimes(h, &created, &exited, &kernel, &user)) {
|
||||
const unsigned long long t = (static_cast<unsigned long long>(created.dwHighDateTime) << 32) |
|
||||
created.dwLowDateTime; // 100ns since 1601
|
||||
if (t > 116444736000000000ULL) utc = static_cast<std::int64_t>((t - 116444736000000000ULL) / 10000000ULL);
|
||||
}
|
||||
CloseHandle(h);
|
||||
return utc;
|
||||
#elif defined(__linux__)
|
||||
std::ifstream f("/proc/" + std::to_string(pid) + "/stat");
|
||||
std::string line;
|
||||
char state = 0;
|
||||
unsigned long long startTicks = 0;
|
||||
if (!f || !std::getline(f, line) || !parseProcStatStart(line, state, startTicks)) return 0;
|
||||
std::ifstream s("/proc/stat"); // "btime <boot time, UTC seconds>"
|
||||
std::string key;
|
||||
long long btime = 0;
|
||||
while (s >> key) {
|
||||
if (key == "btime") { s >> btime; break; }
|
||||
s.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
|
||||
}
|
||||
const long hz = sysconf(_SC_CLK_TCK);
|
||||
if (btime <= 0 || hz <= 0) return 0;
|
||||
return static_cast<std::int64_t>(btime + static_cast<long long>(startTicks / static_cast<unsigned long long>(hz)));
|
||||
#elif defined(__APPLE__)
|
||||
struct proc_bsdinfo info{};
|
||||
if (proc_pidinfo(static_cast<int>(pid), PROC_PIDTBSDINFO, 0, &info, sizeof(info)) != sizeof(info)) return 0;
|
||||
return static_cast<std::int64_t>(info.pbi_start_tvsec);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool EmbeddedDaemon::processIsDaemon(long pid)
|
||||
{
|
||||
if (pid <= 0) return false;
|
||||
#ifdef _WIN32
|
||||
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, static_cast<DWORD>(pid));
|
||||
if (!h) return false;
|
||||
char path[MAX_PATH];
|
||||
bool is = false;
|
||||
// A device path (\Device\HarddiskVolume3\...\dragonxd.exe); only its file name matters.
|
||||
const DWORD len = GetProcessImageFileNameA(h, path, MAX_PATH);
|
||||
if (len > 0) {
|
||||
std::string name(path, len);
|
||||
const size_t slash = name.find_last_of("\\/");
|
||||
if (slash != std::string::npos) name.erase(0, slash + 1);
|
||||
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); });
|
||||
is = name == "dragonxd.exe";
|
||||
}
|
||||
CloseHandle(h);
|
||||
return is;
|
||||
#elif defined(__linux__)
|
||||
const fs::path proc = fs::path("/proc") / std::to_string(pid);
|
||||
std::ifstream f((proc / "comm").string());
|
||||
std::string comm;
|
||||
if (f) std::getline(f, comm);
|
||||
std::error_code ec;
|
||||
return isDaemonProcessIdentity(comm, fs::read_symlink(proc / "exe", ec).filename().string());
|
||||
#elif defined(__APPLE__)
|
||||
char path[PROC_PIDPATHINFO_MAXSIZE];
|
||||
if (proc_pidpath(static_cast<int>(pid), path, sizeof(path)) <= 0) return false;
|
||||
return isDaemonProcessIdentity("", fs::path(path).filename().string());
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string EmbeddedDaemon::ownershipRecordPath()
|
||||
{
|
||||
return (fs::path(util::Platform::getDragonXDataDir()) / "obsidiandragon-dragonxd.pid").string();
|
||||
}
|
||||
|
||||
size_t EmbeddedDaemon::shrinkResumeOffset(const std::string& content, std::int64_t launchUtc)
|
||||
{
|
||||
size_t pos = 0;
|
||||
while (pos < content.size()) {
|
||||
size_t end = content.find('\n', pos);
|
||||
if (end == std::string::npos) end = content.size();
|
||||
const std::string line = content.substr(pos, end - pos);
|
||||
if ((line.find("DragonX version ") != std::string::npos || line.find("Hush version ") != std::string::npos)) {
|
||||
const std::int64_t at = parseDaemonLogUtc(line);
|
||||
if (at > 0 && at >= launchUtc - 5) return pos; // 5s: the log's second vs our clock read
|
||||
}
|
||||
pos = end + 1;
|
||||
}
|
||||
return content.size();
|
||||
}
|
||||
|
||||
void EmbeddedDaemon::writeOwnershipRecord(const std::string& path, const OwnershipRecord& r)
|
||||
{
|
||||
const std::string tmp = path + ".tmp";
|
||||
{
|
||||
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
|
||||
if (!out) return;
|
||||
out << formatOwnershipRecord(r);
|
||||
if (!out) return;
|
||||
}
|
||||
std::error_code ec;
|
||||
fs::rename(tmp, path, ec);
|
||||
if (ec) fs::remove(tmp, ec);
|
||||
else DEBUG_LOGF("[INFO] Recorded our dragonxd (pid %ld) in %s\n", r.pid, path.c_str());
|
||||
}
|
||||
|
||||
bool EmbeddedDaemon::liveOwnershipRecord(const std::string& path, OwnershipRecord* out)
|
||||
{
|
||||
std::string text;
|
||||
{
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in) return false;
|
||||
text.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
||||
}
|
||||
OwnershipRecord r;
|
||||
if (parseOwnershipRecord(text, r) && processStartToken(r.pid) == r.startToken) {
|
||||
if (out) *out = r;
|
||||
return true;
|
||||
}
|
||||
std::error_code ec; // that process is gone (or the pid now belongs to another): the record is stale
|
||||
if (fs::remove(path, ec)) DEBUG_LOGF("[INFO] Removed stale dragonxd record (pid %ld)\n", r.pid);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EmbeddedDaemon::isRpcPortInUse()
|
||||
{
|
||||
int port = std::atoi(DRAGONX_DEFAULT_RPC_PORT);
|
||||
|
||||
Reference in New Issue
Block a user