From 663811a590cd7bda5dfefb36d2bac7a3e0181804 Mon Sep 17 00:00:00 2001 From: JulianGmp Date: Thu, 6 Jul 2023 00:16:05 +0200 Subject: [PATCH 1/3] Implement Unix compatibility * Disable windows specific code using the preprocessor and rely on the C++ standard lib for unix systems. * Add some more cout messages during the crash handler * Add a `header_reason` string that is written at the top of the crashlog and write general information about the crash into it (e.g. `Received signal 11 SIGSEGV`) * Add `TestTool.cpp`, a CLI tool that delibrately crashes to manually test the handler * Updated the readme accordingly --- .gitignore | 3 ++ README.md | 41 ++++++++++++++--- TestTool.cpp | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++ crashlogs.cpp | 79 ++++++++++++++++++++++++++++---- 4 files changed, 231 insertions(+), 16 deletions(-) create mode 100644 .gitignore create mode 100644 TestTool.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb72789 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +bin/ +build/ +test_logs/ diff --git a/README.md b/README.md index 5679ca9..2600fe5 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,42 @@ # Crashlogs -A simple way to output stack traces when a program crashes in C++, using the new C++23 <stacktrace> header -currently windows-only, but if anyone wants to add mac or linux support I'll merge it in +A simple way to output stack traces when a program crashes in C++, using the new C++23 `` header. +Works on Windows. Kind of works on Linux. -usage: include the files in your project, then call -glaiel::crashlogs::begin_monitoring(); -to enable crash handling (probably do this at the start of your program) +## Usage -when the program crashes, it will save a timestamped stack trace to the folder specified with -glaiel::crashlogs::set_crashlog_folder(folder_path); +include the files in your project, then call +Call `glaiel::crashlogs::begin_monitoring` to enable crash handling (probablly do this at the start of your program). +Monitoring and writing the crashlog is done in a worker thread. + +when the program crashes, it will save a timestamped stack trace to the folder specified with `glaiel::crashlogs::set_crashlog_folder(folder_path);` some additional customizability and callbacks are documented in the header file note: the stack trace outputted will include a bunch of error handling stuff at the top. It would be nice to skip printing the first X stack trace entries, but how many to skip seems kinda dependent on optimization settings and which condition triggered the error handler, so I did not bother with that yet. + + + +### Linux (GCC) support + +*Note that at the time of writing GCC (13.1.1) and its implementation of the C++ standard library consider the backtrace feature to be experimental!* +This means that you need to manually link against `stdc++_libbacktrace`. + +Also the backtraces currently look like this +``` +Received signal 11 SIGSEGV + 0# at :32588 + 1# at :32588 + 2# at :32588 + 3# at :32588 + 4# at :32588 + 5# at :32588 + 6# at :32588 + 7# +``` +like they're pretty useless, I'd stick to a different solution until GCC has finished the backtrace feature fully. + +### Testing + +`TestTool.cpp` is a very simple command line tool to test a few different cases. +It will enable the monitoring and delibrately crash so you can manually verify the handler works. diff --git a/TestTool.cpp b/TestTool.cpp new file mode 100644 index 0000000..0d53370 --- /dev/null +++ b/TestTool.cpp @@ -0,0 +1,124 @@ +// A very simple CLI tool to manually test the different cases of Crashlog. +// It's not really possible to unit test crashing, so this will have to suffice. + +#include +#include +#include +#include + +#include + +#include "crashlogs.h" + +enum class TestType +{ + Segfault, + Abort, + Terminate, + IllegalInstruction, + UnhandledException, +}; + +// forward declarations so we can use these in main +[[nodiscard]] std::optional testTypeFromString(std::string_view str); +void usage(const char *arg0); + +int main(int argc, char **argv) +{ + if (argc != 2) + { + usage(argv[0]); + return 1; + } + const auto testTypeParsed = testTypeFromString(argv[1]); + if (!testTypeParsed.has_value()) + { + std::cerr << "Cannot parse `" << argv[1] << "` as a valid test type!\n" + << std::endl; + usage(argv[0]); + return 2; + } + const TestType testType = testTypeParsed.value(); + + std::cout << "Initializing crashlogs" << std::endl; + + glaiel::crashlogs::set_crashlog_folder("./test_logs"); + glaiel::crashlogs::begin_monitoring(); + + std::cout << "Gonna wait a second..." << std::endl; + + for (int cnt = 0; cnt < 10; cnt++) + { + std::cout << "."; + std::cout.flush(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + std::cout << std::endl; + + switch (testType) + { + case TestType::Segfault: + { + std::cout << "Causing Segfault" << std::endl; + int *a = nullptr; + int b = *a + 1; + } + break; + case TestType::Abort: + { + std::cout << "Causing Abort" << std::endl; + std::abort(); + } + break; + case TestType::Terminate: + { + std::cout << "Causing Terminate" << std::endl; + std::terminate(); + } + break; + case TestType::IllegalInstruction: + { + std::cout << "Causing IllegalInstruction" << std::endl; + // no idea how to cause this delibrately so I'm just raising the signal manually + std::raise(SIGILL); + } + break; + case TestType::UnhandledException: + { + std::cout << "Causing UnhandledException" << std::endl; + throw std::logic_error("Whoops"); + } + break; + } + + return 0; +} + +void usage(const char *arg0) +{ + std::cerr << "Usage: \n"; + std::cerr << "\t" << arg0 << " \n"; + std::cerr << "\n" + "\tWith valid test types:\n" + "\t - 'Segfault'\n" + "\t - 'Abort'\n" + "\t - 'Terminate'\n" + "\t - 'IllegalInstruction'\n" + "\t - 'UnhandledException'" + << std::endl; +} + +std::optional testTypeFromString(std::string_view str) +{ + if (str == "Segfault" || str == "segfault") + return TestType::Segfault; + if (str == "Abort" || str == "abort") + return TestType::Abort; + if (str == "Terminate" || str == "terminate") + return TestType::Terminate; + if (str == "IllegalInstruction" || str == "illegalinstruction") + return TestType::IllegalInstruction; + if (str == "UnhandledException" || str == "unhandledexception") + return TestType::UnhandledException; + return std::nullopt; +} diff --git a/crashlogs.cpp b/crashlogs.cpp index b687f85..785c0e1 100644 --- a/crashlogs.cpp +++ b/crashlogs.cpp @@ -1,5 +1,12 @@ #include "crashlogs.h" +#ifdef __unix__ + #define OS_UNIX +#elif defined(_WIN32) || defined(WIN32) + #define OS_WINDOWS +#endif + + //needed to get a stack trace #include @@ -18,20 +25,28 @@ #include #include #include -#define WIN32_LEAN_AND_MEAN -#include + +#ifdef OS_WINDOWS + #define WIN32_LEAN_AND_MEAN + #include +#endif + +//misc std lib stuff +#include +#include //a decent amount of this was copied/modified from backward.cpp (https://github.com/bombela/backward-cpp) //mostly the stuff related to actually getting crash handlers on crashes -//and the thread which is SOLELY there to be able to write a log on a stack overflow, +//and the thread which is SOLELY there to be able to write a log on a stack overflow, //since otherwise there is not enough stack space to output the stack trace //main difference here is utilizing C++23 header for generating stack traces -//and using and a few other more recent C++ features if we're gonna be using C++23 anyway +//and using and a few other more recent C++ features if we're gonna be using C++23 anyway namespace glaiel::crashlogs { //information for where to save stack traces static std::stacktrace trace; static std::string header_message; + static std::string header_reason; static std::filesystem::path output_folder; static std::string filename = "crash_{timestamp}.txt"; static void (*on_output_crashlog)(std::string crashlog_filename) = NULL; @@ -64,6 +79,7 @@ namespace glaiel::crashlogs { header_message = message; } std::string get_crashlog_header_message() { + std::unique_lock lk(mut); return header_message; } @@ -72,8 +88,11 @@ namespace glaiel::crashlogs { //output the crashlog file after a crash has occured static void output_crash_log() { std::filesystem::path path = get_log_filepath(); - std::ofstream log(get_log_filepath()); + const auto output_file = get_log_filepath(); + std::cout << "Writing crashlog to " << output_file << std::endl; + std::ofstream log(output_file); if(!header_message.empty()) log << header_message << std::endl; + if(!header_reason.empty()) log << header_reason << std::endl; log << trace; log.close(); @@ -153,23 +172,59 @@ namespace glaiel::crashlogs { cv.wait(lk, [] { return status != program_status::crashed; }); } + //Try to get the string representation of a signal identifier, return an empty string if none is found. + //This only covers the signals from the C++ std lib and none of the POSIX or OS specific signal names! + static std::string_view try_get_signal_name(int signal) { + switch (signal) + { + case SIGTERM: + return "SIGTERM"; + case SIGSEGV: + return "SIGSEGV"; + case SIGINT: + return "SIGINT"; + case SIGILL: + return "SIGILL"; + case SIGABRT: + return "SIGABRT"; + case SIGFPE: + return "SIGFPE"; + } + return ""; + } + //various callbacks needed to get into the crash handler during a crash (borrowed from backward.cpp) static inline void signal_handler(int signal) { + std::stringstream text; + text << "Received signal " << signal << " " << try_get_signal_name(signal); + + std::cout << text.rdbuf() << std::endl; + header_reason = text.str(); + crash_handler(); - abort(); + std::quick_exit(1); } + static inline void terminator() { + std::cout << "Application terminated" << std::endl; + header_reason = "Application terminated"; + crash_handler(); - abort(); + std::quick_exit(1); } +#ifdef OS_WINDOWS __declspec(noinline) static LONG WINAPI exception_handler(EXCEPTION_POINTERS* info) { + //TODO consider writing additional output from info to header_reason crash_handler(); return EXCEPTION_CONTINUE_SEARCH; } static void __cdecl invalid_parameter_handler(const wchar_t*,const wchar_t*,const wchar_t*,unsigned int,uintptr_t) { + //TODO consider writing additional output from info to header_reason crash_handler(); abort(); } +#endif + //callback needed during a normal exit to shut down the thread static inline void normal_exit() { @@ -182,12 +237,18 @@ namespace glaiel::crashlogs { void begin_monitoring() { output_thread = std::thread(crash_handler_thread); +#ifdef OS_WINDOWS SetUnhandledExceptionFilter(exception_handler); - std::signal(SIGABRT, signal_handler); - std::set_terminate(terminator); _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); _set_purecall_handler(terminator); _set_invalid_parameter_handler(&invalid_parameter_handler); +#endif + std::signal(SIGABRT, signal_handler); + + std::signal(SIGSEGV, signal_handler); + std::signal(SIGILL, signal_handler); + + std::set_terminate(terminator); std::atexit(normal_exit); } From e6259feeb35f14b01e6a5d61d2b08d7ebb02a7f8 Mon Sep 17 00:00:00 2001 From: JulianGmp Date: Thu, 6 Jul 2023 21:10:39 +0200 Subject: [PATCH 2/3] add stack overflow test type --- TestTool.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/TestTool.cpp b/TestTool.cpp index 0d53370..d540e76 100644 --- a/TestTool.cpp +++ b/TestTool.cpp @@ -17,11 +17,13 @@ enum class TestType Terminate, IllegalInstruction, UnhandledException, + StackOverflow, }; // forward declarations so we can use these in main -[[nodiscard]] std::optional testTypeFromString(std::string_view str); void usage(const char *arg0); +[[nodiscard]] std::optional testTypeFromString(std::string_view str); +void causeStackOverflow(int val); int main(int argc, char **argv) { @@ -89,6 +91,12 @@ int main(int argc, char **argv) throw std::logic_error("Whoops"); } break; + case TestType::StackOverflow: + { + std::cout << "Causing StackOverflow" << std::endl; + causeStackOverflow(0); + } + break; } return 0; @@ -120,5 +128,13 @@ std::optional testTypeFromString(std::string_view str) return TestType::IllegalInstruction; if (str == "UnhandledException" || str == "unhandledexception") return TestType::UnhandledException; + if (str == "StackOverflow" || str == "stackoverflow") + return TestType::StackOverflow; return std::nullopt; } + +void causeStackOverflow(int val) +{ + causeStackOverflow(val + 1); + std::cout << " " << val; +} From 13208ab0d86741da078f68ce70074a84bdce83b1 Mon Sep 17 00:00:00 2001 From: JulianGmp Date: Thu, 6 Jul 2023 21:36:39 +0200 Subject: [PATCH 3/3] code review fixes --- crashlogs.cpp | 44 ++++++++++++++---------------- TestTool.cpp => tests/TestTool.cpp | 5 ++-- 2 files changed, 24 insertions(+), 25 deletions(-) rename TestTool.cpp => tests/TestTool.cpp (96%) diff --git a/crashlogs.cpp b/crashlogs.cpp index 785c0e1..bac6ed0 100644 --- a/crashlogs.cpp +++ b/crashlogs.cpp @@ -31,10 +31,6 @@ #include #endif -//misc std lib stuff -#include -#include - //a decent amount of this was copied/modified from backward.cpp (https://github.com/bombela/backward-cpp) //mostly the stuff related to actually getting crash handlers on crashes //and the thread which is SOLELY there to be able to write a log on a stack overflow, @@ -46,7 +42,7 @@ namespace glaiel::crashlogs { //information for where to save stack traces static std::stacktrace trace; static std::string header_message; - static std::string header_reason; + static int crash_signal = 0; // 0 is not a valid signal id static std::filesystem::path output_folder; static std::string filename = "crash_{timestamp}.txt"; static void (*on_output_crashlog)(std::string crashlog_filename) = NULL; @@ -84,15 +80,16 @@ namespace glaiel::crashlogs { } static std::filesystem::path get_log_filepath(); + static const char* try_get_signal_name(int signal); //output the crashlog file after a crash has occured static void output_crash_log() { std::filesystem::path path = get_log_filepath(); - const auto output_file = get_log_filepath(); - std::cout << "Writing crashlog to " << output_file << std::endl; - std::ofstream log(output_file); + std::ofstream log(path); if(!header_message.empty()) log << header_message << std::endl; - if(!header_reason.empty()) log << header_reason << std::endl; + if(crash_signal != 0) { + log << "Received signal " << crash_signal << " " << try_get_signal_name(crash_signal) << std::endl; + } log << trace; log.close(); @@ -174,9 +171,8 @@ namespace glaiel::crashlogs { //Try to get the string representation of a signal identifier, return an empty string if none is found. //This only covers the signals from the C++ std lib and none of the POSIX or OS specific signal names! - static std::string_view try_get_signal_name(int signal) { - switch (signal) - { + static const char* try_get_signal_name(int signal) { + switch (signal) { case SIGTERM: return "SIGTERM"; case SIGSEGV: @@ -195,20 +191,12 @@ namespace glaiel::crashlogs { //various callbacks needed to get into the crash handler during a crash (borrowed from backward.cpp) static inline void signal_handler(int signal) { - std::stringstream text; - text << "Received signal " << signal << " " << try_get_signal_name(signal); - - std::cout << text.rdbuf() << std::endl; - header_reason = text.str(); - + crash_signal = signal; crash_handler(); std::quick_exit(1); } static inline void terminator() { - std::cout << "Application terminated" << std::endl; - header_reason = "Application terminated"; - crash_handler(); std::quick_exit(1); } @@ -233,18 +221,27 @@ namespace glaiel::crashlogs { output_thread.join(); } +#ifdef OS_WINDOWS //set up all the callbacks needed to get into the crash handler during a crash (borrowed from backward.cpp) void begin_monitoring() { output_thread = std::thread(crash_handler_thread); -#ifdef OS_WINDOWS SetUnhandledExceptionFilter(exception_handler); + std::signal(SIGABRT, signal_handler); + std::set_terminate(terminator); _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); _set_purecall_handler(terminator); _set_invalid_parameter_handler(&invalid_parameter_handler); + + std::atexit(normal_exit); + } #endif - std::signal(SIGABRT, signal_handler); +#ifdef OS_UNIX + void begin_monitoring() { + output_thread = std::thread(crash_handler_thread); + + std::signal(SIGABRT, signal_handler); std::signal(SIGSEGV, signal_handler); std::signal(SIGILL, signal_handler); @@ -252,4 +249,5 @@ namespace glaiel::crashlogs { std::atexit(normal_exit); } +#endif } diff --git a/TestTool.cpp b/tests/TestTool.cpp similarity index 96% rename from TestTool.cpp rename to tests/TestTool.cpp index d540e76..a6e0326 100644 --- a/TestTool.cpp +++ b/tests/TestTool.cpp @@ -8,7 +8,7 @@ #include -#include "crashlogs.h" +#include "../crashlogs.h" enum class TestType { @@ -112,7 +112,8 @@ void usage(const char *arg0) "\t - 'Abort'\n" "\t - 'Terminate'\n" "\t - 'IllegalInstruction'\n" - "\t - 'UnhandledException'" + "\t - 'UnhandledException'\n" + "\t - 'StackOverflow'" << std::endl; }