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>
119 lines
5.0 KiB
C++
119 lines
5.0 KiB
C++
// 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
|