feat(diagnostics): make the logging + crash infrastructure actually work (W7-2, W7-3, W7-4)
The Foundation tier — answers the original "make it easier to diagnose problems" ask. - W7-2 (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 every platform. 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 moved to a separate dragonx-stdout.log so the two writers don't contend on one file. Added testLoggerFileSink (also a deadlock guard — it would hang if the fix regressed). - W7-3: no crash handler existed on Linux/macOS. Added an async-signal-safe sigaction handler (SIGSEGV/ABRT/BUS/FPE/ILL) that writes the signal id + a 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. - W7-4: Logger::init now rotates the log to a single .1 backup past 10 MB, so a long or verbose session can't grow it unbounded. Build-clean; ctest 1/1. Remaining Foundation: the QoL bundle (mostly UI). 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-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 | ☑ |
|
| **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 |
|
| **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 | ☐ |
|
| **F** | W7-2, W7-3, W7-4 ✓ · QoL ☐ | Diagnostics foundation + QoL bundle | ◐ infra done |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -112,6 +112,11 @@ Land W7-2 first — it unblocks the rest.
|
|||||||
|
|
||||||
## Progress log
|
## Progress log
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **W7-4 (Low):** `Logger::init` now rotates the log to a single `.1` backup when it exceeds 10 MB, so a long/verbose session can't grow it unbounded.
|
||||||
|
Build-clean; `ctest` 1/1. **Remaining Foundation:** the QoL bundle (mostly UI) — "copy diagnostics for support", an "open log folder" action, persistent alert history, a daemon/RPC error banner, and the W6-2 refresh-staleness badge.
|
||||||
- **P2 / W5-1 · W5-2 · W6-1 · W6-3 (localized batch)** — ☑ landed:
|
- **P2 / W5-1 · W5-2 · W6-1 · W6-3 (localized batch)** — ☑ landed:
|
||||||
- **W5-1 (Med):** `persistAfterBroadcast` (lite send/shield save) returned false on a persistent save failure but both callers discarded it and it never logged — completely silent. It now `liteLog`s the failure (the note re-derives on next sync, so it's a robustness gap, not fund loss).
|
- **W5-1 (Med):** `persistAfterBroadcast` (lite send/shield save) returned false on a persistent save failure but both callers discarded it and it never logged — completely silent. It now `liteLog`s the failure (the note re-derives on next sync, so it's a robustness gap, not fund loss).
|
||||||
- **W5-2 (Med):** the post-**sync** and post-**rescan** `save` results (in the detached scan threads) were ignored; both now `liteLog` on failure (`LiteDiagnostics::log` is mutex-guarded, safe from those threads).
|
- **W5-2 (Med):** the post-**sync** and post-**rescan** `save` results (in the detached scan threads) were ignored; both now `liteLog` on failure (`LiteDiagnostics::log` is mutex-guarded, safe from those threads).
|
||||||
|
|||||||
85
src/main.cpp
85
src/main.cpp
@@ -721,6 +721,63 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !defined(_WIN32)
|
||||||
|
#include <csignal>
|
||||||
|
#include <cstring>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#if defined(__has_include)
|
||||||
|
# if __has_include(<execinfo.h>)
|
||||||
|
# include <execinfo.h>
|
||||||
|
# define DRAGONX_HAVE_BACKTRACE 1
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Absolute path to the crash log, filled at install time so the async-signal handler needs no
|
||||||
|
// allocation. (POSIX counterpart of the Windows SEH CrashHandler above — W7-3.)
|
||||||
|
static char g_crashLogPath[1024] = {0};
|
||||||
|
|
||||||
|
// Async-signal-safe crash handler: only open()/write()/backtrace_symbols_fd()/raise() are used —
|
||||||
|
// no stdio, std::filesystem or malloc (all unsafe inside a signal handler).
|
||||||
|
static void PosixCrashHandler(int sig)
|
||||||
|
{
|
||||||
|
int fd = g_crashLogPath[0] ? open(g_crashLogPath, O_WRONLY | O_CREAT | O_APPEND, 0600) : -1;
|
||||||
|
if (fd >= 0) {
|
||||||
|
auto put = [fd](const char* s) { ssize_t n = write(fd, s, std::strlen(s)); (void)n; };
|
||||||
|
put("\n=== CRASH: signal ");
|
||||||
|
char num[16]; int i = 0, v = sig; // signal number -> decimal, no stdio
|
||||||
|
if (v == 0) { num[i++] = '0'; }
|
||||||
|
else { char tmp[16]; int t = 0; while (v > 0) { tmp[t++] = char('0' + v % 10); v /= 10; }
|
||||||
|
while (t > 0) num[i++] = tmp[--t]; }
|
||||||
|
num[i] = '\n';
|
||||||
|
ssize_t nn = write(fd, num, i + 1); (void)nn;
|
||||||
|
#ifdef DRAGONX_HAVE_BACKTRACE
|
||||||
|
void* frames[64];
|
||||||
|
int nframes = backtrace(frames, 64);
|
||||||
|
backtrace_symbols_fd(frames, nframes, fd); // async-signal-safe
|
||||||
|
#endif
|
||||||
|
put("=== END CRASH ===\n");
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
// Restore the default disposition and re-raise so we still get a core dump / normal termination.
|
||||||
|
signal(sig, SIG_DFL);
|
||||||
|
raise(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void installPosixCrashHandler(const std::string& crashLogPath)
|
||||||
|
{
|
||||||
|
std::snprintf(g_crashLogPath, sizeof(g_crashLogPath), "%s", crashLogPath.c_str());
|
||||||
|
struct sigaction sa;
|
||||||
|
std::memset(&sa, 0, sizeof(sa));
|
||||||
|
sa.sa_handler = PosixCrashHandler;
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
sa.sa_flags = 0;
|
||||||
|
for (int sig : {SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL}) {
|
||||||
|
sigaction(sig, &sa, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // !_WIN32
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
// Ensure ObsidianDragon config directory exists early (before any file I/O)
|
// Ensure ObsidianDragon config directory exists early (before any file I/O)
|
||||||
@@ -738,11 +795,31 @@ int main(int argc, char* argv[])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef _WIN32
|
// W7-2: initialize the app-level Logger's file sink on ALL platforms so LOG/LOGF/VERBOSE_LOGF are
|
||||||
// Redirect stdout/stderr to a log file so diagnostic output is visible
|
// actually persisted to dragonx-debug.log. Previously init() was never called, so on Linux/macOS the
|
||||||
// even when built as a GUI app (WIN32_EXECUTABLE hides the console).
|
// file never existed at all (the Windows-only stdout freopen below is a separate mechanism).
|
||||||
{
|
{
|
||||||
std::string logPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-debug.log").string();
|
const std::string logPath =
|
||||||
|
(std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-debug.log").string();
|
||||||
|
dragonx::util::Logger::instance().init(logPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !defined(_WIN32)
|
||||||
|
// W7-3: install the POSIX crash handler (the Windows SEH filter is installed below). A segfault or
|
||||||
|
// abort now leaves a backtrace in dragonx-crash.log instead of vanishing silently on Linux/macOS.
|
||||||
|
{
|
||||||
|
const std::string crashPath =
|
||||||
|
(std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-crash.log").string();
|
||||||
|
installPosixCrashHandler(crashPath);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Redirect raw stdout/stderr (library / daemon-pipe writes) to a log file so it's visible even when
|
||||||
|
// built as a GUI app (WIN32_EXECUTABLE hides the console). Separate file from the structured Logger
|
||||||
|
// above so the two writers don't interleave/contend on one file.
|
||||||
|
{
|
||||||
|
std::string logPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-stdout.log").string();
|
||||||
freopen(logPath.c_str(), "w", stdout);
|
freopen(logPath.c_str(), "w", stdout);
|
||||||
freopen(logPath.c_str(), "a", stderr);
|
freopen(logPath.c_str(), "a", stderr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
|
|
||||||
#include <cstdarg>
|
#include <cstdarg>
|
||||||
|
#include <cstdint>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace util {
|
namespace util {
|
||||||
@@ -36,11 +38,27 @@ bool Logger::init(const std::string& path)
|
|||||||
file_.close();
|
file_.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// W7-4: cap the log's growth — if the existing file is already large, rotate it to a single .1
|
||||||
|
// backup before reopening in append mode, so a long-lived or verbose session can't grow it
|
||||||
|
// without bound.
|
||||||
|
{
|
||||||
|
std::error_code ec;
|
||||||
|
const auto sz = std::filesystem::file_size(path, ec);
|
||||||
|
constexpr std::uintmax_t kMaxLogBytes = 10ull * 1024ull * 1024ull; // 10 MB
|
||||||
|
if (!ec && sz > kMaxLogBytes) {
|
||||||
|
std::filesystem::rename(path, path + ".1", ec); // replaces any previous .1 backup
|
||||||
|
if (ec) std::filesystem::remove(path, ec); // fall back to truncation if rename fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
file_.open(path, std::ios::out | std::ios::app);
|
file_.open(path, std::ios::out | std::ios::app);
|
||||||
initialized_ = file_.is_open();
|
initialized_ = file_.is_open();
|
||||||
|
|
||||||
if (initialized_) {
|
if (initialized_) {
|
||||||
write("=== Logger initialized ===");
|
// Write the banner directly, NOT via write(): write() re-locks the non-recursive mutex_ we
|
||||||
|
// already hold here, which would deadlock (latent — init() was previously never called, W7-2).
|
||||||
|
file_ << "=== Logger initialized ===" << std::endl;
|
||||||
|
file_.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
return initialized_;
|
return initialized_;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "daemon/daemon_controller.h"
|
#include "daemon/daemon_controller.h"
|
||||||
#include "daemon/embedded_daemon.h"
|
#include "daemon/embedded_daemon.h"
|
||||||
#include "util/connect_stall.h"
|
#include "util/connect_stall.h"
|
||||||
|
#include "util/logger.h"
|
||||||
#include "data/transaction_history_cache.h"
|
#include "data/transaction_history_cache.h"
|
||||||
#include "data/address_book.h"
|
#include "data/address_book.h"
|
||||||
#include "data/wallet_index.h"
|
#include "data/wallet_index.h"
|
||||||
@@ -2546,6 +2547,33 @@ void testConsoleSecretRedaction()
|
|||||||
EXPECT_EQ(RedactConsoleCommand("getwalletinfo"), std::string("getwalletinfo"));
|
EXPECT_EQ(RedactConsoleCommand("getwalletinfo"), std::string("getwalletinfo"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void testLoggerFileSink()
|
||||||
|
{
|
||||||
|
using dragonx::util::Logger;
|
||||||
|
namespace fsn = std::filesystem;
|
||||||
|
fsn::path logPath = fsn::temp_directory_path() / "od_logger_test.log";
|
||||||
|
std::error_code ec;
|
||||||
|
fsn::remove(logPath, ec);
|
||||||
|
fsn::remove(logPath.string() + ".1", ec);
|
||||||
|
|
||||||
|
// W7-2: init() opens the file sink and must NOT deadlock — it writes the banner under the same
|
||||||
|
// non-recursive lock it holds (this test would hang if that regressed).
|
||||||
|
Logger& lg = Logger::instance();
|
||||||
|
EXPECT_TRUE(lg.init(logPath.string()));
|
||||||
|
lg.write("hello-w7-2-sink");
|
||||||
|
EXPECT_TRUE(fsn::exists(logPath));
|
||||||
|
|
||||||
|
std::ifstream f(logPath.string());
|
||||||
|
std::string all, line;
|
||||||
|
while (std::getline(f, line)) all += line + "\n";
|
||||||
|
f.close();
|
||||||
|
EXPECT_TRUE(all.find("hello-w7-2-sink") != std::string::npos);
|
||||||
|
EXPECT_TRUE(all.find("Logger initialized") != std::string::npos);
|
||||||
|
|
||||||
|
fsn::remove(logPath, ec);
|
||||||
|
fsn::remove(logPath.string() + ".1", ec);
|
||||||
|
}
|
||||||
|
|
||||||
void testConnectHasStalled()
|
void testConnectHasStalled()
|
||||||
{
|
{
|
||||||
using dragonx::util::connectHasStalled;
|
using dragonx::util::connectHasStalled;
|
||||||
@@ -6906,6 +6934,7 @@ int main()
|
|||||||
testIsLocalHost();
|
testIsLocalHost();
|
||||||
testAllowsPlaintextRemote();
|
testAllowsPlaintextRemote();
|
||||||
testConsoleSecretRedaction();
|
testConsoleSecretRedaction();
|
||||||
|
testLoggerFileSink();
|
||||||
testDaemonLifecycleExecution();
|
testDaemonLifecycleExecution();
|
||||||
testDaemonLifecycleAdapters();
|
testDaemonLifecycleAdapters();
|
||||||
testConsoleTextLayout();
|
testConsoleTextLayout();
|
||||||
|
|||||||
Reference in New Issue
Block a user