perf(startup): prepare first-run node files in the background

On a first run or after an upgrade, Windows release builds wrote the
bundled Sapling params, asmap, node and miner (about 88 MB) in
App::init, before the first frame: 0.5-3 s of frozen blank window, 5-30 s
on a slow disk or under antivirus. Zip, AppImage and .app builds copied
the bundled files into the daemon dir and hashed the 51.5 MB of params on
the UI thread at the first node start, a 0.3-1.5 s freeze in the wizard
or the connect loop.

A stat-only check at startup decides whether anything is missing, stale
or unverified; a normal launch starts no thread and shows nothing.
Otherwise one background job extracts (Windows) or copies (Linux, macOS)
the files, then verifies the params once:

- The first frame paints at once. If the job takes longer than 0.4 s, a
  chip at the bottom shows "Preparing wallet files... N%", then
  "Verifying wallet files...".
- A node start on the UI thread while the job runs is deferred and
  replayed when it ends (never into a bootstrap download, a restart or a
  live connection); worker threads wait, and give up on quit. After an
  upgrade that ships a new miner, the node starts after the miner is
  written instead of before. Pool-miner starts and the miner version
  check wait too, so the old miner is never run or reported while it is
  being replaced.
- Each file goes to "<name>.partial", is flushed and renamed into place
  under one lock shared with the daemon start, the bundled-node
  reinstall and the miner extraction, and the job stops between 4 MB
  chunks at shutdown. An interrupted write used to leave a truncated
  binary that was trusted forever; a leftover .partial is now removed.

Other behaviour changes:

- Linux and macOS full-node builds copy the bundled node files (about
  77 MB) into the daemon dir at first launch, even if the wallet ends up
  on an external node; before, this happened at the first embedded start.
- Lite no longer writes the 53 MB of params and asmap it never reads.
- A disk-full or permission failure that leaves the node files unusable
  shows a warning; a miner blocked by antivirus alone does not.
- A corrupt or truncated param in a zip, AppImage or .app install is
  replaced from the bundle and verified again instead of trusted forever.
- Reinstalling the bundled node holds off the connect loop and a deferred
  start for its whole run (either could launch the old binary while the
  new one was written), and is refused while another restart is running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 21:09:48 -05:00
