feat(diagnostics): add "Copy diagnostics" + "Open log folder" actions (Foundation QoL)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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(<config>/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.
|
||||
|
||||
57
src/app.cpp
57
src/app.cpp
@@ -105,6 +105,7 @@
|
||||
#include <unordered_set>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <exception>
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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()); }
|
||||
|
||||
@@ -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... ---
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user