From abc82136c48685f8d0bc4b1ea300049f98b6ef27 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 12 Oct 2026 21:42:23 -0500 Subject: [PATCH] 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 --- src/resources/embedded_resources.cpp | 59 +++++++++++++++++++++------- src/resources/embedded_resources.h | 8 +++- src/ui/pages/settings_page.cpp | 23 ++++++++++- tests/test_phase4.cpp | 44 +++++++++++++++++++++ 4 files changed, 117 insertions(+), 17 deletions(-) diff --git a/src/resources/embedded_resources.cpp b/src/resources/embedded_resources.cpp index 87ea337..781c3ea 100644 --- a/src/resources/embedded_resources.cpp +++ b/src/resources/embedded_resources.cpp @@ -833,12 +833,13 @@ bool forceExtractXmrig() // Scan a binary blob for the daemon's version stamp: 'v' .. optionally followed by // '-' =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(-1)) { if (!data || size < 6) return ""; auto isdig = [](uint8_t c) { return std::isdigit(static_cast(c)) != 0; }; auto isxd = [](uint8_t c) { return std::isxdigit(static_cast(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; std::size_t k = i + 1, s; 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 ""; } +// 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 buf(kChunk + kStampMax); + std::size_t carry = 0; + for (;;) { + f.read(reinterpret_cast(buf.data() + carry), static_cast(kChunk)); + const std::size_t got = static_cast(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(-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 info; std::string daemonDir = getDaemonDirectory(); #ifdef _WIN32 - info.path = daemonDir + "\\" + RESOURCE_DRAGONXD; + info.path = daemonDir + "\\" + DAEMON_BINARY_NAME; #else - info.path = daemonDir + "/" + RESOURCE_DRAGONXD; + info.path = daemonDir + "/" + DAEMON_BINARY_NAME; // was RESOURCE_DRAGONXD ("dragonxd.exe"): never existed on POSIX (C123) #endif std::error_code ec; if (!std::filesystem::exists(info.path, ec)) return info; // exists stays false @@ -881,18 +906,24 @@ DaemonBinaryInfo getInstalledDaemonInfo(bool scanVersion) static_cast(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; - 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 buf(static_cast(len)); - f.read(reinterpret_cast(buf.data()), len); - info.version = scanBinaryVersion(buf.data(), static_cast(f.gcount())); + { + static std::mutex cacheMutex; + static std::string cPath, cVersion; + static std::uintmax_t cSize = 0; + static std::int64_t cMtime = 0; + { + std::lock_guard lock(cacheMutex); + if (cPath == info.path && cSize == info.size && cMtime == info.modifiedEpoch && !cVersion.empty()) { + info.version = cVersion; + return info; + } } + info.version = scanFileVersion(info.path); + std::lock_guard lock(cacheMutex); + cPath = info.path; cSize = info.size; cMtime = info.modifiedEpoch; cVersion = info.version; } return info; } diff --git a/src/resources/embedded_resources.h b/src/resources/embedded_resources.h index c7d041b..97249d1 100644 --- a/src/resources/embedded_resources.h +++ b/src/resources/embedded_resources.h @@ -112,7 +112,13 @@ std::string ensureWalletRebuildHelperExtracted(); constexpr const char* RESOURCE_SAPLING_SPEND = "sapling-spend.params"; constexpr const char* RESOURCE_SAPLING_OUTPUT = "sapling-output.params"; 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_TX = "dragonx-tx.exe"; constexpr const char* RESOURCE_DRAGONX_WALLET_REBUILD = "dragonx-wallet-rebuild.exe"; diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index fd20f91..8af7c6a 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -51,9 +51,11 @@ #include #include #include +#include #include #include #include +#include namespace dragonx { namespace ui { @@ -186,6 +188,7 @@ struct SettingsPageState { // 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; + std::future daemon_version_scan; // streamed version scan, off the UI thread (C123) dragonx::resources::BundledDaemonInfo bundled_daemon; bool confirm_restart_daemon = false; bool confirm_lite_redownload = false; @@ -2418,10 +2421,26 @@ void RenderSettingsPage(App* app) { // -------------------- DAEMON BINARY -------------------- if (app->supportsFullNodeLifecycleActions()) { - if (!s_settingsState.daemon_info_loaded) { - s_settingsState.installed_daemon = dragonx::resources::getInstalledDaemonInfo(); + // A std::async future BLOCKS in its destructor until the task finishes. The Refresh/ + // 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.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& bun = s_settingsState.bundled_daemon; diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 0a8f316..bf02a68 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -2951,6 +2951,49 @@ void testSecureVaultWellFormed() } #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() { using dragonx::services::WalletSecurityController; @@ -17652,6 +17695,7 @@ int main() testPinVaultPolicy(); #ifndef _WIN32 testSecureVaultWellFormed(); + testInstalledDaemonInfoPosix(); #endif testWalletSecurityController(); testWalletSecurityWorkflow();