co-authored by Claude Opus 5.5
parent 47180ff840
commit 971f2abae5
19 changed files with 1245 additions and 360 deletions
+275 -110
View File
@@ -274,7 +274,10 @@ void App::applyPoolAutoBalance(const util::PoolStatsService::Snapshot& snap)
std::string App::poolMiningInstalledVersion()
{
daemon::XmrigManager::startVersionDetection(); // idempotent one-shot
// Not while the startup job may still be replacing the miner (`xmrig --version` would cache the old
// binary's version); pollStartupFiles() starts the detection when the job ends.
if (!startup_files_ || !startup_files_->isRunning())
daemon::XmrigManager::startVersionDetection(); // idempotent one-shot
return daemon::XmrigManager::installedVersion();
}
@@ -348,11 +351,11 @@ bool App::init()
{
DEBUG_LOGF("Initializing ObsidianDragon...\n");
// Extract embedded resources (Sapling params, asmap.dat) on first run
if (resources::hasEmbeddedResources() && resources::needsParamsExtraction()) {
DEBUG_LOGF("First run - extracting bundled resources...\n");
resources::extractEmbeddedResources();
}
ui_thread_id_ = std::this_thread::get_id();
// Write the node files (Sapling params, asmap, node, miner) on first run / after an upgrade OFF the UI
// thread: the first frame no longer waits for ~88 MB of disk writes. startEmbeddedDaemon() defers (UI
// thread) or waits (workers) until the job is done, so the node never starts over half-written files.
startStartupFilesProvision();
// Initialize settings
settings_ = std::make_unique<config::Settings>();
@@ -525,7 +528,10 @@ bool App::init()
// hashrates and the installed miner version are shown as soon as the tab is opened,
// instead of only after the user clicks a mine / auto-balance button.
if (supportsPoolMining()) {
daemon::XmrigManager::startVersionDetection(); // idempotent one-shot `xmrig --version`
// Needs the final miner on disk: while the startup job may still be writing it, pollStartupFiles()
// starts this instead.
if (!startup_files_->isRunning())
daemon::XmrigManager::startVersionDetection(); // idempotent one-shot `xmrig --version`
pool_stats_service_.refresh(util::knownPools()); // snapshot lands on a later frame
}
@@ -842,6 +848,9 @@ void App::update()
// capture_mode_ is only set for the offline full sweep, so the live tab-only sweep is unaffected.
if (capture_mode_) return;
// Reap the startup node-file job (failure toast, deferred daemon start, miner version probe).
pollStartupFiles();
// If a wallet switch's daemon failed to start, revert to the previous wallet (main-thread-safe).
processWalletSwitchRevert();
@@ -1845,6 +1854,10 @@ void App::render()
// (Pins current_page_ too, before the sidebar reads it further down.)
updateScreenshotSweep();
// First-run node-file progress. Foreground draw list, so it shows over the wizard, the lock screen and
// the main UI alike (all of which early-return below).
renderStartupFilesChip();
// DragonX logo — load/recolor before the wizard/lock early-returns so every screen shows it.
ensureLogoTexture();
@@ -4294,6 +4307,160 @@ void App::renderBackupDialog()
m::EndOverlayDialog();
}
// ---------------------------------------------------------------------------------------------------
// Startup node files (C14). Windows release exes carry the Sapling params, asmap, the node and the miner
// inside the .exe and write them out on first run / after an upgrade (~88 MB); zip/AppImage/.app builds copy
// them from next to the executable; full-node builds then SHA-256 the params once (cached by marker). All of
// it used to run on the UI thread. It now runs on the "extract-node-files" async task.
// ---------------------------------------------------------------------------------------------------
void App::startStartupFilesProvision()
{
startup_files_ = std::make_shared<resources::StartupProvision>();
const bool embedded = resources::hasEmbeddedResources();
const bool fullNode = supportsEmbeddedDaemon(); // compile-time; Lite provisions only the miner
// Stat-only pre-check (a handful of stats + the .sapling_verified marker): a steady-state launch starts no
// thread and shows nothing.
bool needed = embedded && resources::needsParamsExtraction();
if (!needed && !embedded && fullNode) {
needed = !resources::planBundleCopy(resources::bundleSearchDirs(), resources::getDaemonDirectory(),
resources::nodeBundleFileNames(), /*replaceExisting=*/false).empty();
}
if (!needed && fullNode)
needed = rpc::Connection::saplingParamState() == rpc::Connection::SaplingParamState::NeedsHash;
if (!needed) { startup_files_->markNothingToDo(); return; }
DEBUG_LOGF("Startup: preparing node files in the background...\n");
startup_files_->begin();
auto st = startup_files_; // the job owns a reference and captures NO App state
auto job = [st, embedded, fullNode](const util::AsyncTaskManager::Token& token) {
// Every shutdown path calls async_tasks_.cancelAll() first, so the token is the only cancel source.
auto cancelled = [&token]() { return token.cancelled(); };
resources::StartupProvision::Result r;
try {
if (embedded) {
r.extractOk = resources::extractEmbeddedResources(&st->progress, cancelled);
} else if (fullNode) {
r.extractOk = resources::copyBundledFiles(resources::bundleSearchDirs(),
resources::getDaemonDirectory(),
resources::nodeBundleFileNames(),
/*replaceExisting=*/false, &st->progress, cancelled).ok;
}
// Verify even if some file failed (an antivirus-blocked miner must not skip the param check).
if (fullNode && !cancelled()) {
st->progress.setPhase(resources::ProvisionPhase::Verifying);
r.verifyOk = rpc::Connection::verifySaplingParams(); // hashes once, then writes the marker
if (!r.verifyOk && !embedded && !cancelled()) {
// A copied distribution whose daemon-dir param exists but is corrupt/truncated: the old code
// skipped existing files forever. Replace the params from the bundle and verify again.
st->paramRepairTried.store(true, std::memory_order_relaxed);
const auto rep = resources::copyBundledFiles(resources::bundleSearchDirs(),
resources::getDaemonDirectory(),
resources::saplingParamFileNames(),
/*replaceExisting=*/true, &st->progress, cancelled);
if (rep.copied > 0 && !cancelled()) {
st->progress.setPhase(resources::ProvisionPhase::Verifying);
r.verifyOk = rpc::Connection::verifySaplingParams();
}
}
}
} catch (const std::exception& e) {
DEBUG_LOGF("[ERROR] Startup file preparation threw: %s\n", e.what());
r.extractOk = false;
} catch (...) {
DEBUG_LOGF("[ERROR] Startup file preparation threw an unknown exception\n");
r.extractOk = false;
}
r.cancelled = token.cancelled();
st->finish(r); // ALWAYS: a job stuck in Running would defer every daemon start until shutdown
};
try {
async_tasks_.submit("extract-node-files", job);
} catch (const std::exception& e) {
// No thread could be created: do it in line, exactly as before this change (blocks once).
DEBUG_LOGF("[WARN] Startup: no background thread for file preparation (%s) - running it in line\n", e.what());
job(util::AsyncTaskManager::Token{});
}
}
void App::pollStartupFiles()
{
resources::StartupProvision::Result r;
if (!startup_files_ || !startup_files_->takeCompletion(&r)) return;
if (r.cancelled || shutting_down_) return;
DEBUG_LOGF("Startup: node files done (write=%d verify=%d)\n", r.extractOk ? 1 : 0, r.verifyOk ? 1 : 0);
// Deliberate improvement: the old code swallowed a write failure until the node refused to start. Only
// when the node files are unusable — a miner blocked by antivirus (common) must not nag every launch.
if (supportsEmbeddedDaemon() && !r.extractOk && !r.verifyOk)
ui::Notifications::instance().warning(TR("sb_daemon_extract_failed"), 20.0f);
// The miner version probe waits for the final miner on disk (deferred from init()).
if (supportsPoolMining()) daemon::XmrigManager::startVersionDetection();
// A start was requested while the files were being written (wizard pre-start / Done, first tryConnect, the
// connect MainCb): replay it — but never into a bootstrap download (which stops the node on purpose), an
// orchestrated restart, or an already-connected session. Every UI-thread requester also calls tryConnect().
if (daemon_start_after_files_) {
daemon_start_after_files_ = false;
const bool bootstrapping = bootstrap_downloading_ || wizard_phase_ == WizardPhase::BootstrapInProgress;
if (!bootstrapping && !daemon_restarting_ && !state_.connected) {
if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) startEmbeddedDaemon();
if (!connection_in_progress_) tryConnect();
} else if (getDaemonStatus() == TR("sb_waiting_daemon")) {
setDaemonStatus(std::string()); // the deferral's status, unless another flow replaced it since
}
}
}
void App::renderStartupFilesChip()
{
if (!startup_files_ || !startup_files_->isRunning()) return;
ui::material::RequestProgressFrame(); // keeps the percentage moving (~10 fps) while the job runs
const auto& S = ui::schema::UI();
auto val = [&S](const char* key, float fallback) {
return S.drawElement("components.startup-files-chip", key).sizeOr(fallback);
};
if (!resources::provisionChipVisible(true, startup_files_->elapsedSeconds(), val("show-delay-seconds", 0.4f)))
return; // a job that ends within the grace period never flashes a chip
namespace m = ui::material;
const auto& pr = startup_files_->progress;
const bool verifying = pr.getPhase() == resources::ProvisionPhase::Verifying;
const int pct = resources::provisionPercent(pr.bytesDone.load(std::memory_order_relaxed),
pr.bytesTotal.load(std::memory_order_relaxed));
char buf[160];
if (verifying) snprintf(buf, sizeof(buf), "%s", TR("startup_files_verifying"));
else snprintf(buf, sizeof(buf), TR("startup_files_preparing"), pct);
// Hand-drawn geometry: ui.toml values are logical px -> multiply by dpiScale(); font metrics already are.
const float dp = ui::Layout::dpiScale();
ImFont* f = m::Type().caption();
const float padX = val("padding-x", 14.0f) * dp, barH = val("bar-height", 3.0f) * dp;
const float barBottom = val("bar-bottom", 10.0f) * dp;
const float textW = f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0.0f, buf).x;
ImGuiViewport* vp = ImGui::GetMainViewport();
const float w = std::min(std::max(val("width", 340.0f) * dp, textW + 2.0f * padX), vp->Size.x - 2.0f * padX);
const float h = val("height", 46.0f) * dp;
const float margin = val("bottom-margin", 38.0f) * dp, rounding = val("rounding", 10.0f) * dp;
const ImVec2 mn(vp->Pos.x + (vp->Size.x - w) * 0.5f, vp->Pos.y + vp->Size.y - h - margin);
const ImVec2 mx(mn.x + w, mn.y + h);
ImDrawList* dl = ImGui::GetForegroundDrawList();
dl->AddRectFilled(mn, mx, m::SurfaceAt(m::Elevation::Dp6), rounding); // opaque (Surface() is translucent)
dl->AddRect(mn, mx, m::WithAlpha(m::OnSurface(), 40), rounding, 0, 1.0f * dp);
const float textY = mn.y + (h - barBottom - barH - f->LegacySize) * 0.5f;
dl->AddText(f, f->LegacySize, ImVec2(mn.x + padX, textY), m::OnSurface(), buf);
const float bx0 = mn.x + padX, bx1 = mx.x - padX, by1 = mx.y - barBottom, by0 = by1 - barH;
dl->AddRectFilled(ImVec2(bx0, by0), ImVec2(bx1, by1), m::WithAlpha(m::OnSurface(), 30), barH * 0.5f);
float f0 = 0.0f, f1 = pct / 100.0f;
if (verifying) { // indeterminate: a sliding segment
const float t = std::fmod(static_cast<float>(ImGui::GetTime()) * 0.8f, 1.0f);
f0 = std::max(0.0f, t - 0.15f);
f1 = std::min(1.0f, t + 0.15f);
}
if (f1 > f0)
dl->AddRectFilled(ImVec2(bx0 + (bx1 - bx0) * f0, by0), ImVec2(bx0 + (bx1 - bx0) * f1, by1),
m::Primary(), barH * 0.5f);
}
void App::maybeOfferDaemonUpdate()
{
if (!supportsFullNodeLifecycleActions()) return; // lite / no embedded-daemon build
@@ -5550,6 +5717,31 @@ bool App::startEmbeddedDaemon()
return false;
}
// The startup job may still be writing/copying/verifying the node files. Never launch the node over
// half-written files and never block the UI thread on the job: on the UI thread remember the request
// (pollStartupFiles() replays it) and report true ("starting") so every caller keeps its retry path
// instead of raising a failure — NOTE: true here does NOT mean a process was launched; on a worker,
// wait for the job (ends at shutdown).
if (startup_files_) {
switch (resources::daemonStartGate(startup_files_->isRunning(),
std::this_thread::get_id() == ui_thread_id_)) {
case resources::DaemonStartGate::Defer:
setDaemonStatus(TR("sb_waiting_daemon")); // neutral: the job may be a copy / verify / miner refresh
daemon_start_after_files_ = true;
return true;
case resources::DaemonStartGate::Wait:
setDaemonStatus(TR("sb_waiting_daemon"));
while (!startup_files_->waitUntilDone(100)) {
if (shutting_down_) return false;
}
// The job ends BECAUSE of shutdown (cancelAll): don't fall into the uncancellable extraction/launch.
if (shutting_down_ || startup_files_->endedCancelled()) return false;
break;
case resources::DaemonStartGate::Proceed:
break;
}
}
// Check if Sapling params exist - try extracting if embedded
if (!rpc::Connection::verifySaplingParams()) {
DEBUG_LOGF("Sapling params not found, checking for embedded resources...\n");
@@ -5573,48 +5765,28 @@ bool App::startEmbeddedDaemon()
}
DEBUG_LOGF("Sapling params extracted successfully\n");
} else {
// Fallback: check for params bundled alongside the executable
// (zip distributions bundle sapling-*.params next to the binary)
namespace fs = std::filesystem;
std::string exe_dir = util::Platform::getExecutableDirectory();
std::string daemon_dir = resources::getDaemonDirectory();
const char* paramFiles[] = { "sapling-spend.params", "sapling-output.params", "asmap.dat" };
bool copied = false;
if (!exe_dir.empty()) {
// Fallback: params bundled alongside the executable (zip / AppImage / .app). The startup job
// normally did this already; this repairs a failed or corrupt copy. replaceExisting: we only get
// here because verification FAILED, so an existing daemon-dir param is suspect (the old
// `if (exists) continue` kept a truncated param forever).
const std::string daemon_dir = resources::getDaemonDirectory();
if (!util::Platform::getExecutableDirectory().empty()) {
std::string dirErr;
if (!util::Platform::ensureDirectory(daemon_dir, &dirErr)) {
setDaemonStatus(dirErr);
DEBUG_LOGF("[ERROR] %s\n", dirErr.c_str());
return false;
}
std::error_code ec;
// On macOS .app bundles, params are in Contents/Resources/
// while the executable is in Contents/MacOS/
std::vector<std::string> searchDirs = { exe_dir };
#ifdef __APPLE__
{
fs::path resourcesDir = fs::path(exe_dir).parent_path() / "Resources";
if (fs::is_directory(resourcesDir))
searchDirs.insert(searchDirs.begin(), resourcesDir.string());
}
#endif
for (const char* name : paramFiles) {
fs::path dst = fs::path(daemon_dir) / name;
if (fs::exists(dst)) continue;
for (const auto& dir : searchDirs) {
fs::path src = fs::path(dir) / name;
if (fs::exists(src)) {
DEBUG_LOGF("Copying bundled %s from %s to %s\n", name, dir.c_str(), daemon_dir.c_str());
fs::copy_file(src, dst, ec);
if (!ec) copied = true;
break;
}
}
}
}
if (copied && rpc::Connection::verifySaplingParams()) {
// At most once per session (the startup job or an earlier start may have tried it): a still-failing
// verify must not repeat this ~51.5 MB copy on every 5 s connect retry on the UI thread.
bool repaired = false;
if (!(startup_files_ && startup_files_->paramRepairTried.exchange(true))) {
const auto rep = resources::copyBundledFiles(resources::bundleSearchDirs(), daemon_dir,
resources::saplingParamFileNames(), /*replaceExisting=*/true);
repaired = rep.copied > 0 && rpc::Connection::verifySaplingParams();
}
if (repaired) {
DEBUG_LOGF("Sapling params copied from exe directory successfully\n");
} else {
setDaemonStatus(TR("sb_sapling_not_found"));
@@ -5625,49 +5797,22 @@ bool App::startEmbeddedDaemon()
}
}
// Ensure asmap.dat and daemon binaries are copied from the bundle
// to the daemon directory even if sapling params already existed.
// Ensure asmap.dat and the daemon binaries are copied from the bundle to the daemon directory even if the
// Sapling params already existed (stat-only no-op when present; the startup job normally did it). An absent
// source is optional; only a real copy error counts.
{
namespace fs = std::filesystem;
std::string exe_dir = util::Platform::getExecutableDirectory();
std::string daemon_dir = resources::getDaemonDirectory();
if (!exe_dir.empty()) {
const std::string daemon_dir = resources::getDaemonDirectory();
if (!util::Platform::getExecutableDirectory().empty()) {
std::string dirErr;
if (!util::Platform::ensureDirectory(daemon_dir, &dirErr)) {
setDaemonStatus(dirErr);
DEBUG_LOGF("[ERROR] %s\n", dirErr.c_str());
return false;
}
std::error_code ec;
std::vector<std::string> searchDirs = { exe_dir };
#ifdef __APPLE__
{
fs::path resourcesDir = fs::path(exe_dir).parent_path() / "Resources";
if (fs::is_directory(resourcesDir))
searchDirs.insert(searchDirs.begin(), resourcesDir.string());
}
#endif
const char* extraFiles[] = { "asmap.dat", "dragonxd", "dragonx-cli", "dragonx-tx" };
bool copyFailed = false;
for (const char* name : extraFiles) {
fs::path dst = fs::path(daemon_dir) / name;
if (fs::exists(dst)) continue;
for (const auto& dir : searchDirs) {
fs::path src = fs::path(dir) / name;
if (fs::exists(src)) { // an absent source is optional; only a real copy error counts
DEBUG_LOGF("Copying bundled %s from %s to %s\n", name, dir.c_str(), daemon_dir.c_str());
fs::copy_file(src, dst, ec);
if (ec) {
DEBUG_LOGF("[ERROR] Failed to copy %s: %s\n", name, ec.message().c_str());
copyFailed = true;
ec.clear();
}
break;
}
}
}
if (copyFailed) {
static const std::vector<std::string> kExtraFiles = { "asmap.dat", "dragonxd", "dragonx-cli", "dragonx-tx" };
const auto rep = resources::copyBundledFiles(resources::bundleSearchDirs(), daemon_dir, kExtraFiles,
/*replaceExisting=*/false);
if (!rep.ok) {
char buf[512];
snprintf(buf, sizeof(buf), TR("sb_daemon_files_failed"), daemon_dir.c_str());
setDaemonStatus(buf);
@@ -6118,42 +6263,58 @@ void App::reinstallBundledDaemon()
ui::Notifications::instance().warning(TR("appx_daemon_reinstall_in_progress"));
return;
}
// Don't race a wallet switch / seed-adopt / encryption restart, which drive their own daemon stop/start.
if (daemon_restarting_) {
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
return;
}
if (refuseIfNodeBusy()) return;
DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n");
ui::Notifications::instance().info(TR("appx_installing_bundled_daemon"));
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
AppDaemonLifecycleRuntime runtime(*this);
daemon::AsyncLifecycleTaskContext ctx(token, shutting_down_);
// Stop the running node so its binary is free: ours, or the external/leftover dragonxd we're attached
// to (it would keep the RPC port and the fresh daemon couldn't start). Bounded, never killed.
stopEmbeddedDaemon(StopMode::Background, StopScope::Connected);
// Wait (bounded, ~120s) for the old node's PROCESS to exit, not just its RPC port: dragonxd unbinds
// RPC first and can take a minute or more to flush and close its wallet. Until it exits its exe is
// locked on Windows (the overwrite would fail) and it holds the datadir lock (a restart would fail).
auto stillUp = [] {
return daemon::EmbeddedDaemon::isRpcPortInUse() || daemon::EmbeddedDaemon::isDaemonProcessRunning();
};
for (int i = 0; i < 1200 && stillUp() && !ctx.cancelled() && !ctx.shuttingDown(); ++i) {
ctx.sleepForMs(100);
}
if (ctx.cancelled() || ctx.shuttingDown()) return;
if (stillUp()) {
DEBUG_LOGF("[App] Reinstall bundled daemon: aborted — a dragonxd process is still running; "
"binaries left untouched and no new node started\n");
pending_reinstall_node_busy_.store(true, std::memory_order_relaxed); // told on the UI thread
return;
}
// Overwrite the installed dragonx binaries (dragonxd/cli/tx) with the bundled ones.
const bool ok = resources::reextractBundledDaemon();
DEBUG_LOGF("[App] Reinstall bundled daemon: %s\n", ok ? "ok" : "FAILED");
// Restart on the freshly-installed binary.
runtime.resetOutputOffset();
if (!ctx.cancelled() && !ctx.shuttingDown()) {
runtime.startDaemon();
}
});
// This task owns the node for its whole run: it starts the node itself once the new files are in place, so
// drop a start deferred behind the startup files job and gate tryConnect() + the pollStartupFiles() replay
// (either could otherwise launch the OLD binary while the new one is being written).
daemon_start_after_files_ = false;
daemon_restarting_ = true;
try {
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
struct Rearm { std::atomic<bool>& f; ~Rearm() { f = false; } } rearm{daemon_restarting_}; // every exit
AppDaemonLifecycleRuntime runtime(*this);
daemon::AsyncLifecycleTaskContext ctx(token, shutting_down_);
// Stop the running node so its binary is free: ours, or the external/leftover dragonxd we're attached
// to (it would keep the RPC port and the fresh daemon couldn't start). Bounded, never killed.
stopEmbeddedDaemon(StopMode::Background, StopScope::Connected);
// Wait (bounded, ~120s) for the old node's PROCESS to exit, not just its RPC port: dragonxd unbinds
// RPC first and can take a minute or more to flush and close its wallet. Until it exits its exe is
// locked on Windows (the overwrite would fail) and it holds the datadir lock (a restart would fail).
auto stillUp = [] {
return daemon::EmbeddedDaemon::isRpcPortInUse() || daemon::EmbeddedDaemon::isDaemonProcessRunning();
};
for (int i = 0; i < 1200 && stillUp() && !ctx.cancelled() && !ctx.shuttingDown(); ++i) {
ctx.sleepForMs(100);
}
if (ctx.cancelled() || ctx.shuttingDown()) return;
if (stillUp()) {
DEBUG_LOGF("[App] Reinstall bundled daemon: aborted — a dragonxd process is still running; "
"binaries left untouched and no new node started\n");
pending_reinstall_node_busy_.store(true, std::memory_order_relaxed); // told on the UI thread
return;
}
// Overwrite the installed dragonx binaries (dragonxd/cli/tx) with the bundled ones.
const bool ok = resources::reextractBundledDaemon();
DEBUG_LOGF("[App] Reinstall bundled daemon: %s\n", ok ? "ok" : "FAILED");
// Restart on the freshly-installed binary.
runtime.resetOutputOffset();
if (!ctx.cancelled() && !ctx.shuttingDown()) {
runtime.startDaemon();
}
});
} catch (...) {
daemon_restarting_ = false; // no task ran: re-arm the reconnect gate
throw;
}
}
void App::deleteBlockchainData()
@@ -6199,6 +6360,9 @@ void App::deleteBlockchainData()
bool App::stopDaemonForBootstrap()
{
// A bootstrap download stops the node on purpose: drop a start deferred while the startup files were
// being prepared, or pollStartupFiles() would launch the node into the half-extracted chain data.
daemon_start_after_files_ = false;
if (!supportsFullNodeLifecycleActions()) {
return false;
}
@@ -7548,7 +7712,8 @@ bool App::hasPendingBackgroundResults() const {
return (worker_ && worker_->hasPendingResults())
|| (fast_worker_ && fast_worker_->hasPendingResults())
|| (http_worker_ && http_worker_->hasPendingResults()) // g3: market-data GETs
|| (lite_wallet_ && lite_wallet_->hasWalletOpCompletions()); // g1: lite wallet-op lane
|| (lite_wallet_ && lite_wallet_->hasWalletOpCompletions()) // g1: lite wallet-op lane
|| (startup_files_ && startup_files_->hasCompletion()); // g6: startup node files
}
std::string App::transactionSendProgressText() const