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