Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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.
124 changes: 124 additions & 0 deletions TestTool.cpp
Original file line number Diff line number Diff line change
@@ -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 <iostream>
#include <thread>
#include <optional>
#include <string_view>

#include <csignal>

#include "crashlogs.h"

enum class TestType
{
Segfault,
Abort,
Terminate,
IllegalInstruction,
UnhandledException,
};

// forward declarations so we can use these in main
[[nodiscard]] std::optional<TestType> 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 << " <TestType>\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<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;
return std::nullopt;
}
79 changes: 70 additions & 9 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,28 @@
#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

//misc std lib stuff
#include <string_view>
#include <iostream>
Comment thread
JulianGmp marked this conversation as resolved.
Outdated

//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 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;
Expand Down Expand Up @@ -64,6 +79,7 @@ namespace glaiel::crashlogs {
header_message = message;
}
std::string get_crashlog_header_message() {
std::unique_lock<std::mutex> lk(mut);
return header_message;
}

Expand All @@ -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);
Comment thread
JulianGmp marked this conversation as resolved.
Outdated
if(!header_message.empty()) log << header_message << std::endl;
if(!header_reason.empty()) log << header_reason << std::endl;
log << trace;
log.close();

Expand Down Expand Up @@ -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) {
Comment thread
JulianGmp marked this conversation as resolved.
Outdated
switch (signal)
{
Comment thread
JulianGmp marked this conversation as resolved.
Outdated
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();

Comment thread
JulianGmp marked this conversation as resolved.
Outdated
crash_handler();
abort();
std::quick_exit(1);
}

static inline void terminator() {
std::cout << "Application terminated" << std::endl;
header_reason = "Application terminated";

Comment thread
JulianGmp marked this conversation as resolved.
Outdated
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 @@ -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);
Comment thread
JulianGmp marked this conversation as resolved.

std::atexit(normal_exit);
}
Expand Down