Files
ObsidianDragon/src/resources/embedded_resources.cpp
T
DanSandClaude Opus 4.8 abc82136c4 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>
2026-10-12 21:42:23 -05:00

1057 lines
40 KiB
C++

#include "embedded_resources.h"
#include "../util/platform.h"
#include "../util/logger.h"
#include <algorithm>
#include <cerrno>
#include <fstream>
#include <filesystem>
#include <functional>
#include <mutex>
#include <vector>
#include <cstdio>
#include <cstring>
#include <cctype>
#include <chrono>
#ifdef _WIN32
#include <windows.h>
#include <shlobj.h>
#else
#include <fcntl.h>
#include <unistd.h>
#include <pwd.h>
#include <sys/types.h>
#include <sys/stat.h>
#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<EmbeddedTheme> 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<size_t>(len) == size) {
std::string buf(size, '\0');
cur.seekg(0);
if (size == 0 || (cur.read(&buf[0], static_cast<std::streamsize>(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<const char*>(data), static_cast<std::streamsize>(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<std::uintmax_t>(t->size))
continue; // up to date
}
std::ofstream f(dest, std::ios::binary);
if (f.is_open()) {
f.write(reinterpret_cast<const char*>(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 "<name>.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<const char*>(data);
while (n > 0) {
#ifdef _WIN32
const DWORD chunk = static_cast<DWORD>(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<std::size_t>(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<std::uint64_t>(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<char> buf(kCopyChunk);
for (;;) {
if (cancelled && cancelled()) { good = false; break; }
in.read(buf.data(), static_cast<std::streamsize>(buf.size()));
const std::streamsize got = in.gcount();
if (got > 0) {
if (!out.write(buf.data(), static_cast<std::size_t>(got))) {
DEBUG_LOGF("[ERROR] Failed to write %s\n", tmp.c_str());
good = false; break;
}
if (progress) progress->bytesDone.fetch_add(static_cast<std::uint64_t>(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<std::mutex> lock(provisionMutex());
// All files go to <ObsidianDragonDir>/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> 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<std::string> bundleSearchDirs()
{
std::vector<std::string> 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<std::string>& saplingParamFileNames()
{
static const std::vector<std::string> v = { "sapling-spend.params", "sapling-output.params" };
return v;
}
const std::vector<std::string>& nodeBundleFileNames()
{
static const std::vector<std::string> v = { "sapling-spend.params", "sapling-output.params", "asmap.dat",
"dragonxd", "dragonx-cli", "dragonx-tx" };
return v;
}
std::vector<BundleCopyItem> planBundleCopy(const std::vector<std::string>& searchDirs, const std::string& destDir,
const std::vector<std::string>& names, bool replaceExisting)
{
namespace fs = std::filesystem;
std::vector<BundleCopyItem> 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<std::string>& searchDirs, const std::string& destDir,
const std::vector<std::string>& 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<std::mutex> 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<std::mutex> 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<std::mutex> 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' <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,
std::size_t lastStart = static_cast<std::size_t>(-1))
{
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 <= 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 -<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 "";
}
// 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 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<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));
}
// 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::string> 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