Files
ObsidianDragon/src/ui/windows/xmrig_download_dialog.h
DanS 4473e7e00a feat(updater): in-app dragonxd updater + browse-all-releases
Add a full-node daemon updater (util/DaemonUpdater + daemon_download_dialog)
reachable from Settings -> NODE & SECURITY: downloads/verifies (SHA-256 +
enforced ed25519 signature) and atomically installs the latest dragonxd from
the project Gitea, with a "Restart daemon now" step. Add a shared "Browse all
releases..." picker (release_list_view) to both the miner and daemon updaters
so users can pin older/pre-release builds. Pure no-I/O cores
(daemon_updater_core / xmrig_updater_core) are unit-tested; sign-daemon-release.sh
signs release archives offline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 21:27:13 -05:00

240 lines
9.9 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Modal dialog to download / update the DRG-XMRig miner from the project Gitea, driven by
// util::XmrigUpdater. States follow the updater: Checking -> UpToDate/UpdateAvailable ->
// Downloading/Verifying/Extracting -> Done / Failed. Header-only (static inline state), mirroring
// BootstrapDownloadDialog. The installed release tag is persisted to settings on success.
#pragma once
#include <memory>
#include <string>
#include <vector>
#include "../../app.h"
#include "../../config/settings.h"
#include "../../resources/embedded_resources.h" // resources::getDaemonDirectory()
#include "../../util/i18n.h"
#include "../../util/xmrig_updater.h"
#include "../material/draw_helpers.h"
#include "../material/type.h"
#include "../material/colors.h"
#include "../theme.h"
#include "release_list_view.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
class XmrigDownloadDialog {
public:
static void show(App* app) {
if (!app) return;
s_app = app;
s_open = true;
s_persisted = false;
s_installed_tag = app->settings() ? app->settings()->getXmrigVersion() : std::string();
s_rows.clear();
s_releases.clear();
s_updater = std::make_unique<util::XmrigUpdater>();
s_updater->startCheck(s_installed_tag);
}
static bool isOpen() { return s_open; }
static void render() {
if (!s_open || !s_app || !s_updater) {
if (!s_open) s_updater.reset(); // closed: drop the updater (dtor joins the worker)
return;
}
using namespace material;
const float dp = Layout::dpiScale();
if (BeginOverlayDialog(TR("xmrig_update_title"), &s_open, 480.0f, 0.94f)) {
const auto p = s_updater->getProgress();
using St = util::XmrigUpdater::State;
switch (p.state) {
case St::Checking: renderChecking(dp, p); break;
case St::UpToDate:
case St::UpdateAvailable: renderPrompt(dp, p); break;
case St::Unavailable: renderUnavailable(dp, p); break;
case St::Listing: renderListing(dp, p); break;
case St::ReleaseList: renderReleaseList(dp, p); break;
case St::Downloading:
case St::Verifying:
case St::Extracting: renderProgress(dp, p); break;
case St::Done: renderDone(dp, p); break;
case St::Failed: renderFailed(dp, p); break;
default: break;
}
EndOverlayDialog();
}
if (!s_open) s_updater.reset();
}
private:
using Progress = util::XmrigUpdater::Progress;
using St = util::XmrigUpdater::State;
static float fullW() { return ImGui::GetContentRegionAvail().x; }
// Install button that refuses (with a note) while the miner is running, re-checked live so a
// dialog opened before mining started can't replace a now-running binary (TOCTOU guard).
static void installAction(const char* label) {
using namespace material;
if (s_app->isPoolMinerRunning()) {
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
TR("xmrig_stop_mining_first"));
return;
}
if (TactileButton(label, ImVec2(fullW(), 0)))
s_updater->startInstall(resources::getDaemonDirectory());
}
static void renderChecking(float, const Progress&) {
using namespace material;
Type().text(TypeStyle::Body2, TR("xmrig_checking"));
}
static void renderUnavailable(float, const Progress& p) {
using namespace material;
Type().text(TypeStyle::Subtitle2, TR("xmrig_unavailable_title"));
ImGui::Spacing();
ImGui::TextWrapped("%s", p.status_text.empty()
? TR("xmrig_unavailable_body")
: p.status_text.c_str());
ImGui::Spacing();
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
}
static void renderPrompt(float, const Progress& p) {
using namespace material;
const std::string installed = p.installed_tag.empty() ? "none" : p.installed_tag;
if (p.state == St::UpdateAvailable) {
Type().text(TypeStyle::Subtitle2, TR("xmrig_update_available"));
ImGui::Spacing();
ImGui::Text("%s %s", TR("xmrig_latest"), p.latest_tag.c_str());
ImGui::Text("%s %s", TR("xmrig_installed"), installed.c_str());
ImGui::Spacing();
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("xmrig_verify_note"));
ImGui::Spacing();
installAction(TR("xmrig_download_install"));
} else {
Type().text(TypeStyle::Subtitle2, TR("xmrig_up_to_date"));
ImGui::Spacing();
ImGui::Text("%s %s", TR("xmrig_installed"), p.latest_tag.c_str());
ImGui::Spacing();
installAction(TR("xmrig_reinstall"));
}
ImGui::Spacing();
// Browse all releases (pre-releases / older versions).
if (TactileButton(TR("xmrig_browse_releases"), ImVec2(fullW(), 0))) {
s_rows.clear();
s_updater->startListReleases();
}
ImGui::Spacing();
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
}
static void renderListing(float, const Progress&) {
using namespace material;
Type().text(TypeStyle::Body2, TR("xmrig_loading_releases"));
}
static void renderReleaseList(float dp, const Progress&) {
using namespace material;
// Snapshot + build rows once per listing (not every frame). Cleared on browse / show.
if (s_rows.empty()) {
s_releases = s_updater->getReleases();
const std::string token = util::currentXmrigPlatformToken();
s_rows.reserve(s_releases.size());
for (const auto& r : s_releases) {
ReleaseRow row;
row.tag = r.tag;
row.title = r.name;
row.date = r.publishedAt.size() >= 10 ? r.publishedAt.substr(0, 10) : r.publishedAt;
row.prerelease = r.prerelease;
row.hasAsset = util::selectXmrigAsset(r, token) >= 0;
row.installed = !s_installed_tag.empty() && r.tag == s_installed_tag;
s_rows.push_back(std::move(row));
}
}
// Can't replace a running miner binary — disable installs while mining (TOCTOU guard).
const char* disabledNote = s_app->isPoolMinerRunning() ? TR("xmrig_stop_mining_first") : nullptr;
bool back = false;
const int idx = RenderReleaseList(s_rows, dp, &back, disabledNote);
if (back) { s_rows.clear(); s_updater->startCheck(s_installed_tag); return; }
if (idx >= 0 && !s_app->isPoolMinerRunning() && idx < static_cast<int>(s_releases.size())) {
const util::XmrigRelease rel = s_releases[idx];
s_rows.clear();
s_updater->startInstallRelease(resources::getDaemonDirectory(), rel);
}
}
static void renderProgress(float dp, const Progress& p) {
using namespace material;
const char* title = p.state == St::Downloading ? TR("xmrig_downloading")
: p.state == St::Verifying ? TR("xmrig_verifying")
: TR("xmrig_installing");
Type().text(TypeStyle::Subtitle2, title);
ImGui::Spacing();
const float barH = 8.0f * dp;
const float barW = fullW();
const ImVec2 bMin = ImGui::GetCursorScreenPos();
const ImVec2 bMax(bMin.x + barW, bMin.y + barH);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(bMin, bMax, IM_COL32(255, 255, 255, 30), 4.0f * dp);
const float fillW = barW * (p.percent / 100.0f);
if (fillW > 0)
dl->AddRectFilled(bMin, ImVec2(bMin.x + fillW, bMax.y), Primary(), 4.0f * dp);
ImGui::Dummy(ImVec2(0, barH));
ImGui::Spacing();
ImGui::Text("%s", p.status_text.c_str());
ImGui::Spacing();
// Cancel aborts the in-flight transfer promptly (the curl progress callback returns abort).
if (TactileButton(TR("cancel"), ImVec2(fullW(), 0))) {
s_updater->cancel();
s_open = false;
}
}
static void renderDone(float, const Progress& p) {
using namespace material;
if (!s_persisted) {
if (s_app->settings() && !p.latest_tag.empty()) {
s_app->settings()->setXmrigVersion(p.latest_tag);
s_app->settings()->save();
}
s_persisted = true;
}
Type().textColored(TypeStyle::Subtitle2, Success(), TR("xmrig_installed_ok"));
ImGui::Spacing();
ImGui::Text("%s %s", TR("xmrig_version"), p.latest_tag.c_str());
ImGui::Spacing();
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
}
static void renderFailed(float, const Progress& p) {
using namespace material;
Type().textColored(TypeStyle::Subtitle2, Error(), TR("xmrig_update_failed"));
ImGui::Spacing();
ImGui::TextWrapped("%s", p.error.empty() ? TR("xmrig_unknown_error") : p.error.c_str());
ImGui::Spacing();
installAction(TR("retry"));
ImGui::Spacing();
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
}
static inline bool s_open = false;
static inline bool s_persisted = false;
static inline std::string s_installed_tag;
static inline std::vector<ReleaseRow> s_rows; // built once per listing (UI rows)
static inline std::vector<util::XmrigRelease> s_releases; // matching releases (install by index)
static inline App* s_app = nullptr;
static inline std::unique_ptr<util::XmrigUpdater> s_updater;
};
} // namespace ui
} // namespace dragonx