Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
bin/
build/
test_logs/
41 changes: 34 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 `<stacktrace>` 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.

<!-- TODO add example output (preferably from windows) -->

### 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.
73 changes: 66 additions & 7 deletions crashlogs.cpp
Original file line number Diff line number Diff line change
@@ -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 <stacktrace>

Expand All @@ -18,20 +25,24 @@
#include <csignal>
#include <exception>
#include <cstdlib>
#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#ifdef OS_WINDOWS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#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 <stacktrace> header for generating stack traces
//and using <atmoic> and a few other more recent C++ features if we're gonna be using C++23 anyway
//and using <atomic> 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;
Expand Down Expand Up @@ -64,16 +75,21 @@ namespace glaiel::crashlogs {
header_message = message;
}
std::string get_crashlog_header_message() {
std::unique_lock<std::mutex> 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();

Expand Down Expand Up @@ -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
Comment thread
JulianGmp marked this conversation as resolved.
crash_handler();
abort();
}
#endif


//callback needed during a normal exit to shut down the thread
static inline void normal_exit() {
Expand All @@ -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);
Expand All @@ -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);
Comment thread
JulianGmp marked this conversation as resolved.

std::atexit(normal_exit);
}
#endif
}
141 changes: 141 additions & 0 deletions tests/TestTool.cpp
Original file line number Diff line number Diff line change
@@ -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 <iostream>
#include <thread>
#include <optional>
#include <string_view>

#include <csignal>

#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<TestType> 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 << " <TestType>\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<TestType> 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;
}