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>
1057 lines
40 KiB
C++
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
|