| /* | |
| * Copyright 2025 Rive | |
| */ | |
| #include "common/stacktrace.hpp" | |
| #include <assert.h> | |
| #if defined(NO_REDIRECT_OUTPUT) || defined(__EMSCRIPTEN__) | |
| namespace stacktrace | |
| { | |
| void replace_signal_handlers(SignalFunc signalFunc, | |
| ExitFunc atExitFunc) noexcept | |
| {} | |
| }; // namespace stacktrace | |
| #else | |
| #ifdef _WIN32 | |
| #include <Windows.h> | |
| #include <dbghelp.h> | |
| #else | |
| #include <inttypes.h> | |
| #ifdef RIVE_ANDROID | |
| #include <unwind.h> | |
| #include <android/log.h> | |
| #endif | |
| #include <execinfo.h> | |
| #include <string.h> | |
| #include <errno.h> | |
| #include <unistd.h> | |
| #include <dlfcn.h> | |
| #include <cxxabi.h> | |
| #endif | |
| #ifdef SYS_SIGNAL_H | |
| #include <sys/signal.h> | |
| #else | |
| #include <signal.h> | |
| #endif | |
| #include <stdio.h> | |
| #include <sstream> | |
| static SignalFunc signalFunc; | |
| static int signalRecurseLevel = 0; | |
| // this is outside the namespace so we can use it as if it was defined by the | |
| // c++ runtime | |
| #if defined(SYS_SIGNAL_H) || defined(_WIN32) | |
| const char* strsignal(int signo) | |
| { | |
| switch (signo) | |
| { | |
| case SIGINT: | |
| return "SIGINT"; | |
| case SIGILL: | |
| return "SIGILL"; | |
| case SIGFPE: | |
| return "SIGFPE"; | |
| case SIGSEGV: | |
| return "SIGSEGV"; | |
| case SIGTERM: | |
| return "SIGTERM"; | |
| case SIGBREAK: | |
| return "SIGBREAK"; | |
| case SIGABRT: | |
| return "SIGABRT"; | |
| } | |
| return "Unknown Signal"; | |
| } | |
| #endif | |
| namespace stacktrace | |
| { | |
| #if defined(_WIN32) | |
| LONG WINAPI top_level_exception_handler(PEXCEPTION_POINTERS pExceptionInfo) | |
| { | |
| std::stringstream f; | |
| HANDLE process = GetCurrentProcess(); | |
| SymInitialize(process, NULL, TRUE); | |
| // StackWalk64() may modify context record passed to it, so we will | |
| // use a copy. | |
| CONTEXT context_record = *pExceptionInfo->ContextRecord; | |
| // Initialize stack walking. | |
| STACKFRAME64 stack_frame; | |
| memset(&stack_frame, 0, sizeof(stack_frame)); | |
| #if defined(_WIN64) | |
| int machine_type = IMAGE_FILE_MACHINE_AMD64; | |
| stack_frame.AddrPC.Offset = context_record.Rip; | |
| stack_frame.AddrFrame.Offset = context_record.Rbp; | |
| stack_frame.AddrStack.Offset = context_record.Rsp; | |
| #else | |
| int machine_type = IMAGE_FILE_MACHINE_I386; | |
| stack_frame.AddrPC.Offset = context_record.Eip; | |
| stack_frame.AddrFrame.Offset = context_record.Ebp; | |
| stack_frame.AddrStack.Offset = context_record.Esp; | |
| #endif | |
| stack_frame.AddrPC.Mode = AddrModeFlat; | |
| stack_frame.AddrFrame.Mode = AddrModeFlat; | |
| stack_frame.AddrStack.Mode = AddrModeFlat; | |
| char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)]; | |
| PSYMBOL_INFO pSymbol = (PSYMBOL_INFO)buffer; | |
| pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO); | |
| pSymbol->MaxNameLen = MAX_SYM_NAME; | |
| while (StackWalk64(machine_type, | |
| GetCurrentProcess(), | |
| GetCurrentThread(), | |
| &stack_frame, | |
| &context_record, | |
| NULL, | |
| &SymFunctionTableAccess64, | |
| &SymGetModuleBase64, | |
| NULL)) | |
| { | |
| DWORD64 displacement = 0; | |
| if (!SymFromAddr(process, | |
| (DWORD64)stack_frame.AddrPC.Offset, | |
| &displacement, | |
| pSymbol)) | |
| { | |
| continue; | |
| } | |
| IMAGEHLP_MODULE64 moduleInfo; | |
| ZeroMemory(&moduleInfo, sizeof(IMAGEHLP_MODULE64)); | |
| moduleInfo.SizeOfStruct = sizeof(moduleInfo); | |
| if (::SymGetModuleInfo64(process, pSymbol->ModBase, &moduleInfo)) | |
| f << moduleInfo.ModuleName << ": "; | |
| f << pSymbol->Name << " + 0x" << std::hex << displacement << std::endl; | |
| } | |
| signalFunc(f.str().c_str()); | |
| // match to SymInitialize | |
| SymCleanup(process); | |
| return EXCEPTION_CONTINUE_SEARCH; | |
| } | |
| void build_stack(int signo) | |
| { | |
| CONTEXT context; | |
| ZeroMemory(&context, sizeof(CONTEXT)); | |
| RtlCaptureContext(&context); | |
| EXCEPTION_POINTERS exceptionPointers; | |
| ZeroMemory(&exceptionPointers, sizeof(EXCEPTION_POINTERS)); | |
| exceptionPointers.ContextRecord = &context; | |
| top_level_exception_handler(&exceptionPointers); | |
| } | |
| static void handle_signal(int signo) noexcept | |
| { | |
| // this is because we can accidently spam the signal handler if we can't | |
| // communicate with the server | |
| if (signo == SIGABRT) | |
| { | |
| if (++signalRecurseLevel > 1) | |
| { | |
| return; | |
| } | |
| } | |
| printf("Received signal %i (\"%s\")\n", signo, strsignal(signo)); | |
| build_stack(signo); | |
| // set the signal handler to the original | |
| signal(signo, SIG_DFL); | |
| #ifdef _WIN32 | |
| if (IsDebuggerPresent()) | |
| { | |
| // Cause the debugger to break | |
| DebugBreak(); | |
| } | |
| #endif | |
| // re raise the signal | |
| raise(signo); | |
| } | |
| void replace_signal_handlers(SignalFunc inSignalFunc, | |
| ExitFunc atExitFunc) noexcept | |
| { | |
| assert(inSignalFunc); | |
| assert(atExitFunc); | |
| // signals can only use global vars | |
| signalFunc = inSignalFunc; | |
| SetUnhandledExceptionFilter(top_level_exception_handler); | |
| for (int i = 1; i <= SIGTERM; ++i) | |
| { | |
| signal(i, handle_signal); | |
| } | |
| // for windows SIGABRT is a higher number then SIGTERM, so make sure to | |
| // intercept it as well | |
| signal(SIGABRT, handle_signal); | |
| atexit(atExitFunc); | |
| } | |
| #else | |
| static void get_symbol_line(void* instructionPointer, | |
| uint32_t i, | |
| char* buffer, | |
| std::size_t bufferSize) | |
| { | |
| Dl_info dlInfo; | |
| if (dladdr(instructionPointer, &dlInfo) && dlInfo.dli_sname != nullptr) | |
| { | |
| char* demangled = nullptr; | |
| int status; | |
| demangled = abi::__cxa_demangle(dlInfo.dli_sname, nullptr, 0, &status); | |
| snprintf(buffer, | |
| bufferSize, | |
| "%u %s\n", | |
| i, | |
| (status == 0 && demangled != nullptr) ? demangled | |
| : dlInfo.dli_sname); | |
| free(demangled); | |
| } | |
| else | |
| { | |
| snprintf(buffer, | |
| bufferSize, | |
| "%u <unknown symbol @%p>)\n", | |
| i, | |
| instructionPointer); | |
| } | |
| // snprintf does not null terminate if we hit the limit so add a null | |
| // terminator here to be safe. | |
| buffer[bufferSize - 1] = '\0'; | |
| } | |
| template <std::size_t N> | |
| static void get_symbol_line(void* instructionPointer, | |
| uint32_t i, | |
| char (&buffer)[N]) | |
| { | |
| get_symbol_line(instructionPointer, i, buffer, N); | |
| } | |
| #ifdef RIVE_ANDROID | |
| #define LOG(...) \ | |
| __android_log_print(ANDROID_LOG_ERROR, "rive_android_tests", __VA_ARGS__) | |
| static void handle_signal(int sigNum, siginfo_t* signalInfo, void* userContext) | |
| { | |
| LOG("Received signal %i (\"%s\")\n", sigNum, strsignal(sigNum)); | |
| LOG("Stack trace:\n"); | |
| struct State | |
| { | |
| uint32_t i = 0; | |
| std::stringstream f; | |
| }; | |
| // Not "State st{}": that copy-initializes the stringstream from {}, which | |
| // picks its explicit constructor and fails on older libc++. | |
| State st; | |
| _Unwind_Backtrace( | |
| [](struct _Unwind_Context* context, void* stateVoid) { | |
| auto state = static_cast<State*>(stateVoid); | |
| void* ip = reinterpret_cast<void*>(_Unwind_GetIP(context)); | |
| if (ip != nullptr) | |
| { | |
| char buffer[1024]; | |
| get_symbol_line(ip, state->i, buffer); | |
| LOG("%s", buffer); | |
| state->f << buffer; | |
| } | |
| state->i++; | |
| return _URC_NO_REASON; | |
| }, | |
| &st); | |
| signal(sigNum, SIG_DFL); | |
| signalFunc(st.f.str().c_str()); | |
| raise(sigNum); | |
| } | |
| #else // we are unix | |
| static void handle_signal(int sigNum, siginfo_t* signalInfo, void* userContext) | |
| { | |
| printf("Received signal %i (\"%s\")\n", sigNum, strsignal(sigNum)); | |
| // this is because we can accidently spam the signal handler if we can't | |
| // communicate with the server | |
| if (sigNum == SIGABRT) | |
| { | |
| if (++signalRecurseLevel > 1) | |
| { | |
| return; | |
| } | |
| } | |
| std::stringstream f; | |
| void* stacktrace[1024]; | |
| char** symbols; | |
| char stringBuff[1024]; | |
| // this is technically unsafe since it uses malloc behind the scene but it's | |
| // way simpler then the alternative and this is just for testing anyway so | |
| // it's fine | |
| int numFrames = backtrace(stacktrace, 1024); | |
| symbols = backtrace_symbols(stacktrace, numFrames); | |
| for (int i = 1; i < numFrames; i++) | |
| { | |
| get_symbol_line(stacktrace[i], i, stringBuff, std::size(stringBuff)); | |
| f << stringBuff << symbols[i] << "\n"; | |
| } | |
| free(symbols); | |
| // send stack to server | |
| signalFunc(f.str().c_str()); | |
| // Terminate like the other platforms: no static teardown, which would | |
| // release GPU state against half destroyed globals mid signal. | |
| fflush(nullptr); | |
| signal(sigNum, SIG_DFL); | |
| raise(sigNum); | |
| } | |
| #endif | |
| void replace_signal_handlers(SignalFunc inSignalFunc, | |
| ExitFunc atExitFunc) noexcept | |
| { | |
| assert(inSignalFunc); | |
| assert(atExitFunc); | |
| signalFunc = inSignalFunc; | |
| struct sigaction action; | |
| struct sigaction oldAction; | |
| action.sa_flags = SA_SIGINFO | SA_ONSTACK; | |
| #if defined(__LP64__) && defined(__APPLE__) | |
| action.sa_flags |= SA_64REGSET; | |
| #endif | |
| action.sa_sigaction = &handle_signal; | |
| for (int i = 1; i <= SIGTERM; ++i) | |
| { | |
| sigaction(i, &action, &oldAction); | |
| } | |
| atexit(atExitFunc); | |
| } | |
| #endif // defined _WIN32 | |
| }; // namespace stacktrace | |
| #endif // defined(NO_REDIRECT_OUTPUT) || defined(__EMSCRIPTEN__) |