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:
+153
-37
@@ -944,6 +944,7 @@ void App::update()
|
||||
// The node is too busy to answer: send it nothing that needs cs_main (it would only queue behind the
|
||||
// rest and hold one of its few RPC slots) until the canary gets through. pumpRpcBusyCanary() runs it.
|
||||
pumpRpcBusyCanary();
|
||||
pumpExternalDaemonLog();
|
||||
const bool rpcBusy = rpc_busy_gate_->busy();
|
||||
|
||||
// Keep the "node starting / warming up / busy" overlay current between connect attempts and polls,
|
||||
@@ -5430,8 +5431,11 @@ bool App::startEmbeddedDaemon()
|
||||
});
|
||||
}
|
||||
|
||||
resetDaemonBusy(); // a new process: whatever the last one was doing is over
|
||||
return daemon_controller_->start(settings_.get());
|
||||
// Only a process we actually launched starts a new story: a start that finds a node already running (and
|
||||
// launches nothing) must not wipe what the tracker knows about that node from its debug.log.
|
||||
const bool launched = daemon_controller_->start(settings_.get());
|
||||
if (launched) resetDaemonBusy(); // a new process: whatever the last one was doing is over
|
||||
return launched;
|
||||
}
|
||||
|
||||
// Drives one stop session for DaemonController::executeDaemonStop: a dedicated RPC client for "stop" (not
|
||||
@@ -5441,8 +5445,10 @@ class AppStopRuntime final : public daemon::DaemonController::StopRuntime {
|
||||
public:
|
||||
using DC = daemon::DaemonController;
|
||||
|
||||
AppStopRuntime(App& app, daemon::EmbeddedDaemon* owned, std::vector<rpc::ConnectionConfig> credentials)
|
||||
: app_(app), owned_(owned), creds_(std::move(credentials)), t0_(std::chrono::steady_clock::now())
|
||||
AppStopRuntime(App& app, daemon::EmbeddedDaemon* owned, std::vector<rpc::ConnectionConfig> credentials,
|
||||
daemon::EmbeddedDaemon::OwnershipRecord recorded = {})
|
||||
: app_(app), owned_(owned), creds_(std::move(credentials)), recorded_(std::move(recorded)),
|
||||
t0_(std::chrono::steady_clock::now())
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(app_.stop_client_mutex_);
|
||||
app_.active_stop_client_ = &client_;
|
||||
@@ -5460,6 +5466,9 @@ public:
|
||||
bool processAlive() override
|
||||
{
|
||||
if (owned_) return owned_->pollOwnedProcess();
|
||||
// The node an earlier session of ours left running: that exact process (pid + start time).
|
||||
if (recorded_.pid > 0)
|
||||
return daemon::EmbeddedDaemon::processStartToken(recorded_.pid) == recorded_.startToken;
|
||||
// Not ours: we can only watch for it to go (port and process), never wait on or signal it.
|
||||
return daemon::EmbeddedDaemon::isRpcPortInUse() || daemon::EmbeddedDaemon::isDaemonProcessRunning();
|
||||
}
|
||||
@@ -5536,6 +5545,7 @@ private:
|
||||
App& app_;
|
||||
daemon::EmbeddedDaemon* owned_ = nullptr;
|
||||
std::vector<rpc::ConnectionConfig> creds_;
|
||||
daemon::EmbeddedDaemon::OwnershipRecord recorded_; // pid > 0: our earlier session's detached node
|
||||
size_t credIndex_ = 0;
|
||||
rpc::RPCClient client_;
|
||||
bool clientOpen_ = false;
|
||||
@@ -5571,12 +5581,15 @@ void App::chooseStopAction(daemon::StopUserChoice choice)
|
||||
daemon::StopOutcome App::stopEmbeddedDaemon(StopMode mode, StopScope scope)
|
||||
{
|
||||
if (scope == StopScope::Policy) {
|
||||
if (!daemon_controller_) return daemon::StopOutcome::Exited;
|
||||
auto shutdownDecision = daemon_controller_->shutdownDecision(
|
||||
false, settings_ && settings_->getStopExternalDaemon());
|
||||
if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) {
|
||||
DEBUG_LOGF("stopEmbeddedDaemon: %s — skipping\n", shutdownDecision.logReason);
|
||||
return daemon::StopOutcome::StillRunning;
|
||||
const bool mayStopExternal = mayStopExternalNode();
|
||||
if (!daemon_controller_) {
|
||||
if (!mayStopExternal) return daemon::StopOutcome::Exited; // nothing we may stop
|
||||
} else {
|
||||
auto shutdownDecision = daemon_controller_->shutdownDecision(false, mayStopExternal);
|
||||
if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) {
|
||||
DEBUG_LOGF("stopEmbeddedDaemon: %s — skipping\n", shutdownDecision.logReason);
|
||||
return daemon::StopOutcome::StillRunning;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5590,6 +5603,11 @@ daemon::StopOutcome App::stopEmbeddedDaemon(StopMode mode, StopScope scope)
|
||||
// external node and a node we already let go of are not, and are never waited on, signalled or killed.
|
||||
const bool owned = d && d->beginStop();
|
||||
if (d && !owned) d->endStop(/*keepWatching=*/false);
|
||||
// Not launched by this session, but by an earlier one that left it running: we wait for that exact
|
||||
// process like our own (no time limit when quitting), but still never signal or kill it.
|
||||
daemon::EmbeddedDaemon::OwnershipRecord recorded;
|
||||
const bool ours = !owned && daemon::EmbeddedDaemon::liveOwnershipRecord(
|
||||
daemon::EmbeddedDaemon::ownershipRecordPath(), &recorded);
|
||||
|
||||
if (!owned && !daemon::EmbeddedDaemon::isRpcPortInUse()) {
|
||||
// Nothing listening for a stop: either it is gone, or it is another dragonxd we can't talk to (one
|
||||
@@ -5624,7 +5642,7 @@ daemon::StopOutcome App::stopEmbeddedDaemon(StopMode mode, StopScope scope)
|
||||
switch (mode) {
|
||||
case StopMode::AppShutdown:
|
||||
policy.userChoices = true;
|
||||
policy.maxWaitSec = owned ? 0.0 : 120.0; // a node we don't own: watch it go for up to 2 min
|
||||
policy.maxWaitSec = (owned || ours) ? 0.0 : 120.0; // a node we don't own: watch it go for up to 2 min
|
||||
break;
|
||||
case StopMode::Background:
|
||||
policy.maxWaitSec = 120.0;
|
||||
@@ -5637,15 +5655,19 @@ daemon::StopOutcome App::stopEmbeddedDaemon(StopMode mode, StopScope scope)
|
||||
break;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("stopEmbeddedDaemon: %s node, %s\n", owned ? "our own" : "an external",
|
||||
mode == StopMode::AppShutdown ? "quitting (no time limit; the user may leave it running)"
|
||||
DEBUG_LOGF("stopEmbeddedDaemon: %s node, %s\n",
|
||||
owned ? "our own" : ours ? "our earlier session's (left running)" : "an external",
|
||||
mode == StopMode::AppShutdown
|
||||
? (policy.maxWaitSec > 0 ? "quitting (up to 120s for a node we don't own; never killed)"
|
||||
: "quitting (no time limit; the user may leave it running)")
|
||||
: mode == StopMode::Background ? "background (up to 120s, never killed)"
|
||||
: "fallback (up to 20s, never killed)");
|
||||
stop_kill_offered_ = false;
|
||||
stop_session_active_ = true;
|
||||
daemon::StopOutcome outcome;
|
||||
{
|
||||
AppStopRuntime runtime(*this, owned ? d : nullptr, std::move(creds));
|
||||
AppStopRuntime runtime(*this, owned ? d : nullptr, std::move(creds),
|
||||
ours ? recorded : daemon::EmbeddedDaemon::OwnershipRecord{});
|
||||
AppStopTaskContext task(*this, policy.abandonOnShutdown);
|
||||
outcome = daemon::DaemonController::executeDaemonStop(policy, runtime, task);
|
||||
}
|
||||
@@ -5654,6 +5676,8 @@ daemon::StopOutcome App::stopEmbeddedDaemon(StopMode mode, StopScope scope)
|
||||
// Still running after a background wait: keep watching it (its exit is booked as a clean stop). Left
|
||||
// running on purpose ("Leave running"), exited or killed: let go of it.
|
||||
if (owned) d->endStop(/*keepWatching=*/outcome == daemon::StopOutcome::StillRunning);
|
||||
if (outcome == daemon::StopOutcome::Exited || outcome == daemon::StopOutcome::Killed)
|
||||
daemon::EmbeddedDaemon::liveOwnershipRecord(daemon::EmbeddedDaemon::ownershipRecordPath()); // drops it if gone
|
||||
DEBUG_LOGF("stopEmbeddedDaemon: %s\n",
|
||||
outcome == daemon::StopOutcome::Exited ? "the node exited"
|
||||
: outcome == daemon::StopOutcome::Detached ? "left running at the user's request"
|
||||
@@ -6015,10 +6039,11 @@ void App::beginShutdown()
|
||||
DEBUG_LOGF("beginShutdown: starting (daemon_controller_=%s)\n",
|
||||
daemon_controller_ ? "yes" : "no");
|
||||
|
||||
// If no embedded daemon, just mark done — don't stop
|
||||
// an externally-managed daemon that the user started themselves.
|
||||
// Worker join + RPC disconnect happen in shutdown().
|
||||
if (!daemon_controller_) {
|
||||
// No controller (this session never launched a node): disconnect only — unless the node is the one our
|
||||
// earlier session left running, or stop_external_daemon is on. Worker join + RPC disconnect happen in
|
||||
// shutdown().
|
||||
const bool keepRunning = (settings_ && settings_->getKeepDaemonRunning()) || shutdown_keep_daemon_override_;
|
||||
if (!daemon_controller_ && (keepRunning || !mayStopExternalNode())) {
|
||||
DEBUG_LOGF("beginShutdown: no embedded daemon, disconnecting only\n");
|
||||
shutdown_status_ = TR("appx_disconnecting");
|
||||
if (settings_) {
|
||||
@@ -6033,17 +6058,18 @@ void App::beginShutdown()
|
||||
settings_->save();
|
||||
}
|
||||
|
||||
auto shutdownDecision = daemon_controller_->shutdownDecision(
|
||||
(settings_ && settings_->getKeepDaemonRunning()) || shutdown_keep_daemon_override_,
|
||||
settings_ && settings_->getStopExternalDaemon());
|
||||
if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) {
|
||||
DEBUG_LOGF("beginShutdown: %s, skipping daemon stop\n", shutdownDecision.logReason);
|
||||
if (daemon_controller_) {
|
||||
auto shutdownDecision = daemon_controller_->shutdownDecision(keepRunning, mayStopExternalNode());
|
||||
if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) {
|
||||
DEBUG_LOGF("beginShutdown: %s, skipping daemon stop\n", shutdownDecision.logReason);
|
||||
shutdown_status_ = shutdownDecision.status;
|
||||
shutdown_complete_ = true;
|
||||
return;
|
||||
}
|
||||
shutdown_status_ = shutdownDecision.status;
|
||||
shutdown_complete_ = true;
|
||||
return;
|
||||
} else {
|
||||
shutdown_status_ = TR("appx_sending_stop_command_to_daemon");
|
||||
}
|
||||
|
||||
shutdown_status_ = shutdownDecision.status;
|
||||
DEBUG_LOGF("beginShutdown: spawning shutdown thread for daemon stop\n");
|
||||
|
||||
// Run the daemon shutdown on a background thread so the UI
|
||||
@@ -6119,8 +6145,8 @@ static std::string formatBusyDuration(double secs)
|
||||
App::NodeBusyText App::nodeBusyText() const
|
||||
{
|
||||
NodeBusyText out;
|
||||
// The tracker reads OUR node's output; once that process is gone its last state means nothing.
|
||||
const bool ours = daemon_controller_ && daemon_controller_->isRunning();
|
||||
// Once the node the tracker follows is gone, its last state means nothing.
|
||||
const bool ours = busyTrackerIsLive();
|
||||
const bool rebuilding = ours && daemon_busy_.rebuilding();
|
||||
const bool replaying = ours && daemon_busy_.replaying() && !rebuilding;
|
||||
const bool rpcBusy = rpc_busy_gate_->busy();
|
||||
@@ -6169,6 +6195,87 @@ App::NodeBusyText App::nodeBusyText() const
|
||||
return out;
|
||||
}
|
||||
|
||||
bool App::busyTrackerIsLive() const
|
||||
{
|
||||
return (daemon_controller_ && daemon_controller_->isRunning()) || ext_log_tied_;
|
||||
}
|
||||
|
||||
long App::externalNodePid() const
|
||||
{
|
||||
daemon::EmbeddedDaemon::OwnershipRecord rec;
|
||||
if (daemon::EmbeddedDaemon::liveOwnershipRecord(daemon::EmbeddedDaemon::ownershipRecordPath(), &rec))
|
||||
return rec.pid;
|
||||
#ifndef _WIN32
|
||||
// POSIX dragonxd writes <datadir>/hushd.pid (removed on a clean exit): a live dragonxd under that pid uses
|
||||
// this datadir. (Windows writes none; there only our own record ties a node to this log.)
|
||||
std::ifstream in((std::filesystem::path(util::Platform::getDragonXDataDir()) / "hushd.pid").string());
|
||||
long pid = 0;
|
||||
if (in >> pid && pid > 0 && daemon::EmbeddedDaemon::processIsDaemon(pid)) return pid;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
void App::pumpExternalDaemonLog()
|
||||
{
|
||||
if (!supportsEmbeddedDaemon()) return;
|
||||
const double now = ImGui::GetTime();
|
||||
if (now < ext_log_next_at_) return;
|
||||
ext_log_next_at_ = now + 2.0;
|
||||
|
||||
auto untie = [this] {
|
||||
if (!ext_log_tied_) return;
|
||||
ext_log_tied_ = false;
|
||||
ext_log_pid_ = 0;
|
||||
daemon_busy_.reset();
|
||||
node_rebuilding_ = false;
|
||||
busy_first_seen_ = {};
|
||||
DEBUG_LOGF("[busy] stopped following the external node's debug.log\n");
|
||||
};
|
||||
auto readRange = [](const std::string& path, std::uintmax_t from, std::uintmax_t to) {
|
||||
std::string s;
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in || to <= from) return s;
|
||||
in.seekg(static_cast<std::streamoff>(from));
|
||||
s.resize(static_cast<size_t>(to - from));
|
||||
in.read(&s[0], static_cast<std::streamsize>(s.size()));
|
||||
s.resize(static_cast<size_t>(in.gcount()));
|
||||
return s;
|
||||
};
|
||||
|
||||
if (daemon_controller_ && daemon_controller_->isRunning()) { untie(); return; } // ours feeds it directly
|
||||
const long pid = externalNodePid();
|
||||
if (pid <= 0) { untie(); return; }
|
||||
const std::string path = (std::filesystem::path(util::Platform::getDragonXDataDir()) / "debug.log").string();
|
||||
std::error_code ec;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
if (ec) { untie(); return; }
|
||||
|
||||
if (!ext_log_tied_ || pid != ext_log_pid_ || size < ext_log_offset_) {
|
||||
// (Re)tie: this process's lines from the last 2 MB, into a fresh tracker. The banner may be older than
|
||||
// the window on a long-running node; a rebuild still in progress logged its markers recently.
|
||||
constexpr std::uintmax_t kWindow = 2u * 1024u * 1024u;
|
||||
const std::uintmax_t from = size > kWindow ? size - kWindow : 0;
|
||||
const std::string window = readRange(path, from, size);
|
||||
const size_t at = daemon::externalLogFeedOffset(window, from > 0,
|
||||
daemon::EmbeddedDaemon::processStartUtc(pid));
|
||||
untie();
|
||||
if (at == std::string::npos) return; // not that process's log (it may log to its console)
|
||||
daemon_busy_.reset();
|
||||
ext_log_tied_ = true;
|
||||
ext_log_pid_ = pid;
|
||||
ext_log_offset_ = from + window.size();
|
||||
DEBUG_LOGF("[busy] following the debug.log of a node this session didn't launch (pid %ld)\n", pid);
|
||||
feedDaemonBusy(window.substr(at));
|
||||
return;
|
||||
}
|
||||
if (size > ext_log_offset_) {
|
||||
const std::uintmax_t to = std::min<std::uintmax_t>(size, ext_log_offset_ + 1024u * 1024u);
|
||||
const std::string chunk = readRange(path, ext_log_offset_, to);
|
||||
ext_log_offset_ += chunk.size();
|
||||
feedDaemonBusy(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
void App::resetDaemonBusy()
|
||||
{
|
||||
// Any thread (a restart/rescan task starts the node): the UI thread applies it before its next feed.
|
||||
@@ -6197,15 +6304,24 @@ bool App::rpcBusyWhileAlive() const
|
||||
return (daemon_controller_ && daemon_controller_->isRunning()) || daemon::EmbeddedDaemon::isRpcPortInUse();
|
||||
}
|
||||
|
||||
bool App::mayStopExternalNode() const
|
||||
{
|
||||
if (settings_ && settings_->getStopExternalDaemon()) return true;
|
||||
return daemon::EmbeddedDaemon::liveOwnershipRecord(daemon::EmbeddedDaemon::ownershipRecordPath());
|
||||
}
|
||||
|
||||
bool App::shouldConfirmDaemonStop() const
|
||||
{
|
||||
if (!daemon_controller_) return false;
|
||||
// Only relevant when this shutdown would actually STOP the daemon (embedded, or external with
|
||||
// stop-on-exit) — a DisconnectOnly shutdown leaves it running and loses nothing.
|
||||
const auto decision = daemon_controller_->shutdownDecision(
|
||||
settings_ && settings_->getKeepDaemonRunning(),
|
||||
settings_ && settings_->getStopExternalDaemon());
|
||||
const bool stops = decision.action == daemon::DaemonController::ShutdownAction::StopDaemon;
|
||||
// Only relevant when this shutdown would actually STOP the daemon (ours, ours left running by an
|
||||
// earlier session, or external with stop-on-exit) — a DisconnectOnly shutdown loses nothing.
|
||||
const bool keep = settings_ && settings_->getKeepDaemonRunning();
|
||||
bool stops = false;
|
||||
if (!daemon_controller_) {
|
||||
stops = !keep && mayStopExternalNode();
|
||||
} else {
|
||||
const auto decision = daemon_controller_->shutdownDecision(keep, mayStopExternalNode());
|
||||
stops = decision.action == daemon::DaemonController::ShutdownAction::StopDaemon;
|
||||
}
|
||||
// No daemon version suppresses this: v1.3.0 does not checkpoint a witness rebuild, and nothing survives
|
||||
// a kill.
|
||||
return stops && nodeBusyForStop();
|
||||
@@ -6216,7 +6332,7 @@ bool App::nodeBusyForStop() const
|
||||
// The tracker reads OUR node's output; once that process is gone its last state means nothing. An
|
||||
// external node's busyness shows only as RPC that has stopped answering.
|
||||
daemon::DaemonBusyTracker ours;
|
||||
if (daemon_controller_ && daemon_controller_->isRunning()) ours = daemon_busy_;
|
||||
if (busyTrackerIsLive()) ours = daemon_busy_;
|
||||
return daemon::shouldConfirmStop(true, ours, rpcBusyWhileAlive());
|
||||
}
|
||||
|
||||
|
||||
@@ -203,6 +203,10 @@ public:
|
||||
// The node is rebuilding witnesses (our own node's log says so) or has been too busy to answer for a
|
||||
// while: stopping it now either waits up to an hour or makes it start over. UI thread only.
|
||||
bool nodeBusyForStop() const;
|
||||
// May quitting (or a default-policy stop) stop a node this session didn't launch? With stop_external_daemon
|
||||
// on, or when it is the node an earlier session of ours launched and left running (its ownership record
|
||||
// matches a live process). That one is stopped gracefully and waited for, never killed.
|
||||
bool mayStopExternalNode() const;
|
||||
// For an operation that would stop the node (restart, rescan, switch, reinstall, bootstrap, ...): when
|
||||
// nodeBusyForStop(), tell the user to try again later and return true; the caller then does nothing.
|
||||
bool refuseIfNodeBusy();
|
||||
@@ -1198,6 +1202,16 @@ private:
|
||||
rpc::ConnectRetryBackoff canary_backoff_; // a canary refused as queue-full waits before the next
|
||||
std::chrono::steady_clock::time_point canary_retry_at_{};
|
||||
void pumpRpcBusyCanary();
|
||||
// A node this session didn't launch (left running by an earlier session, or found running): feed the busy
|
||||
// tracker from its <datadir>/debug.log, while that log is tied to the live process (see
|
||||
// daemon::externalLogFeedOffset). Our own running node's output feeds the tracker directly. UI thread.
|
||||
void pumpExternalDaemonLog();
|
||||
long externalNodePid() const; // that node: our ownership record, or (POSIX) its hushd.pid; 0 if none
|
||||
bool busyTrackerIsLive() const; // the tracker describes a node that is running now
|
||||
bool ext_log_tied_ = false;
|
||||
long ext_log_pid_ = 0;
|
||||
std::uintmax_t ext_log_offset_ = 0;
|
||||
double ext_log_next_at_ = 0.0;
|
||||
void abortRpcBusyCanary();
|
||||
void feedDaemonBusy(const std::string& chunk);
|
||||
void resetDaemonBusy();
|
||||
|
||||
+5
-1
@@ -660,7 +660,11 @@ void App::tryConnect()
|
||||
if (startEmbeddedDaemon()) {
|
||||
VERBOSE_LOGF("[connect #%d] Embedded daemon starting, will retry connection...\n", attempt);
|
||||
} else if (daemon_controller_ && daemon_controller_->externalDaemonDetected()) {
|
||||
connection_status_ = TR("sb_connecting_generic");
|
||||
// A node already running (left running, or started by hand). If our probe reached
|
||||
// it but got no answer, say what it is doing (its log may show a rebuild) rather
|
||||
// than a bare "Connecting…" until the next attempt, up to 30s away.
|
||||
if (rpc::rpcKindMeansBusy(connectKind)) applyDaemonInitStatus(/*reachableButBusy=*/true);
|
||||
else connection_status_ = TR("sb_connecting_generic");
|
||||
VERBOSE_LOGF("[connect #%d] External daemon detected, will connect via RPC...\n", attempt);
|
||||
} else {
|
||||
VERBOSE_LOGF("[connect #%d] Failed to start embedded daemon — lastError: %s\n",
|
||||
|
||||
@@ -141,6 +141,38 @@ inline bool shouldConfirmStop(bool stopsDaemon, const DaemonBusyTracker& tracker
|
||||
return stopsDaemon && (tracker.rebuilding() || rpcBusyWhileAlive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Where to start feeding a DaemonBusyTracker from the last `window` of the debug.log of a node someone else
|
||||
* started at `processStartUtc` (UTC seconds). Only lines that process wrote count: a node started with
|
||||
* -printtoconsole leaves an old debug.log behind, whose last run may have died mid-rebuild. Returns the
|
||||
* offset of that process's startup banner in the window; 0 when its banner is older than the window
|
||||
* (`windowStartsMidFile`) and the window's first stamped line postdates the process start; npos when the
|
||||
* log isn't that process's.
|
||||
*/
|
||||
inline size_t externalLogFeedOffset(const std::string& window, bool windowStartsMidFile, std::int64_t processStartUtc)
|
||||
{
|
||||
size_t lastBanner = std::string::npos;
|
||||
std::int64_t lastBannerAt = 0, firstStamp = 0;
|
||||
size_t pos = 0;
|
||||
while (pos < window.size()) {
|
||||
size_t end = window.find('\n', pos);
|
||||
if (end == std::string::npos) end = window.size();
|
||||
const std::string line = window.substr(pos, end - pos);
|
||||
const std::int64_t at = parseDaemonLogUtc(line);
|
||||
if (at > 0 && firstStamp == 0) firstStamp = at;
|
||||
if (line.find("DragonX version ") != std::string::npos || line.find("Hush version ") != std::string::npos) {
|
||||
lastBanner = pos;
|
||||
lastBannerAt = at;
|
||||
}
|
||||
pos = end + 1;
|
||||
}
|
||||
if (processStartUtc <= 0) return std::string::npos;
|
||||
const std::int64_t since = processStartUtc - 5; // the log's whole seconds vs the process's start time
|
||||
if (lastBanner != std::string::npos) return (lastBannerAt > 0 && lastBannerAt >= since) ? lastBanner : std::string::npos;
|
||||
if (windowStartsMidFile && firstStamp > 0 && firstStamp >= since) return 0;
|
||||
return std::string::npos;
|
||||
}
|
||||
|
||||
// What the "node is starting / not answering" overlay should say, most specific first.
|
||||
enum class DaemonStatusKind {
|
||||
Rebuilding, // its own log says it's rebuilding note witnesses
|
||||
|
||||
+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);
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -324,6 +326,93 @@ public:
|
||||
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;
|
||||
}
|
||||
|
||||
/** Decision returned by evaluateDatadirLockGate(): whether start() may spawn now. */
|
||||
struct StartLockGateDecision {
|
||||
bool proceed = true; // false => bail before spawning
|
||||
@@ -378,20 +467,24 @@ private:
|
||||
#ifdef _WIN32
|
||||
HANDLE process_handle_ = nullptr;
|
||||
HANDLE stdout_read_ = nullptr; // pipe handle (Linux-style pipe, unused when tailing debug.log)
|
||||
std::string debug_log_path_; // path to daemon's debug.log for output tailing
|
||||
size_t debug_log_offset_ = 0; // read offset into 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;
|
||||
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::atomic<bool> stop_requested_{false}; // a stop was asked for: the next exit is not a crash
|
||||
|
||||
@@ -4466,6 +4466,138 @@ void testDaemonProcessIdentity()
|
||||
EXPECT_FALSE(EmbeddedDaemon::isDaemonProcessIdentity("dragonx-cli", "dragonx-cli"));
|
||||
}
|
||||
|
||||
// S14: -rpcthreads=16 only when DRAGONX.conf doesn't set it (a command-line flag would override the user's).
|
||||
void testConfSetsKey()
|
||||
{
|
||||
using dragonx::daemon::EmbeddedDaemon;
|
||||
EXPECT_FALSE(EmbeddedDaemon::confSetsKey("", "rpcthreads"));
|
||||
EXPECT_FALSE(EmbeddedDaemon::confSetsKey("rpcuser=u\nrpcpassword=p\n", "rpcthreads"));
|
||||
EXPECT_TRUE(EmbeddedDaemon::confSetsKey("rpcuser=u\nrpcthreads=4\n", "rpcthreads"));
|
||||
EXPECT_TRUE(EmbeddedDaemon::confSetsKey(" rpcthreads = 4\r\n", "rpcthreads")); // spaces, CRLF
|
||||
EXPECT_TRUE(EmbeddedDaemon::confSetsKey("rpcthreads=4", "rpcthreads")); // no final newline
|
||||
EXPECT_FALSE(EmbeddedDaemon::confSetsKey("#rpcthreads=4\n", "rpcthreads")); // commented out
|
||||
EXPECT_FALSE(EmbeddedDaemon::confSetsKey(" # rpcthreads=4\n", "rpcthreads"));
|
||||
EXPECT_FALSE(EmbeddedDaemon::confSetsKey("rpcthreadsx=4\nxrpcthreads=4\n", "rpcthreads"));
|
||||
EXPECT_FALSE(EmbeddedDaemon::confSetsKey("rpcthreads\n", "rpcthreads")); // not a key=value line
|
||||
EXPECT_FALSE(EmbeddedDaemon::confSetsKey("rpcworkqueue=64 # rpcthreads=4\n", "rpcthreads"));
|
||||
}
|
||||
|
||||
// S12: the ownership record names the node we launched, by pid AND start time, so a later session can
|
||||
// recognise it after "Leave running" — and a reused pid, or a record from before a reboot, never matches.
|
||||
void testOwnershipRecord()
|
||||
{
|
||||
using dragonx::daemon::EmbeddedDaemon;
|
||||
using Rec = EmbeddedDaemon::OwnershipRecord;
|
||||
|
||||
Rec r; r.pid = 4242; r.startToken = "boot-1/123456"; r.exePath = "/opt/dragonx/dragonxd";
|
||||
Rec back;
|
||||
EXPECT_TRUE(EmbeddedDaemon::parseOwnershipRecord(EmbeddedDaemon::formatOwnershipRecord(r), back));
|
||||
EXPECT_EQ(back.pid, 4242L);
|
||||
EXPECT_EQ(back.startToken, r.startToken);
|
||||
EXPECT_EQ(back.exePath, r.exePath);
|
||||
EXPECT_TRUE(EmbeddedDaemon::parseOwnershipRecord("pid=7\r\nstart=x\r\n", back) && back.pid == 7);
|
||||
EXPECT_FALSE(EmbeddedDaemon::parseOwnershipRecord("", back));
|
||||
EXPECT_FALSE(EmbeddedDaemon::parseOwnershipRecord("pid=7\n", back)); // no start token
|
||||
EXPECT_FALSE(EmbeddedDaemon::parseOwnershipRecord("start=x\npid=0\n", back)); // no pid
|
||||
EXPECT_FALSE(EmbeddedDaemon::parseOwnershipRecord("garbage", back));
|
||||
|
||||
char state = 0;
|
||||
unsigned long long start = 0;
|
||||
EXPECT_TRUE(EmbeddedDaemon::parseProcStatStart(
|
||||
"4242 (drag on) x) S 1 4242 4242 0 -1 4194560 100 0 0 0 7 3 0 0 20 0 9 0 98765 1000 50", state, start));
|
||||
EXPECT_TRUE(state == 'S');
|
||||
EXPECT_EQ(start, 98765ULL);
|
||||
EXPECT_FALSE(EmbeddedDaemon::parseProcStatStart("garbage", state, start));
|
||||
|
||||
#ifdef _WIN32
|
||||
const long self = static_cast<long>(GetCurrentProcessId());
|
||||
#else
|
||||
const long self = static_cast<long>(getpid());
|
||||
#endif
|
||||
const std::string mine = EmbeddedDaemon::processStartToken(self);
|
||||
EXPECT_FALSE(mine.empty());
|
||||
EXPECT_EQ(EmbeddedDaemon::processStartToken(self), mine); // stable
|
||||
EXPECT_TRUE(EmbeddedDaemon::processStartToken(0).empty());
|
||||
#ifndef _WIN32
|
||||
{ // an exited (reaped) process has no token: its pid may be anyone's now
|
||||
const pid_t child = fork();
|
||||
if (child == 0) _exit(0);
|
||||
int status = 0;
|
||||
waitpid(child, &status, 0);
|
||||
EXPECT_TRUE(EmbeddedDaemon::processStartToken(static_cast<long>(child)).empty());
|
||||
}
|
||||
#endif
|
||||
|
||||
// A record for a live process is kept; one whose pid is alive but started at another time (a reused
|
||||
// pid) is stale and deleted, as is an unreadable one. Temp paths only (never the real datadir).
|
||||
const fs::path dir = makeTempDir();
|
||||
const std::string path = (dir / "obsidiandragon-dragonxd.pid").string();
|
||||
Rec live; live.pid = self; live.startToken = mine; live.exePath = "x";
|
||||
EmbeddedDaemon::writeOwnershipRecord(path, live);
|
||||
Rec got;
|
||||
EXPECT_TRUE(EmbeddedDaemon::liveOwnershipRecord(path, &got));
|
||||
EXPECT_EQ(got.pid, self);
|
||||
EXPECT_TRUE(fs::exists(path));
|
||||
Rec reused = live; reused.startToken = mine + "0";
|
||||
EmbeddedDaemon::writeOwnershipRecord(path, reused);
|
||||
EXPECT_FALSE(EmbeddedDaemon::liveOwnershipRecord(path));
|
||||
EXPECT_FALSE(fs::exists(path));
|
||||
{ std::ofstream(path) << "garbage"; }
|
||||
EXPECT_FALSE(EmbeddedDaemon::liveOwnershipRecord(path));
|
||||
EXPECT_FALSE(fs::exists(path));
|
||||
EXPECT_FALSE(EmbeddedDaemon::liveOwnershipRecord(path)); // missing: just false
|
||||
std::error_code ec;
|
||||
fs::remove_all(dir, ec);
|
||||
}
|
||||
|
||||
// S13: the node writes its timestamped debug.log on every platform, which we tail. Fixed epochs (UTC).
|
||||
void testDebugLogTailRules()
|
||||
{
|
||||
using dragonx::daemon::EmbeddedDaemon;
|
||||
using dragonx::daemon::externalLogFeedOffset;
|
||||
const std::int64_t t0 = 1790000000; // 2026-09-21 13:46:40 UTC
|
||||
auto stamp = [](std::int64_t utc) {
|
||||
const std::time_t tt = static_cast<std::time_t>(utc);
|
||||
std::tm tm{};
|
||||
#ifdef _WIN32
|
||||
gmtime_s(&tm, &tt);
|
||||
#else
|
||||
gmtime_r(&tt, &tm);
|
||||
#endif
|
||||
char buf[32];
|
||||
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S ", &tm);
|
||||
return std::string(buf);
|
||||
};
|
||||
const std::string oldRun = stamp(t0 - 3600) + "DragonX version v1.3.0\n" +
|
||||
stamp(t0 - 3500) + "Reading blocks for witness rebuild: 5000 / 62583\n";
|
||||
const std::string newRun = stamp(t0 + 1) + "DragonX version v1.3.0\n" + stamp(t0 + 2) + "init message: Loading\n";
|
||||
|
||||
// A shrink at startup: resume at THIS run's banner, never replay the old run's rebuild lines.
|
||||
EXPECT_EQ(EmbeddedDaemon::shrinkResumeOffset(oldRun + newRun, t0), oldRun.size());
|
||||
EXPECT_EQ(EmbeddedDaemon::shrinkResumeOffset(oldRun, t0), oldRun.size()); // banner not written yet: skip
|
||||
EXPECT_EQ(EmbeddedDaemon::shrinkResumeOffset("", t0), size_t(0));
|
||||
|
||||
// A node someone else started at t0: only its own lines count.
|
||||
EXPECT_EQ(externalLogFeedOffset(oldRun + newRun, false, t0), oldRun.size());
|
||||
EXPECT_TRUE(externalLogFeedOffset(oldRun, false, t0) == std::string::npos); // a stale log (-printtoconsole node)
|
||||
const std::string midRun = stamp(t0 + 600) + "Reading blocks for witness rebuild: 9000 / 62583\n";
|
||||
EXPECT_EQ(externalLogFeedOffset("ing a partial line\n" + midRun, true, t0), size_t(0)); // banner before the window
|
||||
EXPECT_TRUE(externalLogFeedOffset(oldRun.substr(30), true, t0) == std::string::npos); // ...but old lines: no
|
||||
EXPECT_TRUE(externalLogFeedOffset(midRun, false, t0) == std::string::npos); // whole file, no banner: can't tell
|
||||
EXPECT_TRUE(externalLogFeedOffset(oldRun + newRun, false, 0) == std::string::npos);
|
||||
|
||||
#ifdef _WIN32
|
||||
const long self = static_cast<long>(GetCurrentProcessId());
|
||||
#else
|
||||
const long self = static_cast<long>(getpid());
|
||||
#endif
|
||||
const std::int64_t started = EmbeddedDaemon::processStartUtc(self);
|
||||
const std::int64_t nowUtc = static_cast<std::int64_t>(std::time(nullptr));
|
||||
EXPECT_TRUE(started > 0 && started <= nowUtc + 2 && started > nowUtc - 3600); // this test started recently
|
||||
EXPECT_FALSE(EmbeddedDaemon::processIsDaemon(self));
|
||||
EXPECT_FALSE(EmbeddedDaemon::processIsDaemon(0));
|
||||
}
|
||||
|
||||
void testDatadirLockGate()
|
||||
{
|
||||
using dragonx::daemon::EmbeddedDaemon;
|
||||
@@ -9067,6 +9199,9 @@ int main()
|
||||
testDaemonShutdownPolicy();
|
||||
testDatadirLockGate();
|
||||
testDaemonProcessIdentity();
|
||||
testConfSetsKey();
|
||||
testOwnershipRecord();
|
||||
testDebugLogTailRules();
|
||||
testStopDeliveryPolicy();
|
||||
testProcStatParse();
|
||||
testStopProgressTracker();
|
||||
|
||||
Reference in New Issue
Block a user