fix(daemon): make the "keep the node running?" prompt actually appear
Quitting during a witness rebuild is supposed to ask before stopping the node, because a stop throws the rebuild away and a killed node replays and redoes it. The guard almost never fired: - it returned false for any daemon >= v1.3.0, on the false premise that v1.3.0 checkpoints a rebuild (it does not; nothing survives a kill) - it read a 16 KB debug.log tail and compared the last progress line with the newest line, so any unrelated line during the rebuild's silent second phase turned it off; and on POSIX that file is stale - daemon_version, which the bypass needed, is zeroed when a busy node drops the connection Now a DaemonBusyTracker (src/daemon/daemon_busy_detect.h, pure) follows dragonxd's own markers in order: rebuilding (Phase-1 / legacy markers, ended by the completion line, an abort line or the next UpdateTip) and replaying (Activating best chain ... Done Loading), reset by a new banner or "Shutdown: done". It is fed from the owned output on the UI thread, including the backlog skipped after a restore. The quit guard prompts when the quit would stop the node and it is rebuilding, or has been alive but only timing out / refusing as busy for >= 30 s. A replay alone never prompts: stopping it is safe. The stop loop uses the same signal to never offer Kill during a rebuild. Also: - daemon output offsets are absolute, so a reader that falls behind a trim of the 1 MB buffer no longer skips new output (which could hide a start or end marker) - a quit deferred while minimized restores and raises the window, and the wizard renders the prompt, so it is never waiting unseen Verified live with the fake daemon: quitting mid-rebuild now shows the prompt for a v1.3.0 node and leaves the node alone; a silent busy node prompts after 30 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
#include "chat/chat_service.h"
|
||||
#include "chat/chat_database.h"
|
||||
#include "daemon/daemon_controller.h"
|
||||
#include "daemon/daemon_busy_detect.h"
|
||||
#include "daemon/embedded_daemon.h"
|
||||
#include "util/connect_stall.h"
|
||||
#include "util/logger.h"
|
||||
@@ -4016,6 +4017,156 @@ void testDaemonStopExecution()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------
|
||||
// S5: is the node busy with something stopping it would throw away? (DaemonBusyTracker, order-based)
|
||||
// ---------------------------------------------------------------------------------------------------
|
||||
|
||||
void testParseDaemonLogUtc()
|
||||
{
|
||||
using dragonx::daemon::parseDaemonLogUtc;
|
||||
// UTC, independent of the machine's timezone (no mktime).
|
||||
EXPECT_EQ(parseDaemonLogUtc("2026-09-30 16:34:29 Reading blocks for witness rebuild: 5836 / 62583"),
|
||||
(std::int64_t)1790786069);
|
||||
EXPECT_EQ(parseDaemonLogUtc("2026-09-30 17:21:56 BuildWitnessCache: rebuilt ..."), (std::int64_t)1790788916);
|
||||
EXPECT_EQ(parseDaemonLogUtc("2000-02-29 12:00:00 leap day"), (std::int64_t)951825600);
|
||||
EXPECT_EQ(parseDaemonLogUtc("Reading blocks for witness rebuild: 5836 / 62583"), (std::int64_t)0); // stdout
|
||||
EXPECT_EQ(parseDaemonLogUtc("2026-13-01 00:00:00 bad month"), (std::int64_t)0);
|
||||
}
|
||||
|
||||
void testDaemonBusyTracker()
|
||||
{
|
||||
using dragonx::daemon::DaemonBusyTracker;
|
||||
const std::string ts = "2026-09-30 16:34:29 ";
|
||||
const std::vector<std::string> phase1 = {
|
||||
"Reading blocks for witness rebuild: 836 / 62583",
|
||||
"Reading blocks for witness rebuild: 5836 / 62583",
|
||||
"Reading blocks for witness rebuild: 60836 / 62583",
|
||||
};
|
||||
const std::string endLine =
|
||||
"BuildWitnessCache: rebuilt 13605 note witness cache(s) to height 3326759 in 2820550ms using 24 thread(s)";
|
||||
|
||||
for (const bool withTs : {true, false}) { // Windows debug.log vs POSIX stdout: identical results
|
||||
const std::string p = withTs ? ts : "";
|
||||
{ // The 15s-rule regression: Phase 1, then ~47 min of unrelated lines (Phase 2 logs nothing).
|
||||
DaemonBusyTracker tr;
|
||||
for (const auto& l : phase1) tr.feedLine(p + l);
|
||||
EXPECT_TRUE(tr.rebuilding());
|
||||
EXPECT_EQ(tr.readDone(), 60836L);
|
||||
EXPECT_EQ(tr.readTotal(), 62583L);
|
||||
EXPECT_EQ(tr.rebuildStartUtc(), withTs ? (std::int64_t)1790786069 : (std::int64_t)0);
|
||||
for (int i = 0; i < 2000; ++i) {
|
||||
tr.feedLine(p + "socket sending timeout: 1201s");
|
||||
tr.feedLine(p + "TLS: connection from 203.0.113.10:21768 has been established (initiated)");
|
||||
tr.feedLine(p + "Using /16 prefix for IP bucketing");
|
||||
}
|
||||
EXPECT_TRUE(tr.rebuilding());
|
||||
tr.feedLine(p + endLine); // the real session-1 end line
|
||||
EXPECT_FALSE(tr.rebuilding());
|
||||
}
|
||||
{ // UpdateTip is logged under cs_main: the rebuild released it (no end line on some paths).
|
||||
DaemonBusyTracker tr;
|
||||
tr.feedLine(p + phase1[0]);
|
||||
tr.feedLine(p + "UpdateTip: new best=00ab height=3264178 log2_work=56.8 tx=1 date=x progress=1.0");
|
||||
EXPECT_FALSE(tr.rebuilding());
|
||||
}
|
||||
for (const char* abort : {"BuildWitnessCache: shutdown requested, aborting witness rebuild",
|
||||
"BuildWitnessCache: rescan aborted during witness rebuild",
|
||||
"aborting building witnesses"}) {
|
||||
DaemonBusyTracker tr;
|
||||
tr.feedLine(p + phase1[0]);
|
||||
tr.feedLine(p + abort);
|
||||
EXPECT_FALSE(tr.rebuilding());
|
||||
}
|
||||
{ // A new process (banner) or a finished shutdown forgets everything.
|
||||
DaemonBusyTracker a, b, c;
|
||||
a.feedLine(p + phase1[0]);
|
||||
a.feedLine(p + "DragonX version v1.3.0-c1040028e");
|
||||
EXPECT_FALSE(a.busy());
|
||||
b.feedLine(p + "init message: Activating best chain...");
|
||||
b.feedLine(p + "Hush version v1.2.0");
|
||||
EXPECT_FALSE(b.busy());
|
||||
c.feedLine(p + phase1[0]);
|
||||
c.feedLine(p + "Shutdown: done");
|
||||
EXPECT_FALSE(c.busy());
|
||||
}
|
||||
{ // Routine per-tx witness seeding and the pre-rescan clear are not a rebuild.
|
||||
DaemonBusyTracker tr;
|
||||
for (int i = 0; i < 3; ++i) tr.feedLine(p + "Setting Initial Sapling Witness for tx 00ab, 1 of 5");
|
||||
tr.feedLine(p + "Cleared witness data from 12 notes");
|
||||
EXPECT_FALSE(tr.rebuilding());
|
||||
}
|
||||
{ // The legacy v1.0.2 marker counts.
|
||||
DaemonBusyTracker tr;
|
||||
tr.feedLine(p + "Building Witnesses for block 123 0.5 complete, 40 remaining");
|
||||
EXPECT_TRUE(tr.rebuilding());
|
||||
}
|
||||
{ // The startup replay: UpdateTips don't end it, a rebuild nested inside doesn't either.
|
||||
DaemonBusyTracker tr;
|
||||
tr.feedLine(p + "init message: Activating best chain...");
|
||||
for (int i = 0; i < 5; ++i) tr.feedLine(p + "UpdateTip: new best=00ab height=3264179");
|
||||
EXPECT_TRUE(tr.replaying());
|
||||
EXPECT_FALSE(tr.rebuilding());
|
||||
tr.feedLine(p + phase1[0]);
|
||||
EXPECT_TRUE(tr.rebuilding() && tr.replaying());
|
||||
tr.feedLine(p + endLine);
|
||||
EXPECT_FALSE(tr.rebuilding());
|
||||
EXPECT_TRUE(tr.replaying());
|
||||
tr.feedLine(p + "init message: Full Node Done Loading! :)");
|
||||
EXPECT_FALSE(tr.replaying());
|
||||
}
|
||||
}
|
||||
{ // A marker split across two output chunks is still recognised (and \r\n line ends too).
|
||||
DaemonBusyTracker tr;
|
||||
tr.feed("noise line\r\nReading blocks for wit");
|
||||
EXPECT_FALSE(tr.rebuilding());
|
||||
tr.feed("ness rebuild: 836 / 62583\r\nnext");
|
||||
EXPECT_TRUE(tr.rebuilding());
|
||||
EXPECT_EQ(tr.readDone(), 836L);
|
||||
}
|
||||
}
|
||||
|
||||
void testShouldConfirmStop()
|
||||
{
|
||||
using dragonx::daemon::DaemonBusyTracker;
|
||||
using dragonx::daemon::shouldConfirmStop;
|
||||
DaemonBusyTracker idle, rebuilding, replaying;
|
||||
rebuilding.feedLine("Reading blocks for witness rebuild: 836 / 62583");
|
||||
replaying.feedLine("init message: Activating best chain...");
|
||||
EXPECT_TRUE(shouldConfirmStop(true, rebuilding, false)); // the inverse of the old v1.3.0 bypass
|
||||
EXPECT_FALSE(shouldConfirmStop(false, rebuilding, false)); // a quit that leaves the node running loses nothing
|
||||
EXPECT_TRUE(shouldConfirmStop(true, idle, true)); // busy but alive: a rebuild with no marker
|
||||
EXPECT_FALSE(shouldConfirmStop(true, idle, false));
|
||||
EXPECT_FALSE(shouldConfirmStop(true, replaying, false)); // stopping a replay is safe: no prompt
|
||||
}
|
||||
|
||||
void testOutputSinceAcrossTrim()
|
||||
{
|
||||
using dragonx::daemon::EmbeddedDaemon;
|
||||
// Offsets are absolute: a reader that is behind when old output is trimmed gets everything still
|
||||
// buffered (the old index-based offset pointed past it and skipped up to a trim's worth of new text).
|
||||
std::string all;
|
||||
for (int i = 0; i < 100; ++i) all += "line " + std::to_string(i) + "\n";
|
||||
size_t offset = 0;
|
||||
std::string got = EmbeddedDaemon::outputSince(all, all.size(), offset);
|
||||
EXPECT_EQ(got, all);
|
||||
EXPECT_EQ(offset, all.size());
|
||||
// 20 more lines arrive, then the buffer keeps only its last 300 bytes.
|
||||
std::string more;
|
||||
for (int i = 100; i < 120; ++i) more += "line " + std::to_string(i) + "\n";
|
||||
const std::string full = all + more;
|
||||
const std::string kept = full.substr(full.size() - 300);
|
||||
got = EmbeddedDaemon::outputSince(kept, full.size(), offset);
|
||||
EXPECT_EQ(got, more); // every byte after where we left off, none skipped
|
||||
EXPECT_EQ(offset, full.size());
|
||||
// A reader so far behind that part of its unread text was trimmed resumes at what's left.
|
||||
size_t stale = 10;
|
||||
got = EmbeddedDaemon::outputSince(kept, full.size(), stale);
|
||||
EXPECT_EQ(got, kept);
|
||||
// Nothing new: empty, offset unchanged.
|
||||
got = EmbeddedDaemon::outputSince(kept, full.size(), offset);
|
||||
EXPECT_TRUE(got.empty());
|
||||
}
|
||||
|
||||
// S3: on Linux a node in its final shutdown phase reads comm "hush-shutoff"; its executable still says
|
||||
// dragonxd, so it must still count as running (it holds the datadir lock until it exits).
|
||||
void testDaemonProcessIdentity()
|
||||
@@ -8604,6 +8755,10 @@ int main()
|
||||
testStopProgressTracker();
|
||||
testEvaluateStopWait();
|
||||
testDaemonStopExecution();
|
||||
testParseDaemonLogUtc();
|
||||
testDaemonBusyTracker();
|
||||
testShouldConfirmStop();
|
||||
testOutputSinceAcrossTrim();
|
||||
#ifdef _WIN32
|
||||
testWindowsStopNeverKillsForeignDaemon();
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user