diff --git a/src/app.cpp b/src/app.cpp index 53b7d49..02c44b3 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -3035,6 +3035,45 @@ void App::repairWallet() }); } +void App::reinstallBundledDaemon() +{ + if (!supportsFullNodeLifecycleActions()) { + ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build"); + return; + } + if (!resources::getBundledDaemonInfo().available) { + ui::Notifications::instance().warning("This build has no bundled daemon to install"); + return; + } + if (!daemon_controller_ || !isUsingEmbeddedDaemon()) { + ui::Notifications::instance().warning("Reinstalling the daemon requires the embedded daemon"); + return; + } + + DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n"); + ui::Notifications::instance().info("Installing bundled daemon — the node will stop, update, and restart..."); + + async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) { + AppDaemonLifecycleRuntime runtime(*this); + daemon::AsyncLifecycleTaskContext ctx(token, shutting_down_); + // Stop the daemon so its binary is no longer locked (Windows) / in use (ETXTBSY on Linux). + runtime.stopDaemonWithPolicy(); + // Wait (bounded, ~12s) for the process to exit and release the binary before overwriting. + for (int i = 0; i < 120 && daemon::EmbeddedDaemon::isRpcPortInUse() + && !ctx.cancelled() && !ctx.shuttingDown(); ++i) { + ctx.sleepForMs(100); + } + // 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(); + } + }); +} + void App::deleteBlockchainData() { if (!supportsFullNodeLifecycleActions()) { diff --git a/src/app.h b/src/app.h index 402a01a..efd5ceb 100644 --- a/src/app.h +++ b/src/app.h @@ -307,6 +307,7 @@ public: void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); } void rescanBlockchain(); // restart daemon with -rescan flag void repairWallet(); // restart daemon with -zapwallettxes=2 (wipe & rebuild wallet tx records) + void reinstallBundledDaemon(); // stop daemon, overwrite installed binary with the bundled one, restart void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running bool isBootstrapDownloading() const { return bootstrap_downloading_; } diff --git a/src/resources/embedded_resources.cpp b/src/resources/embedded_resources.cpp index 266f0b5..f1f2c92 100644 --- a/src/resources/embedded_resources.cpp +++ b/src/resources/embedded_resources.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #ifdef _WIN32 #include @@ -225,11 +227,13 @@ bool needsParamsExtraction() if (spendRes && resourceNeedsUpdate(spendRes, spendPath)) return true; if (outputRes && resourceNeedsUpdate(outputRes, outputPath)) return true; - // Also check if daemon binaries need updating + // 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 && resourceNeedsUpdate(daemonRes, daemonPath)) return true; + if (daemonRes && !std::filesystem::exists(daemonPath)) return true; #endif #ifdef HAS_EMBEDDED_XMRIG @@ -369,12 +373,12 @@ bool extractEmbeddedResources() #ifdef HAS_EMBEDDED_DAEMON 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 (resourceNeedsUpdate(daemonRes, dest)) { - if (std::filesystem::exists(dest)) - DEBUG_LOGF("[INFO] Updating stale dragonxd (size mismatch)...\n"); + if (!std::filesystem::exists(dest)) { DEBUG_LOGF("[INFO] Extracting dragonxd (%zu MB)...\n", daemonRes->size / (1024*1024)); if (!extractResource(daemonRes, dest)) { success = false; @@ -384,13 +388,11 @@ bool extractEmbeddedResources() #endif } } - + const EmbeddedResource* cliRes = getEmbeddedResource(RESOURCE_DRAGONX_CLI); if (cliRes) { std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_CLI; - if (resourceNeedsUpdate(cliRes, dest)) { - if (std::filesystem::exists(dest)) - DEBUG_LOGF("[INFO] Updating stale dragonx-cli (size mismatch)...\n"); + if (!std::filesystem::exists(dest)) { DEBUG_LOGF("[INFO] Extracting dragonx-cli (%zu MB)...\n", cliRes->size / (1024*1024)); if (!extractResource(cliRes, dest)) { success = false; @@ -400,13 +402,11 @@ bool extractEmbeddedResources() #endif } } - + const EmbeddedResource* txRes = getEmbeddedResource(RESOURCE_DRAGONX_TX); if (txRes) { std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_TX; - if (resourceNeedsUpdate(txRes, dest)) { - if (std::filesystem::exists(dest)) - DEBUG_LOGF("[INFO] Updating stale dragonx-tx (size mismatch)...\n"); + if (!std::filesystem::exists(dest)) { DEBUG_LOGF("[INFO] Extracting dragonx-tx (%zu MB)...\n", txRes->size / (1024*1024)); if (!extractResource(txRes, dest)) { success = false; @@ -590,6 +590,120 @@ bool forceExtractXmrig() #endif } +// Scan a binary blob for the daemon's version stamp: 'v' .. optionally followed by +// '-' =6 hex)>, e.g. "v1.0.2-ddd851dc1". Returns the first match, or "" if none. +static std::string scanBinaryVersion(const uint8_t* data, std::size_t size) +{ + if (!data || size < 6) return ""; + auto isdig = [](uint8_t c) { return std::isdigit(static_cast(c)) != 0; }; + auto isxd = [](uint8_t c) { return std::isxdigit(static_cast(c)) != 0; }; + for (std::size_t i = 0; i + 5 < size; ++i) { + if (data[i] != 'v') continue; + std::size_t k = i + 1, s; + s = k; while (k < size && isdig(data[k])) ++k; if (k == s) continue; // major + if (k >= size || data[k] != '.') continue; ++k; + s = k; while (k < size && isdig(data[k])) ++k; if (k == s) continue; // minor + if (k >= size || data[k] != '.') continue; ++k; + s = k; while (k < size && isdig(data[k])) ++k; if (k == s) continue; // revision + std::size_t end = k; + if (k < size && data[k] == '-') { // optional - + std::size_t h = k + 1, hs = h; + while (h < size && isxd(data[h])) ++h; + if (h - hs >= 6) end = h; + } + return std::string(reinterpret_cast(data) + i, end - i); + } + return ""; +} + +DaemonBinaryInfo getInstalledDaemonInfo() +{ + DaemonBinaryInfo info; + std::string daemonDir = getDaemonDirectory(); +#ifdef _WIN32 + info.path = daemonDir + "\\" + RESOURCE_DRAGONXD; +#else + info.path = daemonDir + "/" + RESOURCE_DRAGONXD; +#endif + std::error_code ec; + if (!std::filesystem::exists(info.path, ec)) return info; // exists stays false + info.exists = true; + info.size = std::filesystem::file_size(info.path, ec); + if (ec) info.size = 0; + + auto ftime = std::filesystem::last_write_time(info.path, ec); + if (!ec) { + // Convert filesystem clock → system_clock epoch (pre-C++20 portable approximation). + auto sysTime = std::chrono::time_point_cast( + ftime - decltype(ftime)::clock::now() + std::chrono::system_clock::now()); + info.modifiedEpoch = + static_cast(std::chrono::system_clock::to_time_t(sysTime)); + } + + // Read the binary and scan for its version stamp (one-off; caller caches the result). + std::ifstream f(info.path, std::ios::binary); + if (f) { + f.seekg(0, std::ios::end); + std::streamoff len = f.tellg(); + f.seekg(0, std::ios::beg); + if (len > 0) { + std::vector buf(static_cast(len)); + f.read(reinterpret_cast(buf.data()), len); + info.version = scanBinaryVersion(buf.data(), static_cast(f.gcount())); + } + } + return info; +} + +BundledDaemonInfo getBundledDaemonInfo() +{ + BundledDaemonInfo info; +#ifdef HAS_EMBEDDED_DAEMON + const EmbeddedResource* res = getEmbeddedResource(RESOURCE_DRAGONXD); + if (res && res->data && res->size > 0) { + info.available = true; + info.size = res->size; + // The embedded bytes are constant for this build — scan once. + static const std::string cachedVersion = scanBinaryVersion(res->data, res->size); + info.version = cachedVersion; + } +#endif + return info; +} + +bool reextractBundledDaemon() +{ +#ifdef HAS_EMBEDDED_DAEMON + std::string daemonDir = getDaemonDirectory(); +#ifdef _WIN32 + const char pathSep = '\\'; +#else + const char pathSep = '/'; +#endif + bool ok = true; + bool wroteAny = false; + const char* names[] = { RESOURCE_DRAGONXD, RESOURCE_DRAGONX_CLI, RESOURCE_DRAGONX_TX }; + for (const char* name : names) { + const EmbeddedResource* res = getEmbeddedResource(name); + 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)) { + 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 + return false; +#endif +} + std::string getXmrigPath() { std::string daemonDir = getDaemonDirectory(); diff --git a/src/resources/embedded_resources.h b/src/resources/embedded_resources.h index 0a9c059..a45b13d 100644 --- a/src/resources/embedded_resources.h +++ b/src/resources/embedded_resources.h @@ -30,6 +30,31 @@ bool needsParamsExtraction(); // Get the params directory path std::string getParamsDirectory(); +// --- Daemon binary management (Settings → daemon binary panel) ------------------------------ +// Info about the dragonxd binary currently installed in the dragonx/ extraction directory. +struct DaemonBinaryInfo { + bool exists = false; + std::string path; + std::uintmax_t size = 0; + std::string version; // scanned from the binary ("vX.Y.Z-"), empty if not found + std::int64_t modifiedEpoch = 0; // last-write time as unix epoch seconds, 0 if unknown +}; + +// Info about the dragonxd binary bundled inside this wallet build. +struct BundledDaemonInfo { + bool available = false; // a daemon resource is embedded in this build + std::uintmax_t size = 0; + std::string version; +}; + +// Read + scan the installed dragonxd (reads the file; call off the UI thread or cache the result). +DaemonBinaryInfo getInstalledDaemonInfo(); +// Info about the bundled daemon (scans the embedded bytes once, cached). +BundledDaemonInfo getBundledDaemonInfo(); +// Force-overwrite the installed dragonx binaries (dragonxd/cli/tx) with the bundled ones. The +// caller should stop the daemon first. Returns true if all present resources were written. +bool reextractBundledDaemon(); + // Resource names constexpr const char* RESOURCE_SAPLING_SPEND = "sapling-spend.params"; constexpr const char* RESOURCE_SAPLING_OUTPUT = "sapling-output.params"; diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 0be1267..e040170 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -15,6 +15,8 @@ #include "../windows/console_tab.h" #include "../../util/i18n.h" #include "../../util/platform.h" +#include "../../resources/embedded_resources.h" +#include #include "../../rpc/rpc_client.h" #include "../../rpc/connection.h" #include "../../rpc/rpc_worker.h" @@ -147,6 +149,12 @@ struct SettingsPageState { bool confirm_delete_blockchain = false; bool confirm_rescan = false; bool confirm_repair_wallet = false; + bool confirm_reinstall_daemon = false; + // Cached daemon-binary status for the "daemon binary" panel (loaded once / on Refresh, + // since reading the installed binary to scan its version is a one-off disk read). + bool daemon_info_loaded = false; + dragonx::resources::DaemonBinaryInfo installed_daemon; + dragonx::resources::BundledDaemonInfo bundled_daemon; bool confirm_restart_daemon = false; bool confirm_lite_redownload = false; effects::ScrollFadeShader fade_shader; @@ -2055,6 +2063,78 @@ void RenderSettingsPage(App* app) { } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_repair_wallet")); ImGui::EndDisabled(); + + // ---- Daemon binary: installed vs bundled, with explicit reinstall ---- + // The wallet only auto-places dragonxd when it's missing (never overwrites a + // size-mismatched one), so this panel reports the installed binary and lets the + // user deliberately replace it with the version bundled in this wallet build. + if (!s_settingsState.daemon_info_loaded) { + s_settingsState.installed_daemon = dragonx::resources::getInstalledDaemonInfo(); + s_settingsState.bundled_daemon = dragonx::resources::getBundledDaemonInfo(); + s_settingsState.daemon_info_loaded = true; + } + const auto& inst = s_settingsState.installed_daemon; + const auto& bun = s_settingsState.bundled_daemon; + + auto fmtDate = [](std::int64_t epoch) -> std::string { + if (epoch <= 0) return "—"; + std::time_t t = static_cast(epoch); + std::tm tmv{}; +#ifdef _WIN32 + localtime_s(&tmv, &t); +#else + localtime_r(&t, &tmv); +#endif + char buf[32]; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", &tmv); + return std::string(buf); + }; + + ImGui::Dummy(ImVec2(0, Layout::spacingMd())); + Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_binary")); + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + + if (inst.exists) { + ImGui::Text("%s %s", TR("daemon_installed"), + inst.version.empty() ? TR("unknown") : inst.version.c_str()); + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), " %s · %s", + util::Platform::formatFileSize(inst.size).c_str(), + fmtDate(inst.modifiedEpoch).c_str()); + } else { + ImGui::Text("%s %s", TR("daemon_installed"), TR("daemon_not_installed")); + } + + if (bun.available) { + ImGui::Text("%s %s", TR("daemon_bundled"), + bun.version.empty() ? TR("unknown") : bun.version.c_str()); + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), " %s", + util::Platform::formatFileSize(bun.size).c_str()); + } else { + ImGui::Text("%s %s", TR("daemon_bundled"), TR("daemon_none_bundled")); + } + + if (bun.available) { + const bool sameSize = inst.exists && inst.size == bun.size; + if (!inst.exists) + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_missing")); + else if (sameSize) + ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("daemon_status_match")); + else + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_differ")); + } + + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available); + if (TactileButton(TR("daemon_install_bundled"), ImVec2(0, 0), btnFont)) { + s_settingsState.confirm_reinstall_daemon = true; + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_daemon_install_bundled")); + ImGui::EndDisabled(); + ImGui::SameLine(0, Layout::spacingMd()); + if (TactileButton(TR("refresh"), ImVec2(0, 0), btnFont)) { + s_settingsState.daemon_info_loaded = false; // recompute next frame + } } } @@ -2688,6 +2768,37 @@ void RenderSettingsPage(App* app) { } } + // Confirm: reinstall the bundled daemon binary (stop → overwrite → restart) + if (s_settingsState.confirm_reinstall_daemon) { + if (BeginOverlayDialog(TR("confirm_reinstall_daemon_title"), &s_settingsState.confirm_reinstall_daemon, 500.0f, 0.94f)) { + ImGui::PushFont(Type().iconLarge()); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING); + ImGui::PopFont(); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning")); + + ImGui::Spacing(); + ImGui::TextWrapped("%s", TR("confirm_reinstall_daemon_msg")); + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_reinstall_daemon_safe")); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; + if (ImGui::Button(TrId("cancel", "reinstall_daemon_cancel").c_str(), ImVec2(btnW, 40))) { + s_settingsState.confirm_reinstall_daemon = false; + } + ImGui::SameLine(); + if (ImGui::Button(TrId("daemon_install_bundled", "reinstall_daemon_confirm").c_str(), ImVec2(btnW, 40))) { + app->reinstallBundledDaemon(); + s_settingsState.daemon_info_loaded = false; // refresh the panel after the swap + s_settingsState.confirm_reinstall_daemon = false; + } + EndOverlayDialog(); + } + } + // Confirm: restart daemon (briefly drops the connection to apply changed options) if (s_settingsState.confirm_restart_daemon) { if (BeginOverlayDialog(TR("confirm_restart_daemon_title"), &s_settingsState.confirm_restart_daemon, 500.0f, 0.94f)) { diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 1fbdc13..6bc7627 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -423,6 +423,19 @@ void I18n::loadBuiltinEnglish() strings_["confirm_repair_wallet_title"] = "Repair Wallet"; strings_["confirm_repair_wallet_msg"] = "This restarts the daemon with -zapwallettxes=2: it deletes all of the wallet's transaction and note records, then rebuilds them from the blockchain. Use this when transactions fail to build (\"Invalid sapling spend proof\" / \"shielded requirements not met\") even after a full rescan. It takes a long time and the wallet stays offline until it finishes."; strings_["confirm_repair_wallet_safe"] = "Your keys, addresses and balance are preserved — only the cached transaction records are rebuilt."; + strings_["daemon_binary"] = "Daemon binary"; + strings_["daemon_installed"] = "Installed:"; + strings_["daemon_bundled"] = "Bundled:"; + strings_["daemon_not_installed"] = "not installed"; + strings_["daemon_none_bundled"] = "none in this build"; + strings_["daemon_status_match"] = "Installed binary matches the bundled version."; + strings_["daemon_status_differ"] = "Installed binary differs from the bundled version."; + strings_["daemon_status_missing"] = "No daemon installed — install the bundled version."; + strings_["daemon_install_bundled"] = "Install bundled daemon"; + strings_["tt_daemon_install_bundled"] = "Stop the node, overwrite the installed dragonxd with the version bundled in this wallet build, then restart"; + strings_["confirm_reinstall_daemon_title"] = "Install Bundled Daemon"; + strings_["confirm_reinstall_daemon_msg"] = "This stops the daemon, overwrites the installed dragonxd (and dragonx-cli/dragonx-tx) with the versions bundled in this wallet build, then restarts the node. Use this to recover or update the node binary."; + strings_["confirm_reinstall_daemon_safe"] = "Your wallet, keys and blockchain data are not touched — only the daemon program files are replaced."; strings_["confirm_restart_daemon_title"] = "Restart Daemon"; strings_["confirm_restart_daemon_msg"] = "This stops and restarts the daemon to apply the changed options. The wallet will briefly disconnect and reconnect."; strings_["lite_maintenance"] = "Maintenance";