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

@@ -3035,6 +3035,45 @@ void App::repairWallet()
});
}
void App::reinstallBundledDaemon()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
return;
}
if (!resources::getBundledDaemonInfo().available) {
ui::Notifications::instance().warning("This build has no bundled daemon to install");
return;
}
if (!daemon_controller_ || !isUsingEmbeddedDaemon()) {
ui::Notifications::instance().warning("Reinstalling the daemon requires the embedded daemon");
return;
}
DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n");
ui::Notifications::instance().info("Installing bundled daemon — the node will stop, update, and restart...");
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
AppDaemonLifecycleRuntime runtime(*this);
daemon::AsyncLifecycleTaskContext ctx(token, shutting_down_);
// Stop the daemon so its binary is no longer locked (Windows) / in use (ETXTBSY on Linux).
runtime.stopDaemonWithPolicy();
// Wait (bounded, ~12s) for the process to exit and release the binary before overwriting.
for (int i = 0; i < 120 && daemon::EmbeddedDaemon::isRpcPortInUse()
&& !ctx.cancelled() && !ctx.shuttingDown(); ++i) {
ctx.sleepForMs(100);
}
// 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();
}
});
}
void App::deleteBlockchainData()
{
if (!supportsFullNodeLifecycleActions()) {

View File

@@ -307,6 +307,7 @@ public:
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
void rescanBlockchain(); // restart daemon with -rescan flag
void repairWallet(); // restart daemon with -zapwallettxes=2 (wipe & rebuild wallet tx records)
void reinstallBundledDaemon(); // stop daemon, overwrite installed binary with the bundled one, restart
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
bool isBootstrapDownloading() const { return bootstrap_downloading_; }

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();

View File

@@ -30,6 +30,31 @@ bool needsParamsExtraction();
// Get the params directory path
std::string getParamsDirectory();
// --- Daemon binary management (Settings → daemon binary panel) ------------------------------
// Info about the dragonxd binary currently installed in the dragonx/ extraction directory.
struct DaemonBinaryInfo {
bool exists = false;
std::string path;
std::uintmax_t size = 0;
std::string version; // scanned from the binary ("vX.Y.Z-<commit>"), empty if not found
std::int64_t modifiedEpoch = 0; // last-write time as unix epoch seconds, 0 if unknown
};
// Info about the dragonxd binary bundled inside this wallet build.
struct BundledDaemonInfo {
bool available = false; // a daemon resource is embedded in this build
std::uintmax_t size = 0;
std::string version;
};
// Read + scan the installed dragonxd (reads the file; call off the UI thread or cache the result).
DaemonBinaryInfo getInstalledDaemonInfo();
// Info about the bundled daemon (scans the embedded bytes once, cached).
BundledDaemonInfo getBundledDaemonInfo();
// Force-overwrite the installed dragonx binaries (dragonxd/cli/tx) with the bundled ones. The
// caller should stop the daemon first. Returns true if all present resources were written.
bool reextractBundledDaemon();
// Resource names
constexpr const char* RESOURCE_SAPLING_SPEND = "sapling-spend.params";
constexpr const char* RESOURCE_SAPLING_OUTPUT = "sapling-output.params";

View File

@@ -15,6 +15,8 @@
#include "../windows/console_tab.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../../resources/embedded_resources.h"
#include <ctime>
#include "../../rpc/rpc_client.h"
#include "../../rpc/connection.h"
#include "../../rpc/rpc_worker.h"
@@ -147,6 +149,12 @@ struct SettingsPageState {
bool confirm_delete_blockchain = false;
bool confirm_rescan = false;
bool confirm_repair_wallet = false;
bool confirm_reinstall_daemon = false;
// Cached daemon-binary status for the "daemon binary" panel (loaded once / on Refresh,
// since reading the installed binary to scan its version is a one-off disk read).
bool daemon_info_loaded = false;
dragonx::resources::DaemonBinaryInfo installed_daemon;
dragonx::resources::BundledDaemonInfo bundled_daemon;
bool confirm_restart_daemon = false;
bool confirm_lite_redownload = false;
effects::ScrollFadeShader fade_shader;
@@ -2055,6 +2063,78 @@ void RenderSettingsPage(App* app) {
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_repair_wallet"));
ImGui::EndDisabled();
// ---- Daemon binary: installed vs bundled, with explicit reinstall ----
// The wallet only auto-places dragonxd when it's missing (never overwrites a
// size-mismatched one), so this panel reports the installed binary and lets the
// user deliberately replace it with the version bundled in this wallet build.
if (!s_settingsState.daemon_info_loaded) {
s_settingsState.installed_daemon = dragonx::resources::getInstalledDaemonInfo();
s_settingsState.bundled_daemon = dragonx::resources::getBundledDaemonInfo();
s_settingsState.daemon_info_loaded = true;
}
const auto& inst = s_settingsState.installed_daemon;
const auto& bun = s_settingsState.bundled_daemon;
auto fmtDate = [](std::int64_t epoch) -> std::string {
if (epoch <= 0) return "";
std::time_t t = static_cast<std::time_t>(epoch);
std::tm tmv{};
#ifdef _WIN32
localtime_s(&tmv, &t);
#else
localtime_r(&t, &tmv);
#endif
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", &tmv);
return std::string(buf);
};
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_binary"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (inst.exists) {
ImGui::Text("%s %s", TR("daemon_installed"),
inst.version.empty() ? TR("unknown") : inst.version.c_str());
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), " %s · %s",
util::Platform::formatFileSize(inst.size).c_str(),
fmtDate(inst.modifiedEpoch).c_str());
} else {
ImGui::Text("%s %s", TR("daemon_installed"), TR("daemon_not_installed"));
}
if (bun.available) {
ImGui::Text("%s %s", TR("daemon_bundled"),
bun.version.empty() ? TR("unknown") : bun.version.c_str());
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), " %s",
util::Platform::formatFileSize(bun.size).c_str());
} else {
ImGui::Text("%s %s", TR("daemon_bundled"), TR("daemon_none_bundled"));
}
if (bun.available) {
const bool sameSize = inst.exists && inst.size == bun.size;
if (!inst.exists)
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_missing"));
else if (sameSize)
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("daemon_status_match"));
else
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_differ"));
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available);
if (TactileButton(TR("daemon_install_bundled"), ImVec2(0, 0), btnFont)) {
s_settingsState.confirm_reinstall_daemon = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_daemon_install_bundled"));
ImGui::EndDisabled();
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TR("refresh"), ImVec2(0, 0), btnFont)) {
s_settingsState.daemon_info_loaded = false; // recompute next frame
}
}
}
@@ -2688,6 +2768,37 @@ void RenderSettingsPage(App* app) {
}
}
// Confirm: reinstall the bundled daemon binary (stop → overwrite → restart)
if (s_settingsState.confirm_reinstall_daemon) {
if (BeginOverlayDialog(TR("confirm_reinstall_daemon_title"), &s_settingsState.confirm_reinstall_daemon, 500.0f, 0.94f)) {
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning"));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_reinstall_daemon_msg"));
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_reinstall_daemon_safe"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(TrId("cancel", "reinstall_daemon_cancel").c_str(), ImVec2(btnW, 40))) {
s_settingsState.confirm_reinstall_daemon = false;
}
ImGui::SameLine();
if (ImGui::Button(TrId("daemon_install_bundled", "reinstall_daemon_confirm").c_str(), ImVec2(btnW, 40))) {
app->reinstallBundledDaemon();
s_settingsState.daemon_info_loaded = false; // refresh the panel after the swap
s_settingsState.confirm_reinstall_daemon = false;
}
EndOverlayDialog();
}
}
// Confirm: restart daemon (briefly drops the connection to apply changed options)
if (s_settingsState.confirm_restart_daemon) {
if (BeginOverlayDialog(TR("confirm_restart_daemon_title"), &s_settingsState.confirm_restart_daemon, 500.0f, 0.94f)) {

View File

@@ -423,6 +423,19 @@ void I18n::loadBuiltinEnglish()
strings_["confirm_repair_wallet_title"] = "Repair Wallet";
strings_["confirm_repair_wallet_msg"] = "This restarts the daemon with -zapwallettxes=2: it deletes all of the wallet's transaction and note records, then rebuilds them from the blockchain. Use this when transactions fail to build (\"Invalid sapling spend proof\" / \"shielded requirements not met\") even after a full rescan. It takes a long time and the wallet stays offline until it finishes.";
strings_["confirm_repair_wallet_safe"] = "Your keys, addresses and balance are preserved — only the cached transaction records are rebuilt.";
strings_["daemon_binary"] = "Daemon binary";
strings_["daemon_installed"] = "Installed:";
strings_["daemon_bundled"] = "Bundled:";
strings_["daemon_not_installed"] = "not installed";
strings_["daemon_none_bundled"] = "none in this build";
strings_["daemon_status_match"] = "Installed binary matches the bundled version.";
strings_["daemon_status_differ"] = "Installed binary differs from the bundled version.";
strings_["daemon_status_missing"] = "No daemon installed — install the bundled version.";
strings_["daemon_install_bundled"] = "Install bundled daemon";
strings_["tt_daemon_install_bundled"] = "Stop the node, overwrite the installed dragonxd with the version bundled in this wallet build, then restart";
strings_["confirm_reinstall_daemon_title"] = "Install Bundled Daemon";
strings_["confirm_reinstall_daemon_msg"] = "This stops the daemon, overwrites the installed dragonxd (and dragonx-cli/dragonx-tx) with the versions bundled in this wallet build, then restarts the node. Use this to recover or update the node binary.";
strings_["confirm_reinstall_daemon_safe"] = "Your wallet, keys and blockchain data are not touched — only the daemon program files are replaced.";
strings_["confirm_restart_daemon_title"] = "Restart Daemon";
strings_["confirm_restart_daemon_msg"] = "This stops and restarts the daemon to apply the changed options. The wallet will briefly disconnect and reconnect.";
strings_["lite_maintenance"] = "Maintenance";