This sample demonstrates how to intercept and handle crashes in a way that plays nicely with the X-Plane crash reporter.
The core idea is to intercept all crashes by installing appropriate crash handlers and then filtering out crashes that weren’t caused by the plugin. To do so, we track both the threads created by us at runtime, as well as checking whether the plugin is active if we crash on the main thread. If the crash wasn’t caused by the plugin, the crash is forwarded to the next crash handler (potentially X-Plane, or another plugin) which then gets a chance to process the crash.
If the crash is caused by the plugin, we eat the crash to not falsely trigger X-Planes crash reporter and pollute the X-Plane crash database. This example also writes a minidump file on Windows and a simple backtrace on macOS and Linux. Production code might want to integrate more sophisticated crash handling
- Download as a project for Xcode 14 or newer (64-bit Intel)
- Download as a project for Microsoft Visual Studio 2017 (64-bit; requires Windows 8.1 SDK)
- Download as a project for GCC 4.x/Linux (64-bit)
#include "XPLMDataAccess.h"
#include "XPLMMenus.h"
#include "XPLMProcessing.h"
#include "XPLMPlugin.h"
// This option enables support for multi threaded crash handling.
// It requires C++11 or newer
#define SUPPORT_BACKGROUND_THREADS 0
#include <cstring>
#include <cstdlib>
#if SUPPORT_BACKGROUND_THREADS
#include <atomic>
#include <set>
#include <thread>
#include <mutex>
#endif
#if APL || LIN
#include <signal.h>
#include <execinfo.h>
#include <fcntl.h>
#include <unistd.h>
#endif
/*
* This plugin demonstrates how to intercept and handle crashes in a way that plays nicely with the X-Plane crash reporter.
* The core idea is to intercept all crashes by installing appropriate crash handlers and then filtering out crashes that weren't caused
* by our plugin. To do so, we track both the threads created by us at runtime, as well as checking whether our plugin is active when we crash on the main thread.
* If the crash wasn't caused by us, the crash is forwarded to the next crash handler (potentially X-Plane, or another plugin) which then gets a chance
* to process the crash.
* If the crash is caused by us, we eat the crash to not falsely trigger X-Planes crash reporter and pollute the X-Plane crash database. This example
* also writes a minidump file on Windows and a simple backtrace on macOS and Linux. Production code might want to integrate more sophisticated crash handling
*/
#if SUPPORT_BACKGROUND_THREADS
static std::thread::id s_main_thread;
static std::atomic_flag s_thread_lock;
static std::set<std::thread::id> s_known_threads;
#endif
static XPLMPluginID s_my_plugin_id;
// Function called when we detect a crash that was caused by us
void handle_crash(void *context);
#if APL || LIN
static struct sigaction s_prev_sigsegv = {};
static struct sigaction s_prev_sigabrt = {};
static struct sigaction s_prev_sigfpe = {};
static struct sigaction s_prev_sigint = {};
static struct sigaction s_prev_sigill = {};
static struct sigaction s_prev_sigterm = {};
static void handle_posix_sig(int sig, siginfo_t *siginfo, void *context);
#endif
#if IBM
static LPTOP_LEVEL_EXCEPTION_FILTER s_previous_windows_exception_handler;
LONG WINAPI handle_windows_exception(EXCEPTION_POINTERS *ei);
#endif
#if SUPPORT_BACKGROUND_THREADS
// Registers the calling thread with the crash handler. We use this to figure out if a crashed thread belongs to us when we later try to figure out if we caused a crash
void register_thread_for_crash_handler()
{
while(s_thread_lock.test_and_set(std::memory_order_acquire))
{}
s_known_threads.insert(std::this_thread::get_id());
s_thread_lock.clear(std::memory_order_release);
}
// Unregisters the calling thread from the crash handler. MUST be called at the end of thread that was registered via register_thread_for_crash_handler()
void unregister_thread_from_crash_handler()
{
while(s_thread_lock.test_and_set(std::memory_order_acquire))
{}
s_known_threads.erase(std::this_thread::get_id());
s_thread_lock.clear(std::memory_order_release);
}
#endif
// Registers the global crash handler. Should be called from XPluginStart
void register_crash_handler()
{
#if SUPPORT_BACKGROUND_THREADS
s_main_thread = std::this_thread::get_id();
#endif
s_my_plugin_id = XPLMGetMyID();
#if APL || LIN
struct sigaction sig_action = { .sa_sigaction = handle_posix_sig };
sigemptyset(&sig_action.sa_mask);
#if LIN
static uint8_t alternate_stack[SIGSTKSZ];
stack_t ss = {
.ss_sp = (void*)alternate_stack,
.ss_size = SIGSTKSZ,
.ss_flags = 0
};
sigaltstack(&ss, NULL);
sig_action.sa_flags = SA_SIGINFO | SA_ONSTACK;
#else
sig_action.sa_flags = SA_SIGINFO;
#endif
sigaction(SIGSEGV, &sig_action, &s_prev_sigsegv);
sigaction(SIGABRT, &sig_action, &s_prev_sigabrt);
sigaction(SIGFPE, &sig_action, &s_prev_sigfpe);
sigaction(SIGINT, &sig_action, &s_prev_sigint);
sigaction(SIGILL, &sig_action, &s_prev_sigill);
sigaction(SIGTERM, &sig_action, &s_prev_sigterm);
#endif
#if IBM
// Load the debug helper library into the process already, this way we don't have to hit the dynamic loader
// in an exception context where it's potentially unsafe to do so.
HMODULE module = ::GetModuleHandleA("dbghelp.dll");
if(!module)
module = ::LoadLibraryA("dbghelp.dll");
(void)module;
s_previous_windows_exception_handler = SetUnhandledExceptionFilter(handle_windows_exception);
#endif
}
// Unregisters the global crash handler. You need to call this in XPluginStop so we can clean up after ourselves
void unregister_crash_handler()
{
#if APL || LIN
sigaction(SIGSEGV, &s_prev_sigsegv, NULL);
sigaction(SIGABRT, &s_prev_sigabrt, NULL);
sigaction(SIGFPE, &s_prev_sigfpe, NULL);
sigaction(SIGINT, &s_prev_sigint, NULL);
sigaction(SIGILL, &s_prev_sigill, NULL);
sigaction(SIGTERM, &s_prev_sigterm, NULL);
#endif
#if IBM
SetUnhandledExceptionFilter(s_previous_windows_exception_handler);
#endif
}
#if SUPPORT_BACKGROUND_THREADS
// A RAII helper class to register and unregister threads to participate in crash detection
class StThreadCrashCookie
{
public:
StThreadCrashCookie()
{
register_thread_for_crash_handler();
}
~StThreadCrashCookie()
{
unregister_thread_from_crash_handler();
}
};
#endif
// Predicates that returns true if a thread is caused by us
// The main idea is to check the plugin ID if we are on the main thread,
// if not, we check if the current thread is known to be from us.
// Returns false if the crash was caused by code that didn't come from our plugin
bool is_us_executing()
{
#if SUPPORT_BACKGROUND_THREADS
const std::thread::id thread_id = std::this_thread::get_id();
if(thread_id == s_main_thread)
{
// Check if the plugin executing is our plugin.
// XPLMGetMyID() will return the ID of the currently executing plugin. If this is us, then it will return the plugin ID that we have previously stashed away
return (s_my_plugin_id == XPLMGetMyID());
}
if(s_thread_lock.test_and_set(std::memory_order_acquire))
{
// We couldn't acquire our lock. In this case it's better if we just say it's not us so we don't eat the exception
return false;
}
const bool is_our_thread = (s_known_threads.find(thread_id) != s_known_threads.end());
s_thread_lock.clear(std::memory_order_release);
return is_our_thread;
#else
return (s_my_plugin_id == XPLMGetMyID());
#endif
}
#if APL || LIN
static void handle_posix_sig(int sig, siginfo_t *siginfo, void *context)
{
if(is_us_executing())
{
static bool has_called_out = false;
if(!has_called_out)
{
has_called_out = true;
handle_crash((void *)sig);
}
abort();
}
// Forward the signal to the other handlers
#define FORWARD_SIGNAL(sigact) \
do { \
if((sigact)->sa_sigaction && ((sigact)->sa_flags & SA_SIGINFO)) \
(sigact)->sa_sigaction(sig, siginfo, context); \
else if((sigact)->sa_handler) \
(sigact)->sa_handler(sig); \
} while (0)
switch(sig)
{
case SIGSEGV:
FORWARD_SIGNAL(&s_prev_sigsegv);
break;
case SIGABRT:
FORWARD_SIGNAL(&s_prev_sigabrt);
break;
case SIGFPE:
FORWARD_SIGNAL(&s_prev_sigfpe);
break;
case SIGILL:
FORWARD_SIGNAL(&s_prev_sigill);
break;
case SIGTERM:
FORWARD_SIGNAL(&s_prev_sigterm);
break;
}
#undef FORWARD_SIGNAL
abort();
}
#endif
#if IBM
LONG WINAPI handle_windows_exception(EXCEPTION_POINTERS *ei)
{
if(is_us_executing())
{
handle_crash(ei);
return EXCEPTION_CONTINUE_SEARCH;
}
if(s_previous_windows_exception_handler)
return s_previous_windows_exception_handler(ei);
return EXCEPTION_CONTINUE_SEARCH;
}
#endif
// Runtime
#if IBM
void write_mini_dump(PEXCEPTION_POINTERS exception_pointers);
#endif
void handle_crash(void *context)
{
#if APL || LIN
// NOTE: This is definitely NOT production code
// backtrace and backtrace_symbols are NOT signal handler safe and are just put in here for demonstration purposes
// A better alternative would be to use something like libunwind here
void *frames[64];
int frame_count = backtrace(frames, 64);
char **names = backtrace_symbols(frames, frame_count);
const int fd = open("backtrace.txt", O_CREAT | O_RDWR | O_TRUNC | O_SYNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if(fd >= 0)
{
for(int i = 0; i < frame_count; ++ i)
{
write(fd, names[i], strlen(names[i]));
write(fd, "\n", 1);
}
close(fd);
}
#endif
#if IBM
// Create a mini-dump file that can be later opened up in Visual Studio or WinDbg to do post mortem debugging
write_mini_dump((PEXCEPTION_POINTERS)context);
#endif
}
#if IBM
#include <DbgHelp.h>
typedef BOOL(WINAPI *MINIDUMPWRITEDUMP)(HANDLE hProcess, DWORD dwPid, HANDLE hFile,
MINIDUMP_TYPE DumpType,
CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam);
void write_mini_dump(PEXCEPTION_POINTERS exception_pointers)
{
HMODULE module = ::GetModuleHandleA("dbghelp.dll");
if(!module)
module = ::LoadLibraryA("dbghelp.dll");
if(module)
{
const MINIDUMPWRITEDUMP pDump = MINIDUMPWRITEDUMP(::GetProcAddress(module, "MiniDumpWriteDump"));
if(pDump)
{
// Create dump file
const HANDLE handle = ::CreateFileA("crash_dump.dmp", GENERIC_WRITE, FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if(handle != INVALID_HANDLE_VALUE)
{
MINIDUMP_EXCEPTION_INFORMATION exception_information = {};
exception_information.ThreadId = ::GetCurrentThreadId();
exception_information.ExceptionPointers = exception_pointers;
exception_information.ClientPointers = false;
pDump(GetCurrentProcess(), GetCurrentProcessId(), handle, MiniDumpNormal, &exception_information, nullptr, nullptr);
::CloseHandle(handle);
}
}
}
}
#endif
#if SUPPORT_BACKGROUND_THREADS
static std::thread s_background_thread;
static std::atomic<bool> s_background_thread_shutdown(false);
static std::atomic<bool> s_background_thread_want_crash(false);
void background_thread_func()
{
StThreadCrashCookie thread_cookie; // Make sure our thread is registered with the crash handling system
while(!s_background_thread_shutdown.load(std::memory_order_acquire))
{
if(s_background_thread_want_crash.load(std::memory_order_acquire))
{
s_background_thread_want_crash = false;
int *death_is_a_destination = NULL;
*death_is_a_destination = 1;
}
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
#endif
// User Interface
#define MENU_ITEM_CRASH_MAIN_THREAD (void *)0x1
#define MENU_ITEM_CRASH_BACKGROUND_THREAD (void *)0x2
void menu_handler(void *inMenuRef, void *inItemRef)
{
if(inItemRef == MENU_ITEM_CRASH_MAIN_THREAD)
{
int *death_is_a_destination = NULL;
*death_is_a_destination = 1;
}
#if SUPPORT_BACKGROUND_THREADS
else if(inItemRef == MENU_ITEM_CRASH_BACKGROUND_THREAD)
{
s_background_thread_want_crash = true;
}
#endif
}
// Plugin functions
PLUGIN_API int XPluginStart(char* name, char* sig, char* desc)
{
strcpy(name, "Crash Handling example");
strcpy(sig, "com.laminarresearch.example.crash-handling");
strcpy(desc, "Example plugin for crash handling");
register_crash_handler();
#if SUPPORT_BACKGROUND_THREADS
s_background_thread = std::thread(&background_thread_func);
#endif
const int plugin_menu_item = XPLMAppendMenuItem(XPLMFindPluginsMenu(), "Crash Handler", NULL, 0);
const XPLMMenuID menu = XPLMCreateMenu("Crash Handler", XPLMFindPluginsMenu(), plugin_menu_item, menu_handler, NULL);
XPLMAppendMenuItem(menu, "Crash main thread", MENU_ITEM_CRASH_MAIN_THREAD, 0);
#if SUPPORT_BACKGROUND_THREADS
XPLMAppendMenuItem(menu, "Crash background thread", MENU_ITEM_CRASH_BACKGROUND_THREAD, 0);
#endif
return 1;
}
PLUGIN_API void XPluginStop(void)
{
#if SUPPORT_BACKGROUND_THREADS
// Tell the background thread to shutdown and then wait for it
s_background_thread_shutdown.store(true, std::memory_order_release);
s_background_thread.join();
#endif
unregister_crash_handler();
}
PLUGIN_API int XPluginEnable(void)
{
return 1;
}
PLUGIN_API void XPluginDisable(void)
{
}
PLUGIN_API void XPluginReceiveMessage(XPLMPluginID from, int msg, void* param)
{}
Is this a suggestion that plugin developers add similar crash detection into their plugins, or is this just meant to be illustrative of signal handling?
It is not a suggestion that you add crash handling; it provides examples and guidelines so that if you _do_ provide crash handling, you do so in a way that doesn’t interfere with everyone else’s crash handling.
It is not meant to be illustrative of signal handling in general – your plugin should not be using posix signals, except as shown here to catch seg faults.
Hint: If you’re using gcc on Windows instead of Visual Studio, here’s a special version of gdb that can read the DMP files and use the gcc debug symbols in your XPL. It can also show the backtrace and some other stuff: https://github.com/ssbssa/gdb/releases
This example doen’t compile on Linux. It uses C99 feature “designated initializers” that is not avaliable in CPP.
root# make
mkdir -p build/obj64/
g++ -DXPLM200=1 -DXPLM210=1 -DXPLM300=1 -DXPLM301=1 -DXPLM302=1 -DXPLM303=1 -DAPL=0 -DIBM=0 -DLIN=1 -I./SDK/CHeaders/XPLM -I./SDK/CHeaders/Widgets -fPIC -fvisibility=hidden -m64 -c Crash-Handling.cpp -o build/obj64/Crash-Handling.o
Crash-Handling.cpp: In function ‘void register_crash_handler()’:
Crash-Handling.cpp:98:34: error: expected primary-expression before ‘.’ token
98 | struct sigaction sig_action = { .sa_sigaction = handle_posix_sig };
| ^
Crash-Handling.cpp:108:2: error: designator order for field ‘stack_t::ss_flags’ does not match declaration order in ‘stack_t’
108 | };
| ^
Crash-Handling.cpp: In function ‘void handle_posix_sig(int, siginfo_t*, void*)’:
Crash-Handling.cpp:212:25: warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]
212 | handle_crash((void *)sig);
| ^~~
make: *** [Makefile:66: build/obj64/Crash-Handling.o] Error 1