fix(util): use the POSIX daemon binary name, scan versions off-thread

The installed-daemon lookup built the binary path from the Windows
resource key "dragonxd.exe", which never exists on Linux or macOS, so
the installed full node was never found there. The version scan also
read the whole binary, now around 200 MB, into memory on the UI thread.

- A new DAEMON_BINARY_NAME gives the daemon's file name on this platform
  ("dragonxd" on POSIX, "dragonxd.exe" on Windows); the old resource
  constant stays only as the embedded-table key. The installed daemon is
  now found on POSIX.
- The version-stamp scan streams the file in 4 MiB chunks with bounded
  memory, carrying bytes across each boundary so a stamp straddling it is
  neither cut short nor missed, and stops at the first match. The result
  is cached by path, size and mtime, so only a changed binary is
  rescanned.
- Settings stats the binary on the UI thread but runs the streamed scan
  on a worker, guarded so a new scan is not started while one is still
  running (the future blocks in its destructor), and the version fills
  in when it lands.

Tests: the POSIX path and binary name, a version stamp in a small file,
a stamp straddling the 4 MiB chunk boundary, and a file with no stamp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 21:42:23 -05:00
co-authored by Claude Opus 4.8
parent 21dc874988
commit abc82136c4
4 changed files with 117 additions and 17 deletions
+45 -14
View File
@@ -833,12 +833,13 @@ bool forceExtractXmrig()
// Scan a binary blob for the daemon's version stamp: 'v' <maj>.<min>.<rev> optionally followed by // 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. // '-' <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) static std::string scanBinaryVersion(const uint8_t* data, std::size_t size,
std::size_t lastStart = static_cast<std::size_t>(-1))
{ {
if (!data || size < 6) return ""; if (!data || size < 6) return "";
auto isdig = [](uint8_t c) { return std::isdigit(static_cast<unsigned char>(c)) != 0; }; 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; }; auto isxd = [](uint8_t c) { return std::isxdigit(static_cast<unsigned char>(c)) != 0; };
for (std::size_t i = 0; i + 5 < size; ++i) { for (std::size_t i = 0; i + 5 < size && i <= lastStart; ++i) {
if (data[i] != 'v') continue; if (data[i] != 'v') continue;
std::size_t k = i + 1, s; std::size_t k = i + 1, s;
s = k; while (k < size && isdig(data[k])) ++k; if (k == s) continue; // major s = k; while (k < size && isdig(data[k])) ++k; if (k == s) continue; // major
@@ -857,14 +858,38 @@ static std::string scanBinaryVersion(const uint8_t* data, std::size_t size)
return ""; return "";
} }
// Stream the file in 4 MiB chunks (bounded memory, stops at the first stamp). A non-final chunk only starts
// matches where a whole stamp (<= kStampMax bytes) still fits, and the last kStampMax-1 bytes are carried into the
// next chunk, so a stamp straddling a boundary is neither cut short nor missed.
static std::string scanFileVersion(const std::string& path)
{
std::ifstream f(path, std::ios::binary);
if (!f) return "";
constexpr std::size_t kChunk = 4u << 20, kStampMax = 64;
std::vector<uint8_t> buf(kChunk + kStampMax);
std::size_t carry = 0;
for (;;) {
f.read(reinterpret_cast<char*>(buf.data() + carry), static_cast<std::streamsize>(kChunk));
const std::size_t got = static_cast<std::size_t>(f.gcount());
const std::size_t total = carry + got;
const bool last = got < kChunk; // short read => EOF
const std::size_t lastStart = (last || total < kStampMax) ? static_cast<std::size_t>(-1) : total - kStampMax;
std::string v = scanBinaryVersion(buf.data(), total, lastStart);
if (!v.empty()) return v;
if (last) return "";
carry = kStampMax - 1;
std::memmove(buf.data(), buf.data() + total - carry, carry);
}
}
DaemonBinaryInfo getInstalledDaemonInfo(bool scanVersion) DaemonBinaryInfo getInstalledDaemonInfo(bool scanVersion)
{ {
DaemonBinaryInfo info; DaemonBinaryInfo info;
std::string daemonDir = getDaemonDirectory(); std::string daemonDir = getDaemonDirectory();
#ifdef _WIN32 #ifdef _WIN32
info.path = daemonDir + "\\" + RESOURCE_DRAGONXD; info.path = daemonDir + "\\" + DAEMON_BINARY_NAME;
#else #else
info.path = daemonDir + "/" + RESOURCE_DRAGONXD; info.path = daemonDir + "/" + DAEMON_BINARY_NAME; // was RESOURCE_DRAGONXD ("dragonxd.exe"): never existed on POSIX (C123)
#endif #endif
std::error_code ec; std::error_code ec;
if (!std::filesystem::exists(info.path, ec)) return info; // exists stays false if (!std::filesystem::exists(info.path, ec)) return info; // exists stays false
@@ -881,18 +906,24 @@ DaemonBinaryInfo getInstalledDaemonInfo(bool scanVersion)
static_cast<std::int64_t>(std::chrono::system_clock::to_time_t(sysTime)); 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). // Scan for the version stamp (streamed; call off the UI thread). Cached by (path, size, mtime): Refresh and the
// post-install reload only rescan when the file actually changed.
if (!scanVersion) return info; if (!scanVersion) return info;
std::ifstream f(info.path, std::ios::binary); {
if (f) { static std::mutex cacheMutex;
f.seekg(0, std::ios::end); static std::string cPath, cVersion;
std::streamoff len = f.tellg(); static std::uintmax_t cSize = 0;
f.seekg(0, std::ios::beg); static std::int64_t cMtime = 0;
if (len > 0) { {
std::vector<uint8_t> buf(static_cast<std::size_t>(len)); std::lock_guard<std::mutex> lock(cacheMutex);
f.read(reinterpret_cast<char*>(buf.data()), len); if (cPath == info.path && cSize == info.size && cMtime == info.modifiedEpoch && !cVersion.empty()) {
info.version = scanBinaryVersion(buf.data(), static_cast<std::size_t>(f.gcount())); info.version = cVersion;
return info;
}
} }
info.version = scanFileVersion(info.path);
std::lock_guard<std::mutex> lock(cacheMutex);
cPath = info.path; cSize = info.size; cMtime = info.modifiedEpoch; cVersion = info.version;
} }
return info; return info;
} }
+7 -1
View File
@@ -112,7 +112,13 @@ std::string ensureWalletRebuildHelperExtracted();
constexpr const char* RESOURCE_SAPLING_SPEND = "sapling-spend.params"; constexpr const char* RESOURCE_SAPLING_SPEND = "sapling-spend.params";
constexpr const char* RESOURCE_SAPLING_OUTPUT = "sapling-output.params"; constexpr const char* RESOURCE_SAPLING_OUTPUT = "sapling-output.params";
constexpr const char* RESOURCE_ASMAP = "asmap.dat"; constexpr const char* RESOURCE_ASMAP = "asmap.dat";
constexpr const char* RESOURCE_DRAGONXD = "dragonxd.exe"; constexpr const char* RESOURCE_DRAGONXD = "dragonxd.exe"; // embedded-resource table key (Windows builds only)
// The daemon binary's file name on THIS platform. Use it to build paths; RESOURCE_DRAGONXD is only the table key.
#ifdef _WIN32
constexpr const char* DAEMON_BINARY_NAME = "dragonxd.exe";
#else
constexpr const char* DAEMON_BINARY_NAME = "dragonxd";
#endif
constexpr const char* RESOURCE_DRAGONX_CLI = "dragonx-cli.exe"; constexpr const char* RESOURCE_DRAGONX_CLI = "dragonx-cli.exe";
constexpr const char* RESOURCE_DRAGONX_TX = "dragonx-tx.exe"; constexpr const char* RESOURCE_DRAGONX_TX = "dragonx-tx.exe";
constexpr const char* RESOURCE_DRAGONX_WALLET_REBUILD = "dragonx-wallet-rebuild.exe"; constexpr const char* RESOURCE_DRAGONX_WALLET_REBUILD = "dragonx-wallet-rebuild.exe";
+21 -2
View File
@@ -51,9 +51,11 @@
#include <set> #include <set>
#include <filesystem> #include <filesystem>
#include <algorithm> #include <algorithm>
#include <chrono>
#include <cmath> #include <cmath>
#include <cstddef> #include <cstddef>
#include <cstring> #include <cstring>
#include <future>
namespace dragonx { namespace dragonx {
namespace ui { namespace ui {
@@ -186,6 +188,7 @@ struct SettingsPageState {
// since reading the installed binary to scan its version is a one-off disk read). // since reading the installed binary to scan its version is a one-off disk read).
bool daemon_info_loaded = false; bool daemon_info_loaded = false;
dragonx::resources::DaemonBinaryInfo installed_daemon; dragonx::resources::DaemonBinaryInfo installed_daemon;
std::future<dragonx::resources::DaemonBinaryInfo> daemon_version_scan; // streamed version scan, off the UI thread (C123)
dragonx::resources::BundledDaemonInfo bundled_daemon; dragonx::resources::BundledDaemonInfo bundled_daemon;
bool confirm_restart_daemon = false; bool confirm_restart_daemon = false;
bool confirm_lite_redownload = false; bool confirm_lite_redownload = false;
@@ -2418,10 +2421,26 @@ void RenderSettingsPage(App* app) {
// -------------------- DAEMON BINARY -------------------- // -------------------- DAEMON BINARY --------------------
if (app->supportsFullNodeLifecycleActions()) { if (app->supportsFullNodeLifecycleActions()) {
if (!s_settingsState.daemon_info_loaded) { // A std::async future BLOCKS in its destructor until the task finishes. The Refresh/
s_settingsState.installed_daemon = dragonx::resources::getInstalledDaemonInfo(); // post-install buttons set daemon_info_loaded=false; don't start (and so reassign) a new
// scan while the previous one is still running — defer the reload until it lands (C123).
const bool scanBusy = s_settingsState.daemon_version_scan.valid() &&
s_settingsState.daemon_version_scan.wait_for(std::chrono::seconds(0)) != std::future_status::ready;
if (!s_settingsState.daemon_info_loaded && !scanBusy) {
// Stat on the UI thread; the version stamp is a streamed read of a ~200 MB binary on
// POSIX, so it runs on a worker and fills in when done (C123).
s_settingsState.installed_daemon = dragonx::resources::getInstalledDaemonInfo(/*scanVersion=*/false);
s_settingsState.bundled_daemon = dragonx::resources::getBundledDaemonInfo(); s_settingsState.bundled_daemon = dragonx::resources::getBundledDaemonInfo();
s_settingsState.daemon_info_loaded = true; s_settingsState.daemon_info_loaded = true;
if (s_settingsState.installed_daemon.exists)
s_settingsState.daemon_version_scan = std::async(std::launch::async,
[] { return dragonx::resources::getInstalledDaemonInfo(/*scanVersion=*/true); });
}
if (s_settingsState.daemon_version_scan.valid()) {
if (s_settingsState.daemon_version_scan.wait_for(std::chrono::seconds(0)) == std::future_status::ready)
s_settingsState.installed_daemon = s_settingsState.daemon_version_scan.get();
else
ui::material::RequestProgressFrame(); // keep polling until the scan lands
} }
const auto& inst = s_settingsState.installed_daemon; const auto& inst = s_settingsState.installed_daemon;
const auto& bun = s_settingsState.bundled_daemon; const auto& bun = s_settingsState.bundled_daemon;
+44
View File
@@ -2951,6 +2951,49 @@ void testSecureVaultWellFormed()
} }
#endif #endif
#ifndef _WIN32
void testInstalledDaemonInfoPosix()
{
const char* oldHome = std::getenv("HOME");
const std::string savedHome = oldHome ? oldHome : "";
const char* oldXdg = std::getenv("XDG_CONFIG_HOME");
const bool hadXdg = oldXdg != nullptr;
const std::string savedXdg = hadXdg ? oldXdg : "";
const fs::path tmp = makeTempDir();
setenv("HOME", tmp.string().c_str(), 1);
setenv("XDG_CONFIG_HOME", (tmp / ".config").string().c_str(), 1);
{
const fs::path dir = fs::path(dragonx::resources::getDaemonDirectory());
std::error_code ec;
fs::create_directories(dir, ec);
EXPECT_TRUE(dir.string().rfind(tmp.string(), 0) == 0); // never the real config dir
auto info0 = dragonx::resources::getInstalledDaemonInfo(false);
EXPECT_FALSE(info0.exists); // nothing installed yet
{ // a stamp in a small file
std::ofstream o(dir / "dragonxd", std::ios::binary);
o << "garbage v9 x v1.3.0-0123abcdef trailing";
}
auto info1 = dragonx::resources::getInstalledDaemonInfo(true);
EXPECT_TRUE(info1.exists);
EXPECT_EQ(info1.version, std::string("v1.3.0-0123abcdef"));
{ // a stamp straddling the 4 MiB chunk boundary
std::string blob(4u * 1024 * 1024 - 7, 'x');
blob += "v1.4.2-deadbeef12"; blob += std::string(100, 'y');
std::ofstream o(dir / "dragonxd", std::ios::binary | std::ios::trunc); o << blob;
}
EXPECT_EQ(dragonx::resources::getInstalledDaemonInfo(true).version, std::string("v1.4.2-deadbeef12"));
{ // no stamp => ""
std::ofstream o(dir / "dragonxd", std::ios::binary | std::ios::trunc); o << std::string(5000, 'z');
}
EXPECT_TRUE(dragonx::resources::getInstalledDaemonInfo(true).version.empty());
EXPECT_EQ(std::string(dragonx::resources::DAEMON_BINARY_NAME), std::string("dragonxd"));
}
if (!savedHome.empty()) setenv("HOME", savedHome.c_str(), 1); else unsetenv("HOME");
if (hadXdg) setenv("XDG_CONFIG_HOME", savedXdg.c_str(), 1); else unsetenv("XDG_CONFIG_HOME");
removeTree(tmp);
}
#endif
void testWalletSecurityController() void testWalletSecurityController()
{ {
using dragonx::services::WalletSecurityController; using dragonx::services::WalletSecurityController;
@@ -17652,6 +17695,7 @@ int main()
testPinVaultPolicy(); testPinVaultPolicy();
#ifndef _WIN32 #ifndef _WIN32
testSecureVaultWellFormed(); testSecureVaultWellFormed();
testInstalledDaemonInfoPosix();
#endif #endif
testWalletSecurityController(); testWalletSecurityController();
testWalletSecurityWorkflow(); testWalletSecurityWorkflow();