diff --git a/README.md b/README.md index 5679ca9..3444976 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,8 @@ 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. + +### 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 007dcce..75291af 100644 --- a/crashlogs.cpp +++ b/crashlogs.cpp @@ -23,15 +23,16 @@ //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 +65,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,14 +159,35 @@ 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) { + 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); } __declspec(noinline) static LONG WINAPI exception_handler(EXCEPTION_POINTERS*) { crash_handler(); @@ -184,6 +211,8 @@ namespace glaiel::crashlogs { SetUnhandledExceptionFilter(exception_handler); std::signal(SIGABRT, signal_handler); + std::signal(SIGSEGV, signal_handler); + std::signal(SIGILL, signal_handler); std::set_terminate(terminator); _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); _set_purecall_handler(terminator); 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; +}