#include "embedded_resources.h" #include "../util/platform.h" #include "../util/logger.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #include #else #include #include #include #include #include #endif // Include generated resource data if available #if __has_include("embedded_data.h") #include "embedded_data.h" #define HAS_EMBEDDED_RESOURCES 1 #else #define HAS_EMBEDDED_RESOURCES 0 #endif namespace dragonx { namespace resources { #if HAS_EMBEDDED_RESOURCES // Resource table - populated by embedded_data.h (INCBIN symbols: g_NAME_data / g_NAME_size) static const EmbeddedResource s_resources[] = { { g_sapling_spend_params_data, g_sapling_spend_params_size, RESOURCE_SAPLING_SPEND }, { g_sapling_output_params_data, g_sapling_output_params_size, RESOURCE_SAPLING_OUTPUT }, { g_asmap_dat_data, g_asmap_dat_size, RESOURCE_ASMAP }, #ifdef HAS_EMBEDDED_DAEMON { g_dragonxd_exe_data, g_dragonxd_exe_size, RESOURCE_DRAGONXD }, { g_dragonx_cli_exe_data, g_dragonx_cli_exe_size, RESOURCE_DRAGONX_CLI }, { g_dragonx_tx_exe_data, g_dragonx_tx_exe_size, RESOURCE_DRAGONX_TX }, #endif #ifdef HAS_EMBEDDED_WALLET_REBUILD { g_dragonx_wallet_rebuild_exe_data, g_dragonx_wallet_rebuild_exe_size, RESOURCE_DRAGONX_WALLET_REBUILD }, #endif #ifdef HAS_EMBEDDED_XMRIG { g_xmrig_exe_data, g_xmrig_exe_size, RESOURCE_XMRIG }, #endif #ifdef HAS_EMBEDDED_GRADIENT { g_dark_gradient_png_data, g_dark_gradient_png_size, RESOURCE_DARK_GRADIENT }, #endif #ifdef HAS_EMBEDDED_LOGO { g_logo_ObsidianDragon_dark_png_data, g_logo_ObsidianDragon_dark_png_size, RESOURCE_LOGO }, #endif { nullptr, 0, nullptr } // Sentinel }; // Embedded themes table is generated by s_embedded_themes[] in embedded_data.h #else static const EmbeddedResource s_resources[] = { { nullptr, 0, nullptr } // No embedded resources }; // No embedded themes on non-Windows builds (themes live on disk) struct EmbeddedThemeEntry { const uint8_t* data; unsigned int size; const char* filename; }; static const EmbeddedThemeEntry s_embedded_themes[] = { { nullptr, 0, nullptr } }; // No embedded images on non-Windows builds (images live on disk) struct EmbeddedImageEntry { const uint8_t* data; unsigned int size; const char* filename; }; static const EmbeddedImageEntry s_embedded_images[] = { { nullptr, 0, nullptr } }; #endif bool hasEmbeddedResources() { #if HAS_EMBEDDED_RESOURCES return true; #else return false; #endif } const EmbeddedResource* getEmbeddedResource(const std::string& name) { // Search static resource table (params, daemon binaries, etc.) for (const auto* res = s_resources; res->filename != nullptr; ++res) { if (res->data && res->size && name == res->filename) { return res; } } // Search dynamically generated image table (backgrounds + logos) // These are generated by build.sh from res/img/ contents. for (const auto* img = s_embedded_images; img->data != nullptr; ++img) { if (name == img->filename) { static thread_local EmbeddedResource imageResult; imageResult = { img->data, img->size, img->filename }; return &imageResult; } } return nullptr; } const EmbeddedTheme* getEmbeddedThemes() { // Map from generated table to public struct // s_embedded_themes is generated by build.sh (or empty fallback) static std::vector themes; static bool init = false; if (!init) { for (const auto* t = s_embedded_themes; t->data != nullptr; ++t) { themes.push_back({ t->data, t->size, t->filename }); } themes.push_back({ nullptr, 0, nullptr }); // Sentinel init = true; } return themes.data(); } int writeFileIfDifferent(const std::string& path, const uint8_t* data, size_t size) { { std::ifstream cur(path, std::ios::binary | std::ios::ate); if (cur.is_open()) { const std::streamoff len = cur.tellg(); if (len >= 0 && static_cast(len) == size) { std::string buf(size, '\0'); cur.seekg(0); if (size == 0 || (cur.read(&buf[0], static_cast(size)) && std::memcmp(buf.data(), data, size) == 0)) return 0; } } } // reader closed before the writer opens (Windows sharing) std::ofstream f(path, std::ios::binary | std::ios::trunc); if (!f.is_open()) return -1; f.write(reinterpret_cast(data), static_cast(size)); return f.good() ? 1 : -1; } int extractBundledThemes(const std::string& destDir) { namespace fs = std::filesystem; int count = 0; const auto* themes = getEmbeddedThemes(); if (!themes || !themes->data) return 0; // Ensure destination exists std::error_code ec; fs::create_directories(destDir, ec); if (ec) { DEBUG_LOGF("[ERROR] EmbeddedResources: Failed to create theme dir: %s\n", destDir.c_str()); return 0; } for (const auto* t = themes; t->data != nullptr; ++t) { fs::path dest = fs::path(destDir) / t->filename; // Bundled themes must match the binary version: rewrite only when the bytes differ (not size-only: // a same-length palette edit must still update). const int w = writeFileIfDifferent(dest.string(), t->data, t->size); if (w >= 0) { if (w == 1) DEBUG_LOGF("[INFO] EmbeddedResources: Extracted theme: %s (%zu bytes)\n", t->filename, t->size); count++; // present and current (caller only checks > 0) } else { DEBUG_LOGF("[ERROR] EmbeddedResources: Failed to write theme: %s\n", dest.string().c_str()); } } return count; } int updateBundledThemes(const std::string& dir) { namespace fs = std::filesystem; int count = 0; const auto* themes = getEmbeddedThemes(); if (!themes || !themes->data) return 0; if (!fs::exists(dir)) return 0; for (const auto* t = themes; t->data != nullptr; ++t) { fs::path dest = fs::path(dir) / t->filename; if (!fs::exists(dest)) { // New theme not yet on disk — extract it } else { std::error_code ec; auto diskSize = fs::file_size(dest, ec); if (!ec && diskSize == static_cast(t->size)) continue; // up to date } std::ofstream f(dest, std::ios::binary); if (f.is_open()) { f.write(reinterpret_cast(t->data), t->size); f.close(); DEBUG_LOGF("[INFO] EmbeddedResources: Updated stale theme: %s (%zu bytes)\n", t->filename, t->size); count++; } } return count; } std::string getParamsDirectory() { #ifdef _WIN32 char appdata[MAX_PATH]; if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata))) { return std::string(appdata) + "\\ZcashParams"; } return ""; #elif defined(__APPLE__) const char* home = getenv("HOME"); if (!home) { struct passwd* pw = getpwuid(getuid()); home = pw ? pw->pw_dir : "/tmp"; } return std::string(home) + "/Library/Application Support/ZcashParams"; #else const char* home = getenv("HOME"); if (!home) { struct passwd* pw = getpwuid(getuid()); home = pw ? pw->pw_dir : "/tmp"; } return std::string(home) + "/.zcash-params"; #endif } // --------------------------------------------------------------------------------------------------- // Writing into the daemon directory: one lock, atomic (temp + flush + rename) files. // --------------------------------------------------------------------------------------------------- namespace { // Every writer of the daemon directory (startup job, getDaemonPath(), getXmrigPath(), forceExtractXmrig(), // reextractBundledDaemon(), ensureWalletRebuildHelperExtracted(), copyBundledFiles()) holds this for its whole // write; whoever comes second finds the files complete and does nothing. Never taken re-entrantly. Lock order: // provisionMutex -> Logger mutex (DEBUG_LOGF) only. The in-app updaters do not take it (see residual risks). std::mutex& provisionMutex() { static std::mutex m; return m; } struct ProvisionEntry { const char* name; bool refreshOnSizeMismatch; // re-extract when the bundled size differs (params, asmap) bool executable; bool fullNodeOnly; // not written by Lite builds (the lite backend bakes in its own params) bool startupTrigger; // makes needsParamsExtraction() true }; // Params and asmap follow the bundled size; the node binaries, the rebuild helper and the miner are placed only // when MISSING (user-updatable via the updaters: the user may run a specific dragonxd or an "Update miner..." // build; replacing the node is the explicit Settings action). asmap/cli/tx/rebuild are written along, never a trigger. const ProvisionEntry kProvisionEntries[] = { { RESOURCE_SAPLING_SPEND, true, false, true, true }, { RESOURCE_SAPLING_OUTPUT, true, false, true, true }, { RESOURCE_ASMAP, true, false, true, false }, #ifdef HAS_EMBEDDED_DAEMON { RESOURCE_DRAGONXD, false, true, true, true }, { RESOURCE_DRAGONX_CLI, false, true, true, false }, { RESOURCE_DRAGONX_TX, false, true, true, false }, #endif #ifdef HAS_EMBEDDED_XMRIG { RESOURCE_XMRIG, kXmrigRefreshOnSizeMismatch, true, false, true }, #endif #ifdef HAS_EMBEDDED_WALLET_REBUILD { RESOURCE_DRAGONX_WALLET_REBUILD, false, true, true, false }, #endif }; bool entryActive(const ProvisionEntry& e) { #if defined(DRAGONX_LITE_BUILD) && DRAGONX_LITE_BUILD return !e.fullNodeOnly; #else (void)e; return true; #endif } std::string joinDir(const std::string& dir, const char* name) { #ifdef _WIN32 return dir + "\\" + name; #else return dir + "/" + name; #endif } constexpr std::size_t kCopyChunk = 4u * 1024u * 1024u; bool prepareParentDir(const std::string& destPath) { const std::filesystem::path dest(destPath); if (!dest.has_parent_path()) return true; std::error_code ec; std::filesystem::create_directories(dest.parent_path(), ec); if (ec) { DEBUG_LOGF("[ERROR] Failed to create directory %s: %s\n", dest.parent_path().string().c_str(), ec.message().c_str()); return false; } return true; } // Temp-file sink for writeFileAtomic / copyFileAtomic. Windows: ONE CreateFileA handle with no SECURITY_ATTRIBUTES, // i.e. NOT inheritable: std::ofstream's CRT handle is, and a child created with bInheritHandles=TRUE meanwhile // (xmrig, runHiddenCapture) would hold ".partial" without FILE_SHARE_DELETE, so the MoveFileEx in // replaceFromTemp fails while it runs (same fix as Platform::writeFileAtomically). commit() flushes on the same // handle (FlushFileBuffers / fsync) before close, so the bytes are on disk before the rename. POSIX: O_CLOEXEC. class TempSink { public: explicit TempSink(const std::string& path) { #ifdef _WIN32 h_ = CreateFileA(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); #else fd_ = ::open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); #endif } ~TempSink() { (void)close(); } TempSink(const TempSink&) = delete; TempSink& operator=(const TempSink&) = delete; bool ok() const { #ifdef _WIN32 return h_ != INVALID_HANDLE_VALUE; #else return fd_ >= 0; #endif } bool write(const void* data, std::size_t n) { const char* p = static_cast(data); while (n > 0) { #ifdef _WIN32 const DWORD chunk = static_cast(n > (1u << 30) ? (1u << 30) : n); DWORD w = 0; if (!WriteFile(h_, p, chunk, &w, nullptr) || w == 0) return false; #else const ssize_t w = ::write(fd_, p, n); if (w < 0) { if (errno == EINTR) continue; return false; } if (w == 0) return false; #endif p += w; n -= static_cast(w); } return true; } // Flush to stable storage, then close. False on any error. bool commit() { bool good = ok(); #ifdef _WIN32 if (good && !FlushFileBuffers(h_)) good = false; #else if (good && ::fsync(fd_) != 0) good = false; #endif if (!close()) good = false; return good; } private: bool close() { #ifdef _WIN32 if (h_ == INVALID_HANDLE_VALUE) return true; const bool r = CloseHandle(h_) != 0; h_ = INVALID_HANDLE_VALUE; return r; #else if (fd_ < 0) return true; const bool r = ::close(fd_) == 0; fd_ = -1; return r; #endif } #ifdef _WIN32 HANDLE h_ = INVALID_HANDLE_VALUE; #else int fd_ = -1; #endif }; // Move `tmp` over `dest`. POSIX: rename() replaces atomically (even a running binary) + best-effort dir fsync. // Windows (same primitive as Platform::writeFileAtomically): MoveFileEx REPLACE_EXISTING|WRITE_THROUGH. A // running .exe cannot be replaced but can be renamed, so it is moved aside to ".old" once (the running process // keeps it; cleaned on a later run). A just-closed file is often held for a moment by antivirus (sharing // violation), so retry for up to ~2 s, but stop at once if the temp vanished (quarantined). On failure the // temp is removed and a moved-aside original is put back. bool replaceFromTemp(const std::string& tmp, const std::string& dest) { #ifdef _WIN32 const std::string aside = dest + ".old"; bool movedAside = false; DWORD err = 0; for (int attempt = 0; attempt < 20; ++attempt) { if (MoveFileExA(tmp.c_str(), dest.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) return true; err = GetLastError(); if (GetFileAttributesA(tmp.c_str()) == INVALID_FILE_ATTRIBUTES) break; // temp gone (AV quarantine) if (!movedAside && GetFileAttributesA(dest.c_str()) != INVALID_FILE_ATTRIBUTES) { DeleteFileA(aside.c_str()); if (MoveFileExA(dest.c_str(), aside.c_str(), MOVEFILE_REPLACE_EXISTING)) { movedAside = true; DEBUG_LOGF("[INFO] %s is in use - moved the old copy to .old\n", dest.c_str()); continue; } } Sleep(100); } DEBUG_LOGF("[ERROR] Could not move %s into place (Win32 error %lu)\n", dest.c_str(), (unsigned long)err); DeleteFileA(tmp.c_str()); if (movedAside) MoveFileExA(aside.c_str(), dest.c_str(), MOVEFILE_REPLACE_EXISTING); return false; #else if (::rename(tmp.c_str(), dest.c_str()) != 0) { DEBUG_LOGF("[ERROR] Could not move %s into place: %s\n", dest.c_str(), std::strerror(errno)); ::remove(tmp.c_str()); return false; } const std::string dir = std::filesystem::path(dest).parent_path().string(); const int dfd = ::open(dir.empty() ? "." : dir.c_str(), O_RDONLY | O_CLOEXEC); if (dfd >= 0) { ::fsync(dfd); ::close(dfd); } return true; #endif } } // namespace bool resourceNeedsWrite(std::uint64_t embeddedSize, bool refreshOnSizeMismatch, const std::string& destPath) { if (embeddedSize == 0) return false; std::error_code ec; if (!std::filesystem::exists(destPath, ec)) return true; if (!refreshOnSizeMismatch) return false; const auto diskSize = std::filesystem::file_size(destPath, ec); if (ec) return true; // can't stat -> rewrite return static_cast(diskSize) != embeddedSize; } // Check if an on-disk file is missing or differs in size from the embedded resource. // A size mismatch means the binary was updated in a newer wallet build. [[maybe_unused]] static bool resourceNeedsUpdate(const EmbeddedResource* res, const std::string& destPath) { if (!res || !res->data || res->size == 0) return false; return resourceNeedsWrite(res->size, /*refreshOnSizeMismatch=*/true, destPath); } bool needsParamsExtraction() { if (!hasEmbeddedResources()) return false; const std::string dir = getDaemonDirectory(); // the only extraction target for (const auto& e : kProvisionEntries) { if (!entryActive(e) || !e.startupTrigger) continue; const EmbeddedResource* res = getEmbeddedResource(e.name); if (res && resourceNeedsWrite(res->size, e.refreshOnSizeMismatch, joinDir(dir, e.name))) return true; } return false; } bool writeFileAtomic(const std::string& destPath, const std::uint8_t* data, std::size_t size, bool executable, ProvisionProgress* progress, const CancelFn& cancelled) { if (!data || size == 0) return false; if (!prepareParentDir(destPath)) return false; const std::string tmp = destPath + kPartialFileSuffix; std::error_code ec; bool good = true; { TempSink f(tmp); if (!f.ok()) { DEBUG_LOGF("[ERROR] Failed to open %s for writing\n", tmp.c_str()); return false; } for (std::size_t off = 0; off < size; off += kCopyChunk) { if (cancelled && cancelled()) { good = false; break; } const std::size_t n = std::min(kCopyChunk, size - off); if (!f.write(data + off, n)) { DEBUG_LOGF("[ERROR] Failed to write %zu bytes to %s\n", n, tmp.c_str()); good = false; break; } if (progress) progress->bytesDone.fetch_add(n, std::memory_order_relaxed); } // Flush on the same handle BEFORE the rename: a power cut can't leave a truncated file under the final name. if (good && !f.commit()) { DEBUG_LOGF("[ERROR] Failed to finish writing %s\n", tmp.c_str()); good = false; } } // closed here: Windows can't delete a file that is still open without FILE_SHARE_DELETE if (!good) { std::filesystem::remove(tmp, ec); return false; } #ifndef _WIN32 if (executable) ::chmod(tmp.c_str(), 0755); // before it becomes visible under the final name #else (void)executable; #endif if (!replaceFromTemp(tmp, destPath)) return false; DEBUG_LOGF("[INFO] Extracted %s (%zu bytes)\n", destPath.c_str(), size); return true; } bool copyFileAtomic(const std::string& srcPath, const std::string& destPath, ProvisionProgress* progress, const CancelFn& cancelled) { namespace fs = std::filesystem; if (!prepareParentDir(destPath)) return false; const std::string tmp = destPath + kPartialFileSuffix; std::error_code ec; bool good = true; { std::ifstream in(srcPath, std::ios::binary); // a read handle on the source is harmless if inherited if (!in) { DEBUG_LOGF("[ERROR] Failed to open %s\n", srcPath.c_str()); return false; } TempSink out(tmp); if (!out.ok()) { DEBUG_LOGF("[ERROR] Failed to open %s for writing\n", tmp.c_str()); return false; } std::vector buf(kCopyChunk); for (;;) { if (cancelled && cancelled()) { good = false; break; } in.read(buf.data(), static_cast(buf.size())); const std::streamsize got = in.gcount(); if (got > 0) { if (!out.write(buf.data(), static_cast(got))) { DEBUG_LOGF("[ERROR] Failed to write %s\n", tmp.c_str()); good = false; break; } if (progress) progress->bytesDone.fetch_add(static_cast(got), std::memory_order_relaxed); } if (!in) break; // EOF (eof|fail) or a read error (bad, checked below) } if (good && in.bad()) { DEBUG_LOGF("[ERROR] Failed to read %s\n", srcPath.c_str()); good = false; } // Flush on the same handle BEFORE the rename: a power cut can't leave a truncated file under the final name. if (good && !out.commit()) { DEBUG_LOGF("[ERROR] Failed to finish writing %s\n", tmp.c_str()); good = false; } } // closed here: Windows can't delete a file that is still open without FILE_SHARE_DELETE if (!good) { fs::remove(tmp, ec); return false; } const auto st = fs::status(srcPath, ec); if (!ec) fs::permissions(tmp, st.permissions(), ec); // keeps dragonxd/cli/tx executable (as copy_file did) return replaceFromTemp(tmp, destPath); } // Requires provisionMutex() held. static bool extractResourceLocked(const EmbeddedResource* res, const std::string& destPath, bool executable, ProvisionProgress* progress = nullptr, const CancelFn& cancelled = {}) { if (!res || !res->data || res->size == 0) return false; return writeFileAtomic(destPath, res->data, res->size, executable, progress, cancelled); } bool extractEmbeddedResources(ProvisionProgress* progress, const CancelFn& cancelled) { if (!hasEmbeddedResources()) { DEBUG_LOGF("[ERROR] No embedded resources available\n"); return false; } std::lock_guard lock(provisionMutex()); // All files go to /dragonx/ — NOT the data directory (running dragonxd.exe from inside // the data directory, where it writes chain data / debug.log / lock files, crashes on some Windows machines). const std::string daemonDir = getDaemonDirectory(); DEBUG_LOGF("[INFO] Daemon extraction directory: %s\n", daemonDir.c_str()); std::error_code ec; std::filesystem::create_directories(daemonDir, ec); ec.clear(); // Temp files left by an interrupted run (killed process, power cut). Only OUR suffix. for (const auto& e : kProvisionEntries) { std::filesystem::remove(joinDir(daemonDir, e.name) + kPartialFileSuffix, ec); ec.clear(); } struct Todo { const ProvisionEntry* entry; const EmbeddedResource* res; std::string dest; }; std::vector todo; std::uint64_t total = 0; for (const auto& e : kProvisionEntries) { if (!entryActive(e)) continue; const EmbeddedResource* res = getEmbeddedResource(e.name); if (!res) continue; std::string dest = joinDir(daemonDir, e.name); if (!resourceNeedsWrite(res->size, e.refreshOnSizeMismatch, dest)) continue; total += res->size; todo.push_back({ &e, res, std::move(dest) }); } if (progress) { progress->bytesDone.store(0, std::memory_order_relaxed); progress->bytesTotal.store(total, std::memory_order_relaxed); progress->setPhase(ProvisionPhase::Extracting); } bool success = true; for (const auto& t : todo) { if (cancelled && cancelled()) return false; DEBUG_LOGF("[INFO] Extracting %s (%zu MB)...\n", t.entry->name, t.res->size / (1024 * 1024)); if (!extractResourceLocked(t.res, t.dest, t.entry->executable, progress, cancelled)) { if (cancelled && cancelled()) return false; success = false; } } // Best-effort cleanup of ".old" copies left by an in-use replacement. Once the old process has exited the // file is no longer locked and removes cleanly; if it still runs, the remove fails and we retry next run. for (const auto& e : kProvisionEntries) { std::filesystem::remove(joinDir(daemonDir, e.name) + std::string(".old"), ec); ec.clear(); } return success; } std::vector bundleSearchDirs() { std::vector dirs; const std::string exeDir = util::Platform::getExecutableDirectory(); if (exeDir.empty()) return dirs; #ifdef __APPLE__ // Inside a .app the params live in Contents/Resources while the executable is in Contents/MacOS. std::error_code ec; const std::filesystem::path res = std::filesystem::path(exeDir).parent_path() / "Resources"; if (std::filesystem::is_directory(res, ec)) dirs.push_back(res.string()); #endif dirs.push_back(exeDir); return dirs; } const std::vector& saplingParamFileNames() { static const std::vector v = { "sapling-spend.params", "sapling-output.params" }; return v; } const std::vector& nodeBundleFileNames() { static const std::vector v = { "sapling-spend.params", "sapling-output.params", "asmap.dat", "dragonxd", "dragonx-cli", "dragonx-tx" }; return v; } std::vector planBundleCopy(const std::vector& searchDirs, const std::string& destDir, const std::vector& names, bool replaceExisting) { namespace fs = std::filesystem; std::vector plan; std::error_code ec; for (const auto& name : names) { const fs::path dst = fs::path(destDir) / name; if (!replaceExisting && fs::exists(dst, ec)) continue; for (const auto& dir : searchDirs) { const fs::path src = fs::path(dir) / name; if (fs::exists(src, ec)) { plan.push_back({ name, src.string(), dst.string() }); break; } } } return plan; } BundleCopyResult copyBundledFiles(const std::vector& searchDirs, const std::string& destDir, const std::vector& names, bool replaceExisting, ProvisionProgress* progress, const CancelFn& cancelled) { BundleCopyResult r; // Stat-only check first: the steady-state call (every daemon start) never touches the lock. if (planBundleCopy(searchDirs, destDir, names, replaceExisting).empty()) return r; std::lock_guard lock(provisionMutex()); const auto plan = planBundleCopy(searchDirs, destDir, names, replaceExisting); // re-check under the lock std::uint64_t total = 0; std::error_code ec; for (const auto& it : plan) { std::filesystem::remove(it.dst + kPartialFileSuffix, ec); // stale temp from an interrupted copy ec.clear(); const auto sz = std::filesystem::file_size(it.src, ec); if (!ec) total += sz; ec.clear(); } if (progress) { progress->bytesDone.store(0, std::memory_order_relaxed); progress->bytesTotal.store(total, std::memory_order_relaxed); progress->setPhase(ProvisionPhase::Copying); } for (const auto& it : plan) { if (cancelled && cancelled()) { r.ok = false; return r; } DEBUG_LOGF("Copying bundled %s to %s\n", it.name.c_str(), destDir.c_str()); if (copyFileAtomic(it.src, it.dst, progress, cancelled)) { ++r.copied; } else { if (cancelled && cancelled()) { r.ok = false; return r; } DEBUG_LOGF("[ERROR] Failed to copy %s\n", it.name.c_str()); r.ok = false; if (r.firstFailure.empty()) r.firstFailure = it.name; } } return r; } std::string ensureWalletRebuildHelperExtracted() { #ifdef HAS_EMBEDDED_WALLET_REBUILD const EmbeddedResource* res = getEmbeddedResource(RESOURCE_DRAGONX_WALLET_REBUILD); if (!res || res->size == 0) return {}; #ifdef _WIN32 const char sep = '\\'; #else const char sep = '/'; #endif const std::string dir = getDaemonDirectory(); const std::string dest = dir + sep + RESOURCE_DRAGONX_WALLET_REBUILD; std::error_code ec; if (std::filesystem::exists(dest, ec)) return dest; // already extracted (atomic write: exists == complete) std::lock_guard lock(provisionMutex()); // serialise with the startup job / other writers if (std::filesystem::exists(dest, ec)) return dest; // written meanwhile by the startup job std::filesystem::create_directories(dir, ec); if (!extractResourceLocked(res, dest, /*executable=*/true)) return {}; DEBUG_LOGF("[INFO] Extracted wallet-rebuild helper on demand: %s\n", dest.c_str()); return dest; #else return {}; #endif } std::string getDaemonDirectory() { // Daemon binaries live in %APPDATA%/ObsidianDragon/dragonx/ (Windows) or // ~/.config/ObsidianDragon/dragonx/ (Linux) — separate from the blockchain // data directory to avoid lock-file conflicts. std::string obsidianDir = util::Platform::getObsidianDragonDir(); #ifdef _WIN32 return obsidianDir + "\\dragonx"; #else return obsidianDir + "/dragonx"; #endif } bool needsDaemonExtraction() { #ifdef HAS_EMBEDDED_DAEMON std::string daemonDir = getDaemonDirectory(); #ifdef _WIN32 std::string daemonPath = daemonDir + "\\dragonxd.exe"; #else std::string daemonPath = daemonDir + "/dragonxd"; #endif const auto* res = getEmbeddedResource(RESOURCE_DRAGONXD); return resourceNeedsUpdate(res, daemonPath); #else return false; #endif } bool hasDaemonAvailable() { #ifdef HAS_EMBEDDED_DAEMON return true; #else // Check if daemon exists alongside the executable #ifdef _WIN32 return std::filesystem::exists("dragonxd.exe"); #else return std::filesystem::exists("dragonxd"); #endif #endif } std::string getDaemonPath() { std::string daemonDir = getDaemonDirectory(); #ifdef _WIN32 const char pathSep = '\\'; const char* daemonName = "dragonxd.exe"; #else const char pathSep = '/'; const char* daemonName = "dragonxd"; #endif DEBUG_LOGF("[DEBUG] getDaemonPath: daemonDir=%s\n", daemonDir.c_str()); #ifdef HAS_EMBEDDED_DAEMON // Extract if needed std::string embeddedPath = daemonDir + pathSep + daemonName; DEBUG_LOGF("[DEBUG] getDaemonPath: checking embedded path %s\n", embeddedPath.c_str()); if (!std::filesystem::exists(embeddedPath)) { DEBUG_LOGF("[INFO] getDaemonPath: daemon not found, extracting embedded resources...\n"); extractEmbeddedResources(); } if (std::filesystem::exists(embeddedPath)) { DEBUG_LOGF("[INFO] getDaemonPath: found at %s\n", embeddedPath.c_str()); return embeddedPath; } #endif // Check local directory if (std::filesystem::exists(daemonName)) { DEBUG_LOGF("[INFO] getDaemonPath: found in local directory\n"); return daemonName; } DEBUG_LOGF("[ERROR] getDaemonPath: daemon binary not found anywhere\n"); return ""; } bool hasXmrigAvailable() { #ifdef HAS_EMBEDDED_XMRIG return true; #else // Check if xmrig exists alongside the executable #ifdef _WIN32 return std::filesystem::exists("xmrig.exe"); #else return std::filesystem::exists("xmrig"); #endif #endif } bool forceExtractXmrig() { #ifdef HAS_EMBEDDED_XMRIG std::string daemonDir = getDaemonDirectory(); #ifdef _WIN32 std::string dest = daemonDir + "\\" + RESOURCE_XMRIG; #else std::string dest = daemonDir + "/" + RESOURCE_XMRIG; #endif const EmbeddedResource* xmrigRes = getEmbeddedResource(RESOURCE_XMRIG); if (!xmrigRes) { DEBUG_LOGF("[ERROR] forceExtractXmrig: no embedded xmrig resource\n"); return false; } DEBUG_LOGF("[INFO] forceExtractXmrig: extracting xmrig (%zu MB) to %s\n", xmrigRes->size / (1024*1024), dest.c_str()); std::lock_guard lock(provisionMutex()); if (!extractResourceLocked(xmrigRes, dest, /*executable=*/true)) { DEBUG_LOGF("[ERROR] forceExtractXmrig: extraction failed\n"); return false; } return true; #else return false; #endif } // 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, 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 <= 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 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 - 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(data) + i, end - i); } 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 + "\\" + DAEMON_BINARY_NAME; #else 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 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( ftime - decltype(ftime)::clock::now() + std::chrono::system_clock::now()); info.modifiedEpoch = static_cast(std::chrono::system_clock::to_time_t(sysTime)); } // 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; { 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; } 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::lock_guard lock(provisionMutex()); 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 (!extractResourceLocked(res, dest, /*executable=*/true)) { DEBUG_LOGF("[ERROR] reextractBundledDaemon: failed to write %s\n", name); ok = false; continue; } wroteAny = true; } return ok && wroteAny; #else return false; #endif } std::string getXmrigPath() { std::string daemonDir = getDaemonDirectory(); #ifdef _WIN32 const char pathSep = '\\'; const char* xmrigName = "xmrig.exe"; #else const char pathSep = '/'; const char* xmrigName = "xmrig"; #endif DEBUG_LOGF("[DEBUG] getXmrigPath: daemonDir=%s\n", daemonDir.c_str()); #ifdef HAS_EMBEDDED_XMRIG // Extract if needed — force re-extract in case Defender deleted it std::string embeddedPath = daemonDir + pathSep + xmrigName; DEBUG_LOGF("[DEBUG] getXmrigPath: checking embedded path %s\n", embeddedPath.c_str()); if (!std::filesystem::exists(embeddedPath)) { // Serialise with the startup job: if it just wrote xmrig, the re-check finds it complete. std::lock_guard lock(provisionMutex()); if (!std::filesystem::exists(embeddedPath)) { DEBUG_LOGF("[DEBUG] getXmrigPath: not found, force re-extracting xmrig\n"); const EmbeddedResource* xr = getEmbeddedResource(RESOURCE_XMRIG); if (xr) extractResourceLocked(xr, embeddedPath, /*executable=*/true); } } if (std::filesystem::exists(embeddedPath)) { DEBUG_LOGF("[INFO] getXmrigPath: found at %s\n", embeddedPath.c_str()); return embeddedPath; } #endif // Check local directory if (std::filesystem::exists(xmrigName)) { DEBUG_LOGF("[INFO] getXmrigPath: found in local directory\n"); return xmrigName; } // Check alongside the wallet executable (zip / AppImage distributions) { std::string exeDir = util::Platform::getExecutableDirectory(); if (!exeDir.empty()) { std::string exeDirPath = exeDir + "/" + xmrigName; if (std::filesystem::exists(exeDirPath)) { DEBUG_LOGF("[INFO] getXmrigPath: found alongside wallet at %s\n", exeDirPath.c_str()); return exeDirPath; } } } // Check development paths #ifdef _WIN32 // Not applicable for cross-compiled Windows builds #else // Linux development build — resolve relative to executable directory // (build/bin/), not CWD which may differ at runtime std::string exeDir = util::Platform::getExecutableDirectory(); std::vector devPaths = { exeDir + "/../../external/xmrig-HAC/xmrig/build/xmrig", // from build/linux/bin/ exeDir + "/../external/xmrig-HAC/xmrig/build/xmrig", // from build/linux/ "../external/xmrig-HAC/xmrig/build/xmrig", // CWD = build/linux/ "../../external/xmrig-HAC/xmrig/build/xmrig", // CWD = build/linux/bin/ "external/xmrig-HAC/xmrig/build/xmrig", // CWD = project root }; for (const auto& dp : devPaths) { if (std::filesystem::exists(dp)) { std::string resolved = std::filesystem::canonical(dp).string(); DEBUG_LOGF("[INFO] getXmrigPath: found dev binary at %s\n", resolved.c_str()); return resolved; } } #endif DEBUG_LOGF("[ERROR] getXmrigPath: xmrig binary not found anywhere\n"); return ""; } } // namespace resources } // namespace dragonx