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:
+81
-4
@@ -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[])
|
||||
{
|
||||
// Ensure ObsidianDragon config directory exists early (before any file I/O)
|
||||
@@ -738,11 +795,31 @@ int main(int argc, char* argv[])
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Redirect stdout/stderr to a log file so diagnostic output is visible
|
||||
// even when built as a GUI app (WIN32_EXECUTABLE hides the console).
|
||||
// W7-2: initialize the app-level Logger's file sink on ALL platforms so LOG/LOGF/VERBOSE_LOGF are
|
||||
// actually persisted to dragonx-debug.log. Previously init() was never called, so on Linux/macOS the
|
||||
// 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(), "a", stderr);
|
||||
}
|
||||
|
||||
+22
-4
@@ -5,10 +5,12 @@
|
||||
#include "logger.h"
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <chrono>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <filesystem>
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
@@ -35,14 +37,30 @@ bool Logger::init(const std::string& path)
|
||||
if (file_.is_open()) {
|
||||
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);
|
||||
initialized_ = file_.is_open();
|
||||
|
||||
|
||||
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_;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user