perf(startup): prepare first-run node files in the background
On a first run or after an upgrade, Windows release builds wrote the bundled Sapling params, asmap, node and miner (about 88 MB) in App::init, before the first frame: 0.5-3 s of frozen blank window, 5-30 s on a slow disk or under antivirus. Zip, AppImage and .app builds copied the bundled files into the daemon dir and hashed the 51.5 MB of params on the UI thread at the first node start, a 0.3-1.5 s freeze in the wizard or the connect loop. A stat-only check at startup decides whether anything is missing, stale or unverified; a normal launch starts no thread and shows nothing. Otherwise one background job extracts (Windows) or copies (Linux, macOS) the files, then verifies the params once: - The first frame paints at once. If the job takes longer than 0.4 s, a chip at the bottom shows "Preparing wallet files... N%", then "Verifying wallet files...". - A node start on the UI thread while the job runs is deferred and replayed when it ends (never into a bootstrap download, a restart or a live connection); worker threads wait, and give up on quit. After an upgrade that ships a new miner, the node starts after the miner is written instead of before. Pool-miner starts and the miner version check wait too, so the old miner is never run or reported while it is being replaced. - Each file goes to "<name>.partial", is flushed and renamed into place under one lock shared with the daemon start, the bundled-node reinstall and the miner extraction, and the job stops between 4 MB chunks at shutdown. An interrupted write used to leave a truncated binary that was trusted forever; a leftover .partial is now removed. Other behaviour changes: - Linux and macOS full-node builds copy the bundled node files (about 77 MB) into the daemon dir at first launch, even if the wallet ends up on an external node; before, this happened at the first embedded start. - Lite no longer writes the 53 MB of params and asmap it never reads. - A disk-full or permission failure that leaves the node files unusable shows a warning; a miner blocked by antivirus alone does not. - A corrupt or truncated param in a zip, AppImage or .app install is replaced from the bundle and verified again instead of trusted forever. - Reinstalling the bundled node holds off the connect loop and a deferred start for its whole run (either could launch the old binary while the new one was written), and is refused while another restart is running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1926,6 +1926,8 @@
|
||||
"sort_date_newest": "Neueste zuerst",
|
||||
"sort_date_oldest": "Älteste zuerst",
|
||||
"start_mining": "Mining starten",
|
||||
"startup_files_preparing": "Wallet-Dateien werden vorbereitet... %d%%",
|
||||
"startup_files_verifying": "Wallet-Dateien werden überprüft...",
|
||||
"status": "Status",
|
||||
"stop_external": "Externen Daemon stoppen",
|
||||
"stop_mining": "Mining stoppen",
|
||||
|
||||
@@ -1926,6 +1926,8 @@
|
||||
"sort_date_newest": "Más recientes primero",
|
||||
"sort_date_oldest": "Más antiguas primero",
|
||||
"start_mining": "Iniciar Minería",
|
||||
"startup_files_preparing": "Preparando los archivos de la cartera... %d%%",
|
||||
"startup_files_verifying": "Verificando los archivos de la cartera...",
|
||||
"status": "Estado",
|
||||
"stop_external": "Detener daemon externo",
|
||||
"stop_mining": "Detener Minería",
|
||||
|
||||
@@ -1926,6 +1926,8 @@
|
||||
"sort_date_newest": "Plus récent d'abord",
|
||||
"sort_date_oldest": "Plus ancien d'abord",
|
||||
"start_mining": "Démarrer le minage",
|
||||
"startup_files_preparing": "Préparation des fichiers du portefeuille... %d%%",
|
||||
"startup_files_verifying": "Vérification des fichiers du portefeuille...",
|
||||
"status": "Statut",
|
||||
"stop_external": "Arrêter le daemon externe",
|
||||
"stop_mining": "Arrêter le minage",
|
||||
|
||||
@@ -1926,6 +1926,8 @@
|
||||
"sort_date_newest": "新しい順",
|
||||
"sort_date_oldest": "古い順",
|
||||
"start_mining": "マイニング開始",
|
||||
"startup_files_preparing": "ウォレットファイルを準備しています... %d%%",
|
||||
"startup_files_verifying": "ウォレットファイルを検証しています...",
|
||||
"status": "ステータス",
|
||||
"stop_external": "外部デーモンを停止",
|
||||
"stop_mining": "マイニング停止",
|
||||
|
||||
@@ -1926,6 +1926,8 @@
|
||||
"sort_date_newest": "최신순",
|
||||
"sort_date_oldest": "오래된 순",
|
||||
"start_mining": "채굴 시작",
|
||||
"startup_files_preparing": "지갑 파일 준비 중... %d%%",
|
||||
"startup_files_verifying": "지갑 파일 확인 중...",
|
||||
"status": "상태",
|
||||
"stop_external": "외부 데몬 중지",
|
||||
"stop_mining": "채굴 중지",
|
||||
|
||||
@@ -1926,6 +1926,8 @@
|
||||
"sort_date_newest": "Mais recentes primeiro",
|
||||
"sort_date_oldest": "Mais antigas primeiro",
|
||||
"start_mining": "Iniciar Mineração",
|
||||
"startup_files_preparing": "Preparando os arquivos da carteira... %d%%",
|
||||
"startup_files_verifying": "Verificando os arquivos da carteira...",
|
||||
"status": "Status",
|
||||
"stop_external": "Parar daemon externo",
|
||||
"stop_mining": "Parar Mineração",
|
||||
|
||||
@@ -1926,6 +1926,8 @@
|
||||
"sort_date_newest": "Сначала новые",
|
||||
"sort_date_oldest": "Сначала старые",
|
||||
"start_mining": "Начать майнинг",
|
||||
"startup_files_preparing": "Подготовка файлов кошелька... %d%%",
|
||||
"startup_files_verifying": "Проверка файлов кошелька...",
|
||||
"status": "Статус",
|
||||
"stop_external": "Остановить внешний daemon",
|
||||
"stop_mining": "Остановить майнинг",
|
||||
|
||||
@@ -1926,6 +1926,8 @@
|
||||
"sort_date_newest": "最新优先",
|
||||
"sort_date_oldest": "最早优先",
|
||||
"start_mining": "开始挖矿",
|
||||
"startup_files_preparing": "正在准备钱包文件... %d%%",
|
||||
"startup_files_verifying": "正在验证钱包文件...",
|
||||
"status": "状态",
|
||||
"stop_external": "停止外部守护进程",
|
||||
"stop_mining": "停止挖矿",
|
||||
|
||||
@@ -1203,6 +1203,16 @@ connection-dot = { opacity = 1.0 }
|
||||
height = 30
|
||||
padding = [8, 0]
|
||||
|
||||
[components.startup-files-chip]
|
||||
width = { size = 340.0 }
|
||||
height = { size = 46.0 }
|
||||
bottom-margin = { size = 38.0 }
|
||||
rounding = { size = 10.0 }
|
||||
padding-x = { size = 14.0 }
|
||||
bar-height = { size = 3.0 }
|
||||
bar-bottom = { size = 10.0 }
|
||||
show-delay-seconds = { size = 0.4 }
|
||||
|
||||
[components.notifications]
|
||||
width = 350.0
|
||||
height = 60.0
|
||||
|
||||
+275
-110
@@ -274,7 +274,10 @@ void App::applyPoolAutoBalance(const util::PoolStatsService::Snapshot& snap)
|
||||
|
||||
std::string App::poolMiningInstalledVersion()
|
||||
{
|
||||
daemon::XmrigManager::startVersionDetection(); // idempotent one-shot
|
||||
// Not while the startup job may still be replacing the miner (`xmrig --version` would cache the old
|
||||
// binary's version); pollStartupFiles() starts the detection when the job ends.
|
||||
if (!startup_files_ || !startup_files_->isRunning())
|
||||
daemon::XmrigManager::startVersionDetection(); // idempotent one-shot
|
||||
return daemon::XmrigManager::installedVersion();
|
||||
}
|
||||
|
||||
@@ -348,11 +351,11 @@ bool App::init()
|
||||
{
|
||||
DEBUG_LOGF("Initializing ObsidianDragon...\n");
|
||||
|
||||
// Extract embedded resources (Sapling params, asmap.dat) on first run
|
||||
if (resources::hasEmbeddedResources() && resources::needsParamsExtraction()) {
|
||||
DEBUG_LOGF("First run - extracting bundled resources...\n");
|
||||
resources::extractEmbeddedResources();
|
||||
}
|
||||
ui_thread_id_ = std::this_thread::get_id();
|
||||
// Write the node files (Sapling params, asmap, node, miner) on first run / after an upgrade OFF the UI
|
||||
// thread: the first frame no longer waits for ~88 MB of disk writes. startEmbeddedDaemon() defers (UI
|
||||
// thread) or waits (workers) until the job is done, so the node never starts over half-written files.
|
||||
startStartupFilesProvision();
|
||||
|
||||
// Initialize settings
|
||||
settings_ = std::make_unique<config::Settings>();
|
||||
@@ -525,7 +528,10 @@ bool App::init()
|
||||
// hashrates and the installed miner version are shown as soon as the tab is opened,
|
||||
// instead of only after the user clicks a mine / auto-balance button.
|
||||
if (supportsPoolMining()) {
|
||||
daemon::XmrigManager::startVersionDetection(); // idempotent one-shot `xmrig --version`
|
||||
// Needs the final miner on disk: while the startup job may still be writing it, pollStartupFiles()
|
||||
// starts this instead.
|
||||
if (!startup_files_->isRunning())
|
||||
daemon::XmrigManager::startVersionDetection(); // idempotent one-shot `xmrig --version`
|
||||
pool_stats_service_.refresh(util::knownPools()); // snapshot lands on a later frame
|
||||
}
|
||||
|
||||
@@ -842,6 +848,9 @@ void App::update()
|
||||
// capture_mode_ is only set for the offline full sweep, so the live tab-only sweep is unaffected.
|
||||
if (capture_mode_) return;
|
||||
|
||||
// Reap the startup node-file job (failure toast, deferred daemon start, miner version probe).
|
||||
pollStartupFiles();
|
||||
|
||||
// If a wallet switch's daemon failed to start, revert to the previous wallet (main-thread-safe).
|
||||
processWalletSwitchRevert();
|
||||
|
||||
@@ -1845,6 +1854,10 @@ void App::render()
|
||||
// (Pins current_page_ too, before the sidebar reads it further down.)
|
||||
updateScreenshotSweep();
|
||||
|
||||
// First-run node-file progress. Foreground draw list, so it shows over the wizard, the lock screen and
|
||||
// the main UI alike (all of which early-return below).
|
||||
renderStartupFilesChip();
|
||||
|
||||
// DragonX logo — load/recolor before the wizard/lock early-returns so every screen shows it.
|
||||
ensureLogoTexture();
|
||||
|
||||
@@ -4294,6 +4307,160 @@ void App::renderBackupDialog()
|
||||
m::EndOverlayDialog();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------
|
||||
// Startup node files (C14). Windows release exes carry the Sapling params, asmap, the node and the miner
|
||||
// inside the .exe and write them out on first run / after an upgrade (~88 MB); zip/AppImage/.app builds copy
|
||||
// them from next to the executable; full-node builds then SHA-256 the params once (cached by marker). All of
|
||||
// it used to run on the UI thread. It now runs on the "extract-node-files" async task.
|
||||
// ---------------------------------------------------------------------------------------------------
|
||||
void App::startStartupFilesProvision()
|
||||
{
|
||||
startup_files_ = std::make_shared<resources::StartupProvision>();
|
||||
|
||||
const bool embedded = resources::hasEmbeddedResources();
|
||||
const bool fullNode = supportsEmbeddedDaemon(); // compile-time; Lite provisions only the miner
|
||||
// Stat-only pre-check (a handful of stats + the .sapling_verified marker): a steady-state launch starts no
|
||||
// thread and shows nothing.
|
||||
bool needed = embedded && resources::needsParamsExtraction();
|
||||
if (!needed && !embedded && fullNode) {
|
||||
needed = !resources::planBundleCopy(resources::bundleSearchDirs(), resources::getDaemonDirectory(),
|
||||
resources::nodeBundleFileNames(), /*replaceExisting=*/false).empty();
|
||||
}
|
||||
if (!needed && fullNode)
|
||||
needed = rpc::Connection::saplingParamState() == rpc::Connection::SaplingParamState::NeedsHash;
|
||||
if (!needed) { startup_files_->markNothingToDo(); return; }
|
||||
|
||||
DEBUG_LOGF("Startup: preparing node files in the background...\n");
|
||||
startup_files_->begin();
|
||||
auto st = startup_files_; // the job owns a reference and captures NO App state
|
||||
auto job = [st, embedded, fullNode](const util::AsyncTaskManager::Token& token) {
|
||||
// Every shutdown path calls async_tasks_.cancelAll() first, so the token is the only cancel source.
|
||||
auto cancelled = [&token]() { return token.cancelled(); };
|
||||
resources::StartupProvision::Result r;
|
||||
try {
|
||||
if (embedded) {
|
||||
r.extractOk = resources::extractEmbeddedResources(&st->progress, cancelled);
|
||||
} else if (fullNode) {
|
||||
r.extractOk = resources::copyBundledFiles(resources::bundleSearchDirs(),
|
||||
resources::getDaemonDirectory(),
|
||||
resources::nodeBundleFileNames(),
|
||||
/*replaceExisting=*/false, &st->progress, cancelled).ok;
|
||||
}
|
||||
// Verify even if some file failed (an antivirus-blocked miner must not skip the param check).
|
||||
if (fullNode && !cancelled()) {
|
||||
st->progress.setPhase(resources::ProvisionPhase::Verifying);
|
||||
r.verifyOk = rpc::Connection::verifySaplingParams(); // hashes once, then writes the marker
|
||||
if (!r.verifyOk && !embedded && !cancelled()) {
|
||||
// A copied distribution whose daemon-dir param exists but is corrupt/truncated: the old code
|
||||
// skipped existing files forever. Replace the params from the bundle and verify again.
|
||||
st->paramRepairTried.store(true, std::memory_order_relaxed);
|
||||
const auto rep = resources::copyBundledFiles(resources::bundleSearchDirs(),
|
||||
resources::getDaemonDirectory(),
|
||||
resources::saplingParamFileNames(),
|
||||
/*replaceExisting=*/true, &st->progress, cancelled);
|
||||
if (rep.copied > 0 && !cancelled()) {
|
||||
st->progress.setPhase(resources::ProvisionPhase::Verifying);
|
||||
r.verifyOk = rpc::Connection::verifySaplingParams();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("[ERROR] Startup file preparation threw: %s\n", e.what());
|
||||
r.extractOk = false;
|
||||
} catch (...) {
|
||||
DEBUG_LOGF("[ERROR] Startup file preparation threw an unknown exception\n");
|
||||
r.extractOk = false;
|
||||
}
|
||||
r.cancelled = token.cancelled();
|
||||
st->finish(r); // ALWAYS: a job stuck in Running would defer every daemon start until shutdown
|
||||
};
|
||||
try {
|
||||
async_tasks_.submit("extract-node-files", job);
|
||||
} catch (const std::exception& e) {
|
||||
// No thread could be created: do it in line, exactly as before this change (blocks once).
|
||||
DEBUG_LOGF("[WARN] Startup: no background thread for file preparation (%s) - running it in line\n", e.what());
|
||||
job(util::AsyncTaskManager::Token{});
|
||||
}
|
||||
}
|
||||
|
||||
void App::pollStartupFiles()
|
||||
{
|
||||
resources::StartupProvision::Result r;
|
||||
if (!startup_files_ || !startup_files_->takeCompletion(&r)) return;
|
||||
if (r.cancelled || shutting_down_) return;
|
||||
DEBUG_LOGF("Startup: node files done (write=%d verify=%d)\n", r.extractOk ? 1 : 0, r.verifyOk ? 1 : 0);
|
||||
// Deliberate improvement: the old code swallowed a write failure until the node refused to start. Only
|
||||
// when the node files are unusable — a miner blocked by antivirus (common) must not nag every launch.
|
||||
if (supportsEmbeddedDaemon() && !r.extractOk && !r.verifyOk)
|
||||
ui::Notifications::instance().warning(TR("sb_daemon_extract_failed"), 20.0f);
|
||||
// The miner version probe waits for the final miner on disk (deferred from init()).
|
||||
if (supportsPoolMining()) daemon::XmrigManager::startVersionDetection();
|
||||
// A start was requested while the files were being written (wizard pre-start / Done, first tryConnect, the
|
||||
// connect MainCb): replay it — but never into a bootstrap download (which stops the node on purpose), an
|
||||
// orchestrated restart, or an already-connected session. Every UI-thread requester also calls tryConnect().
|
||||
if (daemon_start_after_files_) {
|
||||
daemon_start_after_files_ = false;
|
||||
const bool bootstrapping = bootstrap_downloading_ || wizard_phase_ == WizardPhase::BootstrapInProgress;
|
||||
if (!bootstrapping && !daemon_restarting_ && !state_.connected) {
|
||||
if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) startEmbeddedDaemon();
|
||||
if (!connection_in_progress_) tryConnect();
|
||||
} else if (getDaemonStatus() == TR("sb_waiting_daemon")) {
|
||||
setDaemonStatus(std::string()); // the deferral's status, unless another flow replaced it since
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void App::renderStartupFilesChip()
|
||||
{
|
||||
if (!startup_files_ || !startup_files_->isRunning()) return;
|
||||
ui::material::RequestProgressFrame(); // keeps the percentage moving (~10 fps) while the job runs
|
||||
const auto& S = ui::schema::UI();
|
||||
auto val = [&S](const char* key, float fallback) {
|
||||
return S.drawElement("components.startup-files-chip", key).sizeOr(fallback);
|
||||
};
|
||||
if (!resources::provisionChipVisible(true, startup_files_->elapsedSeconds(), val("show-delay-seconds", 0.4f)))
|
||||
return; // a job that ends within the grace period never flashes a chip
|
||||
|
||||
namespace m = ui::material;
|
||||
const auto& pr = startup_files_->progress;
|
||||
const bool verifying = pr.getPhase() == resources::ProvisionPhase::Verifying;
|
||||
const int pct = resources::provisionPercent(pr.bytesDone.load(std::memory_order_relaxed),
|
||||
pr.bytesTotal.load(std::memory_order_relaxed));
|
||||
char buf[160];
|
||||
if (verifying) snprintf(buf, sizeof(buf), "%s", TR("startup_files_verifying"));
|
||||
else snprintf(buf, sizeof(buf), TR("startup_files_preparing"), pct);
|
||||
|
||||
// Hand-drawn geometry: ui.toml values are logical px -> multiply by dpiScale(); font metrics already are.
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
ImFont* f = m::Type().caption();
|
||||
const float padX = val("padding-x", 14.0f) * dp, barH = val("bar-height", 3.0f) * dp;
|
||||
const float barBottom = val("bar-bottom", 10.0f) * dp;
|
||||
const float textW = f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0.0f, buf).x;
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
const float w = std::min(std::max(val("width", 340.0f) * dp, textW + 2.0f * padX), vp->Size.x - 2.0f * padX);
|
||||
const float h = val("height", 46.0f) * dp;
|
||||
const float margin = val("bottom-margin", 38.0f) * dp, rounding = val("rounding", 10.0f) * dp;
|
||||
const ImVec2 mn(vp->Pos.x + (vp->Size.x - w) * 0.5f, vp->Pos.y + vp->Size.y - h - margin);
|
||||
const ImVec2 mx(mn.x + w, mn.y + h);
|
||||
ImDrawList* dl = ImGui::GetForegroundDrawList();
|
||||
dl->AddRectFilled(mn, mx, m::SurfaceAt(m::Elevation::Dp6), rounding); // opaque (Surface() is translucent)
|
||||
dl->AddRect(mn, mx, m::WithAlpha(m::OnSurface(), 40), rounding, 0, 1.0f * dp);
|
||||
const float textY = mn.y + (h - barBottom - barH - f->LegacySize) * 0.5f;
|
||||
dl->AddText(f, f->LegacySize, ImVec2(mn.x + padX, textY), m::OnSurface(), buf);
|
||||
|
||||
const float bx0 = mn.x + padX, bx1 = mx.x - padX, by1 = mx.y - barBottom, by0 = by1 - barH;
|
||||
dl->AddRectFilled(ImVec2(bx0, by0), ImVec2(bx1, by1), m::WithAlpha(m::OnSurface(), 30), barH * 0.5f);
|
||||
float f0 = 0.0f, f1 = pct / 100.0f;
|
||||
if (verifying) { // indeterminate: a sliding segment
|
||||
const float t = std::fmod(static_cast<float>(ImGui::GetTime()) * 0.8f, 1.0f);
|
||||
f0 = std::max(0.0f, t - 0.15f);
|
||||
f1 = std::min(1.0f, t + 0.15f);
|
||||
}
|
||||
if (f1 > f0)
|
||||
dl->AddRectFilled(ImVec2(bx0 + (bx1 - bx0) * f0, by0), ImVec2(bx0 + (bx1 - bx0) * f1, by1),
|
||||
m::Primary(), barH * 0.5f);
|
||||
}
|
||||
|
||||
void App::maybeOfferDaemonUpdate()
|
||||
{
|
||||
if (!supportsFullNodeLifecycleActions()) return; // lite / no embedded-daemon build
|
||||
@@ -5550,6 +5717,31 @@ bool App::startEmbeddedDaemon()
|
||||
return false;
|
||||
}
|
||||
|
||||
// The startup job may still be writing/copying/verifying the node files. Never launch the node over
|
||||
// half-written files and never block the UI thread on the job: on the UI thread remember the request
|
||||
// (pollStartupFiles() replays it) and report true ("starting") so every caller keeps its retry path
|
||||
// instead of raising a failure — NOTE: true here does NOT mean a process was launched; on a worker,
|
||||
// wait for the job (ends at shutdown).
|
||||
if (startup_files_) {
|
||||
switch (resources::daemonStartGate(startup_files_->isRunning(),
|
||||
std::this_thread::get_id() == ui_thread_id_)) {
|
||||
case resources::DaemonStartGate::Defer:
|
||||
setDaemonStatus(TR("sb_waiting_daemon")); // neutral: the job may be a copy / verify / miner refresh
|
||||
daemon_start_after_files_ = true;
|
||||
return true;
|
||||
case resources::DaemonStartGate::Wait:
|
||||
setDaemonStatus(TR("sb_waiting_daemon"));
|
||||
while (!startup_files_->waitUntilDone(100)) {
|
||||
if (shutting_down_) return false;
|
||||
}
|
||||
// The job ends BECAUSE of shutdown (cancelAll): don't fall into the uncancellable extraction/launch.
|
||||
if (shutting_down_ || startup_files_->endedCancelled()) return false;
|
||||
break;
|
||||
case resources::DaemonStartGate::Proceed:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if Sapling params exist - try extracting if embedded
|
||||
if (!rpc::Connection::verifySaplingParams()) {
|
||||
DEBUG_LOGF("Sapling params not found, checking for embedded resources...\n");
|
||||
@@ -5573,48 +5765,28 @@ bool App::startEmbeddedDaemon()
|
||||
}
|
||||
DEBUG_LOGF("Sapling params extracted successfully\n");
|
||||
} else {
|
||||
// Fallback: check for params bundled alongside the executable
|
||||
// (zip distributions bundle sapling-*.params next to the binary)
|
||||
namespace fs = std::filesystem;
|
||||
std::string exe_dir = util::Platform::getExecutableDirectory();
|
||||
std::string daemon_dir = resources::getDaemonDirectory();
|
||||
const char* paramFiles[] = { "sapling-spend.params", "sapling-output.params", "asmap.dat" };
|
||||
bool copied = false;
|
||||
if (!exe_dir.empty()) {
|
||||
// Fallback: params bundled alongside the executable (zip / AppImage / .app). The startup job
|
||||
// normally did this already; this repairs a failed or corrupt copy. replaceExisting: we only get
|
||||
// here because verification FAILED, so an existing daemon-dir param is suspect (the old
|
||||
// `if (exists) continue` kept a truncated param forever).
|
||||
const std::string daemon_dir = resources::getDaemonDirectory();
|
||||
if (!util::Platform::getExecutableDirectory().empty()) {
|
||||
std::string dirErr;
|
||||
if (!util::Platform::ensureDirectory(daemon_dir, &dirErr)) {
|
||||
setDaemonStatus(dirErr);
|
||||
DEBUG_LOGF("[ERROR] %s\n", dirErr.c_str());
|
||||
return false;
|
||||
}
|
||||
std::error_code ec;
|
||||
|
||||
// On macOS .app bundles, params are in Contents/Resources/
|
||||
// while the executable is in Contents/MacOS/
|
||||
std::vector<std::string> searchDirs = { exe_dir };
|
||||
#ifdef __APPLE__
|
||||
{
|
||||
fs::path resourcesDir = fs::path(exe_dir).parent_path() / "Resources";
|
||||
if (fs::is_directory(resourcesDir))
|
||||
searchDirs.insert(searchDirs.begin(), resourcesDir.string());
|
||||
}
|
||||
#endif
|
||||
|
||||
for (const char* name : paramFiles) {
|
||||
fs::path dst = fs::path(daemon_dir) / name;
|
||||
if (fs::exists(dst)) continue;
|
||||
for (const auto& dir : searchDirs) {
|
||||
fs::path src = fs::path(dir) / name;
|
||||
if (fs::exists(src)) {
|
||||
DEBUG_LOGF("Copying bundled %s from %s to %s\n", name, dir.c_str(), daemon_dir.c_str());
|
||||
fs::copy_file(src, dst, ec);
|
||||
if (!ec) copied = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (copied && rpc::Connection::verifySaplingParams()) {
|
||||
// At most once per session (the startup job or an earlier start may have tried it): a still-failing
|
||||
// verify must not repeat this ~51.5 MB copy on every 5 s connect retry on the UI thread.
|
||||
bool repaired = false;
|
||||
if (!(startup_files_ && startup_files_->paramRepairTried.exchange(true))) {
|
||||
const auto rep = resources::copyBundledFiles(resources::bundleSearchDirs(), daemon_dir,
|
||||
resources::saplingParamFileNames(), /*replaceExisting=*/true);
|
||||
repaired = rep.copied > 0 && rpc::Connection::verifySaplingParams();
|
||||
}
|
||||
if (repaired) {
|
||||
DEBUG_LOGF("Sapling params copied from exe directory successfully\n");
|
||||
} else {
|
||||
setDaemonStatus(TR("sb_sapling_not_found"));
|
||||
@@ -5625,49 +5797,22 @@ bool App::startEmbeddedDaemon()
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure asmap.dat and daemon binaries are copied from the bundle
|
||||
// to the daemon directory even if sapling params already existed.
|
||||
// Ensure asmap.dat and the daemon binaries are copied from the bundle to the daemon directory even if the
|
||||
// Sapling params already existed (stat-only no-op when present; the startup job normally did it). An absent
|
||||
// source is optional; only a real copy error counts.
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
std::string exe_dir = util::Platform::getExecutableDirectory();
|
||||
std::string daemon_dir = resources::getDaemonDirectory();
|
||||
if (!exe_dir.empty()) {
|
||||
const std::string daemon_dir = resources::getDaemonDirectory();
|
||||
if (!util::Platform::getExecutableDirectory().empty()) {
|
||||
std::string dirErr;
|
||||
if (!util::Platform::ensureDirectory(daemon_dir, &dirErr)) {
|
||||
setDaemonStatus(dirErr);
|
||||
DEBUG_LOGF("[ERROR] %s\n", dirErr.c_str());
|
||||
return false;
|
||||
}
|
||||
std::error_code ec;
|
||||
|
||||
std::vector<std::string> searchDirs = { exe_dir };
|
||||
#ifdef __APPLE__
|
||||
{
|
||||
fs::path resourcesDir = fs::path(exe_dir).parent_path() / "Resources";
|
||||
if (fs::is_directory(resourcesDir))
|
||||
searchDirs.insert(searchDirs.begin(), resourcesDir.string());
|
||||
}
|
||||
#endif
|
||||
const char* extraFiles[] = { "asmap.dat", "dragonxd", "dragonx-cli", "dragonx-tx" };
|
||||
bool copyFailed = false;
|
||||
for (const char* name : extraFiles) {
|
||||
fs::path dst = fs::path(daemon_dir) / name;
|
||||
if (fs::exists(dst)) continue;
|
||||
for (const auto& dir : searchDirs) {
|
||||
fs::path src = fs::path(dir) / name;
|
||||
if (fs::exists(src)) { // an absent source is optional; only a real copy error counts
|
||||
DEBUG_LOGF("Copying bundled %s from %s to %s\n", name, dir.c_str(), daemon_dir.c_str());
|
||||
fs::copy_file(src, dst, ec);
|
||||
if (ec) {
|
||||
DEBUG_LOGF("[ERROR] Failed to copy %s: %s\n", name, ec.message().c_str());
|
||||
copyFailed = true;
|
||||
ec.clear();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (copyFailed) {
|
||||
static const std::vector<std::string> kExtraFiles = { "asmap.dat", "dragonxd", "dragonx-cli", "dragonx-tx" };
|
||||
const auto rep = resources::copyBundledFiles(resources::bundleSearchDirs(), daemon_dir, kExtraFiles,
|
||||
/*replaceExisting=*/false);
|
||||
if (!rep.ok) {
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof(buf), TR("sb_daemon_files_failed"), daemon_dir.c_str());
|
||||
setDaemonStatus(buf);
|
||||
@@ -6118,42 +6263,58 @@ void App::reinstallBundledDaemon()
|
||||
ui::Notifications::instance().warning(TR("appx_daemon_reinstall_in_progress"));
|
||||
return;
|
||||
}
|
||||
// Don't race a wallet switch / seed-adopt / encryption restart, which drive their own daemon stop/start.
|
||||
if (daemon_restarting_) {
|
||||
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
|
||||
return;
|
||||
}
|
||||
if (refuseIfNodeBusy()) return;
|
||||
|
||||
DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n");
|
||||
ui::Notifications::instance().info(TR("appx_installing_bundled_daemon"));
|
||||
|
||||
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||
AppDaemonLifecycleRuntime runtime(*this);
|
||||
daemon::AsyncLifecycleTaskContext ctx(token, shutting_down_);
|
||||
// Stop the running node so its binary is free: ours, or the external/leftover dragonxd we're attached
|
||||
// to (it would keep the RPC port and the fresh daemon couldn't start). Bounded, never killed.
|
||||
stopEmbeddedDaemon(StopMode::Background, StopScope::Connected);
|
||||
// Wait (bounded, ~120s) for the old node's PROCESS to exit, not just its RPC port: dragonxd unbinds
|
||||
// RPC first and can take a minute or more to flush and close its wallet. Until it exits its exe is
|
||||
// locked on Windows (the overwrite would fail) and it holds the datadir lock (a restart would fail).
|
||||
auto stillUp = [] {
|
||||
return daemon::EmbeddedDaemon::isRpcPortInUse() || daemon::EmbeddedDaemon::isDaemonProcessRunning();
|
||||
};
|
||||
for (int i = 0; i < 1200 && stillUp() && !ctx.cancelled() && !ctx.shuttingDown(); ++i) {
|
||||
ctx.sleepForMs(100);
|
||||
}
|
||||
if (ctx.cancelled() || ctx.shuttingDown()) return;
|
||||
if (stillUp()) {
|
||||
DEBUG_LOGF("[App] Reinstall bundled daemon: aborted — a dragonxd process is still running; "
|
||||
"binaries left untouched and no new node started\n");
|
||||
pending_reinstall_node_busy_.store(true, std::memory_order_relaxed); // told on the UI thread
|
||||
return;
|
||||
}
|
||||
// Overwrite the installed dragonx binaries (dragonxd/cli/tx) with the bundled ones.
|
||||
const bool ok = resources::reextractBundledDaemon();
|
||||
DEBUG_LOGF("[App] Reinstall bundled daemon: %s\n", ok ? "ok" : "FAILED");
|
||||
// Restart on the freshly-installed binary.
|
||||
runtime.resetOutputOffset();
|
||||
if (!ctx.cancelled() && !ctx.shuttingDown()) {
|
||||
runtime.startDaemon();
|
||||
}
|
||||
});
|
||||
// This task owns the node for its whole run: it starts the node itself once the new files are in place, so
|
||||
// drop a start deferred behind the startup files job and gate tryConnect() + the pollStartupFiles() replay
|
||||
// (either could otherwise launch the OLD binary while the new one is being written).
|
||||
daemon_start_after_files_ = false;
|
||||
daemon_restarting_ = true;
|
||||
try {
|
||||
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||
struct Rearm { std::atomic<bool>& f; ~Rearm() { f = false; } } rearm{daemon_restarting_}; // every exit
|
||||
AppDaemonLifecycleRuntime runtime(*this);
|
||||
daemon::AsyncLifecycleTaskContext ctx(token, shutting_down_);
|
||||
// Stop the running node so its binary is free: ours, or the external/leftover dragonxd we're attached
|
||||
// to (it would keep the RPC port and the fresh daemon couldn't start). Bounded, never killed.
|
||||
stopEmbeddedDaemon(StopMode::Background, StopScope::Connected);
|
||||
// Wait (bounded, ~120s) for the old node's PROCESS to exit, not just its RPC port: dragonxd unbinds
|
||||
// RPC first and can take a minute or more to flush and close its wallet. Until it exits its exe is
|
||||
// locked on Windows (the overwrite would fail) and it holds the datadir lock (a restart would fail).
|
||||
auto stillUp = [] {
|
||||
return daemon::EmbeddedDaemon::isRpcPortInUse() || daemon::EmbeddedDaemon::isDaemonProcessRunning();
|
||||
};
|
||||
for (int i = 0; i < 1200 && stillUp() && !ctx.cancelled() && !ctx.shuttingDown(); ++i) {
|
||||
ctx.sleepForMs(100);
|
||||
}
|
||||
if (ctx.cancelled() || ctx.shuttingDown()) return;
|
||||
if (stillUp()) {
|
||||
DEBUG_LOGF("[App] Reinstall bundled daemon: aborted — a dragonxd process is still running; "
|
||||
"binaries left untouched and no new node started\n");
|
||||
pending_reinstall_node_busy_.store(true, std::memory_order_relaxed); // told on the UI thread
|
||||
return;
|
||||
}
|
||||
// Overwrite the installed dragonx binaries (dragonxd/cli/tx) with the bundled ones.
|
||||
const bool ok = resources::reextractBundledDaemon();
|
||||
DEBUG_LOGF("[App] Reinstall bundled daemon: %s\n", ok ? "ok" : "FAILED");
|
||||
// Restart on the freshly-installed binary.
|
||||
runtime.resetOutputOffset();
|
||||
if (!ctx.cancelled() && !ctx.shuttingDown()) {
|
||||
runtime.startDaemon();
|
||||
}
|
||||
});
|
||||
} catch (...) {
|
||||
daemon_restarting_ = false; // no task ran: re-arm the reconnect gate
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void App::deleteBlockchainData()
|
||||
@@ -6199,6 +6360,9 @@ void App::deleteBlockchainData()
|
||||
|
||||
bool App::stopDaemonForBootstrap()
|
||||
{
|
||||
// A bootstrap download stops the node on purpose: drop a start deferred while the startup files were
|
||||
// being prepared, or pollStartupFiles() would launch the node into the half-extracted chain data.
|
||||
daemon_start_after_files_ = false;
|
||||
if (!supportsFullNodeLifecycleActions()) {
|
||||
return false;
|
||||
}
|
||||
@@ -7548,7 +7712,8 @@ bool App::hasPendingBackgroundResults() const {
|
||||
return (worker_ && worker_->hasPendingResults())
|
||||
|| (fast_worker_ && fast_worker_->hasPendingResults())
|
||||
|| (http_worker_ && http_worker_->hasPendingResults()) // g3: market-data GETs
|
||||
|| (lite_wallet_ && lite_wallet_->hasWalletOpCompletions()); // g1: lite wallet-op lane
|
||||
|| (lite_wallet_ && lite_wallet_->hasWalletOpCompletions()) // g1: lite wallet-op lane
|
||||
|| (startup_files_ && startup_files_->hasCompletion()); // g6: startup node files
|
||||
}
|
||||
|
||||
std::string App::transactionSendProgressText() const
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "services/wallet_security_controller.h"
|
||||
#include "services/wallet_security_workflow.h"
|
||||
#include "util/async_task_manager.h"
|
||||
#include "resources/provision_state.h"
|
||||
#include "util/background_sampler.h"
|
||||
#include "util/coalescing_writer.h"
|
||||
#include "util/fetch_backoff.h"
|
||||
@@ -768,9 +769,10 @@ public:
|
||||
void verifyDebugCredential(const std::string& secret, std::function<void(bool)> cb);
|
||||
|
||||
/// @brief Something finished on a background thread and waits for update() to apply it on the main thread
|
||||
/// (the RPC worker lanes, the HTTP lane, lite wallet ops). main.cpp's idle loop polls this every <= 200 ms
|
||||
/// (hasImmediateWork) and renders a frame when it is true, so the result shows without input, even while the
|
||||
/// window is hidden. A new lane whose results are applied in update() adds its term HERE.
|
||||
/// (the RPC worker lanes, the HTTP lane, lite wallet ops, the startup node-file job). main.cpp's idle loop
|
||||
/// polls this every <= 200 ms (hasImmediateWork) and renders a frame when it is true, so the result shows
|
||||
/// without input, even while the window is hidden. A new lane whose results are applied in update() adds
|
||||
/// its term HERE.
|
||||
bool hasPendingBackgroundResults() const;
|
||||
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
|
||||
std::string transactionSendProgressText() const;
|
||||
@@ -1191,6 +1193,15 @@ private:
|
||||
std::chrono::steady_clock::time_point fetchedAt{};
|
||||
};
|
||||
std::unordered_map<std::string, ExchangeChartCache> exchange_chart_cache_;
|
||||
// Startup node files (Windows: extract the bundled params/node/miner; Linux/macOS: copy them from the exe
|
||||
// dir; full node: then verify the Sapling params) on the "extract-node-files" async task. Declared BEFORE
|
||||
// async_tasks_ so it outlives the task threads if the App is destroyed without shutdown().
|
||||
std::shared_ptr<resources::StartupProvision> startup_files_;
|
||||
std::thread::id ui_thread_id_{}; // set first thing in init(); startEmbeddedDaemon() never blocks it
|
||||
bool daemon_start_after_files_ = false; // UI thread only: a daemon start was deferred while the job ran
|
||||
void startStartupFilesProvision();
|
||||
void pollStartupFiles(); // UI thread, every update()
|
||||
void renderStartupFilesChip(); // UI thread, top of render()
|
||||
util::AsyncTaskManager async_tasks_;
|
||||
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
|
||||
|
||||
|
||||
+16
-1
@@ -1647,6 +1647,7 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed
|
||||
// stopDaemonForWalletSwitch() wait already ensured the old node's process is gone, so a valid
|
||||
// wallet opens cleanly; a bad/corrupt wallet exits during init and we revert.
|
||||
ok = shutting_down_ ? true : startEmbeddedDaemon();
|
||||
if (shutting_down_) ok = true; // a start aborted BY shutdown is not a failed switch (no revert)
|
||||
// A missing/corrupt wallet makes dragonxd exit during init — confirm the process survived a
|
||||
// moment; if not, the wallet is bad. (A valid launch keeps the process alive while it syncs.)
|
||||
if (ok && !shutting_down_) {
|
||||
@@ -3511,7 +3512,8 @@ void App::postPoolMinerJob(int threads, bool retargetOnly)
|
||||
daemon::XmrigManager::Config cfgCopy = cfg;
|
||||
const int target = cfg.threads;
|
||||
pool_threads_target_.store(target, std::memory_order_relaxed);
|
||||
postMiningControl([this, cfgCopy, target, retargetOnly]() {
|
||||
auto files = startup_files_; // shared_ptr copy (set once in init()): the startup node-files job
|
||||
postMiningControl([this, cfgCopy, target, retargetOnly, files]() {
|
||||
// A retarget is decided HERE, after every earlier start/stop ran: nothing happens unless a miner is
|
||||
// running now and at another count. So it never starts a miner the user stopped, and a restart's
|
||||
// stop window (isRunning() false on the UI thread) can't hide the running one.
|
||||
@@ -3520,6 +3522,19 @@ void App::postPoolMinerJob(int threads, bool retargetOnly)
|
||||
if (xmrig_manager_->getRequestedThreads() == target) return;
|
||||
}
|
||||
if (xmrig_manager_->isRunning()) xmrig_manager_->stop(3000);
|
||||
// The startup job may still be replacing the miner binary: never launch the old one under it. Waiting
|
||||
// here (the mining-control thread) never blocks the UI; shutdown, or the control thread being stopped
|
||||
// (shutdown()'s fallback path joins it before cancelling the job), ends the wait.
|
||||
if (files) {
|
||||
auto stopping = [this]() {
|
||||
if (shutting_down_) return true;
|
||||
std::lock_guard<std::mutex> lk(mining_ctl_mutex_);
|
||||
return mining_ctl_stop_;
|
||||
};
|
||||
bool abort = false;
|
||||
while (!abort && !files->waitUntilDone(100)) abort = stopping();
|
||||
if (abort || shutting_down_ || files->endedCancelled()) { clearPoolThreadsTarget(target); return; }
|
||||
}
|
||||
const bool ok = xmrig_manager_->start(cfgCopy);
|
||||
const std::string err = ok ? std::string() : xmrig_manager_->getLastError();
|
||||
if (!ok) clearPoolThreadsTarget(target);
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
#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>
|
||||
|
||||
@@ -12,6 +17,7 @@
|
||||
#include <windows.h>
|
||||
#include <shlobj.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <pwd.h>
|
||||
#include <sys/types.h>
|
||||
@@ -205,272 +211,461 @@ std::string getParamsDirectory()
|
||||
#endif
|
||||
}
|
||||
|
||||
// Forward declaration — defined below extractResource()
|
||||
static bool resourceNeedsUpdate(const EmbeddedResource* res, const std::string& destPath);
|
||||
// ---------------------------------------------------------------------------------------------------
|
||||
// Writing into the daemon directory: one lock, atomic (temp + flush + rename) files.
|
||||
// ---------------------------------------------------------------------------------------------------
|
||||
namespace {
|
||||
|
||||
bool needsParamsExtraction()
|
||||
// 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, xmrig)
|
||||
bool executable;
|
||||
bool fullNodeOnly; // not written by Lite builds (the lite backend bakes in its own params)
|
||||
bool startupTrigger; // makes needsParamsExtraction() true
|
||||
};
|
||||
|
||||
// The same files and rules as the old hand-written extraction: params/asmap/xmrig follow the bundled size;
|
||||
// the node binaries and the rebuild helper are placed only when MISSING (the user may run a specific dragonxd;
|
||||
// replacing it 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, true, true, false, true },
|
||||
#endif
|
||||
#ifdef HAS_EMBEDDED_WALLET_REBUILD
|
||||
{ RESOURCE_DRAGONX_WALLET_REBUILD, false, true, true, false },
|
||||
#endif
|
||||
};
|
||||
|
||||
bool entryActive(const ProvisionEntry& e)
|
||||
{
|
||||
if (!hasEmbeddedResources()) {
|
||||
#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;
|
||||
}
|
||||
|
||||
// Check daemon directory (dragonx/) — the only extraction target
|
||||
std::string daemonDir = getDaemonDirectory();
|
||||
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
|
||||
const char pathSep = '\\';
|
||||
h_ = CreateFileA(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
#else
|
||||
const char pathSep = '/';
|
||||
#endif
|
||||
std::string spendPath = daemonDir + pathSep + RESOURCE_SAPLING_SPEND;
|
||||
std::string outputPath = daemonDir + pathSep + RESOURCE_SAPLING_OUTPUT;
|
||||
|
||||
// Check if params are missing or stale (size mismatch → updated in newer build)
|
||||
const auto* spendRes = getEmbeddedResource(RESOURCE_SAPLING_SPEND);
|
||||
const auto* outputRes = getEmbeddedResource(RESOURCE_SAPLING_OUTPUT);
|
||||
if (spendRes && resourceNeedsUpdate(spendRes, spendPath)) return true;
|
||||
if (outputRes && resourceNeedsUpdate(outputRes, outputPath)) return true;
|
||||
|
||||
// Daemon binaries are only auto-placed when MISSING (never auto-overwritten on a size
|
||||
// mismatch) — the user may be running a specific dragonxd. Replacing the bundled daemon is
|
||||
// an explicit action via Settings → daemon binary. So only trigger extraction if it's absent.
|
||||
#ifdef HAS_EMBEDDED_DAEMON
|
||||
const auto* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
|
||||
std::string daemonPath = daemonDir + pathSep + RESOURCE_DRAGONXD;
|
||||
if (daemonRes && !std::filesystem::exists(daemonPath)) return true;
|
||||
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;
|
||||
|
||||
#ifdef HAS_EMBEDDED_XMRIG
|
||||
const auto* xmrigRes = getEmbeddedResource(RESOURCE_XMRIG);
|
||||
std::string xmrigPath = daemonDir + pathSep + RESOURCE_XMRIG;
|
||||
if (xmrigRes && resourceNeedsUpdate(xmrigRes, xmrigPath)) return true;
|
||||
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.
|
||||
static bool resourceNeedsUpdate(const EmbeddedResource* res, const std::string& destPath)
|
||||
[[maybe_unused]] static bool resourceNeedsUpdate(const EmbeddedResource* res, const std::string& destPath)
|
||||
{
|
||||
if (!res || !res->data || res->size == 0) return false;
|
||||
if (!std::filesystem::exists(destPath)) return true;
|
||||
std::error_code ec;
|
||||
auto diskSize = std::filesystem::file_size(destPath, ec);
|
||||
if (ec) return true; // can't stat → re-extract
|
||||
return diskSize != static_cast<std::uintmax_t>(res->size);
|
||||
return resourceNeedsWrite(res->size, /*refreshOnSizeMismatch=*/true, destPath);
|
||||
}
|
||||
|
||||
static bool extractResource(const EmbeddedResource* res, const std::string& destPath)
|
||||
bool needsParamsExtraction()
|
||||
{
|
||||
if (!res || !res->data || res->size == 0) {
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
|
||||
// Create parent directories
|
||||
std::filesystem::path path(destPath);
|
||||
if (path.has_parent_path()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
if (ec) {
|
||||
DEBUG_LOGF("[ERROR] Failed to create directory %s: %s\n",
|
||||
path.parent_path().string().c_str(), ec.message().c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Write file
|
||||
std::ofstream file(destPath, std::ios::binary);
|
||||
if (!file) {
|
||||
// The destination may be locked because the previous daemon is still using the binary:
|
||||
// Windows locks a running .exe against truncation, Linux returns ETXTBSY. Both platforms
|
||||
// DO allow renaming/moving such a file — the running process keeps the moved copy — so move
|
||||
// the stale binary aside and write a fresh one at the original path.
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(destPath)) {
|
||||
std::string sidelined = destPath + ".old";
|
||||
std::filesystem::remove(sidelined, ec); // clear any leftover from a prior swap
|
||||
ec.clear();
|
||||
std::filesystem::rename(destPath, sidelined, ec);
|
||||
if (!ec) {
|
||||
file.clear();
|
||||
file.open(destPath, std::ios::binary);
|
||||
if (file)
|
||||
DEBUG_LOGF("[INFO] Replaced in-use %s (old copy moved to .old)\n", destPath.c_str());
|
||||
} else {
|
||||
DEBUG_LOGF("[WARN] Could not move stale %s aside: %s\n",
|
||||
destPath.c_str(), ec.message().c_str());
|
||||
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);
|
||||
}
|
||||
if (!file) {
|
||||
DEBUG_LOGF("[ERROR] Failed to open %s for writing\n", destPath.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
file.write(reinterpret_cast<const char*>(res->data), res->size);
|
||||
if (!file) {
|
||||
DEBUG_LOGF("[ERROR] Failed to write %zu bytes to %s\n", res->size, destPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[INFO] Extracted %s (%zu bytes)\n", destPath.c_str(), res->size);
|
||||
// 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 extractEmbeddedResources()
|
||||
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;
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
|
||||
#ifdef _WIN32
|
||||
const char pathSep = '\\';
|
||||
#else
|
||||
const char pathSep = '/';
|
||||
#endif
|
||||
|
||||
// All files go to <ObsidianDragonDir>/dragonx/
|
||||
std::string daemonDir = getDaemonDirectory();
|
||||
std::lock_guard<std::mutex> lock(provisionMutex());
|
||||
|
||||
// Extract Sapling params to daemon directory alongside dragonxd
|
||||
const EmbeddedResource* spendRes = getEmbeddedResource(RESOURCE_SAPLING_SPEND);
|
||||
if (spendRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_SAPLING_SPEND;
|
||||
if (resourceNeedsUpdate(spendRes, dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting sapling-spend.params (%zu MB)...\n", spendRes->size / (1024*1024));
|
||||
if (!extractResource(spendRes, dest)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const EmbeddedResource* outputRes = getEmbeddedResource(RESOURCE_SAPLING_OUTPUT);
|
||||
if (outputRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_SAPLING_OUTPUT;
|
||||
if (resourceNeedsUpdate(outputRes, dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting sapling-output.params (%zu MB)...\n", outputRes->size / (1024*1024));
|
||||
if (!extractResource(outputRes, dest)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract asmap.dat to daemon directory
|
||||
const EmbeddedResource* asmapRes = getEmbeddedResource(RESOURCE_ASMAP);
|
||||
if (asmapRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_ASMAP;
|
||||
if (resourceNeedsUpdate(asmapRes, dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting asmap.dat...\n");
|
||||
if (!extractResource(asmapRes, dest)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract daemon binaries — NOT the data directory.
|
||||
// Running dragonxd.exe from inside the data directory (where it writes blockchain
|
||||
// data, debug.log, lock files, etc.) causes crashes on some Windows machines.
|
||||
#ifdef HAS_EMBEDDED_DAEMON
|
||||
// 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());
|
||||
|
||||
// Daemon binaries are placed ONLY when missing — never auto-overwritten on a size mismatch
|
||||
// (the user may run a specific dragonxd; replacing it is an explicit Settings action).
|
||||
const EmbeddedResource* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
|
||||
if (daemonRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONXD;
|
||||
if (!std::filesystem::exists(dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting dragonxd (%zu MB)...\n", daemonRes->size / (1024*1024));
|
||||
if (!extractResource(daemonRes, dest)) {
|
||||
success = false;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
else { chmod(dest.c_str(), 0755); }
|
||||
#endif
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const EmbeddedResource* cliRes = getEmbeddedResource(RESOURCE_DRAGONX_CLI);
|
||||
if (cliRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_CLI;
|
||||
if (!std::filesystem::exists(dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting dragonx-cli (%zu MB)...\n", cliRes->size / (1024*1024));
|
||||
if (!extractResource(cliRes, dest)) {
|
||||
success = false;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
else { chmod(dest.c_str(), 0755); }
|
||||
#endif
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
|
||||
const EmbeddedResource* txRes = getEmbeddedResource(RESOURCE_DRAGONX_TX);
|
||||
if (txRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_TX;
|
||||
if (!std::filesystem::exists(dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting dragonx-tx (%zu MB)...\n", txRes->size / (1024*1024));
|
||||
if (!extractResource(txRes, dest)) {
|
||||
success = false;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
else { chmod(dest.c_str(), 0755); }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAS_EMBEDDED_XMRIG
|
||||
const EmbeddedResource* xmrigRes = getEmbeddedResource(RESOURCE_XMRIG);
|
||||
if (xmrigRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_XMRIG;
|
||||
if (resourceNeedsUpdate(xmrigRes, dest)) {
|
||||
if (std::filesystem::exists(dest))
|
||||
DEBUG_LOGF("[INFO] Updating stale xmrig (size mismatch)...\n");
|
||||
DEBUG_LOGF("[INFO] Extracting xmrig (%zu MB)...\n", xmrigRes->size / (1024*1024));
|
||||
if (!extractResource(xmrigRes, dest)) {
|
||||
success = false;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
else { chmod(dest.c_str(), 0755); }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAS_EMBEDDED_WALLET_REBUILD
|
||||
// Offline wallet-rebuild recovery helper — extracted next to the daemon so a bare, self-extracting
|
||||
// ObsidianDragon.exe still offers "Repair automatically" (findWalletRebuildHelper() checks this dir).
|
||||
const EmbeddedResource* rebuildRes = getEmbeddedResource(RESOURCE_DRAGONX_WALLET_REBUILD);
|
||||
if (rebuildRes) {
|
||||
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_WALLET_REBUILD;
|
||||
if (!std::filesystem::exists(dest)) {
|
||||
DEBUG_LOGF("[INFO] Extracting dragonx-wallet-rebuild (%zu MB)...\n", rebuildRes->size / (1024*1024));
|
||||
if (!extractResource(rebuildRes, dest)) {
|
||||
success = false;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
else { chmod(dest.c_str(), 0755); }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Best-effort cleanup of any ".old" binaries left behind by a previous in-use replacement.
|
||||
// Once the old daemon/xmrig process has exited, the file is no longer locked and removes cleanly;
|
||||
// if it's still running, the remove fails harmlessly and we retry on the next startup.
|
||||
{
|
||||
std::error_code ec;
|
||||
for (const char* name : { RESOURCE_DRAGONXD, RESOURCE_XMRIG }) {
|
||||
std::filesystem::remove(daemonDir + pathSep + 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
|
||||
@@ -484,12 +679,11 @@ std::string ensureWalletRebuildHelperExtracted()
|
||||
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
|
||||
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 (!extractResource(res, dest)) return {};
|
||||
#ifndef _WIN32
|
||||
chmod(dest.c_str(), 0755);
|
||||
#endif
|
||||
if (!extractResourceLocked(res, dest, /*executable=*/true)) return {};
|
||||
DEBUG_LOGF("[INFO] Extracted wallet-rebuild helper on demand: %s\n", dest.c_str());
|
||||
return dest;
|
||||
#else
|
||||
@@ -623,14 +817,11 @@ bool forceExtractXmrig()
|
||||
}
|
||||
DEBUG_LOGF("[INFO] forceExtractXmrig: extracting xmrig (%zu MB) to %s\n",
|
||||
xmrigRes->size / (1024*1024), dest.c_str());
|
||||
if (!extractResource(xmrigRes, dest)) {
|
||||
std::lock_guard<std::mutex> lock(provisionMutex());
|
||||
if (!extractResourceLocked(xmrigRes, dest, /*executable=*/true)) {
|
||||
DEBUG_LOGF("[ERROR] forceExtractXmrig: extraction failed\n");
|
||||
return false;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
// Set executable permission on Linux
|
||||
chmod(dest.c_str(), 0755);
|
||||
#endif
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
@@ -722,6 +913,7 @@ BundledDaemonInfo getBundledDaemonInfo()
|
||||
bool reextractBundledDaemon()
|
||||
{
|
||||
#ifdef HAS_EMBEDDED_DAEMON
|
||||
std::lock_guard<std::mutex> lock(provisionMutex());
|
||||
std::string daemonDir = getDaemonDirectory();
|
||||
#ifdef _WIN32
|
||||
const char pathSep = '\\';
|
||||
@@ -736,15 +928,12 @@ bool reextractBundledDaemon()
|
||||
if (!res) continue;
|
||||
std::string dest = daemonDir + pathSep + name;
|
||||
DEBUG_LOGF("[INFO] reextractBundledDaemon: writing %s (%zu MB)\n", name, res->size / (1024*1024));
|
||||
if (!extractResource(res, dest)) {
|
||||
if (!extractResourceLocked(res, dest, /*executable=*/true)) {
|
||||
DEBUG_LOGF("[ERROR] reextractBundledDaemon: failed to write %s\n", name);
|
||||
ok = false;
|
||||
continue;
|
||||
}
|
||||
wroteAny = true;
|
||||
#ifndef _WIN32
|
||||
chmod(dest.c_str(), 0755);
|
||||
#endif
|
||||
}
|
||||
return ok && wroteAny;
|
||||
#else
|
||||
@@ -770,8 +959,13 @@ std::string getXmrigPath()
|
||||
std::string embeddedPath = daemonDir + pathSep + xmrigName;
|
||||
DEBUG_LOGF("[DEBUG] getXmrigPath: checking embedded path %s\n", embeddedPath.c_str());
|
||||
if (!std::filesystem::exists(embeddedPath)) {
|
||||
DEBUG_LOGF("[DEBUG] getXmrigPath: not found, force re-extracting xmrig\n");
|
||||
forceExtractXmrig();
|
||||
// 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());
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "provision_state.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace resources {
|
||||
@@ -20,9 +25,47 @@ bool hasEmbeddedResources();
|
||||
// Returns nullptr if not found or not embedded
|
||||
const EmbeddedResource* getEmbeddedResource(const std::string& name);
|
||||
|
||||
// Extract all embedded resources to appropriate locations
|
||||
// Returns true if all resources extracted successfully
|
||||
bool extractEmbeddedResources();
|
||||
using CancelFn = std::function<bool()>; // polled between 4 MB chunks; true = stop now
|
||||
|
||||
// Suffix of our own temp files in the daemon dir ("<name>.partial"). NOT ".tmp": the in-app miner/node
|
||||
// updaters stage "<name>.tmp" in the same directory and must never have it deleted under them.
|
||||
inline constexpr const char* kPartialFileSuffix = ".partial";
|
||||
|
||||
// Extract all embedded resources to the daemon directory. Serialised with every other writer of that
|
||||
// directory by an internal mutex; atomic per file (temp + flush + rename), so a destination is either the old
|
||||
// complete file or the new one. `progress` (optional) receives bytes written; `cancelled` (optional) aborts
|
||||
// between chunks leaving no partial destination. Returns true if every needed file was written (false also on
|
||||
// cancel). Safe on any thread.
|
||||
bool extractEmbeddedResources(ProvisionProgress* progress = nullptr, const CancelFn& cancelled = {});
|
||||
|
||||
// Does `destPath` need (re)writing? Missing -> yes. Present: only when `refreshOnSizeMismatch` and the size
|
||||
// differs. embeddedSize == 0 -> never. (Pure apart from the stat; exposed for tests.)
|
||||
bool resourceNeedsWrite(std::uint64_t embeddedSize, bool refreshOnSizeMismatch, const std::string& destPath);
|
||||
// Write `data` to `destPath` via `destPath + kPartialFileSuffix`, flush it to disk, then move it into place
|
||||
// (POSIX rename; Windows MoveFileEx with retry and move-aside of a running .exe to ".old"). POSIX: chmod 0755
|
||||
// first when `executable`. Returns false (temp removed, original kept) on any failure or on cancel.
|
||||
// Takes no lock itself (extractEmbeddedResources / copyBundledFiles / the on-demand extractors do).
|
||||
bool writeFileAtomic(const std::string& destPath, const std::uint8_t* data, std::size_t size, bool executable,
|
||||
ProvisionProgress* progress = nullptr, const CancelFn& cancelled = {});
|
||||
// Same contract, streaming from a source file in 4 MB chunks; keeps the source's permissions.
|
||||
bool copyFileAtomic(const std::string& srcPath, const std::string& destPath,
|
||||
ProvisionProgress* progress = nullptr, const CancelFn& cancelled = {});
|
||||
|
||||
// Node files shipped NEXT TO the executable (zip / AppImage / .app; no embedded_data.h).
|
||||
struct BundleCopyItem { std::string name, src, dst; };
|
||||
std::vector<std::string> bundleSearchDirs(); // macOS: .app Contents/Resources first; then exe dir
|
||||
const std::vector<std::string>& saplingParamFileNames(); // sapling-spend.params, sapling-output.params
|
||||
const std::vector<std::string>& nodeBundleFileNames(); // params + asmap.dat + dragonxd + dragonx-cli + dragonx-tx
|
||||
// First search dir holding a name wins; names with no source are skipped (optional); names whose destination
|
||||
// exists are skipped unless replaceExisting. Stat-only.
|
||||
std::vector<BundleCopyItem> planBundleCopy(const std::vector<std::string>& searchDirs, const std::string& destDir,
|
||||
const std::vector<std::string>& names, bool replaceExisting);
|
||||
struct BundleCopyResult { bool ok = true; int copied = 0; std::string firstFailure; };
|
||||
// Executes planBundleCopy under the provisioning lock (checks the plan lock-free first: the common
|
||||
// "nothing to copy" call never touches the lock).
|
||||
BundleCopyResult copyBundledFiles(const std::vector<std::string>& searchDirs, const std::string& destDir,
|
||||
const std::vector<std::string>& names, bool replaceExisting,
|
||||
ProvisionProgress* progress = nullptr, const CancelFn& cancelled = {});
|
||||
|
||||
// Check if params need to be extracted (not already present)
|
||||
bool needsParamsExtraction();
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// provision_state.h - state of the startup "node files" job (App::startStartupFilesProvision): on Windows it
|
||||
// extracts the bundled Sapling params / asmap / node / miner from the .exe, on Linux/macOS it copies them
|
||||
// from the executable's directory, and on full-node builds it then verifies the Sapling params. The job runs
|
||||
// on a worker thread; the UI thread only polls this. No app dependencies, so tests include it directly.
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
|
||||
namespace dragonx {
|
||||
namespace resources {
|
||||
|
||||
enum class ProvisionPhase : int { Idle = 0, Extracting, Copying, Verifying };
|
||||
|
||||
// Lock-free progress: written by the worker, read by the UI every frame.
|
||||
struct ProvisionProgress {
|
||||
std::atomic<std::uint64_t> bytesDone{0};
|
||||
std::atomic<std::uint64_t> bytesTotal{0};
|
||||
std::atomic<int> phase{static_cast<int>(ProvisionPhase::Idle)};
|
||||
void setPhase(ProvisionPhase p) { phase.store(static_cast<int>(p), std::memory_order_relaxed); }
|
||||
ProvisionPhase getPhase() const { return static_cast<ProvisionPhase>(phase.load(std::memory_order_relaxed)); }
|
||||
};
|
||||
|
||||
// 0..100; 0 while the total is unknown.
|
||||
inline int provisionPercent(std::uint64_t done, std::uint64_t total)
|
||||
{
|
||||
if (total == 0) return 0;
|
||||
if (done >= total) return 100;
|
||||
return static_cast<int>((done * 100u) / total);
|
||||
}
|
||||
|
||||
// The chip stays hidden for a short grace period so a job that ends in a few ms never flashes.
|
||||
inline bool provisionChipVisible(bool running, double elapsedSeconds, double delaySeconds)
|
||||
{
|
||||
return running && elapsedSeconds >= delaySeconds;
|
||||
}
|
||||
|
||||
// What App::startEmbeddedDaemon() does while the job may still be writing the node files: never launch the
|
||||
// node over half-written files, never block the UI thread (defer + replay), workers simply wait.
|
||||
enum class DaemonStartGate { Proceed, Defer, Wait };
|
||||
inline DaemonStartGate daemonStartGate(bool jobRunning, bool onUiThread)
|
||||
{
|
||||
if (!jobRunning) return DaemonStartGate::Proceed;
|
||||
return onUiThread ? DaemonStartGate::Defer : DaemonStartGate::Wait;
|
||||
}
|
||||
|
||||
class StartupProvision {
|
||||
public:
|
||||
struct Result {
|
||||
bool extractOk = true; // every needed file written/copied (false: disk full / permissions / AV)
|
||||
bool verifyOk = true; // Sapling SHA-256 check (full node only)
|
||||
bool cancelled = false; // shutdown stopped it; nothing is wrong
|
||||
};
|
||||
enum class State : int { Idle, Running, Finished, Reaped };
|
||||
|
||||
ProvisionProgress progress;
|
||||
// The Sapling param repair copy (replace the daemon-dir params from the bundle, ~51.5 MB) ran this session:
|
||||
// set by the job and by App::startEmbeddedDaemon(), so a still-failing verify never repeats it on every
|
||||
// connect retry (the UI thread).
|
||||
std::atomic<bool> paramRepairTried{false};
|
||||
|
||||
void begin() {
|
||||
std::lock_guard<std::mutex> lk(m_);
|
||||
state_ = State::Running;
|
||||
startedAt_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
// Nothing needed doing (the steady-state launch): no thread was started.
|
||||
void markNothingToDo() { std::lock_guard<std::mutex> lk(m_); state_ = State::Reaped; }
|
||||
void finish(const Result& r) {
|
||||
{ std::lock_guard<std::mutex> lk(m_); result_ = r; state_ = State::Finished; }
|
||||
cv_.notify_all();
|
||||
}
|
||||
bool isRunning() const { std::lock_guard<std::mutex> lk(m_); return state_ == State::Running; }
|
||||
// Non-reaping peek: the job has ended and shutdown cancelled it (synchronized with finish() through m_).
|
||||
bool endedCancelled() const {
|
||||
std::lock_guard<std::mutex> lk(m_);
|
||||
return state_ != State::Running && result_.cancelled;
|
||||
}
|
||||
// App::hasPendingBackgroundResults(): finished and not yet reaped (the reap needs one update() tick).
|
||||
// NOT while Running: nothing is reapable then, and the chip already requests progress frames when visible.
|
||||
bool hasCompletion() const { std::lock_guard<std::mutex> lk(m_); return state_ == State::Finished; }
|
||||
// UI thread: true exactly once, after the job finished.
|
||||
bool takeCompletion(Result* out) {
|
||||
std::lock_guard<std::mutex> lk(m_);
|
||||
if (state_ != State::Finished) return false;
|
||||
if (out) *out = result_;
|
||||
state_ = State::Reaped;
|
||||
return true;
|
||||
}
|
||||
// Worker threads: true when the job is no longer running.
|
||||
bool waitUntilDone(int timeoutMs) {
|
||||
std::unique_lock<std::mutex> lk(m_);
|
||||
cv_.wait_for(lk, std::chrono::milliseconds(timeoutMs), [&] { return state_ != State::Running; });
|
||||
return state_ != State::Running;
|
||||
}
|
||||
double elapsedSeconds() const {
|
||||
std::lock_guard<std::mutex> lk(m_);
|
||||
if (state_ != State::Running) return 0.0;
|
||||
return std::chrono::duration<double>(std::chrono::steady_clock::now() - startedAt_).count();
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex m_;
|
||||
std::condition_variable cv_;
|
||||
State state_ = State::Idle;
|
||||
Result result_;
|
||||
std::chrono::steady_clock::time_point startedAt_{};
|
||||
};
|
||||
|
||||
} // namespace resources
|
||||
} // namespace dragonx
|
||||
@@ -241,6 +241,30 @@ bool Connection::verifySaplingParams()
|
||||
return verifySaplingParamsIn(getSaplingParamsDir(), digests);
|
||||
}
|
||||
|
||||
Connection::SaplingParamState Connection::saplingParamStateIn(
|
||||
const std::string& dir,
|
||||
const std::vector<std::pair<std::string, std::string>>& digests)
|
||||
{
|
||||
if (dir.empty() || digests.empty()) return SaplingParamState::Missing;
|
||||
std::vector<std::string> current;
|
||||
current.reserve(digests.size());
|
||||
for (const auto& d : digests) {
|
||||
const std::string p = joinParamPath(dir, d.first);
|
||||
std::error_code ec;
|
||||
if (!fs::exists(p, ec)) return SaplingParamState::Missing;
|
||||
current.push_back(paramStatLine(p));
|
||||
}
|
||||
return saplingMarkerMatches(joinParamPath(dir, ".sapling_verified"), current)
|
||||
? SaplingParamState::Verified : SaplingParamState::NeedsHash;
|
||||
}
|
||||
|
||||
Connection::SaplingParamState Connection::saplingParamState()
|
||||
{
|
||||
std::vector<std::pair<std::string, std::string>> digests;
|
||||
for (const auto& d : kSaplingParamDigests) digests.emplace_back(d.first, d.second);
|
||||
return saplingParamStateIn(getSaplingParamsDir(), digests);
|
||||
}
|
||||
|
||||
ConnectionConfig Connection::parseConfFile(const std::string& path)
|
||||
{
|
||||
ConnectionConfig config;
|
||||
|
||||
@@ -83,6 +83,14 @@ public:
|
||||
const std::string& dir,
|
||||
const std::vector<std::pair<std::string, std::string>>& digests);
|
||||
|
||||
// Stat-only (never reads or hashes the params): Missing = a param is absent, Verified = the
|
||||
// .sapling_verified marker matches every param's size:mtime, NeedsHash = present but unverified.
|
||||
enum class SaplingParamState { Missing, NeedsHash, Verified };
|
||||
static SaplingParamState saplingParamState();
|
||||
static SaplingParamState saplingParamStateIn(
|
||||
const std::string& dir,
|
||||
const std::vector<std::pair<std::string, std::string>>& digests);
|
||||
|
||||
/**
|
||||
* @brief Get the Sapling params directory
|
||||
*/
|
||||
|
||||
@@ -1513,6 +1513,8 @@ void I18n::loadBuiltinEnglish()
|
||||
"Close the program using it (or free the port), then restart — the wallet can't start "
|
||||
"its own node while the port is taken.";
|
||||
strings_["sb_extracting_sapling"] = "Extracting Sapling parameters...";
|
||||
strings_["startup_files_preparing"] = "Preparing wallet files... %d%%";
|
||||
strings_["startup_files_verifying"] = "Verifying wallet files...";
|
||||
strings_["sb_sapling_failed"] = "Failed to extract Sapling parameters.";
|
||||
strings_["sb_sapling_not_found"] = "Sapling parameters not found.";
|
||||
strings_["sb_daemon_extract_failed"] = "Failed to write daemon files — check free disk space and permissions.";
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "rpc/rpc_failure.h"
|
||||
#include "rpc/rpc_busy_gate.h"
|
||||
#include "resources/embedded_resources.h"
|
||||
#include "resources/provision_state.h"
|
||||
#include "services/history_scan_policy.h"
|
||||
#include "services/wallet_scan_governor.h"
|
||||
#include "services/scan_policy.h"
|
||||
@@ -3341,6 +3342,278 @@ void testVerifySaplingParams()
|
||||
fsn::remove_all(dir2, rmec);
|
||||
}
|
||||
|
||||
// ── Startup node files (resources/provision_state.h, embedded_resources atomic writers, Sapling param state) ──
|
||||
static std::string readProvisionFile(const fs::path& p)
|
||||
{
|
||||
std::ifstream f(p.string(), std::ios::binary);
|
||||
return std::string((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
void testStartupProvisionState()
|
||||
{
|
||||
using namespace dragonx::resources;
|
||||
EXPECT_EQ(provisionPercent(0, 0), 0);
|
||||
EXPECT_EQ(provisionPercent(50, 100), 50);
|
||||
EXPECT_EQ(provisionPercent(100, 100), 100);
|
||||
EXPECT_EQ(provisionPercent(150, 100), 100);
|
||||
EXPECT_EQ(provisionPercent(1, 3), 33);
|
||||
EXPECT_TRUE(!provisionChipVisible(true, 0.1, 0.4));
|
||||
EXPECT_TRUE(provisionChipVisible(true, 0.5, 0.4));
|
||||
EXPECT_TRUE(!provisionChipVisible(false, 5.0, 0.4));
|
||||
EXPECT_TRUE(daemonStartGate(false, true) == DaemonStartGate::Proceed);
|
||||
EXPECT_TRUE(daemonStartGate(false, false) == DaemonStartGate::Proceed);
|
||||
EXPECT_TRUE(daemonStartGate(true, true) == DaemonStartGate::Defer);
|
||||
EXPECT_TRUE(daemonStartGate(true, false) == DaemonStartGate::Wait);
|
||||
|
||||
ProvisionProgress progress;
|
||||
EXPECT_TRUE(progress.getPhase() == ProvisionPhase::Idle);
|
||||
progress.setPhase(ProvisionPhase::Verifying);
|
||||
EXPECT_TRUE(progress.getPhase() == ProvisionPhase::Verifying);
|
||||
|
||||
StartupProvision sp;
|
||||
StartupProvision::Result r;
|
||||
EXPECT_TRUE(!sp.isRunning());
|
||||
EXPECT_TRUE(!sp.hasCompletion());
|
||||
EXPECT_TRUE(!sp.takeCompletion(&r));
|
||||
EXPECT_TRUE(sp.waitUntilDone(0));
|
||||
|
||||
sp.begin();
|
||||
EXPECT_TRUE(sp.isRunning());
|
||||
EXPECT_TRUE(!sp.hasCompletion()); // nothing to reap while it runs (no idle-loop wake for the whole job)
|
||||
EXPECT_TRUE(!sp.takeCompletion(&r));
|
||||
EXPECT_TRUE(!sp.waitUntilDone(10));
|
||||
EXPECT_TRUE(sp.elapsedSeconds() >= 0.0);
|
||||
|
||||
std::thread worker([&sp] { sp.finish({ true, false, false }); });
|
||||
EXPECT_TRUE(sp.waitUntilDone(5000));
|
||||
worker.join();
|
||||
EXPECT_TRUE(!sp.isRunning());
|
||||
EXPECT_TRUE(sp.hasCompletion());
|
||||
EXPECT_TRUE(sp.takeCompletion(&r));
|
||||
EXPECT_TRUE(r.extractOk && !r.verifyOk && !r.cancelled);
|
||||
EXPECT_TRUE(!sp.takeCompletion(&r)); // exactly once
|
||||
EXPECT_TRUE(!sp.hasCompletion());
|
||||
EXPECT_NEAR(sp.elapsedSeconds(), 0.0, 1e-9);
|
||||
EXPECT_TRUE(!sp.endedCancelled());
|
||||
|
||||
// A job that shutdown cancelled: a waiting daemon start / miner start must abort, not fall through.
|
||||
StartupProvision cancelledJob;
|
||||
cancelledJob.begin();
|
||||
EXPECT_TRUE(!cancelledJob.endedCancelled()); // still running
|
||||
cancelledJob.finish({ false, true, true });
|
||||
EXPECT_TRUE(cancelledJob.waitUntilDone(0));
|
||||
EXPECT_TRUE(cancelledJob.endedCancelled());
|
||||
EXPECT_TRUE(cancelledJob.takeCompletion(&r) && r.cancelled);
|
||||
EXPECT_TRUE(cancelledJob.endedCancelled()); // a non-reaping peek: still true once reaped
|
||||
|
||||
StartupProvision idle;
|
||||
idle.markNothingToDo();
|
||||
EXPECT_TRUE(!idle.hasCompletion() && !idle.isRunning());
|
||||
EXPECT_TRUE(!idle.takeCompletion(nullptr));
|
||||
EXPECT_TRUE(idle.waitUntilDone(0));
|
||||
EXPECT_TRUE(!idle.endedCancelled());
|
||||
}
|
||||
|
||||
void testResourceNeedsWrite()
|
||||
{
|
||||
using dragonx::resources::resourceNeedsWrite;
|
||||
const fs::path dir = makeTempDir();
|
||||
const std::string f = (dir / "res.bin").string();
|
||||
EXPECT_TRUE(resourceNeedsWrite(10, false, f)); // missing
|
||||
EXPECT_TRUE(resourceNeedsWrite(10, true, f));
|
||||
EXPECT_TRUE(!resourceNeedsWrite(0, true, f)); // nothing embedded
|
||||
{ std::ofstream(f, std::ios::binary) << "12345"; }
|
||||
EXPECT_TRUE(!resourceNeedsWrite(10, false, f)); // present, size not tracked
|
||||
EXPECT_TRUE(!resourceNeedsWrite(5, true, f)); // same size
|
||||
EXPECT_TRUE(resourceNeedsWrite(10, true, f)); // the bundled size differs
|
||||
EXPECT_TRUE(!resourceNeedsWrite(0, true, f));
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
void testWriteFileAtomic()
|
||||
{
|
||||
using namespace dragonx::resources;
|
||||
const fs::path dir = makeTempDir();
|
||||
std::vector<std::uint8_t> buf(9u * 1024u * 1024u); // > two 4 MB chunks
|
||||
for (std::size_t i = 0; i < buf.size(); ++i) buf[i] = static_cast<std::uint8_t>((i * 31u + (i >> 13)) & 0xFFu);
|
||||
const std::string expect(reinterpret_cast<const char*>(buf.data()), buf.size());
|
||||
const fs::path dest = dir / "sub" / "out.bin"; // parent created on demand
|
||||
const std::string partial = dest.string() + kPartialFileSuffix;
|
||||
|
||||
// (a) a new file: byte-exact, no temp left, progress counts every byte
|
||||
{
|
||||
ProvisionProgress p;
|
||||
EXPECT_TRUE(writeFileAtomic(dest.string(), buf.data(), buf.size(), false, &p));
|
||||
EXPECT_TRUE(readProvisionFile(dest) == expect);
|
||||
EXPECT_TRUE(!fs::exists(partial));
|
||||
EXPECT_EQ(p.bytesDone.load(), static_cast<std::uint64_t>(buf.size()));
|
||||
}
|
||||
// (b) an existing file is replaced
|
||||
{
|
||||
{ std::ofstream(dest.string(), std::ios::binary | std::ios::trunc) << "old contents"; }
|
||||
EXPECT_TRUE(writeFileAtomic(dest.string(), buf.data(), buf.size(), false));
|
||||
EXPECT_TRUE(readProvisionFile(dest) == expect);
|
||||
EXPECT_TRUE(!fs::exists(partial));
|
||||
}
|
||||
// (c) cancelled on the 2nd poll: false, the destination is absent / unchanged, no temp left
|
||||
{
|
||||
int calls = 0;
|
||||
const CancelFn cancel = [&calls]() { return ++calls >= 2; };
|
||||
const fs::path fresh = dir / "fresh.bin";
|
||||
EXPECT_TRUE(!writeFileAtomic(fresh.string(), buf.data(), buf.size(), false, nullptr, cancel));
|
||||
EXPECT_TRUE(!fs::exists(fresh));
|
||||
EXPECT_TRUE(!fs::exists(fresh.string() + kPartialFileSuffix));
|
||||
calls = 0;
|
||||
{ std::ofstream(dest.string(), std::ios::binary | std::ios::trunc) << "keep me"; }
|
||||
EXPECT_TRUE(!writeFileAtomic(dest.string(), buf.data(), buf.size(), false, nullptr, cancel));
|
||||
EXPECT_TRUE(readProvisionFile(dest) == "keep me");
|
||||
EXPECT_TRUE(!fs::exists(partial));
|
||||
}
|
||||
// (d) nothing to write
|
||||
{
|
||||
EXPECT_TRUE(!writeFileAtomic((dir / "null.bin").string(), nullptr, 10, false));
|
||||
EXPECT_TRUE(!writeFileAtomic((dir / "zero.bin").string(), buf.data(), 0, false));
|
||||
EXPECT_TRUE(!fs::exists(dir / "null.bin"));
|
||||
EXPECT_TRUE(!fs::exists(dir / "zero.bin"));
|
||||
}
|
||||
#ifndef _WIN32
|
||||
// (e) executable: +x is set before the file appears under its final name
|
||||
{
|
||||
const fs::path exe = dir / "tool";
|
||||
EXPECT_TRUE(writeFileAtomic(exe.string(), buf.data(), 1024, true));
|
||||
EXPECT_TRUE((fs::status(exe).permissions() & fs::perms::owner_exec) != fs::perms::none);
|
||||
}
|
||||
#endif
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
void testCopyFileAtomic()
|
||||
{
|
||||
using namespace dragonx::resources;
|
||||
const fs::path dir = makeTempDir();
|
||||
std::string body(5u * 1024u * 1024u, '\0');
|
||||
for (std::size_t i = 0; i < body.size(); ++i) body[i] = static_cast<char>((i * 7u + 3u) & 0xFFu);
|
||||
const fs::path src = dir / "src.bin";
|
||||
{ std::ofstream out(src.string(), std::ios::binary); out.write(body.data(), static_cast<std::streamsize>(body.size())); }
|
||||
#ifndef _WIN32
|
||||
fs::permissions(src, fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec |
|
||||
fs::perms::others_read | fs::perms::others_exec, fs::perm_options::replace); // 0755
|
||||
#endif
|
||||
const fs::path dst = dir / "d" / "dst.bin";
|
||||
{
|
||||
ProvisionProgress p;
|
||||
EXPECT_TRUE(copyFileAtomic(src.string(), dst.string(), &p));
|
||||
EXPECT_TRUE(readProvisionFile(dst) == body);
|
||||
EXPECT_EQ(p.bytesDone.load(), static_cast<std::uint64_t>(body.size()));
|
||||
EXPECT_TRUE(!fs::exists(dst.string() + kPartialFileSuffix));
|
||||
#ifndef _WIN32
|
||||
EXPECT_TRUE((fs::status(dst).permissions() & fs::perms::owner_exec) != fs::perms::none);
|
||||
#endif
|
||||
}
|
||||
// Cancelled on the first poll: false, no destination, no temp.
|
||||
{
|
||||
const fs::path d2 = dir / "d" / "cancelled.bin";
|
||||
const CancelFn cancelNow = []() { return true; };
|
||||
EXPECT_TRUE(!copyFileAtomic(src.string(), d2.string(), nullptr, cancelNow));
|
||||
EXPECT_TRUE(!fs::exists(d2));
|
||||
EXPECT_TRUE(!fs::exists(d2.string() + kPartialFileSuffix));
|
||||
}
|
||||
// A missing source: false, nothing written.
|
||||
{
|
||||
const fs::path d3 = dir / "d" / "missing.bin";
|
||||
EXPECT_TRUE(!copyFileAtomic((dir / "nope.bin").string(), d3.string()));
|
||||
EXPECT_TRUE(!fs::exists(d3));
|
||||
EXPECT_TRUE(!fs::exists(d3.string() + kPartialFileSuffix));
|
||||
}
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
void testBundleCopy()
|
||||
{
|
||||
using namespace dragonx::resources;
|
||||
const fs::path root = makeTempDir();
|
||||
const fs::path A = root / "A", B = root / "B", D = root / "D";
|
||||
fs::create_directories(A);
|
||||
fs::create_directories(B);
|
||||
auto put = [](const fs::path& p, const std::string& c) {
|
||||
std::ofstream(p.string(), std::ios::binary | std::ios::trunc) << c;
|
||||
};
|
||||
put(A / "one", "A-one");
|
||||
put(B / "one", "B-one");
|
||||
put(B / "two", "B-two");
|
||||
const std::vector<std::string> dirs = { A.string(), B.string() };
|
||||
const std::vector<std::string> names = { "one", "two", "three" }; // "three" has no source: optional
|
||||
const std::string partialOne = (D / "one").string() + kPartialFileSuffix;
|
||||
|
||||
// The plan: the first search dir holding a name wins; names with no source are skipped.
|
||||
const auto plan = planBundleCopy(dirs, D.string(), names, false);
|
||||
EXPECT_EQ(plan.size(), static_cast<std::size_t>(2));
|
||||
bool sawOne = false;
|
||||
for (const auto& it : plan) {
|
||||
if (it.name != "one") continue;
|
||||
sawOne = true;
|
||||
EXPECT_TRUE(fs::path(it.src) == A / "one");
|
||||
EXPECT_TRUE(fs::path(it.dst) == D / "one");
|
||||
}
|
||||
EXPECT_TRUE(sawOne);
|
||||
|
||||
// A stale temp from an interrupted copy is removed by the run that copies that name.
|
||||
fs::create_directories(D);
|
||||
put(fs::path(partialOne), "stale");
|
||||
auto r = copyBundledFiles(dirs, D.string(), names, false);
|
||||
EXPECT_TRUE(r.ok);
|
||||
EXPECT_EQ(r.copied, 2);
|
||||
EXPECT_TRUE(r.firstFailure.empty());
|
||||
EXPECT_TRUE(readProvisionFile(D / "one") == "A-one");
|
||||
EXPECT_TRUE(readProvisionFile(D / "two") == "B-two");
|
||||
EXPECT_TRUE(!fs::exists(D / "three"));
|
||||
EXPECT_TRUE(!fs::exists(partialOne));
|
||||
|
||||
// Everything present: the stat-only early return copies nothing.
|
||||
r = copyBundledFiles(dirs, D.string(), names, false);
|
||||
EXPECT_TRUE(r.ok);
|
||||
EXPECT_EQ(r.copied, 0);
|
||||
EXPECT_TRUE(planBundleCopy(dirs, D.string(), names, false).empty());
|
||||
|
||||
// An existing destination is kept without replaceExisting and overwritten with it.
|
||||
put(D / "one", "local");
|
||||
r = copyBundledFiles(dirs, D.string(), { "one" }, false);
|
||||
EXPECT_EQ(r.copied, 0);
|
||||
EXPECT_TRUE(readProvisionFile(D / "one") == "local");
|
||||
r = copyBundledFiles(dirs, D.string(), { "one" }, true);
|
||||
EXPECT_TRUE(r.ok);
|
||||
EXPECT_EQ(r.copied, 1);
|
||||
EXPECT_TRUE(readProvisionFile(D / "one") == "A-one");
|
||||
removeTree(root);
|
||||
}
|
||||
|
||||
void testSaplingParamState()
|
||||
{
|
||||
using dragonx::rpc::Connection;
|
||||
using State = Connection::SaplingParamState;
|
||||
const fs::path dir = makeTempDir() / "sapling_state";
|
||||
fs::create_directories(dir);
|
||||
auto writeFile = [](const fs::path& p, const std::string& c) {
|
||||
std::ofstream(p.string(), std::ios::binary | std::ios::trunc) << c;
|
||||
};
|
||||
const std::string spend = "fake-spend-params-contents";
|
||||
const std::string output = "fake-output-params-contents";
|
||||
const std::vector<std::pair<std::string, std::string>> digests = {
|
||||
{ "sapling-spend.params", dragonx::util::sha256Hex(spend.data(), spend.size()) },
|
||||
{ "sapling-output.params", dragonx::util::sha256Hex(output.data(), output.size()) },
|
||||
};
|
||||
writeFile(dir / "sapling-spend.params", spend);
|
||||
EXPECT_TRUE(Connection::saplingParamStateIn(dir.string(), digests) == State::Missing); // one absent
|
||||
writeFile(dir / "sapling-output.params", output);
|
||||
EXPECT_TRUE(Connection::saplingParamStateIn(dir.string(), digests) == State::NeedsHash); // no marker yet
|
||||
EXPECT_TRUE(Connection::verifySaplingParamsIn(dir.string(), digests));
|
||||
EXPECT_TRUE(Connection::saplingParamStateIn(dir.string(), digests) == State::Verified);
|
||||
writeFile(dir / "sapling-spend.params", spend + "-longer"); // size changed
|
||||
EXPECT_TRUE(Connection::saplingParamStateIn(dir.string(), digests) == State::NeedsHash);
|
||||
EXPECT_TRUE(Connection::saplingParamStateIn("", digests) == State::Missing);
|
||||
removeTree(dir.parent_path());
|
||||
}
|
||||
|
||||
// Wallets dialog: the pure folder walk that moved out of WalletsDialog::scan() (now run on a worker).
|
||||
void testWalletFolderScan()
|
||||
{
|
||||
@@ -13688,6 +13961,12 @@ int main()
|
||||
#endif
|
||||
testPlatformEnsureDirectory();
|
||||
testVerifySaplingParams();
|
||||
testStartupProvisionState();
|
||||
testResourceNeedsWrite();
|
||||
testWriteFileAtomic();
|
||||
testCopyFileAtomic();
|
||||
testBundleCopy();
|
||||
testSaplingParamState();
|
||||
testWalletFolderScan();
|
||||
testExternalScanCache();
|
||||
testConnectHasStalled();
|
||||
|
||||
Reference in New Issue
Block a user