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:
2026-06-27 21:27:13 -05:00
parent 2e8e214689
commit 4473e7e00a
19 changed files with 1848 additions and 108 deletions

View File

@@ -40,37 +40,71 @@ bool isHex64(const std::string& s)
} // namespace
namespace {
// Fill an XmrigRelease from one Gitea release JSON object (ok=true iff it has a tag).
XmrigRelease parseOneXmrigRelease(const json& j)
{
XmrigRelease r;
if (!j.is_object()) return r;
if (j.contains("tag_name") && j["tag_name"].is_string())
r.tag = j["tag_name"].get<std::string>();
if (j.contains("name") && j["name"].is_string())
r.name = j["name"].get<std::string>();
if (j.contains("body") && j["body"].is_string())
r.body = j["body"].get<std::string>();
if (j.contains("prerelease") && j["prerelease"].is_boolean())
r.prerelease = j["prerelease"].get<bool>();
if (j.contains("published_at") && j["published_at"].is_string())
r.publishedAt = j["published_at"].get<std::string>();
if (j.contains("assets") && j["assets"].is_array()) {
for (const auto& a : j["assets"]) {
if (!a.is_object()) continue;
XmrigReleaseAsset asset;
if (a.contains("name") && a["name"].is_string())
asset.name = a["name"].get<std::string>();
if (a.contains("browser_download_url") && a["browser_download_url"].is_string())
asset.downloadUrl = a["browser_download_url"].get<std::string>();
if (a.contains("size") && a["size"].is_number_integer())
asset.size = a["size"].get<long long>();
if (!asset.name.empty() && !asset.downloadUrl.empty())
r.assets.push_back(std::move(asset));
}
}
if (!r.tag.empty()) r.ok = true;
return r;
}
} // namespace
XmrigRelease parseXmrigRelease(const std::string& jsonStr)
{
XmrigRelease r;
try {
const json j = json::parse(jsonStr);
if (j.contains("tag_name") && j["tag_name"].is_string())
r.tag = j["tag_name"].get<std::string>();
if (j.contains("body") && j["body"].is_string())
r.body = j["body"].get<std::string>();
if (j.contains("assets") && j["assets"].is_array()) {
for (const auto& a : j["assets"]) {
if (!a.is_object()) continue;
XmrigReleaseAsset asset;
if (a.contains("name") && a["name"].is_string())
asset.name = a["name"].get<std::string>();
if (a.contains("browser_download_url") && a["browser_download_url"].is_string())
asset.downloadUrl = a["browser_download_url"].get<std::string>();
if (a.contains("size") && a["size"].is_number_integer())
asset.size = a["size"].get<long long>();
if (!asset.name.empty() && !asset.downloadUrl.empty())
r.assets.push_back(std::move(asset));
}
}
if (r.tag.empty()) { r.error = "release JSON has no tag_name"; return r; }
r.ok = true;
r = parseOneXmrigRelease(json::parse(jsonStr));
if (!r.ok) r.error = "release JSON has no tag_name";
} catch (const std::exception& e) {
r.error = std::string("failed to parse release JSON: ") + e.what();
}
return r;
}
std::vector<XmrigRelease> parseXmrigReleaseList(const std::string& jsonStr)
{
std::vector<XmrigRelease> out;
try {
const json j = json::parse(jsonStr);
if (!j.is_array()) return out;
for (const auto& e : j) {
if (e.contains("draft") && e["draft"].is_boolean() && e["draft"].get<bool>())
continue; // skip drafts (not meant for end users)
XmrigRelease r = parseOneXmrigRelease(e);
if (r.ok) out.push_back(std::move(r));
}
} catch (const std::exception&) {
// malformed list -> empty (caller treats as "could not load releases")
}
return out;
}
std::string currentXmrigPlatformToken()
{
#if defined(_WIN32)