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/crashlogs.cpp b/crashlogs.cpp index b687f85..bac6ed0 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,24 @@ #include #include #include -#define WIN32_LEAN_AND_MEAN -#include + +#ifdef OS_WINDOWS + #define WIN32_LEAN_AND_MEAN + #include +#endif //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 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; @@ -64,16 +75,21 @@ namespace glaiel::crashlogs { header_message = message; } std::string get_crashlog_header_message() { + std::unique_lock lk(mut); return header_message; } 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(); - std::ofstream log(get_log_filepath()); + std::ofstream log(path); if(!header_message.empty()) log << header_message << std::endl; + if(crash_signal != 0) { + log << "Received signal " << crash_signal << " " << try_get_signal_name(crash_signal) << std::endl; + } log << trace; log.close(); @@ -153,23 +169,50 @@ 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 const char* 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) { + crash_signal = signal; crash_handler(); - abort(); + std::quick_exit(1); } + static inline void terminator() { 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() { @@ -178,6 +221,7 @@ 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); @@ -191,4 +235,19 @@ namespace glaiel::crashlogs { std::atexit(normal_exit); } +#endif + +#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); + + std::set_terminate(terminator); + + std::atexit(normal_exit); + } +#endif } diff --git a/tests/TestTool.cpp b/tests/TestTool.cpp new file mode 100644 index 0000000..a6e0326 --- /dev/null +++ b/tests/TestTool.cpp @@ -0,0 +1,141 @@ +// 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, + StackOverflow, +}; + +// forward declarations so we can use these in main +void usage(const char *arg0); +[[nodiscard]] std::optional testTypeFromString(std::string_view str); +void causeStackOverflow(int val); + +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; + case TestType::StackOverflow: + { + std::cout << "Causing StackOverflow" << std::endl; + causeStackOverflow(0); + } + 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'\n" + "\t - 'StackOverflow'" + << 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; + if (str == "StackOverflow" || str == "stackoverflow") + return TestType::StackOverflow; + return std::nullopt; +} + +void causeStackOverflow(int val) +{ + causeStackOverflow(val + 1); + std::cout << " " << val; +}