From 940dd21464b0bf3a3fa4cad3560cfa699b1869c5 Mon Sep 17 00:00:00 2001 From: DanS Date: Sun, 2 Aug 2026 15:37:52 -0500 Subject: [PATCH] feat(diagnostics): add "Copy diagnostics" + "Open log folder" actions (Foundation QoL) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings (logging section) gains two support-friendly actions, now that the logging foundation actually produces logs (W7-2): - Open log folder: opens the config dir (Platform::openFolder) so users can find dragonx-debug.log / dragonx-crash.log. - Copy diagnostics: copies a plaintext support snapshot to the clipboard via the new App::buildDiagnosticsReport() — version, build variant, platform, connection status, active wallet path + existence + size, encryption/lock state, sync heights, and (full- node) daemon status/running/crash-count/lastError, plus the log paths. No secrets. Build-clean; ctest 1/1. Remaining QoL: persistent alert history, a daemon/RPC error banner, and the W6-2 refresh-staleness badge. See docs/wallet-hardening.md. Co-Authored-By: Claude Opus 4.8 --- docs/wallet-hardening.md | 3 +- src/app.cpp | 57 ++++++++++++++++++++++++++++++++++ src/app.h | 4 +++ src/ui/pages/settings_page.cpp | 15 +++++++++ src/util/i18n.cpp | 5 +++ 5 files changed, 83 insertions(+), 1 deletion(-) diff --git a/docs/wallet-hardening.md b/docs/wallet-hardening.md index 8d94fe2..dceb8d5 100644 --- a/docs/wallet-hardening.md +++ b/docs/wallet-hardening.md @@ -21,7 +21,7 @@ Status legend: ☐ not started · ◐ in progress · ☑ landed & verified | **P1-A** | W3-1, W3-2, W3-4 ✓ · W3-3 ⚑ | Migrate-to-seed correctness (fund-adjacent) | ◐ 3/4 | | **P1-B** | W1-1, W1-2, W1-3, W1-4 ✓ + startup guard | Missing/wrong wallet-file safety | ☑ | | **P2** | W5-1, W5-2, W6-1, W6-3 ✓ · W6-2 ☐ | Stale state & lite save-failure surfacing | ◐ 4/5 | -| **F** | W7-2, W7-3, W7-4 ✓ · QoL ☐ | Diagnostics foundation + QoL bundle | ◐ infra done | +| **F** | W7-2, W7-3, W7-4 ✓ · QoL: copy-diag + open-log ✓, alert-history/error-banner/staleness ☐ | Diagnostics foundation + QoL bundle | ◐ | --- @@ -112,6 +112,7 @@ Land W7-2 first — it unblocks the rest. ## Progress log +- **Foundation QoL / "Copy diagnostics" + "Open log folder"** — ☑ landed: Settings (logging section) now has two actions. **Open log folder** opens the config dir (`Platform::openFolder`) so users can actually find `dragonx-debug.log`/`dragonx-crash.log`. **Copy diagnostics** copies a plaintext support snapshot to the clipboard via the new `App::buildDiagnosticsReport()` — version, build variant, platform, connection status, active wallet path + existence + size, encryption/lock state, sync heights, daemon status/running/crash-count/lastError (full-node), and the log paths. No secrets. Build-clean; `ctest` 1/1. **Remaining QoL:** persistent alert history, a daemon/RPC error banner, and the W6-2 refresh-staleness badge. - **Foundation / W7-2 · W7-3 · W7-4 (diagnostics infrastructure)** — ☑ landed (answers the original "easier to diagnose" ask — the logging/crash foundation now actually works): - **W7-2 (Med, keystone):** the app-level `Logger` file sink was never initialized, so `LOG`/`LOGF`/`VERBOSE_LOGF` went nowhere and `dragonx-debug.log` didn't exist on Linux/macOS at all. `main()` now calls `Logger::init(/dragonx-debug.log)` on all platforms. Also fixed a **latent deadlock** this exposed: `init()` wrote its banner via `write()`, which re-locks the non-recursive `mutex_` it already holds — now written directly. On Windows the raw stdout/stderr `freopen` was moved to a separate `dragonx-stdout.log` so the two writers don't contend. New `testLoggerFileSink` (also a deadlock guard — it would hang if that regressed). - **W7-3 (Med):** no crash handler existed on Linux/macOS. Added an **async-signal-safe** `sigaction` handler (SIGSEGV/ABRT/BUS/FPE/ILL) that writes a signal id + `backtrace_symbols_fd` backtrace to `dragonx-crash.log`, then re-raises the default disposition for a core dump — the POSIX counterpart of the Windows SEH filter. diff --git a/src/app.cpp b/src/app.cpp index 666fdf0..ad7201d 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -105,6 +105,7 @@ #include #include #include +#include #include #include #include @@ -5658,6 +5659,62 @@ void App::maybeFinishTransactionSendProgress() if (addresses_dirty_ || network_refresh_.jobInProgress(Job::Addresses)) return; send_progress_active_ = false; } +std::string App::buildDiagnosticsReport() +{ + std::ostringstream os; + os << "=== ObsidianDragon diagnostics ===\n"; + os << "version: " << DRAGONX_VERSION << "\n"; +#if DRAGONX_LITE_BUILD + os << "variant: Lite\n"; +#else + os << "variant: Full-node\n"; +#endif +#if defined(_WIN32) + os << "platform: windows\n"; +#elif defined(__APPLE__) + os << "platform: macos\n"; +#else + os << "platform: linux\n"; +#endif + os << "connected: " << (state_.connected ? "yes" : "no") << "\n"; + os << "status: " << connection_status_ << "\n"; + + const std::string activeWallet = settings_ ? settings_->getActiveWalletFile() : std::string("(none)"); + os << "active wallet: " << activeWallet << "\n"; + { + std::error_code ec; + const std::string wp = util::Platform::getDragonXDataDir() + "/" + activeWallet; + const bool present = std::filesystem::exists(wp, ec); + os << " path: " << wp << (present ? " [present" : " [MISSING"); + if (present) { auto sz = std::filesystem::file_size(wp, ec); if (!ec) os << ", " << sz << " bytes"; } + os << "]\n"; + } + os << "encryption: " + << (state_.encryption_state_known + ? (state_.encrypted ? (state_.locked ? "encrypted, locked" : "encrypted, unlocked") : "unencrypted") + : "unknown") + << "\n"; + os << "sync: block " << state_.sync.blocks << " / " << state_.sync.headers + << (state_.sync.syncing ? " (syncing)" : "") + << (state_.warming_up ? " (warming up)" : "") << "\n"; + +#if !DRAGONX_LITE_BUILD + os << "daemon status: " << daemon_status_ << "\n"; + if (daemon_controller_) { + os << "daemon running: " << (daemon_controller_->isRunning() ? "yes" : "no") + << ", crashes: " << daemon_controller_->crashCount() << "\n"; + const std::string derr = daemon_controller_->lastError(); + if (!derr.empty()) os << "daemon lastError: " << derr << "\n"; + } +#endif + + const std::string cfg = util::Platform::getObsidianDragonDir(); + os << "log folder: " << cfg << "\n"; + os << " " << cfg << "/dragonx-debug.log\n"; + os << " " << cfg << "/dragonx-crash.log\n"; + return os.str(); +} + void App::restartDaemon() { if (!supportsFullNodeLifecycleActions()) { diff --git a/src/app.h b/src/app.h index d0a65a2..98c2c00 100644 --- a/src/app.h +++ b/src/app.h @@ -147,6 +147,10 @@ public: bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); } bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); } bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); } + + // W7 QoL: a plaintext support-diagnostics snapshot (version, variant, daemon/RPC/wallet/log state) + // for the "Copy diagnostics" action. Contains no secrets. + std::string buildDiagnosticsReport(); bool supportsSoloMining() const { return wallet::supportsSoloMining(walletCapabilities()); } bool supportsPoolMining() const { return wallet::supportsPoolMining(walletCapabilities()); } bool supportsLiteBackend() const { return wallet::supportsLiteBackend(walletCapabilities()); } diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 7283f24..d4d3dca 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -1351,6 +1351,21 @@ void RenderSettingsPage(App* app) { if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_verbose")); } + // W7 QoL: quick diagnostics actions — open the log folder, and copy a plaintext support bundle + // (version, variant, daemon/RPC/wallet/log state) to the clipboard. + { + const float diagBtnW = (contentW - Layout::spacingMd()) * 0.5f; + if (TactileButton(TR("settings_open_log_folder"), ImVec2(diagBtnW, 0), S.resolveFont("button"))) + dragonx::util::Platform::openFolder(dragonx::util::Platform::getObsidianDragonDir()); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_log_folder")); + ImGui::SameLine(0, Layout::spacingMd()); + if (TactileButton(TR("settings_copy_diagnostics"), ImVec2(diagBtnW, 0), S.resolveFont("button"))) { + ImGui::SetClipboardText(app->buildDiagnosticsReport().c_str()); + ui::Notifications::instance().info(TR("settings_diagnostics_copied"), 4.0f); + } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_copy_diagnostics")); + } + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // --- Collapsible: Tools & Actions... --- diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 52193ee..656aadc 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1322,6 +1322,11 @@ void I18n::loadBuiltinEnglish() strings_["loading_stall_body"] = "The daemon has been initializing for %.0fs. This can be normal after an update or on first launch (loading the block index or rescanning) — it will connect automatically once ready."; strings_["loading_stall_hint"] = "Still stuck? Open Settings and use Restart Daemon, or check the Console for details."; strings_["sb_plaintext_remote_blocked"] = "Refusing to send RPC credentials over plaintext to a remote host. Add rpcallowplaintext=1 to DRAGONX.conf to allow it, or enable TLS with rpctls=1."; + strings_["settings_open_log_folder"] = "Open log folder"; + strings_["settings_copy_diagnostics"] = "Copy diagnostics"; + strings_["settings_diagnostics_copied"] = "Diagnostics copied to clipboard"; + strings_["tt_open_log_folder"] = "Open the folder containing the debug and crash logs"; + strings_["tt_copy_diagnostics"] = "Copy a support snapshot (version, daemon/wallet/log state — no secrets) to the clipboard"; strings_["sb_dragonxd_running"] = "dragonxd running"; strings_["sb_dragonxd_stopping"] = "Stopping dragonxd..."; strings_["sb_dragonxd_stopped"] = "dragonxd stopped";