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>
This commit is contained in:
254
src/ui/windows/daemon_download_dialog.h
Normal file
254
src/ui/windows/daemon_download_dialog.h
Normal file
@@ -0,0 +1,254 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Modal dialog to download / update the dragonxd full node from the project Gitea, driven by
|
||||
// util::DaemonUpdater. Sibling of XmrigDownloadDialog (the miner updater). States follow the
|
||||
// updater: Checking -> UpToDate/UpdateAvailable -> Downloading/Verifying/Extracting -> Done /
|
||||
// Failed. Header-only (static inline state). The new binary is installed into the daemon directory
|
||||
// without touching the running node; on Done the user is offered a daemon restart to apply it.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../../app.h"
|
||||
#include "../../resources/embedded_resources.h" // resources::getDaemonDirectory()
|
||||
#include "../../util/daemon_updater.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/type.h"
|
||||
#include "../theme.h"
|
||||
#include "release_list_view.h"
|
||||
#include "imgui.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
class DaemonUpdateDialog {
|
||||
public:
|
||||
// `installedVersion` is the version scanned from the installed dragonxd (may be empty/unknown),
|
||||
// used to tell "up to date" from "update available".
|
||||
static void show(App* app, const std::string& installedVersion) {
|
||||
if (!app) return;
|
||||
s_app = app;
|
||||
s_open = true;
|
||||
s_notified = false;
|
||||
s_installed_flag = false; // start each session clean (don't leak a prior install's flag)
|
||||
s_installed_version = installedVersion;
|
||||
s_rows.clear();
|
||||
s_releases.clear();
|
||||
s_updater = std::make_unique<util::DaemonUpdater>();
|
||||
s_updater->startCheck(installedVersion);
|
||||
}
|
||||
|
||||
static bool isOpen() { return s_open; }
|
||||
|
||||
// True (once) after an install succeeded, so the Settings panel can refresh its cached daemon
|
||||
// info. Clears on read.
|
||||
static bool consumeInstalled() {
|
||||
const bool v = s_installed_flag;
|
||||
s_installed_flag = false;
|
||||
return v;
|
||||
}
|
||||
|
||||
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("daemon_update_title"), &s_open, 480.0f, 0.94f)) {
|
||||
const auto p = s_updater->getProgress();
|
||||
using St = util::DaemonUpdater::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::DaemonUpdater::Progress;
|
||||
using St = util::DaemonUpdater::State;
|
||||
|
||||
static float fullW() { return ImGui::GetContentRegionAvail().x; }
|
||||
|
||||
static void installAction(const char* label) {
|
||||
using namespace material;
|
||||
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("daemon_update_checking"));
|
||||
}
|
||||
|
||||
static void renderUnavailable(float, const Progress& p) {
|
||||
using namespace material;
|
||||
Type().text(TypeStyle::Subtitle2, TR("daemon_update_unavailable_title"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", p.status_text.empty()
|
||||
? TR("daemon_update_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() ? TR("xmrig_none") : p.installed_tag;
|
||||
if (p.state == St::UpdateAvailable) {
|
||||
Type().text(TypeStyle::Subtitle2, TR("daemon_update_available"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("%s %s", TR("daemon_update_latest"), p.latest_tag.c_str());
|
||||
ImGui::Text("%s %s", TR("daemon_update_installed"), installed.c_str());
|
||||
ImGui::Spacing();
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_verify_note"));
|
||||
ImGui::Spacing();
|
||||
installAction(TR("daemon_update_download_install"));
|
||||
} else {
|
||||
Type().text(TypeStyle::Subtitle2, TR("daemon_update_up_to_date"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("%s %s", TR("daemon_update_installed"), p.latest_tag.c_str());
|
||||
ImGui::Spacing();
|
||||
installAction(TR("daemon_update_reinstall"));
|
||||
}
|
||||
ImGui::Spacing();
|
||||
// Browse all releases (pre-releases / older versions).
|
||||
if (TactileButton(TR("daemon_update_browse"), 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("daemon_update_loading"));
|
||||
}
|
||||
|
||||
static void renderReleaseList(float dp, const Progress&) {
|
||||
using namespace material;
|
||||
// Snapshot the worker's list + build the rows once per listing (not every frame — the
|
||||
// release bodies are large). Caches are cleared when a new listing starts (browse / show).
|
||||
if (s_rows.empty()) {
|
||||
s_releases = s_updater->getReleases();
|
||||
const std::string token = util::currentDaemonPlatformToken();
|
||||
const std::string instCore = util::daemonVersionCore(s_installed_version);
|
||||
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::selectDaemonAsset(r, token) >= 0;
|
||||
row.installed = !instCore.empty() && util::daemonVersionCore(r.tag) == instCore;
|
||||
s_rows.push_back(std::move(row));
|
||||
}
|
||||
}
|
||||
// Downgrade caution: an older node binary may not match the current chain data.
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_downgrade_note"));
|
||||
ImGui::Spacing();
|
||||
bool back = false;
|
||||
const int idx = RenderReleaseList(s_rows, dp, &back);
|
||||
if (back) { s_rows.clear(); s_updater->startCheck(s_installed_version); return; }
|
||||
if (idx >= 0 && idx < static_cast<int>(s_releases.size())) {
|
||||
const util::DaemonRelease 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("daemon_update_downloading")
|
||||
: p.state == St::Verifying ? TR("daemon_update_verifying")
|
||||
: TR("daemon_update_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;
|
||||
// Tell the Settings panel to refresh its cached daemon info — once, not every frame (a
|
||||
// re-scan reads the whole daemon binary).
|
||||
if (!s_notified) { s_installed_flag = true; s_notified = true; }
|
||||
Type().textColored(TypeStyle::Subtitle2, Success(), TR("daemon_update_installed_ok"));
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("%s %s", TR("daemon_update_version"), p.latest_tag.c_str());
|
||||
ImGui::Spacing();
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_restart_note"));
|
||||
ImGui::Spacing();
|
||||
if (s_app->isUsingEmbeddedDaemon()) {
|
||||
if (TactileButton(TR("daemon_update_restart_now"), ImVec2(fullW(), 0))) {
|
||||
s_app->restartDaemon();
|
||||
s_open = false;
|
||||
}
|
||||
ImGui::Spacing();
|
||||
if (TactileButton(TR("daemon_update_later"), ImVec2(fullW(), 0))) s_open = false;
|
||||
} else {
|
||||
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("daemon_update_failed"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", p.error.empty() ? TR("daemon_update_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_installed_flag = false;
|
||||
static inline bool s_notified = false;
|
||||
static inline std::string s_installed_version;
|
||||
static inline std::vector<ReleaseRow> s_rows; // built once per listing (UI rows)
|
||||
static inline std::vector<util::DaemonRelease> s_releases; // matching releases (install by index)
|
||||
static inline App* s_app = nullptr;
|
||||
static inline std::unique_ptr<util::DaemonUpdater> s_updater;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
87
src/ui/windows/release_list_view.h
Normal file
87
src/ui/windows/release_list_view.h
Normal file
@@ -0,0 +1,87 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Shared "Browse all releases" picker, used by both the miner updater (XmrigDownloadDialog) and the
|
||||
// node updater (DaemonUpdateDialog). Renders a scrollable list of releases (tag, title, date, with
|
||||
// pre-release / installed badges) inside the current overlay dialog; the caller maps its updater's
|
||||
// release list into ReleaseRow values and acts on the clicked index.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../../util/i18n.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/type.h"
|
||||
#include "imgui.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
struct ReleaseRow {
|
||||
std::string tag; // version tag, e.g. "v1.0.2"
|
||||
std::string title; // human title (release "name"), shown dimmed
|
||||
std::string date; // YYYY-MM-DD (already trimmed)
|
||||
bool prerelease = false; // show a pre-release badge
|
||||
bool hasAsset = true; // a build exists for this platform (else the row's Install is disabled)
|
||||
bool installed = false; // matches the currently-installed version (Install -> Reinstall)
|
||||
};
|
||||
|
||||
// Renders the picker. Returns the row index whose Install button was clicked this frame, or -1.
|
||||
// Sets *back when the Back button is clicked. If `globalDisabledTooltip` is non-null, every Install
|
||||
// button is disabled and shows that tooltip (e.g. "stop mining before updating the miner").
|
||||
inline int RenderReleaseList(const std::vector<ReleaseRow>& rows, float dp, bool* back,
|
||||
const char* globalDisabledTooltip = nullptr) {
|
||||
using namespace material;
|
||||
int clicked = -1;
|
||||
|
||||
Type().text(TypeStyle::Subtitle2, TR("upd_select_version"));
|
||||
ImGui::Spacing();
|
||||
|
||||
const float listH = 300.0f * dp;
|
||||
if (ImGui::BeginChild("##release_list", ImVec2(0, listH), true)) {
|
||||
const ImVec4 dim = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium());
|
||||
const ImVec4 warn = ImVec4(1.0f, 0.78f, 0.25f, 1.0f);
|
||||
const ImVec4 succ = ImGui::ColorConvertU32ToFloat4(Success());
|
||||
for (int i = 0; i < static_cast<int>(rows.size()); ++i) {
|
||||
const ReleaseRow& r = rows[i];
|
||||
ImGui::PushID(i);
|
||||
|
||||
ImGui::TextUnformatted(r.tag.c_str());
|
||||
if (r.prerelease) { ImGui::SameLine(); ImGui::TextColored(warn, "[%s]", TR("upd_prerelease")); }
|
||||
if (r.installed) { ImGui::SameLine(); ImGui::TextColored(succ, "[%s]", TR("upd_installed_badge")); }
|
||||
|
||||
if (!r.title.empty() || !r.date.empty()) {
|
||||
std::string meta = r.title;
|
||||
if (!r.title.empty() && !r.date.empty()) meta += " · ";
|
||||
meta += r.date;
|
||||
ImGui::TextColored(dim, "%s", meta.c_str());
|
||||
}
|
||||
|
||||
const char* lbl = r.installed ? TR("upd_reinstall") : TR("upd_install");
|
||||
const bool disabled = !r.hasAsset || globalDisabledTooltip != nullptr;
|
||||
ImGui::BeginDisabled(disabled);
|
||||
if (TactileButton(lbl, ImVec2(140.0f * dp, 0))) clicked = i;
|
||||
ImGui::EndDisabled();
|
||||
if (disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
material::Tooltip("%s", globalDisabledTooltip ? globalDisabledTooltip
|
||||
: TR("upd_no_build_platform"));
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::Spacing();
|
||||
if (TactileButton(TR("upd_back"), ImVec2(ImGui::GetContentRegionAvail().x, 0))) *back = true;
|
||||
return clicked;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../../app.h"
|
||||
#include "../../config/settings.h"
|
||||
@@ -21,6 +22,7 @@
|
||||
#include "../material/type.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../theme.h"
|
||||
#include "release_list_view.h"
|
||||
#include "imgui.h"
|
||||
|
||||
namespace dragonx {
|
||||
@@ -33,8 +35,11 @@ public:
|
||||
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(app->settings() ? app->settings()->getXmrigVersion() : std::string());
|
||||
s_updater->startCheck(s_installed_tag);
|
||||
}
|
||||
|
||||
static bool isOpen() { return s_open; }
|
||||
@@ -54,6 +59,8 @@ public:
|
||||
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;
|
||||
@@ -121,9 +128,50 @@ private:
|
||||
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")
|
||||
@@ -180,6 +228,9 @@ private:
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user