feat(fullnode): manage the daemon binary in Settings; stop auto-overwriting it

Previously the wallet re-extracted the bundled dragonxd on startup whenever the
installed binary's size differed from the bundle ("stale" overwrite), which could
replace a node a user had deliberately placed in dragonx/.

Now dragonx binaries (dragonxd/cli/tx) are auto-placed ONLY when missing — never
auto-overwritten on a size mismatch (needsParamsExtraction + extractEmbeddedResources).
Params/asmap keep their size-based refresh; a daemon dropped next to the wallet exe
still takes priority and is never touched.

Replacing the daemon is now an explicit action: Settings → "Daemon binary" reports the
installed binary's version (scanned from the file), size and modified date, compares it
to the version bundled in this build, and offers an "Install bundled daemon" button.
That stops the node, overwrites dragonxd/cli/tx with the bundled copies (waiting for the
process to release the file lock), and restarts — wallet/keys/chain data untouched.

Adds resources::{getInstalledDaemonInfo,getBundledDaemonInfo,reextractBundledDaemon}
(+ a version-string scanner) and App::reinstallBundledDaemon().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 21:06:59 -05:00
parent de70e68472
commit b2e104358d
6 changed files with 316 additions and 13 deletions

View File

@@ -5,6 +5,8 @@
#include <filesystem>
#include <vector>
#include <cstdio>
#include <cctype>
#include <chrono>
#ifdef _WIN32
#include <windows.h>
@@ -225,11 +227,13 @@ bool needsParamsExtraction()
if (spendRes && resourceNeedsUpdate(spendRes, spendPath)) return true;
if (outputRes && resourceNeedsUpdate(outputRes, outputPath)) return true;
// Also check if daemon binaries need updating
// Daemon binaries are only auto-placed when MISSING (never auto-overwritten on a size
// mismatch) — the user may be running a specific dragonxd. Replacing the bundled daemon is
// an explicit action via Settings → daemon binary. So only trigger extraction if it's absent.
#ifdef HAS_EMBEDDED_DAEMON
const auto* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
std::string daemonPath = daemonDir + pathSep + RESOURCE_DRAGONXD;
if (daemonRes && resourceNeedsUpdate(daemonRes, daemonPath)) return true;
if (daemonRes && !std::filesystem::exists(daemonPath)) return true;
#endif
#ifdef HAS_EMBEDDED_XMRIG
@@ -369,12 +373,12 @@ bool extractEmbeddedResources()
#ifdef HAS_EMBEDDED_DAEMON
DEBUG_LOGF("[INFO] Daemon extraction directory: %s\n", daemonDir.c_str());
// Daemon binaries are placed ONLY when missing — never auto-overwritten on a size mismatch
// (the user may run a specific dragonxd; replacing it is an explicit Settings action).
const EmbeddedResource* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
if (daemonRes) {
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONXD;
if (resourceNeedsUpdate(daemonRes, dest)) {
if (std::filesystem::exists(dest))
DEBUG_LOGF("[INFO] Updating stale dragonxd (size mismatch)...\n");
if (!std::filesystem::exists(dest)) {
DEBUG_LOGF("[INFO] Extracting dragonxd (%zu MB)...\n", daemonRes->size / (1024*1024));
if (!extractResource(daemonRes, dest)) {
success = false;
@@ -384,13 +388,11 @@ bool extractEmbeddedResources()
#endif
}
}
const EmbeddedResource* cliRes = getEmbeddedResource(RESOURCE_DRAGONX_CLI);
if (cliRes) {
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_CLI;
if (resourceNeedsUpdate(cliRes, dest)) {
if (std::filesystem::exists(dest))
DEBUG_LOGF("[INFO] Updating stale dragonx-cli (size mismatch)...\n");
if (!std::filesystem::exists(dest)) {
DEBUG_LOGF("[INFO] Extracting dragonx-cli (%zu MB)...\n", cliRes->size / (1024*1024));
if (!extractResource(cliRes, dest)) {
success = false;
@@ -400,13 +402,11 @@ bool extractEmbeddedResources()
#endif
}
}
const EmbeddedResource* txRes = getEmbeddedResource(RESOURCE_DRAGONX_TX);
if (txRes) {
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_TX;
if (resourceNeedsUpdate(txRes, dest)) {
if (std::filesystem::exists(dest))
DEBUG_LOGF("[INFO] Updating stale dragonx-tx (size mismatch)...\n");
if (!std::filesystem::exists(dest)) {
DEBUG_LOGF("[INFO] Extracting dragonx-tx (%zu MB)...\n", txRes->size / (1024*1024));
if (!extractResource(txRes, dest)) {
success = false;
@@ -590,6 +590,120 @@ bool forceExtractXmrig()
#endif
}
// Scan a binary blob for the daemon's version stamp: 'v' <maj>.<min>.<rev> optionally followed by
// '-' <commit hash (>=6 hex)>, e.g. "v1.0.2-ddd851dc1". Returns the first match, or "" if none.
static std::string scanBinaryVersion(const uint8_t* data, std::size_t size)
{
if (!data || size < 6) return "";
auto isdig = [](uint8_t c) { return std::isdigit(static_cast<unsigned char>(c)) != 0; };
auto isxd = [](uint8_t c) { return std::isxdigit(static_cast<unsigned char>(c)) != 0; };
for (std::size_t i = 0; i + 5 < size; ++i) {
if (data[i] != 'v') continue;
std::size_t k = i + 1, s;
s = k; while (k < size && isdig(data[k])) ++k; if (k == s) continue; // major
if (k >= size || data[k] != '.') continue; ++k;
s = k; while (k < size && isdig(data[k])) ++k; if (k == s) continue; // minor
if (k >= size || data[k] != '.') continue; ++k;
s = k; while (k < size && isdig(data[k])) ++k; if (k == s) continue; // revision
std::size_t end = k;
if (k < size && data[k] == '-') { // optional -<commit>
std::size_t h = k + 1, hs = h;
while (h < size && isxd(data[h])) ++h;
if (h - hs >= 6) end = h;
}
return std::string(reinterpret_cast<const char*>(data) + i, end - i);
}
return "";
}
DaemonBinaryInfo getInstalledDaemonInfo()
{
DaemonBinaryInfo info;
std::string daemonDir = getDaemonDirectory();
#ifdef _WIN32
info.path = daemonDir + "\\" + RESOURCE_DRAGONXD;
#else
info.path = daemonDir + "/" + RESOURCE_DRAGONXD;
#endif
std::error_code ec;
if (!std::filesystem::exists(info.path, ec)) return info; // exists stays false
info.exists = true;
info.size = std::filesystem::file_size(info.path, ec);
if (ec) info.size = 0;
auto ftime = std::filesystem::last_write_time(info.path, ec);
if (!ec) {
// Convert filesystem clock → system_clock epoch (pre-C++20 portable approximation).
auto sysTime = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
ftime - decltype(ftime)::clock::now() + std::chrono::system_clock::now());
info.modifiedEpoch =
static_cast<std::int64_t>(std::chrono::system_clock::to_time_t(sysTime));
}
// Read the binary and scan for its version stamp (one-off; caller caches the result).
std::ifstream f(info.path, std::ios::binary);
if (f) {
f.seekg(0, std::ios::end);
std::streamoff len = f.tellg();
f.seekg(0, std::ios::beg);
if (len > 0) {
std::vector<uint8_t> buf(static_cast<std::size_t>(len));
f.read(reinterpret_cast<char*>(buf.data()), len);
info.version = scanBinaryVersion(buf.data(), static_cast<std::size_t>(f.gcount()));
}
}
return info;
}
BundledDaemonInfo getBundledDaemonInfo()
{
BundledDaemonInfo info;
#ifdef HAS_EMBEDDED_DAEMON
const EmbeddedResource* res = getEmbeddedResource(RESOURCE_DRAGONXD);
if (res && res->data && res->size > 0) {
info.available = true;
info.size = res->size;
// The embedded bytes are constant for this build — scan once.
static const std::string cachedVersion = scanBinaryVersion(res->data, res->size);
info.version = cachedVersion;
}
#endif
return info;
}
bool reextractBundledDaemon()
{
#ifdef HAS_EMBEDDED_DAEMON
std::string daemonDir = getDaemonDirectory();
#ifdef _WIN32
const char pathSep = '\\';
#else
const char pathSep = '/';
#endif
bool ok = true;
bool wroteAny = false;
const char* names[] = { RESOURCE_DRAGONXD, RESOURCE_DRAGONX_CLI, RESOURCE_DRAGONX_TX };
for (const char* name : names) {
const EmbeddedResource* res = getEmbeddedResource(name);
if (!res) continue;
std::string dest = daemonDir + pathSep + name;
DEBUG_LOGF("[INFO] reextractBundledDaemon: writing %s (%zu MB)\n", name, res->size / (1024*1024));
if (!extractResource(res, dest)) {
DEBUG_LOGF("[ERROR] reextractBundledDaemon: failed to write %s\n", name);
ok = false;
continue;
}
wroteAny = true;
#ifndef _WIN32
chmod(dest.c_str(), 0755);
#endif
}
return ok && wroteAny;
#else
return false;
#endif
}
std::string getXmrigPath()
{
std::string daemonDir = getDaemonDirectory();