From 0871519d7c9af8a9b31fa5327a05c550f742218e Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Wed, 5 Nov 2025 21:11:49 -0500 Subject: [PATCH 1/7] Initial implementation for a linux crashlog --- library/CMakeLists.txt | 1 + library/Core.cpp | 10 ++++++ library/Crashlog.cpp | 81 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 library/Crashlog.cpp diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 895ae6eb86..fa5c482bf6 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -136,6 +136,7 @@ endif() set(MAIN_SOURCES_LINUX ${CONSOLE_SOURCES} + Crashlog.cpp ) set(MAIN_SOURCES_DARWIN diff --git a/library/Core.cpp b/library/Core.cpp index faec04db61..42c7dda2cb 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1540,6 +1540,11 @@ bool Core::InitMainThread() { Filesystem::init(); + #ifdef LINUX_BUILD + extern void dfhack_crashlog_init(); + dfhack_crashlog_init(); + #endif + // Re-route stdout and stderr again - DF seems to set up stdout and // stderr.txt on Windows as of 0.43.05. Also, log before switching files to // make it obvious what's going on if someone checks the *.txt files. @@ -2375,6 +2380,11 @@ void Core::onStateChange(color_ostream &out, state_change_event event) int Core::Shutdown ( void ) { + #ifdef LINUX_BUILD + extern void dfhack_crashlog_shutdown(); + dfhack_crashlog_shutdown(); + #endif + if(errorstate) return true; errorstate = 1; diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp new file mode 100644 index 0000000000..fa2ec6dad5 --- /dev/null +++ b/library/Crashlog.cpp @@ -0,0 +1,81 @@ +#include "DFHackVersion.h" +#include +#include +#include +#include +#include + +#include + +const int BT_ENTRY_MAX = 25; +int bt_entries = 0; +void* bt[BT_ENTRY_MAX]; +int crash_signal = 0; + +std::binary_semaphore crashlog_ready{0}; +std::binary_semaphore crashlog_complete{0}; + +std::thread crashlog_thread; +volatile bool shutdown = false; + +extern "C" void dfhack_crashlog_handle_signal(int sig) { + crash_signal = sig; + bt_entries = backtrace(bt, BT_ENTRY_MAX); + + // Signal saving of crashlog and wait for completion + crashlog_ready.release(); + crashlog_complete.acquire(); + std::quick_exit(1); +} + +void dfhack_save_crashlog() { + char** backtrace_strings = backtrace_symbols(bt, bt_entries); + if (!backtrace_strings) { + // Something has gone terribly wrong + return; + } + std::filesystem::path crashlog_path = "./crash.txt"; + std::ofstream crashlog(crashlog_path); + + crashlog << "Dwarf Fortress has crashed!" << "\n"; + crashlog << "DwarfFortress Version " << DFHack::Version::df_version() << "\n"; + crashlog << "DFHack Version " << DFHack::Version::dfhack_version() << "\n\n"; + + for (int i = 0; i < bt_entries; i++) { + crashlog << i << "> " << backtrace_strings[i] << "\n"; + } + + free(backtrace_strings); +} + +void dfhack_crashlog_thread() { + // Wait for crash or shutdown signal + crashlog_ready.acquire(); + if (shutdown) + return; + + dfhack_save_crashlog(); + crashlog_complete.release(); + std::quick_exit(1); +} + +const int desired_signals[3] = {SIGSEGV,SIGILL,SIGABRT}; +namespace DFHack { +void dfhack_crashlog_init() { + for (int signal : desired_signals) { + std::signal(signal, dfhack_crashlog_handle_signal); + } + + // Ensure the library is initialized to avoid AsyncSignal-Unsafe init during crash + int _ = backtrace(bt, 1); + + crashlog_thread = std::thread(dfhack_crashlog_thread); +} + +void dfhack_crashlog_shutdown() { + shutdown = true; + crashlog_ready.release(); + crashlog_thread.join(); + return; +} +} From 093896d2d8e21be084a951465192c16392a10cd9 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 6 Nov 2025 11:11:32 -0500 Subject: [PATCH 2/7] Improve linux crashlog functionality and safety --- library/Crashlog.cpp | 119 ++++++++++++++++++++++++++++++------------- 1 file changed, 85 insertions(+), 34 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index fa2ec6dad5..2c8ac0f17f 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -1,47 +1,94 @@ #include "DFHackVersion.h" #include #include -#include #include #include #include const int BT_ENTRY_MAX = 25; -int bt_entries = 0; -void* bt[BT_ENTRY_MAX]; -int crash_signal = 0; +struct CrashInfo { + int backtrace_entries = 0; + void* backtrace[BT_ENTRY_MAX]; + int signal = 0; +}; -std::binary_semaphore crashlog_ready{0}; -std::binary_semaphore crashlog_complete{0}; +CrashInfo crash_info; + +/* + * As of c++17 the only safe stdc++ methods are plain lock-free atomic methods + * This sadly means that using std::semaphore *could* cause issues according to the standard. + */ +std::atomic_bool crashed = false; +std::atomic_bool crashlog_ready = false; +std::atomic_bool crashlog_complete = false; + +void flag_set(std::atomic_bool &atom) { + atom.store(true); + atom.notify_all(); +} +void flag_wait(std::atomic_bool &atom) { + atom.wait(false); +} std::thread crashlog_thread; -volatile bool shutdown = false; +bool shutdown = false; extern "C" void dfhack_crashlog_handle_signal(int sig) { - crash_signal = sig; - bt_entries = backtrace(bt, BT_ENTRY_MAX); + if (crashed.exchange(true)) { + // Crashlog already produced, bail thread. + std::quick_exit(1); + } + crash_info.signal = sig; + crash_info.backtrace_entries = backtrace(crash_info.backtrace, BT_ENTRY_MAX); // Signal saving of crashlog and wait for completion - crashlog_ready.release(); - crashlog_complete.acquire(); + flag_set(crashlog_ready); + flag_wait(crashlog_complete); std::quick_exit(1); } +void dfhack_crashlog_handle_terminate() { + dfhack_crashlog_handle_signal(0); +} + +std::string signal_name(int sig) { + switch (sig) { + case SIGINT: + return "SIGINT"; + case SIGILL: + return "SIGILL"; + case SIGABRT: + return "SIGABRT"; + case SIGFPE: + return "SIGFPE"; + case SIGSEGV: + return "SIGSEGV"; + case SIGTERM: + return "SIGTERM"; + } + return ""; +} + void dfhack_save_crashlog() { - char** backtrace_strings = backtrace_symbols(bt, bt_entries); + char** backtrace_strings = backtrace_symbols(crash_info.backtrace, crash_info.backtrace_entries); if (!backtrace_strings) { - // Something has gone terribly wrong + // Allocation failed, give up return; } std::filesystem::path crashlog_path = "./crash.txt"; std::ofstream crashlog(crashlog_path); - crashlog << "Dwarf Fortress has crashed!" << "\n"; - crashlog << "DwarfFortress Version " << DFHack::Version::df_version() << "\n"; + crashlog << "Dwarf Fortress Linux has crashed!" << "\n"; + crashlog << "Dwarf Fortress Version " << DFHack::Version::df_version() << "\n"; crashlog << "DFHack Version " << DFHack::Version::dfhack_version() << "\n\n"; - for (int i = 0; i < bt_entries; i++) { + std::string signal = signal_name(crash_info.signal); + if (!signal.empty()) { + crashlog << "Signal " << signal << "\n"; + } + + for (int i = 0; i < crash_info.backtrace_entries; i++) { crashlog << i << "> " << backtrace_strings[i] << "\n"; } @@ -49,33 +96,37 @@ void dfhack_save_crashlog() { } void dfhack_crashlog_thread() { - // Wait for crash or shutdown signal - crashlog_ready.acquire(); - if (shutdown) + // Wait for activation signal + flag_wait(crashlog_ready); + if (shutdown) // Shutting down gracefully, end thread. return; dfhack_save_crashlog(); - crashlog_complete.release(); + + flag_set(crashlog_complete); std::quick_exit(1); } const int desired_signals[3] = {SIGSEGV,SIGILL,SIGABRT}; namespace DFHack { -void dfhack_crashlog_init() { - for (int signal : desired_signals) { - std::signal(signal, dfhack_crashlog_handle_signal); - } + void dfhack_crashlog_init() { + for (int signal : desired_signals) { + std::signal(signal, dfhack_crashlog_handle_signal); + } + std::set_terminate(dfhack_crashlog_handle_terminate); - // Ensure the library is initialized to avoid AsyncSignal-Unsafe init during crash - int _ = backtrace(bt, 1); + // https://sourceware.org/glibc/manual/latest/html_mono/libc.html#index-backtrace-1 + // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s + // Using it here ensures it is loaded before use in the signal handler. + int _ = backtrace(crash_info.backtrace, 1); - crashlog_thread = std::thread(dfhack_crashlog_thread); -} + crashlog_thread = std::thread(dfhack_crashlog_thread); + } -void dfhack_crashlog_shutdown() { - shutdown = true; - crashlog_ready.release(); - crashlog_thread.join(); - return; -} + void dfhack_crashlog_shutdown() { + shutdown = true; + flag_set(crashlog_ready); + crashlog_thread.join(); + return; + } } From 6dabe6adb454f550ca30849494ce21ad095fea3d Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 13 Nov 2025 22:21:28 -0500 Subject: [PATCH 3/7] Generate filepath using timestamp --- library/Crashlog.cpp | 53 +++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index 2c8ac0f17f..e73a5e8b48 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -1,8 +1,9 @@ #include "DFHackVersion.h" #include -#include +#include #include #include +#include #include @@ -41,7 +42,7 @@ extern "C" void dfhack_crashlog_handle_signal(int sig) { } crash_info.signal = sig; crash_info.backtrace_entries = backtrace(crash_info.backtrace, BT_ENTRY_MAX); - + // Signal saving of crashlog and wait for completion flag_set(crashlog_ready); flag_wait(crashlog_complete); @@ -70,27 +71,49 @@ std::string signal_name(int sig) { return ""; } +std::filesystem::path get_crashlog_path() { + std::time_t time = std::time(nullptr); + std::tm* tm = std::localtime(&time); + + std::string timestamp = "unknown"; + if (tm) { + char stamp[64]; + std::size_t out = strftime(&stamp[0], 63, "%Y-%m-%d-%H-%M-%S", tm); + if (out != 0) + timestamp = stamp; + } + + std::filesystem::path dir = "crashlog"; + std::error_code err; + std::filesystem::create_directories(dir, err); + + std::filesystem::path log_path = dir / ("crash_" + timestamp + ".txt"); + return log_path; +} + void dfhack_save_crashlog() { char** backtrace_strings = backtrace_symbols(crash_info.backtrace, crash_info.backtrace_entries); if (!backtrace_strings) { // Allocation failed, give up return; } - std::filesystem::path crashlog_path = "./crash.txt"; - std::ofstream crashlog(crashlog_path); + try { + std::filesystem::path crashlog_path = get_crashlog_path(); + std::ofstream crashlog(crashlog_path); - crashlog << "Dwarf Fortress Linux has crashed!" << "\n"; - crashlog << "Dwarf Fortress Version " << DFHack::Version::df_version() << "\n"; - crashlog << "DFHack Version " << DFHack::Version::dfhack_version() << "\n\n"; + crashlog << "Dwarf Fortress Linux has crashed!" << "\n"; + crashlog << "Dwarf Fortress Version " << DFHack::Version::df_version() << "\n"; + crashlog << "DFHack Version " << DFHack::Version::dfhack_version() << "\n\n"; - std::string signal = signal_name(crash_info.signal); - if (!signal.empty()) { - crashlog << "Signal " << signal << "\n"; - } + std::string signal = signal_name(crash_info.signal); + if (!signal.empty()) { + crashlog << "Signal " << signal << "\n"; + } - for (int i = 0; i < crash_info.backtrace_entries; i++) { - crashlog << i << "> " << backtrace_strings[i] << "\n"; - } + for (int i = 0; i < crash_info.backtrace_entries; i++) { + crashlog << i << "> " << backtrace_strings[i] << "\n"; + } + } catch (...) {} free(backtrace_strings); } @@ -118,7 +141,7 @@ namespace DFHack { // https://sourceware.org/glibc/manual/latest/html_mono/libc.html#index-backtrace-1 // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s // Using it here ensures it is loaded before use in the signal handler. - int _ = backtrace(crash_info.backtrace, 1); + [[maybe_unused]] int _ = backtrace(crash_info.backtrace, 1); crashlog_thread = std::thread(dfhack_crashlog_thread); } From 0d4bc8ac4e092e7f21481c69179d189c0c0cfbeb Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 13 Nov 2025 23:45:59 -0500 Subject: [PATCH 4/7] Remove signal and terminate handlers on shutdown --- library/Crashlog.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index e73a5e8b48..c3ab5d4d67 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -130,13 +130,15 @@ void dfhack_crashlog_thread() { std::quick_exit(1); } +std::terminate_handler term_handler = nullptr; + const int desired_signals[3] = {SIGSEGV,SIGILL,SIGABRT}; namespace DFHack { void dfhack_crashlog_init() { for (int signal : desired_signals) { std::signal(signal, dfhack_crashlog_handle_signal); } - std::set_terminate(dfhack_crashlog_handle_terminate); + term_handler = std::set_terminate(dfhack_crashlog_handle_terminate); // https://sourceware.org/glibc/manual/latest/html_mono/libc.html#index-backtrace-1 // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s @@ -147,6 +149,11 @@ namespace DFHack { } void dfhack_crashlog_shutdown() { + for (int signal : desired_signals) { + std::signal(signal, SIG_DFL); + } + std::set_terminate(term_handler); + shutdown = true; flag_set(crashlog_ready); crashlog_thread.join(); From 25ac663f5a9327a1cac825fab5069e3f263783e7 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 14 Nov 2025 09:20:06 -0500 Subject: [PATCH 5/7] Use eventfd to wait instead of cvar for as-safety --- library/Crashlog.cpp | 54 ++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index c3ab5d4d67..7286d83232 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -5,7 +5,9 @@ #include #include +#include #include +#include const int BT_ENTRY_MAX = 25; struct CrashInfo { @@ -16,13 +18,11 @@ struct CrashInfo { CrashInfo crash_info; -/* - * As of c++17 the only safe stdc++ methods are plain lock-free atomic methods - * This sadly means that using std::semaphore *could* cause issues according to the standard. - */ -std::atomic_bool crashed = false; -std::atomic_bool crashlog_ready = false; -std::atomic_bool crashlog_complete = false; +std::atomic crashed = false; +std::atomic crashlog_ready = false; + +// Use eventfd for async-signal safe waiting +int crashlog_complete = -1; void flag_set(std::atomic_bool &atom) { atom.store(true); @@ -32,20 +32,32 @@ void flag_wait(std::atomic_bool &atom) { atom.wait(false); } +void signal_crashlog_complete() { + if (crashlog_complete == -1) + return; + uint64_t v = 1; + write(crashlog_complete, &v, sizeof(v)); +} + std::thread crashlog_thread; -bool shutdown = false; +volatile bool shutdown = false; extern "C" void dfhack_crashlog_handle_signal(int sig) { - if (crashed.exchange(true)) { - // Crashlog already produced, bail thread. + if (shutdown || crashed.exchange(true) || crashlog_ready.load()) { + // Ensure the signal handler doesn't try to write a crashlog + // whilst the crashlog thread is unavailable. std::quick_exit(1); } crash_info.signal = sig; crash_info.backtrace_entries = backtrace(crash_info.backtrace, BT_ENTRY_MAX); - // Signal saving of crashlog and wait for completion + // Signal saving of crashlog flag_set(crashlog_ready); - flag_wait(crashlog_complete); + // Wait for completion via eventfd read, if fd isn't valid, bail + if (crashlog_complete != -1) { + [[maybe_unused]] uint64_t _; + read(crashlog_complete, &_, sizeof(_)); + } std::quick_exit(1); } @@ -125,8 +137,7 @@ void dfhack_crashlog_thread() { return; dfhack_save_crashlog(); - - flag_set(crashlog_complete); + signal_crashlog_complete(); std::quick_exit(1); } @@ -135,6 +146,11 @@ std::terminate_handler term_handler = nullptr; const int desired_signals[3] = {SIGSEGV,SIGILL,SIGABRT}; namespace DFHack { void dfhack_crashlog_init() { + // Initialize eventfd flag + crashlog_complete = eventfd(0, EFD_CLOEXEC); + + crashlog_thread = std::thread(dfhack_crashlog_thread); + for (int signal : desired_signals) { std::signal(signal, dfhack_crashlog_handle_signal); } @@ -144,19 +160,23 @@ namespace DFHack { // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s // Using it here ensures it is loaded before use in the signal handler. [[maybe_unused]] int _ = backtrace(crash_info.backtrace, 1); - - crashlog_thread = std::thread(dfhack_crashlog_thread); } void dfhack_crashlog_shutdown() { + shutdown = true; for (int signal : desired_signals) { std::signal(signal, SIG_DFL); } std::set_terminate(term_handler); - shutdown = true; + // Shutdown the crashlog thread. flag_set(crashlog_ready); crashlog_thread.join(); + + // If the signal handler is somehow running whilst here, let it terminate + signal_crashlog_complete(); + if (crashlog_complete != -1) + close(crashlog_complete); // Close fd return; } } From cf28bd362121eb87bcaa8ea69463ae43039fe0e2 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 14 Nov 2025 09:41:38 -0500 Subject: [PATCH 6/7] More maybe_unused --- library/Crashlog.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index 7286d83232..22bd40cf31 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -36,7 +36,7 @@ void signal_crashlog_complete() { if (crashlog_complete == -1) return; uint64_t v = 1; - write(crashlog_complete, &v, sizeof(v)); + [[maybe_unused]] auto _ = write(crashlog_complete, &v, sizeof(v)); } std::thread crashlog_thread; @@ -55,8 +55,8 @@ extern "C" void dfhack_crashlog_handle_signal(int sig) { flag_set(crashlog_ready); // Wait for completion via eventfd read, if fd isn't valid, bail if (crashlog_complete != -1) { - [[maybe_unused]] uint64_t _; - read(crashlog_complete, &_, sizeof(_)); + [[maybe_unused]] uint64_t v; + [[maybe_unused]] auto _ = read(crashlog_complete, &v, sizeof(v)); } std::quick_exit(1); } From 8fbaedf20932c1710e267fcbd0a5a5bf61985ff1 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 14 Nov 2025 21:48:12 -0500 Subject: [PATCH 7/7] Use atomic bool for shutdown flag --- library/Crashlog.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index 22bd40cf31..c560666f99 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -20,6 +20,7 @@ CrashInfo crash_info; std::atomic crashed = false; std::atomic crashlog_ready = false; +std::atomic shutdown = false; // Use eventfd for async-signal safe waiting int crashlog_complete = -1; @@ -40,10 +41,9 @@ void signal_crashlog_complete() { } std::thread crashlog_thread; -volatile bool shutdown = false; extern "C" void dfhack_crashlog_handle_signal(int sig) { - if (shutdown || crashed.exchange(true) || crashlog_ready.load()) { + if (shutdown.load() || crashed.exchange(true) || crashlog_ready.load()) { // Ensure the signal handler doesn't try to write a crashlog // whilst the crashlog thread is unavailable. std::quick_exit(1); @@ -133,7 +133,7 @@ void dfhack_save_crashlog() { void dfhack_crashlog_thread() { // Wait for activation signal flag_wait(crashlog_ready); - if (shutdown) // Shutting down gracefully, end thread. + if (shutdown.load()) // Shutting down gracefully, end thread. return; dfhack_save_crashlog(); @@ -163,7 +163,7 @@ namespace DFHack { } void dfhack_crashlog_shutdown() { - shutdown = true; + shutdown.exchange(true); for (int signal : desired_signals) { std::signal(signal, SIG_DFL); }