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:
@@ -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