diff --git a/Obelisk/EntryPoint.cpp b/Obelisk/EntryPoint.cpp index 6aa26b7e..0d41584a 100644 --- a/Obelisk/EntryPoint.cpp +++ b/Obelisk/EntryPoint.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -12,9 +13,12 @@ using namespace ZEngine; using namespace ZEngine::Logging; using namespace ZEngine::Core::Memory; using namespace ZEngine::Applications; +using namespace ZEngine::CrashHandlers; int applicationEntryPoint(int argc, char* argv[]) { + CrashHandler::Install("Obelisk", "1.0.0", "CrashDumps"); + MemoryManager manager = {}; MemoryConfiguration config = {.BufferSize = ZGiga(3u)}; manager.Initialize(config); @@ -43,7 +47,10 @@ int applicationEntryPoint(int argc, char* argv[]) app->EnableRenderOverlay = true; } - app->ConfigFile = config_file.c_str(); + auto config_file_str_size = config_file.size() + 1; + auto config_file_str = ZPushString(arena, config_file_str_size); + Helpers::secure_strncpy(config_file_str, config_file_str_size, config_file.c_str(), config_file.size()); + app->ConfigFile = config_file_str; app->Initialize(arena); app->Run(); @@ -53,6 +60,7 @@ int applicationEntryPoint(int argc, char* argv[]) manager.Shutdown(); + CrashHandler::Uninstall(); return 0; } diff --git a/Scripts/ClangFormat.ps1 b/Scripts/ClangFormat.ps1 index 7bebfbab..5cd8ed12 100644 --- a/Scripts/ClangFormat.ps1 +++ b/Scripts/ClangFormat.ps1 @@ -34,7 +34,7 @@ $ErrorActionPreference = "Stop" . (Join-Path $PSScriptRoot Shared.ps1) -$srcFiles = Get-ChildItem -Path $SourceDirectory -Recurse -File | Where-Object { $_.Name -notlike "CMakeLists*" -and $_.Name -notlike "*.json" -and $_.Name -notlike "*.spv" -and $_.Name -notlike "*.md"} +$srcFiles = Get-ChildItem -Path $SourceDirectory -Recurse -File | Where-Object { $_.Name -notlike "CMakeLists*" -and $_.Name -notlike "*.json" -and $_.Name -notlike "*.spv" -and $_.Name -notlike "*.md" -and $_.Name -notlike "*.mm"} if ($srcFiles.Count -eq 0) { Write-Host "No source files found in the specified directory." diff --git a/ZEngine/ZEngine/CMakeLists.txt b/ZEngine/ZEngine/CMakeLists.txt index f7d9b837..6b9a73a8 100644 --- a/ZEngine/ZEngine/CMakeLists.txt +++ b/ZEngine/ZEngine/CMakeLists.txt @@ -31,26 +31,97 @@ configure_file( @ONLY ) -file (GLOB_RECURSE HEADER_FILES_LIST CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.h) -file (GLOB_RECURSE CPP_FILES_LIST CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) -file (GLOB_RECURSE RESOURCE_FILES_LIST CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/../Resources/Shaders/*.*) +# Per-subsystem source globs — add platform-specific .cpp files by appending to +# the relevant variable inside the per-platform blocks further below, e.g.: +# list(APPEND ZENGINE_SOURCES_WINDOWS +# ${CMAKE_CURRENT_SOURCE_DIR}/Windows/Inputs/Platform/Win32/Win32Input.cpp) +# +file(GLOB_RECURSE ZENGINE_SOURCES_CORE CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/Core/*.cpp +) +file(GLOB_RECURSE ZENGINE_SOURCES_RENDERING CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/Rendering/*.cpp +) +file(GLOB_RECURSE ZENGINE_SOURCES_WINDOWS CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/Windows/*.cpp +) +file(GLOB_RECURSE ZENGINE_SOURCES_APPLICATION CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/Applications/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Layers/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Controllers/*.cpp +) +file(GLOB_RECURSE ZENGINE_SOURCES_MANAGERS CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/Managers/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Serializers/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Importers/*.cpp +) +file(GLOB_RECURSE ZENGINE_SOURCES_HELPERS CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/Helpers/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Logging/*.cpp +) +file(GLOB_RECURSE ZENGINE_SOURCES_EVENT CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/Event/*.cpp +) +file(GLOB_RECURSE ZENGINE_SOURCES_HARDWARE CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/Hardwares/*.cpp +) + +if(APPLE) + list(APPEND ZENGINE_SOURCES_CRASH_HANDLERS + ${CMAKE_CURRENT_SOURCE_DIR}/CrashHandlers/CrashHandlerMacOS.mm + ) +elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + list(APPEND ZENGINE_SOURCES_CRASH_HANDLERS + ${CMAKE_CURRENT_SOURCE_DIR}/CrashHandlers/CrashHandlerWindows.cpp + ) +elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + list(APPEND ZENGINE_SOURCES_CRASH_HANDLERS + ${CMAKE_CURRENT_SOURCE_DIR}/CrashHandlers/CrashHandlerLinux.cpp + ) +else() + message(WARNING "CrashHandler: unsupported platform '${CMAKE_SYSTEM_NAME}' — crash reporting disabled") +endif() -source_group (TREE ${PROJECT_SOURCE_DIR}/ZEngine PREFIX "Source Files" FILES ${HEADER_FILES_LIST} ${CPP_FILES_LIST}) -source_group (TREE ${PROJECT_SOURCE_DIR}/../Resources PREFIX "Resources Files" FILES ${RESOURCE_FILES_LIST}) +file(GLOB_RECURSE ZENGINE_RESOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/../Resources/Shaders/*.*) + +source_group(TREE ${PROJECT_SOURCE_DIR}/ZEngine PREFIX "Source Files" FILES + ${ZENGINE_HEADERS} + ${ZENGINE_SOURCES_CORE} + ${ZENGINE_SOURCES_RENDERING} + ${ZENGINE_SOURCES_WINDOWS} + ${ZENGINE_SOURCES_APPLICATION} + ${ZENGINE_SOURCES_MANAGERS} + ${ZENGINE_SOURCES_HELPERS} + ${ZENGINE_SOURCES_EVENT} + ${ZENGINE_SOURCES_HARDWARE} + ${ZENGINE_SOURCES_CRASH_HANDLERS} +) +source_group(TREE ${PROJECT_SOURCE_DIR}/../Resources PREFIX "Resources Files" FILES ${ZENGINE_RESOURCES}) # ZEngine source files # -add_library (zEngineLib STATIC) - -target_sources(zEngineLib PRIVATE ${CPP_FILES_LIST} - PUBLIC FILE_SET HEADERS BASE_DIRS ${PROJECT_SOURCE_DIR}) +add_library(zEngineLib STATIC) +target_sources(zEngineLib PUBLIC FILE_SET HEADERS BASE_DIRS ${PROJECT_SOURCE_DIR}) -if(${CMAKE_BUILD_TYPE} STREQUAL "Debug") - target_compile_definitions (zEngineLib PUBLIC ENABLE_VULKAN_VALIDATION_LAYER) -endif() +target_sources(zEngineLib PRIVATE + ${ZENGINE_SOURCES_CORE} + ${ZENGINE_SOURCES_RENDERING} + ${ZENGINE_SOURCES_WINDOWS} + ${ZENGINE_SOURCES_APPLICATION} + ${ZENGINE_SOURCES_MANAGERS} + ${ZENGINE_SOURCES_HELPERS} + ${ZENGINE_SOURCES_EVENT} + ${ZENGINE_SOURCES_HARDWARE} + ${ZENGINE_SOURCES_CRASH_HANDLERS} + ${CMAKE_CURRENT_SOURCE_DIR}/Engine.cpp +) target_compile_definitions (zEngineLib + PRIVATE + $<$,$>:ZENGINE_CRASH_HANDLER_ENABLED=1> + $<$:ZENGINE_RELEASE=1> + $<$:ZENGINE_RELWITHDEBINFO=1> PUBLIC ZENGINE_PLATFORM ENABLE_VULKAN_SYNCHRONIZATION_LAYER @@ -60,15 +131,12 @@ target_compile_definitions (zEngineLib target_link_libraries (zEngineLib PUBLIC imported::ZEngine_External_Dependencies) if(CMAKE_SYSTEM_NAME STREQUAL "Windows") - target_link_libraries (zEngineLib PUBLIC imported::cppwinrt_headers WindowsApp.lib) + target_link_libraries (zEngineLib PUBLIC imported::cppwinrt_headers WindowsApp.lib DbgHelp.lib User32.lib) target_compile_definitions(zEngineLib PUBLIC NOMINMAX GLFW_EXPOSE_NATIVE_WIN32) -endif() - -if (CMAKE_SYSTEM_NAME STREQUAL "Linux") - target_link_libraries(zEngineLib PUBLIC stdc++fs) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_libraries(zEngineLib PUBLIC stdc++fs dl) target_compile_definitions(zEngineLib PUBLIC GLFW_EXPOSE_NATIVE_WAYLAND) -endif () - -if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") +elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") + target_link_libraries(zEngineLib PRIVATE "-lobjc" "-framework AppKit" "-framework CoreGraphics") target_compile_definitions(zEngineLib PUBLIC GLFW_EXPOSE_NATIVE_COCOA) endif() diff --git a/ZEngine/ZEngine/CrashHandlers/CrashHandler.h b/ZEngine/ZEngine/CrashHandlers/CrashHandler.h new file mode 100644 index 00000000..fd63571b --- /dev/null +++ b/ZEngine/ZEngine/CrashHandlers/CrashHandler.h @@ -0,0 +1,57 @@ +#pragma once + +namespace ZEngine::CrashHandlers +{ + struct CrashHandler + { + using PreCrashFn = void (*)(void* ctx); + + // Install the platform crash handler. + // app_name — e.g. "GameName" + // version — e.g. "1.0.0" + // crash_log_dir — directory where crash logs and .dmp files are written. + // Created if it does not exist. + // + // Call once, before any engine subsystem is initialized. + // No-op if already installed. + static void Install(const char* app_name, const char* version, const char* crash_log_dir); + + // Remove the platform crash handler and restore default OS behavior. + // Call at engine shutdown. Safe to call if Install() was never called. + static void Uninstall(); + + // Set a callback to be called just before the crash dump is written. + // This is useful for flushing logs, saving game state, etc. + // + // The callback must complete within a defined timeout (default 2 seconds) or the crash handler will proceed to write the dump anyway. + // On Windows, a helper thread's timeout fires + // On Linux, macOS SIGALRM is raised on the callback thread + // + // Only one callback can be set at a time. Setting a new callback replaces the previous one. + // It must not allocate memory on the ZEngine heap, or call ZENGINE_VALIDATE_ASSERT. + static void SetPreCrashCallback(PreCrashFn fn, void* ctx = nullptr); + + // Called by the platform handler (SEH filter on Windows, signal handler + // on POSIX) to perform the cross-platform crash response. + // signal_or_exception — human-readable description, e.g. + // "Access Violation at 0x0000000000000000" + // context — platform-specific context: + // Windows: EXCEPTION_POINTERS* + // POSIX: ucontext_t* + // + // This function does not return under normal conditions. + [[noreturn]] static void OnCrash(const char* signal_or_exception, void* ctx = nullptr); + + // Called by ZENGINE_VALIDATE_ASSERT to produce a crash report for + // assertion failures in Release/RelWithDebInfo builds. + // file — __FILE__ + // line — __LINE__ + // message — the assertion condition and user message string + [[noreturn]] static void OnAssertionFailure(const char* file, int line, const char* message); + + private: + CrashHandler() = delete; + + static void StoreMetadata(const char* app_name, const char* version, const char* crash_log_dir); + }; +} // namespace ZEngine::CrashHandlers \ No newline at end of file diff --git a/ZEngine/ZEngine/CrashHandlers/CrashHandlerInternal.h b/ZEngine/ZEngine/CrashHandlers/CrashHandlerInternal.h new file mode 100644 index 00000000..6e6688fb --- /dev/null +++ b/ZEngine/ZEngine/CrashHandlers/CrashHandlerInternal.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include + +namespace ZEngine::CrashHandlers +{ + namespace + { + constexpr size_t kMaxPathLen = 512; + constexpr size_t kMaxNameLen = 128; + constexpr size_t kMaxCommentLen = 1024; + constexpr int kCallbackTimeoutSec = 2; + + struct CrashHandlerState + { + bool Installed = false; + bool UserConsentUpload = false; + char AppName[kMaxNameLen] = {}; + char Version[kMaxNameLen] = {}; + char CrashLogDir[kMaxPathLen] = {}; + char UserComment[kMaxCommentLen] = {}; + + void* PreCrashCtx = nullptr; + CrashHandler::PreCrashFn PreCrashFn = nullptr; + }; + + CrashHandlerState g_state = {}; + } // namespace +} // namespace ZEngine::CrashHandlers \ No newline at end of file diff --git a/ZEngine/ZEngine/CrashHandlers/CrashHandlerLinux.cpp b/ZEngine/ZEngine/CrashHandlers/CrashHandlerLinux.cpp new file mode 100644 index 00000000..c40f23d9 --- /dev/null +++ b/ZEngine/ZEngine/CrashHandlers/CrashHandlerLinux.cpp @@ -0,0 +1,870 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ZEngine::CrashHandlers +{ + using ZEngine::Helpers::secure_strlen; + + static constexpr int kTrackedSignals[] = {SIGSEGV, SIGBUS, SIGFPE, SIGILL, SIGTRAP, SIGABRT, SIGSYS}; + static constexpr int kNumTrackedSignals = sizeof(kTrackedSignals) / sizeof(kTrackedSignals[0]); + static constexpr int kMaxBacktraceFrames = 64; + + struct WorkerThreadParams + { + int SignalNumber = 0; + cstring SignalOrException = nullptr; + void* BacktraceAddrs[kMaxBacktraceFrames] = {}; + int BacktraceSize = 0; + char LogPath[kMaxPathLen] = {}; + bool BacktraceFromSignal = false; + }; + + static constexpr size_t kAltStackSize = 32 * 1024; + static struct sigaction s_previous_actions[NSIG] = {}; + static char s_alt_stack_mem[kAltStackSize]; + static int s_crash_pipe[2] = {-1, -1}; + static WorkerThreadParams s_crash_params = {}; + static pthread_t s_worker_thread; + + static const char* SignalToString(int sig) + { + switch (sig) + { + case SIGSEGV: + return "Segmentation Fault (SIGSEGV)"; + case SIGBUS: + return "Bus Error (SIGBUS)"; + case SIGFPE: + return "Floating Point Exception (SIGFPE)"; + case SIGILL: + return "Illegal Instruction (SIGILL)"; + case SIGTRAP: + return "Trace Trap (SIGTRAP)"; + case SIGABRT: + return "Abort Signal (SIGABRT)"; + case SIGSYS: + return "Bad System Call (SIGSYS)"; + default: + return "Unknown Signal"; + } + } + + static void SafeWrite(int fd, cstring str) + { + if (fd < 0 || !str) + return; + size_t len = secure_strlen(str); + while (len > 0) + { + ssize_t written = write(fd, str, len); + if (written <= 0) + break; + len -= static_cast(written); + str += written; + } + } + + static bool HasGUISession() + { + if (getenv("ZENGINE_CRASH_NO_DIALOG")) + return false; + if (getenv("CI")) + return false; + if (!getenv("DISPLAY") && !getenv("WAYLAND_DISPLAY")) + return false; + return true; + } + + typedef void GtkWidget; + typedef void GtkTextBuffer; + typedef void GtkStyleContext; + typedef void GtkStyleProvider; + typedef void PangoFontDescription; + typedef void GtkCssProvider; + typedef char gchar; + typedef int gint; + typedef int gboolean; + typedef void* gpointer; + typedef long gssize; + + // GtkTextIter is a value type — its ABI size on 64-bit Linux is 80 bytes. + struct GtkTextIter + { + alignas(8) char _[80]; + }; + + enum : unsigned int + { + kGtkWindowToplevel = 0, + kGtkOrientationHoriz = 0, + kGtkOrientationVert = 1, + kGtkPolicyAutomatic = 1, + kGtkWrapNone = 0, + kGtkWrapWordChar = 3, + kGtkWinPosCenter = 1, + kGtkButtonBoxEnd = 4, // GTK_BUTTONBOX_END (SPREAD=1,EDGE=2,START=3,END=4) + kGtkStyleProviderApp = 600, + kGConnectSwapped = 2, // G_CONNECT_SWAPPED + kGSignalMatchFunc = 1u << 3, // G_SIGNAL_MATCH_FUNC (ID=1,DETAIL=2,CLOSURE=4,FUNC=8,DATA=16) + kGSignalMatchData = 1u << 4, // G_SIGNAL_MATCH_DATA + }; + + struct GtkFns + { + gboolean (*init_check)(int*, char***); + GtkWidget* (*window_new)(int); + void (*window_set_title)(GtkWidget*, const char*); + void (*window_set_default_size)(GtkWidget*, int, int); + void (*window_set_resizable)(GtkWidget*, gboolean); + void (*window_set_position)(GtkWidget*, int); + void (*window_set_default)(GtkWidget*, GtkWidget*); + void (*container_set_border_width)(GtkWidget*, unsigned int); + void (*container_add)(GtkWidget*, GtkWidget*); + GtkWidget* (*box_new)(int, int); + void (*box_pack_start)(GtkWidget*, GtkWidget*, gboolean, gboolean, unsigned int); + void (*box_set_spacing)(GtkWidget*, int); + GtkWidget* (*label_new)(const char*); + void (*label_set_markup)(GtkWidget*, const char*); + void (*label_set_xalign)(GtkWidget*, float); + void (*label_set_line_wrap)(GtkWidget*, gboolean); + GtkWidget* (*scrolled_window_new)(void*, void*); + void (*scrolled_window_set_policy)(GtkWidget*, int, int); + GtkWidget* (*text_view_new)(); + void (*text_view_set_wrap_mode)(GtkWidget*, int); + void (*text_view_set_editable)(GtkWidget*, gboolean); + void (*text_view_set_cursor_visible)(GtkWidget*, gboolean); + GtkTextBuffer* (*text_view_get_buffer)(GtkWidget*); + gboolean (*text_view_scroll_to_iter)(GtkWidget*, GtkTextIter*, double, gboolean, double, double); + void (*text_buffer_get_end_iter)(GtkTextBuffer*, GtkTextIter*); + void (*text_buffer_insert)(GtkTextBuffer*, GtkTextIter*, const char*, gint); + void (*text_buffer_get_bounds)(GtkTextBuffer*, GtkTextIter*, GtkTextIter*); + gchar* (*text_buffer_get_text)(GtkTextBuffer*, GtkTextIter*, GtkTextIter*, gboolean); + void (*widget_set_size_request)(GtkWidget*, int, int); + void (*widget_set_can_default)(GtkWidget*, gboolean); + void (*widget_show_all)(GtkWidget*); + void (*widget_destroy)(GtkWidget*); + GtkStyleContext* (*widget_get_style_context)(GtkWidget*); + void (*style_context_add_class)(GtkStyleContext*, const char*); + void (*style_context_add_provider)(GtkStyleContext*, GtkStyleProvider*, unsigned int); + GtkWidget* (*button_box_new)(int); + void (*button_box_set_layout)(GtkWidget*, int); + GtkWidget* (*button_new_with_label)(const char*); + PangoFontDescription* (*pango_font_desc_from_string)(const char*); + void (*widget_override_font)(GtkWidget*, PangoFontDescription*); + void (*pango_font_desc_free)(PangoFontDescription*); + GtkCssProvider* (*css_provider_new)(); + gboolean (*css_provider_load_from_data)(GtkCssProvider*, const char*, gssize, void*); + void (*g_object_unref)(gpointer); + unsigned long (*g_signal_connect_data)(gpointer, const char*, void*, gpointer, void*, unsigned int); + unsigned int (*g_signal_handlers_disconnect_matched)(gpointer, unsigned int, unsigned int, unsigned int, void*, void*, void*); + gchar* (*g_markup_escape_text)(const char*, gssize); + void (*g_free)(gpointer); + void (*main_loop)(); + void (*main_quit)(); + }; + + static bool LoadGtkFns(void* handle, GtkFns& g) + { +#define GLOAD(sym, field) \ + *(void**) (&g.field) = dlsym(handle, sym); \ + if (!g.field) \ + return false + + GLOAD("gtk_init_check", init_check); + GLOAD("gtk_window_new", window_new); + GLOAD("gtk_window_set_title", window_set_title); + GLOAD("gtk_window_set_default_size", window_set_default_size); + GLOAD("gtk_window_set_resizable", window_set_resizable); + GLOAD("gtk_window_set_position", window_set_position); + GLOAD("gtk_window_set_default", window_set_default); + GLOAD("gtk_container_set_border_width", container_set_border_width); + GLOAD("gtk_container_add", container_add); + GLOAD("gtk_box_new", box_new); + GLOAD("gtk_box_pack_start", box_pack_start); + GLOAD("gtk_box_set_spacing", box_set_spacing); + GLOAD("gtk_label_new", label_new); + GLOAD("gtk_label_set_markup", label_set_markup); + GLOAD("gtk_label_set_xalign", label_set_xalign); + GLOAD("gtk_label_set_line_wrap", label_set_line_wrap); + GLOAD("gtk_scrolled_window_new", scrolled_window_new); + GLOAD("gtk_scrolled_window_set_policy", scrolled_window_set_policy); + GLOAD("gtk_text_view_new", text_view_new); + GLOAD("gtk_text_view_set_wrap_mode", text_view_set_wrap_mode); + GLOAD("gtk_text_view_set_editable", text_view_set_editable); + GLOAD("gtk_text_view_set_cursor_visible", text_view_set_cursor_visible); + GLOAD("gtk_text_view_get_buffer", text_view_get_buffer); + GLOAD("gtk_text_view_scroll_to_iter", text_view_scroll_to_iter); + GLOAD("gtk_text_buffer_get_end_iter", text_buffer_get_end_iter); + GLOAD("gtk_text_buffer_insert", text_buffer_insert); + GLOAD("gtk_text_buffer_get_bounds", text_buffer_get_bounds); + GLOAD("gtk_text_buffer_get_text", text_buffer_get_text); + GLOAD("gtk_widget_set_size_request", widget_set_size_request); + GLOAD("gtk_widget_set_can_default", widget_set_can_default); + GLOAD("gtk_widget_show_all", widget_show_all); + GLOAD("gtk_widget_destroy", widget_destroy); + GLOAD("gtk_widget_get_style_context", widget_get_style_context); + GLOAD("gtk_style_context_add_class", style_context_add_class); + GLOAD("gtk_style_context_add_provider", style_context_add_provider); + GLOAD("gtk_button_box_new", button_box_new); + GLOAD("gtk_button_box_set_layout", button_box_set_layout); + GLOAD("gtk_button_new_with_label", button_new_with_label); + GLOAD("pango_font_description_from_string", pango_font_desc_from_string); + GLOAD("gtk_widget_override_font", widget_override_font); + GLOAD("pango_font_description_free", pango_font_desc_free); + GLOAD("gtk_css_provider_new", css_provider_new); + GLOAD("gtk_css_provider_load_from_data", css_provider_load_from_data); + GLOAD("g_object_unref", g_object_unref); + GLOAD("g_signal_connect_data", g_signal_connect_data); + GLOAD("g_signal_handlers_disconnect_matched", g_signal_handlers_disconnect_matched); + GLOAD("g_markup_escape_text", g_markup_escape_text); + GLOAD("g_free", g_free); + GLOAD("gtk_main", main_loop); + GLOAD("gtk_main_quit", main_quit); +#undef GLOAD + return true; + } + + static bool ShowCrashDialogGTK(const GtkFns& gtk, cstring app_name, cstring signal_description, cstring log_path, char* out_comment, size_t comment_cap) + { + int argc = 0; + if (!gtk.init_check(&argc, nullptr)) + return false; + + GtkWidget* window = gtk.window_new(kGtkWindowToplevel); + gtk.window_set_title(window, "Application Crash"); + gtk.window_set_default_size(window, 720, 510); + gtk.window_set_resizable(window, 0); + gtk.window_set_position(window, kGtkWinPosCenter); + gtk.container_set_border_width(window, 20); + + GtkWidget* vbox = gtk.box_new(kGtkOrientationVert, 0); + gtk.container_add(window, vbox); + + char header_text[512]; + { + gchar* escaped = gtk.g_markup_escape_text(app_name, -1); + snprintf(header_text, sizeof(header_text), "An application has crashed: %s", escaped); + gtk.g_free(escaped); + } + GtkWidget* header_label = gtk.label_new(nullptr); + gtk.label_set_markup(header_label, header_text); + gtk.label_set_xalign(header_label, 0.0f); + gtk.box_pack_start(vbox, header_label, 0, 0, 0); + + char desc_text[512]; + snprintf(desc_text, sizeof(desc_text), "The application encountered an unexpected error and could not continue.\n\nSignal: %s", signal_description); + GtkWidget* desc_label = gtk.label_new(desc_text); + gtk.label_set_xalign(desc_label, 0.0f); + gtk.label_set_line_wrap(desc_label, 1); + gtk.box_pack_start(vbox, desc_label, 0, 0, 10); + + GtkWidget* comment_hint = gtk.label_new("Describe what you were doing when the crash occurred (optional):"); + gtk.label_set_xalign(comment_hint, 0.0f); + gtk.box_pack_start(vbox, comment_hint, 0, 0, 6); + + GtkWidget* comment_scroll = gtk.scrolled_window_new(nullptr, nullptr); + gtk.scrolled_window_set_policy(comment_scroll, kGtkPolicyAutomatic, kGtkPolicyAutomatic); + gtk.widget_set_size_request(comment_scroll, -1, 90); + GtkWidget* comment_view = gtk.text_view_new(); + gtk.text_view_set_wrap_mode(comment_view, kGtkWrapWordChar); + gtk.container_add(comment_scroll, comment_view); + gtk.box_pack_start(vbox, comment_scroll, 0, 0, 0); + + GtkWidget* log_caption = gtk.label_new("Crash log:"); + gtk.label_set_xalign(log_caption, 0.0f); + gtk.style_context_add_class(gtk.widget_get_style_context(log_caption), "dim-label"); + gtk.box_pack_start(vbox, log_caption, 0, 0, 8); + + GtkWidget* log_scroll = gtk.scrolled_window_new(nullptr, nullptr); + gtk.scrolled_window_set_policy(log_scroll, kGtkPolicyAutomatic, kGtkPolicyAutomatic); + gtk.widget_set_size_request(log_scroll, -1, 200); + GtkWidget* log_view = gtk.text_view_new(); + gtk.text_view_set_editable(log_view, 0); + gtk.text_view_set_cursor_visible(log_view, 0); + gtk.text_view_set_wrap_mode(log_view, kGtkWrapNone); + + PangoFontDescription* mono_font = gtk.pango_font_desc_from_string("Monospace 10"); + gtk.widget_override_font(log_view, mono_font); + gtk.pango_font_desc_free(mono_font); + + GtkCssProvider* css = gtk.css_provider_new(); + gtk.css_provider_load_from_data(css, "textview { background-color: #1e1e1e; color: #d4d4d4; }", -1, nullptr); + gtk.style_context_add_provider(gtk.widget_get_style_context(log_view), (GtkStyleProvider*) css, kGtkStyleProviderApp); + gtk.g_object_unref(css); + + { + FILE* f = fopen(log_path, "r"); + if (f) + { + char chunk[4096]; + size_t n; + GtkTextBuffer* buf = gtk.text_view_get_buffer(log_view); + while ((n = fread(chunk, 1, sizeof(chunk) - 1, f)) > 0) + { + chunk[n] = '\0'; + GtkTextIter end = {}; + gtk.text_buffer_get_end_iter(buf, &end); + gtk.text_buffer_insert(buf, &end, chunk, (gint) n); + } + fclose(f); + } + } + gtk.container_add(log_scroll, log_view); + gtk.box_pack_start(vbox, log_scroll, 1, 1, 0); + + GtkWidget* btn_box = gtk.button_box_new(kGtkOrientationHoriz); + gtk.button_box_set_layout(btn_box, kGtkButtonBoxEnd); + gtk.box_set_spacing(btn_box, 8); + gtk.box_pack_start(vbox, btn_box, 0, 0, 12); + + GtkWidget* close_btn = gtk.button_new_with_label("Close Without Sending"); + GtkWidget* send_btn = gtk.button_new_with_label("Send and Close"); + gtk.widget_set_can_default(send_btn, 1); + gtk.box_pack_start(btn_box, close_btn, 0, 0, 0); + gtk.box_pack_start(btn_box, send_btn, 0, 0, 0); + + struct State + { + bool did_send; + GtkWidget* window; + GtkWidget* comment_view; + const GtkFns* gtk_ptr; + }; + State state = {false, window, comment_view, >k}; + + gtk.g_signal_connect_data( + close_btn, + "clicked", + (void*) +[](State* s) { + s->did_send = false; + s->gtk_ptr->main_quit(); + }, + &state, + nullptr, + kGConnectSwapped); + + gtk.g_signal_connect_data( + send_btn, + "clicked", + (void*) +[](State* s) { + s->did_send = true; + s->gtk_ptr->main_quit(); + }, + &state, + nullptr, + kGConnectSwapped); + + gtk.g_signal_connect_data(window, "destroy", (void*) gtk.main_quit, nullptr, nullptr, 0); + + gtk.widget_show_all(window); + gtk.window_set_default(window, send_btn); + + // scroll to bottom only after show_all — widget must be realized first + { + GtkTextBuffer* buf = gtk.text_view_get_buffer(log_view); + GtkTextIter last = {}; + gtk.text_buffer_get_end_iter(buf, &last); + gtk.text_view_scroll_to_iter(log_view, &last, 0.0, 0, 0.0, 1.0); + } + + gtk.main_loop(); + + if (state.did_send) + { + GtkTextBuffer* buf = gtk.text_view_get_buffer(comment_view); + GtkTextIter start_it = {}, end_it = {}; + gtk.text_buffer_get_bounds(buf, &start_it, &end_it); + gchar* text = gtk.text_buffer_get_text(buf, &start_it, &end_it, 0); + if (text && text[0] != '\0') + snprintf(out_comment, comment_cap, "%s", text); + gtk.g_free(text); + } + + // disconnect before destroy — gtk_main() has returned; firing main_quit on a dead loop warns + gtk.g_signal_handlers_disconnect_matched(window, kGSignalMatchFunc | kGSignalMatchData, 0, 0, nullptr, (void*) gtk.main_quit, nullptr); + gtk.widget_destroy(window); + return state.did_send; + } + + static bool ShowCrashDialog(cstring log_path, cstring signal_description) + { + void* handle = dlopen("libgtk-3.so.0", RTLD_LAZY | RTLD_LOCAL); + if (handle) + { + GtkFns gtk = {}; + if (LoadGtkFns(handle, gtk)) + { + bool sent = ShowCrashDialogGTK(gtk, g_state.AppName, signal_description, log_path, g_state.UserComment, kMaxCommentLen); + dlclose(handle); + return sent; + } + dlclose(handle); + } + + char cmd[kMaxPathLen * 4]; + + if (system("command -v zenity >/dev/null 2>&1") == 0) + { + snprintf( + cmd, + sizeof(cmd), + "zenity --text-info --title=\"Application Crash\"" + " --filename=\"%s\" --width=600 --height=400" + " --ok-label=\"Send Report\" --cancel-label=\"Don't Send\"" + " 2>/dev/null", + log_path); + if (system(cmd) != 0) + return false; + + snprintf( + cmd, + sizeof(cmd), + "zenity --entry --title=\"Add Comment\"" + " --text=\"Optional: describe what you were doing:\"" + " --entry-text=\"\" 2>/dev/null"); + FILE* pipe = popen(cmd, "r"); + if (pipe) + { + fgets(g_state.UserComment, kMaxCommentLen, pipe); + pclose(pipe); + size_t len = secure_strlen(g_state.UserComment); + if (len > 0 && g_state.UserComment[len - 1] == '\n') + g_state.UserComment[len - 1] = '\0'; + } + return true; + } + + if (system("command -v kdialog >/dev/null 2>&1") == 0) + { + snprintf( + cmd, + sizeof(cmd), + "kdialog --yesno \"The application encountered an unexpected error." + "\\n\\nSignal: %s\\n\\nCrash report: %s\"" + " --title \"Application Crash\" 2>/dev/null", + signal_description, + log_path); + if (system(cmd) != 0) + return false; + + snprintf( + cmd, + sizeof(cmd), + "kdialog --inputbox \"Optional: describe what you were doing:\"" + " \"\" --title \"Add Comment\" 2>/dev/null"); + FILE* pipe = popen(cmd, "r"); + if (pipe) + { + fgets(g_state.UserComment, kMaxCommentLen, pipe); + pclose(pipe); + size_t len = secure_strlen(g_state.UserComment); + if (len > 0 && g_state.UserComment[len - 1] == '\n') + g_state.UserComment[len - 1] = '\0'; + } + return true; + } + + snprintf( + cmd, + sizeof(cmd), + "xmessage -center \"The application encountered an unexpected error." + "\\n\\nSignal: %s\\n\\nCrash report: %s\" 2>/dev/null", + signal_description, + log_path); + system(cmd); + return false; + } + + static void WriteSystemInfo(int fd) + { + struct utsname uts = {}; + uname(&uts); + + char cpu_model[256] = "(unknown)"; + FILE* cpuinfo = fopen("/proc/cpuinfo", "r"); + if (cpuinfo) + { + char line[256]; + while (fgets(line, sizeof(line), cpuinfo)) + { + if (strncmp(line, "model name", 10) == 0) + { + const char* colon = strchr(line, ':'); + if (colon) + { + ++colon; + while (*colon == ' ' || *colon == '\t') + ++colon; + snprintf(cpu_model, sizeof(cpu_model), "%s", colon); + size_t len = secure_strlen(cpu_model); + if (len > 0 && cpu_model[len - 1] == '\n') + cpu_model[len - 1] = '\0'; + } + break; + } + } + fclose(cpuinfo); + } + + char line_buf[512]; + snprintf(line_buf, sizeof(line_buf), "OS: %s %s (%s)\n", uts.sysname, uts.release, uts.version); + SafeWrite(fd, line_buf); + snprintf(line_buf, sizeof(line_buf), "Arch: %s\n", uts.machine); + SafeWrite(fd, line_buf); + snprintf(line_buf, sizeof(line_buf), "CPU: %s\n", cpu_model); + SafeWrite(fd, line_buf); + } + + static void WriteStackTrace(int fd, void* const* addrs, int count) + { + char** symbols = backtrace_symbols(addrs, count); + char line_buf[512]; + + for (int i = 0; i < count; ++i) + { + Dl_info info = {}; + int written = 0; + + if (dladdr(addrs[i], &info) && info.dli_sname) + { + int status = 0; + char* demangled = abi::__cxa_demangle(info.dli_sname, nullptr, nullptr, &status); + const char* name = (status == 0 && demangled) ? demangled : info.dli_sname; + written = snprintf(line_buf, sizeof(line_buf), "\t#%-2d %s [%p]\n", i, name, addrs[i]); + free(demangled); + } + else if (symbols && symbols[i]) + written = snprintf(line_buf, sizeof(line_buf), "\t#%-2d %s\n", i, symbols[i]); + else + written = snprintf(line_buf, sizeof(line_buf), "\t#%-2d [%p]\n", i, addrs[i]); + + if (written > 0) + SafeWrite(fd, line_buf); + } + + free(symbols); + } + + struct ModuleCallbackData + { + int fd = -1; + }; + + static int WriteModuleCallback(struct dl_phdr_info* info, size_t /*size*/, void* data) + { + auto* cb = static_cast(data); + char line_buf[512]; + int written = snprintf(line_buf, sizeof(line_buf), "\t%s | Load Addr: %p\n", (info->dlpi_name && info->dlpi_name[0]) ? info->dlpi_name : "
", reinterpret_cast(info->dlpi_addr)); + if (written > 0) + SafeWrite(cb->fd, line_buf); + return 0; + } + + static void* CrashWorkerThreadFn(void*) + { + uint8_t byte = 0; + ssize_t r; + do + { + r = read(s_crash_pipe[0], &byte, 1); + } while (r == -1 && errno == EINTR); + + if (byte == 0) + return nullptr; + + WorkerThreadParams* params = &s_crash_params; + if (!params->BacktraceFromSignal && params->BacktraceSize == 0) + params->BacktraceSize = backtrace(params->BacktraceAddrs, kMaxBacktraceFrames); + + { + time_t t_now = time(nullptr); + struct tm* t_utc = gmtime(&t_now); + char t_str[32] = {}; + strftime(t_str, sizeof(t_str), "%Y-%m-%d_%H-%M-%S", t_utc); + snprintf(params->LogPath, sizeof(params->LogPath), "%s/crash_%s.log", g_state.CrashLogDir, t_str); + } + + if (g_state.PreCrashFn) + { + struct CallbackSync + { + pthread_mutex_t mutex; + pthread_cond_t cond; + bool done; + CrashHandler::PreCrashFn fn; + void* ctx; + }; + + CallbackSync sync; + pthread_mutex_init(&sync.mutex, nullptr); + pthread_cond_init(&sync.cond, nullptr); + sync.done = false; + sync.fn = g_state.PreCrashFn; + sync.ctx = g_state.PreCrashCtx; + + pthread_t callback_thread; + pthread_create( + &callback_thread, + nullptr, + [](void* p) -> void* { + auto* s = static_cast(p); + s->fn(s->ctx); + pthread_mutex_lock(&s->mutex); + s->done = true; + pthread_cond_signal(&s->cond); + pthread_mutex_unlock(&s->mutex); + return nullptr; + }, + &sync); + + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_sec += kCallbackTimeoutSec; + + pthread_mutex_lock(&sync.mutex); + while (!sync.done) + { + if (pthread_cond_timedwait(&sync.cond, &sync.mutex, &deadline) == ETIMEDOUT) + break; + } + bool completed = sync.done; + pthread_mutex_unlock(&sync.mutex); + + if (completed) + { + pthread_join(callback_thread, nullptr); + pthread_mutex_destroy(&sync.mutex); + pthread_cond_destroy(&sync.cond); + } + else + { + // PreCrashFn timed out — detach so the thread cleans up when it + // eventually exits. Do not destroy mutex/cond here; _exit() + // terminates all threads before they can access freed stack memory. + pthread_detach(callback_thread); + } + } + + int log_fd = open(params->LogPath, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (log_fd >= 0) + { + char time_buf[64] = {}; + time_t now = time(nullptr); + struct tm* tm_info = gmtime(&now); + strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S UTC", tm_info); + char log_buf[512]; + + SafeWrite(log_fd, "==================================================================\n"); + SafeWrite(log_fd, "ZEngine Crash Report\n"); + SafeWrite(log_fd, "==================================================================\n"); + snprintf(log_buf, sizeof(log_buf), "App: %s %s\n", g_state.AppName, g_state.Version); + SafeWrite(log_fd, log_buf); + snprintf(log_buf, sizeof(log_buf), "Date: %s\n", time_buf); + SafeWrite(log_fd, log_buf); + WriteSystemInfo(log_fd); + snprintf(log_buf, sizeof(log_buf), "Signal: %s\n\n", params->SignalOrException); + SafeWrite(log_fd, log_buf); + + SafeWrite(log_fd, "==================================================================\n"); + SafeWrite(log_fd, "Stack Trace (most recent call first):\n"); + SafeWrite(log_fd, "==================================================================\n"); + WriteStackTrace(log_fd, params->BacktraceAddrs, params->BacktraceSize); + + SafeWrite(log_fd, "\n==================================================================\n"); + SafeWrite(log_fd, "Loaded Modules:\n"); + SafeWrite(log_fd, "==================================================================\n"); + ModuleCallbackData cb_data{log_fd}; + dl_iterate_phdr(WriteModuleCallback, &cb_data); + + SafeWrite(log_fd, "==================================================================\n"); + SafeWrite(log_fd, "End of Report\n"); + SafeWrite(log_fd, "==================================================================\n"); + close(log_fd); + } + + if (HasGUISession()) + { +#ifdef ZENGINE_CRASH_UPLOAD + if (ShowCrashDialog(params->LogPath, params->SignalOrException)) + g_state.UserConsentUpload = true; + // TODO(jeanphilippekernel): implement crash report upload +#else + ShowCrashDialog(params->LogPath, params->SignalOrException); +#endif + } + + if (g_state.UserComment[0] != '\0') + { + int comment_fd = open(params->LogPath, O_WRONLY | O_APPEND, 0644); + if (comment_fd >= 0) + { + SafeWrite(comment_fd, "\n==================================================================\n"); + SafeWrite(comment_fd, "User Comment:\n"); + SafeWrite(comment_fd, "==================================================================\n"); + SafeWrite(comment_fd, g_state.UserComment); + SafeWrite(comment_fd, "\n"); + close(comment_fd); + } + } + + char stderr_buf[512]; + snprintf(stderr_buf, sizeof(stderr_buf), "\n[ZEngine] CRASH: %s\nReport: %s\n", params->SignalOrException, params->LogPath); + fputs(stderr_buf, stderr); + + _exit(EXIT_FAILURE); + return nullptr; + } + + static void SignalHandler(int sig, siginfo_t* /*info*/, void* ctx) + { + s_crash_params.SignalNumber = sig; + s_crash_params.SignalOrException = SignalToString(sig); + + // Capture the crashing thread's backtrace here, in the signal handler, + // where the crashing thread's stack is live. The worker thread's stack + // would be meaningless in a crash log. + s_crash_params.BacktraceSize = backtrace(s_crash_params.BacktraceAddrs, kMaxBacktraceFrames); + s_crash_params.BacktraceFromSignal = true; + + // If the ucontext is available, overwrite frame 0 with the actual fault + // address so the log points at the instruction that faulted, not at the + // signal trampoline. + if (ctx) + { + auto* uc = static_cast(ctx); +#if defined(__x86_64__) + if (s_crash_params.BacktraceSize > 0) + s_crash_params.BacktraceAddrs[0] = reinterpret_cast(uc->uc_mcontext.gregs[REG_RIP]); +#elif defined(__aarch64__) + if (s_crash_params.BacktraceSize > 0) + s_crash_params.BacktraceAddrs[0] = reinterpret_cast(uc->uc_mcontext.pc); +#endif + } + + uint8_t byte = 1; + write(s_crash_pipe[1], &byte, 1); + sigset_t all; + sigfillset(&all); + while (true) + sigsuspend(&all); + } + + void CrashHandler::Install(const char* app_name, const char* version, const char* crash_log_dir) + { +#ifdef ZENGINE_CRASH_HANDLER_ENABLED + if (g_state.Installed) + return; + + StoreMetadata(app_name, version, crash_log_dir); + mkdir(crash_log_dir, 0755); + + if (pipe(s_crash_pipe) != 0) + { + fputs("[CrashHandler] Failed to create crash notification pipe.\n", stderr); + return; + } + + stack_t alt_stack = {}; + alt_stack.ss_sp = s_alt_stack_mem; + alt_stack.ss_size = sizeof(s_alt_stack_mem); + alt_stack.ss_flags = 0; + sigaltstack(&alt_stack, nullptr); + + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); + pthread_create(&s_worker_thread, &attr, CrashWorkerThreadFn, nullptr); + pthread_attr_destroy(&attr); + + struct sigaction sa = {}; + sa.sa_sigaction = SignalHandler; + sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESETHAND; + sigemptyset(&sa.sa_mask); + + for (int i = 0; i < kNumTrackedSignals; ++i) + sigaction(kTrackedSignals[i], &sa, &s_previous_actions[kTrackedSignals[i]]); + + g_state.Installed = true; +#endif + } + + void CrashHandler::Uninstall() + { +#ifdef ZENGINE_CRASH_HANDLER_ENABLED + if (!g_state.Installed) + return; + + for (int i = 0; i < kNumTrackedSignals; ++i) + sigaction(kTrackedSignals[i], &s_previous_actions[kTrackedSignals[i]], nullptr); + + uint8_t byte = 0; + write(s_crash_pipe[1], &byte, 1); + pthread_join(s_worker_thread, nullptr); + + close(s_crash_pipe[0]); + close(s_crash_pipe[1]); + s_crash_pipe[0] = s_crash_pipe[1] = -1; + g_state.Installed = false; +#endif + } + + void CrashHandler::StoreMetadata(const char* app_name, const char* version, const char* crash_log_dir) + { + snprintf(g_state.AppName, kMaxNameLen, "%s", app_name); + snprintf(g_state.Version, kMaxNameLen, "%s", version); + snprintf(g_state.CrashLogDir, kMaxPathLen, "%s", crash_log_dir); + } + + void CrashHandler::SetPreCrashCallback(PreCrashFn fn, void* ctx) + { + g_state.PreCrashFn = fn; + g_state.PreCrashCtx = ctx; + } + + [[noreturn]] void CrashHandler::OnCrash(const char* signal_or_exception, void* /*ctx*/) + { + static std::atomic s_in_crash_handler{false}; + if (s_in_crash_handler.exchange(true, std::memory_order_acq_rel)) + { + fputs("[CrashHandler] FATAL: crash handler reentered. Aborting.\n", stderr); + _exit(EXIT_FAILURE); + } + + // Capture the call-site stack here, on the calling thread, before waking + // the worker. BacktraceFromSignal stays false so the worker skips its own + // (meaningless) backtrace and uses these addresses instead. + s_crash_params.BacktraceSize = backtrace(s_crash_params.BacktraceAddrs, kMaxBacktraceFrames); + + s_crash_params.SignalOrException = signal_or_exception; + uint8_t byte = 1; + write(s_crash_pipe[1], &byte, 1); + pthread_join(s_worker_thread, nullptr); + _exit(EXIT_FAILURE); + } + + [[noreturn]] void CrashHandler::OnAssertionFailure(const char* file, int line, const char* message) + { + char assertion_message[1024]; + snprintf(assertion_message, sizeof(assertion_message), "Assertion Failure: %s\nFile: %s\nLine: %d", message ? message : "(null)", file ? file : "(null)", line); + OnCrash(assertion_message, nullptr); + } +} // namespace ZEngine::CrashHandlers diff --git a/ZEngine/ZEngine/CrashHandlers/CrashHandlerMacOS.mm b/ZEngine/ZEngine/CrashHandlers/CrashHandlerMacOS.mm new file mode 100644 index 00000000..8857a2fb --- /dev/null +++ b/ZEngine/ZEngine/CrashHandlers/CrashHandlerMacOS.mm @@ -0,0 +1,726 @@ +#include +#include +#include +#import +#import +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ───────────────────────────────────────────────────────────────────────────── +// Objective-C class declarations must live at global scope (not inside a C++ +// namespace). The C++ crash handler calls ShowCrashDialogCocoa() which is a +// plain C linkage bridge declared below. +// ───────────────────────────────────────────────────────────────────────────── + +@interface ZEngineCrashWindowController : NSObject +@property (nonatomic, assign) BOOL userDidSend; +@property (nonatomic, copy) NSString* userComment; +- (instancetype)initWithAppName:(NSString*)appName + signal:(NSString*)signal + logText:(NSString*)logText; +- (void)runModal; +@end + +@implementation ZEngineCrashWindowController +{ + NSWindow* _window; + NSTextView* _commentView; + NSTextView* _logView; +} + +- (instancetype)initWithAppName:(NSString*)appName + signal:(NSString*)signal + logText:(NSString*)logText +{ + self = [super init]; + if (!self) return nil; + _userDidSend = NO; + _userComment = @""; + + //Window + const CGFloat W = 720.0; + const CGFloat H = 510.0; + const CGFloat pad = 20.0; + + NSRect frame = NSMakeRect(0, 0, W, H); + _window = [[NSWindow alloc] + initWithContentRect:frame + styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable + backing:NSBackingStoreBuffered + defer:NO]; + [_window setTitle:[NSString stringWithFormat:@"%@ Crash Reporter", appName]]; + [_window center]; + [_window setReleasedWhenClosed:NO]; + + NSView* content = _window.contentView; + CGFloat y = pad; + + //Buttons (bottom row) + NSButton* sendBtn = [NSButton buttonWithTitle:@"Send and Close" + target:self + action:@selector(onSend:)]; + sendBtn.bezelStyle = NSBezelStyleRounded; + sendBtn.keyEquivalent = @"\r"; + sendBtn.frame = NSMakeRect(W - pad - 160, y, 160, 32); + [content addSubview:sendBtn]; + + NSButton* closeBtn = [NSButton buttonWithTitle:@"Close Without Sending" + target:self + action:@selector(onClose:)]; + closeBtn.bezelStyle = NSBezelStyleRounded; + closeBtn.keyEquivalent = @"\033"; + closeBtn.frame = NSMakeRect(pad, y, 190, 32); + [content addSubview:closeBtn]; + + y += 32 + 16; + + //Log scroll view (read-only) + const CGFloat logH = 200.0; + NSScrollView* logScroll = [[NSScrollView alloc] + initWithFrame:NSMakeRect(pad, y, W - pad * 2, logH)]; + logScroll.borderType = NSBezelBorder; + logScroll.hasVerticalScroller = YES; + logScroll.autohidesScrollers = YES; + + _logView = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, W - pad * 2, logH)]; + _logView.editable = NO; + _logView.selectable = YES; + _logView.string = logText; + _logView.font = [NSFont monospacedSystemFontOfSize:10.5 weight:NSFontWeightRegular]; + _logView.backgroundColor= [NSColor colorWithWhite:0.12 alpha:1.0]; + _logView.textColor = [NSColor colorWithWhite:0.85 alpha:1.0]; + _logView.drawsBackground= YES; + _logView.automaticQuoteSubstitutionEnabled = NO; + _logView.automaticDashSubstitutionEnabled = NO; + + logScroll.documentView = _logView; + [content addSubview:logScroll]; + [_logView scrollRangeToVisible:NSMakeRange(_logView.string.length, 0)]; + + y += logH + 8; + + //Log caption + NSTextField* logLabel = [NSTextField labelWithString: + @"Crash reports comprise diagnostics files and the following summary information:"]; + logLabel.font = [NSFont systemFontOfSize:11.0]; + logLabel.textColor = [NSColor secondaryLabelColor]; + logLabel.frame = NSMakeRect(pad, y, W - pad * 2, 18); + logLabel.lineBreakMode = NSLineBreakByTruncatingTail; + [content addSubview:logLabel]; + + y += 18 + 12; + + //Editable comment text view + const CGFloat commentH = 90.0; + NSScrollView* commentScroll = [[NSScrollView alloc] + initWithFrame:NSMakeRect(pad, y, W - pad * 2, commentH)]; + commentScroll.borderType = NSBezelBorder; + commentScroll.hasVerticalScroller = YES; + commentScroll.autohidesScrollers = YES; + + _commentView = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, W - pad * 2, commentH)]; + _commentView.editable = YES; + _commentView.richText = NO; + _commentView.font = [NSFont systemFontOfSize:13.0]; + _commentView.automaticQuoteSubstitutionEnabled = NO; + _commentView.automaticDashSubstitutionEnabled = NO; + + commentScroll.documentView = _commentView; + [content addSubview:commentScroll]; + + y += commentH + 4; + + //Comment hint + NSTextField* commentHint = [NSTextField labelWithString: + @"Please provide detailed information about what you were doing when the crash occurred."]; + commentHint.font = [NSFont systemFontOfSize:11.5]; + commentHint.textColor = [NSColor secondaryLabelColor]; + commentHint.frame = NSMakeRect(pad, y, W - pad * 2, 18); + [content addSubview:commentHint]; + + y += 18 + 14; + + //Description paragraph + NSString* desc = [NSString stringWithFormat: + @"The application encountered an unexpected error and could not continue.\n\nSignal: %@", signal]; + NSTextField* descLabel = [NSTextField wrappingLabelWithString:desc]; + descLabel.font = [NSFont systemFontOfSize:12.5]; + descLabel.frame = NSMakeRect(pad, y, W - pad * 2, 36); + [content addSubview:descLabel]; + + y += 36 + 10; + + //Header label + NSTextField* headerLabel = [NSTextField labelWithString: + [NSString stringWithFormat:@"An application has crashed: %@", appName]]; + headerLabel.font = [NSFont boldSystemFontOfSize:15.0]; + headerLabel.frame = NSMakeRect(pad, y, W - pad * 2, 22); + [content addSubview:headerLabel]; + + return self; +} + +- (void)onSend:(id)sender +{ + _userDidSend = YES; + _userComment = [_commentView.string copy]; + [NSApp stopModalWithCode:NSModalResponseOK]; + [_window orderOut:nil]; +} + +- (void)onClose:(id)sender +{ + _userDidSend = NO; + [NSApp stopModalWithCode:NSModalResponseCancel]; + [_window orderOut:nil]; +} + +- (void)runModal +{ + [_window makeKeyAndOrderFront:nil]; + [NSApp runModalForWindow:_window]; +} + +@end + +// ───────────────────────────────────────────────────────────────────────────── +// C++ bridge — called from inside the ZEngine::CrashHandlers namespace below. +// Returns true if the user clicked "Send and Close". +// Writes the user comment into out_comment (up to comment_cap bytes). +// ───────────────────────────────────────────────────────────────────────────── +static bool ShowCrashDialogCocoa(cstring app_name, + cstring signal_description, + cstring log_path, + char* out_comment, + size_t comment_cap, + bool from_signal) +{ + @autoreleasepool + { + NSString* appName = [NSString stringWithUTF8String:app_name[0] ? app_name : "ZEngine"]; + NSString* signal = [NSString stringWithUTF8String:signal_description]; + NSString* nsPath = [NSString stringWithUTF8String:log_path]; + NSString* logText = [NSString stringWithContentsOfFile:nsPath + encoding:NSUTF8StringEncoding + error:nil]; + if (!logText) logText = @"(log unavailable)"; + + // Two call paths reach here: + // + // A) OnCrash() path (from_signal == false): + // OnCrash() was called from the main thread and is pumping + // [NSRunLoop mainRunLoop] in a tight loop. dispatch_sync to the + // main queue is safe — the main thread will drain it. + // + // B) Signal path (from_signal == true): + // The crashing thread is parked in sigsuspend; the worker thread + // called us. No one is pumping the main queue, so dispatch_sync + // would deadlock. Instead we create a fresh NSThread and bootstrap + // NSApp there. In a crash scenario the OS does not enforce AppKit + // thread affinity, so this works reliably on macOS 10.12+. + __block bool didSend = false; + __block NSString* comment = @""; + + if (!from_signal) + { + // Path A: OnCrash() is pumping the main run loop — dispatch_sync is safe. + dispatch_sync(dispatch_get_main_queue(), ^{ + @autoreleasepool + { + if (NSApp == nil) + { + [NSApplication sharedApplication]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + [NSApp finishLaunching]; + } + else + { + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + } + ZEngineCrashWindowController* ctrl = + [[ZEngineCrashWindowController alloc] initWithAppName:appName + signal:signal + logText:logText]; + [NSApp activateIgnoringOtherApps:YES]; + [ctrl runModal]; + didSend = ctrl.userDidSend; + comment = ctrl.userComment; + } + }); + } + else + { + // Path B: signal path — bootstrap NSApp on a dedicated thread. + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + NSThread* dlgThread = [[NSThread alloc] initWithBlock:^{ + @autoreleasepool + { + [NSApplication sharedApplication]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + [NSApp finishLaunching]; + ZEngineCrashWindowController* ctrl = + [[ZEngineCrashWindowController alloc] initWithAppName:appName + signal:signal + logText:logText]; + [NSApp activateIgnoringOtherApps:YES]; + [ctrl runModal]; + didSend = ctrl.userDidSend; + comment = ctrl.userComment; + dispatch_semaphore_signal(sem); + } + }]; + [dlgThread start]; + dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); + } + + if (didSend && comment.length > 0) + snprintf(out_comment, comment_cap, "%s", comment.UTF8String); + + return didSend; + } +} + +namespace ZEngine::CrashHandlers +{ + using ZEngine::Helpers::secure_strlen; + + static constexpr int kTrackedSignals[] = {SIGSEGV, SIGBUS, SIGFPE, SIGILL, SIGTRAP, SIGABRT, SIGSYS}; + static constexpr int kNumTrackedSignals = sizeof(kTrackedSignals) / sizeof(kTrackedSignals[0]); + static constexpr int kMaxBacktraceFrames = 64; + + struct WorkerThreadParams + { + int SignalNumber = 0; + cstring SignalOrException = nullptr; + void* BacktraceAddrs[kMaxBacktraceFrames] = {}; + int BacktraceSize = 0; + char LogPath[kMaxPathLen] = {}; + bool BacktraceFromSignal = false; + }; + + static constexpr size_t kAltStackSize = 32 * 1024; + static struct sigaction s_previous_actions[NSIG] = {}; + static char s_alt_stack_mem[kAltStackSize]; + static int s_crash_pipe[2] = {-1, -1}; + static WorkerThreadParams s_crash_params = {}; + static pthread_t s_worker_thread; + + static const char* SignalToString(int sig) + { + switch (sig) + { + case SIGSEGV: return "Segmentation Fault (SIGSEGV)"; + case SIGBUS: return "Bus Error (SIGBUS)"; + case SIGFPE: return "Floating Point Exception (SIGFPE)"; + case SIGILL: return "Illegal Instruction (SIGILL)"; + case SIGTRAP: return "Trace Trap (SIGTRAP)"; + case SIGABRT: return "Abort Signal (SIGABRT)"; + case SIGSYS: return "Bad System Call (SIGSYS)"; + default: return "Unknown Signal"; + } + } + + static void SafeWrite(int fd, cstring str) + { + if (fd < 0 || !str) + return; + size_t len = secure_strlen(str); + while (len > 0) + { + ssize_t written = write(fd, str, len); + if (written <= 0) + break; + len -= static_cast(written); + str += written; + } + } + + static bool HasGUISession() + { + if (getenv("ZENGINE_CRASH_NO_DIALOG")) + return false; + if (getenv("CI")) + return false; + // Check for a live window server connection. This works whether the + // process was launched from a terminal, an IDE, or directly by launchd. + // It returns false in headless SSH sessions and pure CI environments. + return CGMainDisplayID() != 0; + } + + static void WriteSystemInfo(int fd) + { + struct utsname uts = {}; + uname(&uts); + + // macOS marketing version from sysctl kern.osproductversion + char os_version[64] = {}; + { + size_t len = sizeof(os_version); + sysctlbyname("kern.osproductversion", os_version, &len, nullptr, 0); + } + + // CPU brand string from sysctl machdep.cpu.brand_string (x86_64) or + // hw.targettype / hw.model (arm64) + char cpu_model[256] = "(unknown)"; + { + size_t len = sizeof(cpu_model); + if (sysctlbyname("machdep.cpu.brand_string", cpu_model, &len, nullptr, 0) != 0) + sysctlbyname("hw.model", cpu_model, &len, nullptr, 0); + } + + char line_buf[512]; + snprintf(line_buf, sizeof(line_buf), "OS: macOS %s (%s %s)\n", + os_version[0] ? os_version : "unknown", uts.sysname, uts.release); + SafeWrite(fd, line_buf); + snprintf(line_buf, sizeof(line_buf), "Arch: %s\n", uts.machine); + SafeWrite(fd, line_buf); + snprintf(line_buf, sizeof(line_buf), "CPU: %s\n", cpu_model); + SafeWrite(fd, line_buf); + } + + static void WriteStackTrace(int fd, void* const* addrs, int count) + { + char** symbols = backtrace_symbols(addrs, count); + char line_buf[512]; + for (int i = 0; i < count; ++i) + { + Dl_info info = {}; + int written = 0; + if (dladdr(addrs[i], &info) && info.dli_sname) + { + int status = 0; + char* demangled = abi::__cxa_demangle(info.dli_sname, nullptr, nullptr, &status); + const char* name = (status == 0 && demangled) ? demangled : info.dli_sname; + written = snprintf(line_buf, sizeof(line_buf), "\t#%-2d %s [%p]\n", i, name, addrs[i]); + free(demangled); + } + else if (symbols && symbols[i]) + written = snprintf(line_buf, sizeof(line_buf), "\t#%-2d %s\n", i, symbols[i]); + else + written = snprintf(line_buf, sizeof(line_buf), "\t#%-2d [%p]\n", i, addrs[i]); + if (written > 0) + SafeWrite(fd, line_buf); + } + free(symbols); + } + + static void WriteModuleList(int fd) + { + uint32_t count = _dyld_image_count(); + char line_buf[512]; + for (uint32_t i = 0; i < count; ++i) + { + const char* name = _dyld_get_image_name(i); + if (!name) continue; + int written = snprintf(line_buf, sizeof(line_buf), "\t%s\n", name); + if (written > 0) + SafeWrite(fd, line_buf); + } + } + + static void* CrashWorkerThreadFn(void*) + { + uint8_t byte = 0; + ssize_t r; + do { r = read(s_crash_pipe[0], &byte, 1); } while (r == -1 && errno == EINTR); + + if (byte == 0) + return nullptr; + + WorkerThreadParams* params = &s_crash_params; + if (!params->BacktraceFromSignal && params->BacktraceSize == 0) + params->BacktraceSize = backtrace(params->BacktraceAddrs, kMaxBacktraceFrames); + + { + time_t t_now = time(nullptr); + struct tm* t_utc = gmtime(&t_now); + char t_str[32] = {}; + strftime(t_str, sizeof(t_str), "%Y-%m-%d_%H-%M-%S", t_utc); + snprintf(params->LogPath, sizeof(params->LogPath), "%s/crash_%s.log", + g_state.CrashLogDir, t_str); + } + + if (g_state.PreCrashFn) + { + struct CallbackSync + { + pthread_mutex_t mutex; + pthread_cond_t cond; + bool done; + CrashHandler::PreCrashFn fn; + void* ctx; + }; + + CallbackSync sync; + pthread_mutex_init(&sync.mutex, nullptr); + pthread_cond_init(&sync.cond, nullptr); + sync.done = false; + sync.fn = g_state.PreCrashFn; + sync.ctx = g_state.PreCrashCtx; + + pthread_t callback_thread; + pthread_create( + &callback_thread, nullptr, + [](void* p) -> void* { + auto* s = static_cast(p); + s->fn(s->ctx); + pthread_mutex_lock(&s->mutex); + s->done = true; + pthread_cond_signal(&s->cond); + pthread_mutex_unlock(&s->mutex); + return nullptr; + }, + &sync); + + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_sec += kCallbackTimeoutSec; + + pthread_mutex_lock(&sync.mutex); + while (!sync.done) + { + if (pthread_cond_timedwait(&sync.cond, &sync.mutex, &deadline) == ETIMEDOUT) + break; + } + bool completed = sync.done; + pthread_mutex_unlock(&sync.mutex); + + if (completed) + { + pthread_join(callback_thread, nullptr); + pthread_mutex_destroy(&sync.mutex); + pthread_cond_destroy(&sync.cond); + } + else + { + pthread_detach(callback_thread); + } + } + + int log_fd = open(params->LogPath, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (log_fd >= 0) + { + char time_buf[64] = {}; + time_t now = time(nullptr); + struct tm* tm_info = gmtime(&now); + strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S UTC", tm_info); + char log_buf[512]; + + SafeWrite(log_fd, "==================================================================\n"); + SafeWrite(log_fd, "ZEngine Crash Report\n"); + SafeWrite(log_fd, "==================================================================\n"); + snprintf(log_buf, sizeof(log_buf), "App: %s %s\n", g_state.AppName, g_state.Version); + SafeWrite(log_fd, log_buf); + snprintf(log_buf, sizeof(log_buf), "Date: %s\n", time_buf); + SafeWrite(log_fd, log_buf); + WriteSystemInfo(log_fd); + snprintf(log_buf, sizeof(log_buf), "Signal: %s\n\n", params->SignalOrException); + SafeWrite(log_fd, log_buf); + + SafeWrite(log_fd, "==================================================================\n"); + SafeWrite(log_fd, "Stack Trace (most recent call first):\n"); + SafeWrite(log_fd, "==================================================================\n"); + WriteStackTrace(log_fd, params->BacktraceAddrs, params->BacktraceSize); + + SafeWrite(log_fd, "\n==================================================================\n"); + SafeWrite(log_fd, "Loaded Images:\n"); + SafeWrite(log_fd, "==================================================================\n"); + WriteModuleList(log_fd); + + SafeWrite(log_fd, "==================================================================\n"); + SafeWrite(log_fd, "End of Report\n"); + SafeWrite(log_fd, "==================================================================\n"); + close(log_fd); + } + + if (HasGUISession()) + { + bool sent = ShowCrashDialogCocoa(g_state.AppName, + params->SignalOrException, + params->LogPath, + g_state.UserComment, + kMaxCommentLen, + /*from_signal=*/params->BacktraceFromSignal); +#ifdef ZENGINE_CRASH_UPLOAD + if (sent) + g_state.UserConsentUpload = true; + // TODO(jeanphilippekernel): Implement crash report upload functionality here. +#else + (void) sent; +#endif + } + + if (g_state.UserComment[0] != '\0') + { + int comment_fd = open(params->LogPath, O_WRONLY | O_APPEND, 0644); + if (comment_fd >= 0) + { + SafeWrite(comment_fd, "\n==================================================================\n"); + SafeWrite(comment_fd, "User Comment:\n"); + SafeWrite(comment_fd, "==================================================================\n"); + SafeWrite(comment_fd, g_state.UserComment); + SafeWrite(comment_fd, "\n"); + close(comment_fd); + } + } + + char stderr_buf[512]; + snprintf(stderr_buf, sizeof(stderr_buf), "\n[ZEngine] CRASH: %s\nReport: %s\n", + params->SignalOrException, params->LogPath); + fputs(stderr_buf, stderr); + + _exit(EXIT_FAILURE); + return nullptr; + } + + static void SignalHandler(int sig, siginfo_t* /*info*/, void* ctx) + { + s_crash_params.SignalNumber = sig; + s_crash_params.SignalOrException = SignalToString(sig); + + s_crash_params.BacktraceSize = backtrace(s_crash_params.BacktraceAddrs, kMaxBacktraceFrames); + s_crash_params.BacktraceFromSignal = true; + + if (ctx) + { + auto* uc = static_cast(ctx); +#if defined(__x86_64__) + if (s_crash_params.BacktraceSize > 0) + s_crash_params.BacktraceAddrs[0] = reinterpret_cast(uc->uc_mcontext->__ss.__rip); +#elif defined(__aarch64__) + if (s_crash_params.BacktraceSize > 0) + s_crash_params.BacktraceAddrs[0] = reinterpret_cast(uc->uc_mcontext->__ss.__pc); +#endif + } + + uint8_t byte = 1; + write(s_crash_pipe[1], &byte, 1); + sigset_t all; + sigfillset(&all); + while (true) + sigsuspend(&all); + } + + void CrashHandler::Install(cstring app_name, cstring version, cstring crash_log_dir) + { +#ifdef ZENGINE_CRASH_HANDLER_ENABLED + if (g_state.Installed) + return; + + StoreMetadata(app_name, version, crash_log_dir); + mkdir(crash_log_dir, 0755); + + if (pipe(s_crash_pipe) != 0) + { + fputs("[CrashHandler] Failed to create crash notification pipe.\n", stderr); + return; + } + + stack_t alt_stack = {}; + alt_stack.ss_sp = s_alt_stack_mem; + alt_stack.ss_size = sizeof(s_alt_stack_mem); + alt_stack.ss_flags = 0; + sigaltstack(&alt_stack, nullptr); + + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); + pthread_create(&s_worker_thread, &attr, CrashWorkerThreadFn, nullptr); + pthread_attr_destroy(&attr); + + struct sigaction sa = {}; + sa.sa_sigaction = SignalHandler; + sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESETHAND; + sigemptyset(&sa.sa_mask); + + for (int i = 0; i < kNumTrackedSignals; ++i) + sigaction(kTrackedSignals[i], &sa, &s_previous_actions[kTrackedSignals[i]]); + + g_state.Installed = true; +#endif + } + + void CrashHandler::Uninstall() + { +#ifdef ZENGINE_CRASH_HANDLER_ENABLED + if (!g_state.Installed) + return; + + for (int i = 0; i < kNumTrackedSignals; ++i) + sigaction(kTrackedSignals[i], &s_previous_actions[kTrackedSignals[i]], nullptr); + + uint8_t byte = 0; + write(s_crash_pipe[1], &byte, 1); + pthread_join(s_worker_thread, nullptr); + + close(s_crash_pipe[0]); + close(s_crash_pipe[1]); + s_crash_pipe[0] = s_crash_pipe[1] = -1; + + g_state.Installed = false; +#endif + } + + void CrashHandler::StoreMetadata(cstring app_name, cstring version, cstring crash_log_dir) + { + snprintf(g_state.AppName, kMaxNameLen, "%s", app_name); + snprintf(g_state.Version, kMaxNameLen, "%s", version); + snprintf(g_state.CrashLogDir, kMaxPathLen, "%s", crash_log_dir); + } + + void CrashHandler::SetPreCrashCallback(PreCrashFn fn, void* ctx) + { + g_state.PreCrashFn = fn; + g_state.PreCrashCtx = ctx; + } + + [[noreturn]] void CrashHandler::OnCrash(cstring signal_or_exception, void* /*ctx*/) + { + static std::atomic s_in_crash_handler{false}; + if (s_in_crash_handler.exchange(true, std::memory_order_acq_rel)) + { + fputs("[CrashHandler] FATAL: crash handler reentered. Aborting.\n", stderr); + _exit(EXIT_FAILURE); + } + + // Capture the call-site stack here, on the calling thread, before waking + // the worker. BacktraceFromSignal stays false so the worker skips its own + // (meaningless) backtrace and uses these addresses instead. + s_crash_params.BacktraceSize = backtrace(s_crash_params.BacktraceAddrs, kMaxBacktraceFrames); + + s_crash_params.SignalOrException = signal_or_exception; + uint8_t byte = 1; + write(s_crash_pipe[1], &byte, 1); + // Pump the main run loop so the dispatch_sync block posted by the + // worker thread (which shows the Cocoa dialog) can execute here on + // the main thread. The worker calls _exit() when the dialog closes. + while (true) + [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + } + + [[noreturn]] void CrashHandler::OnAssertionFailure(cstring file, int line, cstring message) + { + char assertion_message[1024]; + snprintf(assertion_message, sizeof(assertion_message), + "Assertion Failure: %s\nFile: %s\nLine: %d", + message ? message : "(null)", file ? file : "(null)", line); + OnCrash(assertion_message, nullptr); + } + +} // namespace ZEngine::CrashHandlers diff --git a/ZEngine/ZEngine/CrashHandlers/CrashHandlerWindows.cpp b/ZEngine/ZEngine/CrashHandlers/CrashHandlerWindows.cpp new file mode 100644 index 00000000..17e8f527 --- /dev/null +++ b/ZEngine/ZEngine/CrashHandlers/CrashHandlerWindows.cpp @@ -0,0 +1,891 @@ +#include +#include +#include +// clang-format off +#include +// clang-format on +#include +#include +#include +#include +#include +#include + +#pragma comment(linker, "\"/manifestdependency:type='win32' \ +name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \ +processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") + +namespace ZEngine::CrashHandlers +{ + using ZEngine::Helpers::secure_strlen; + using ZEngine::Helpers::secure_strncpy; + + struct WorkerThreadParams + { + DWORD FaultedThreadId = 0; + HANDLE FaultedThreadHandle = nullptr; + cstring SignalOrException = nullptr; + EXCEPTION_POINTERS* ExceptionInfo = nullptr; + char LogPath[kMaxPathLen] = {0}; + char DumpPath[kMaxPathLen] = {0}; + }; + + enum : int + { + IDC_HEADER = 101, + IDC_DESC = 102, + IDC_COMMENT_HINT = 103, + IDC_COMMENT_EDIT = 104, + IDC_LOG_CAPTION = 105, + IDC_LOG_EDIT = 106, + IDC_CLOSE_BTN = 107, + IDC_SEND_BTN = 108, + }; + + struct CrashDlgState + { + cstring AppName; + cstring Signal; + cstring LogPath; + bool DidSend; + WCHAR Comment[kMaxCommentLen]; + HFONT hFontBold; + HFONT hFontNormal; + HFONT hFontMono; + }; + + static LPTOP_LEVEL_EXCEPTION_FILTER g_previous_exception_filter = nullptr; + // Minimum stack for the crash worker thread. + // MiniDumpWriteDump with MiniDumpWithFullMemory requires significant stack space + // (~100-200 KB observed in practice). 64 KB is insufficient. + // Use 256 KB as a safe minimum. + static constexpr size_t kCrashWorkerStackSize = 256 * 1024; // 256 KB minimum + + static void ExceptionToString(ULONG_PTR crash_address, DWORD code, char* buffer, size_t buffer_size) + { + const char* exception_name = nullptr; + switch (code) + { + case EXCEPTION_ACCESS_VIOLATION: + exception_name = "Access Violation"; + break; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + exception_name = "Array Bounds Exceeded"; + break; + case EXCEPTION_BREAKPOINT: + exception_name = "Breakpoint"; + break; + case EXCEPTION_DATATYPE_MISALIGNMENT: + exception_name = "Datatype Misalignment"; + break; + case EXCEPTION_FLT_DENORMAL_OPERAND: + exception_name = "Float Denormal Operand"; + break; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + exception_name = "Float Divide by Zero"; + break; + case EXCEPTION_FLT_INEXACT_RESULT: + exception_name = "Float Inexact Result"; + break; + case EXCEPTION_FLT_INVALID_OPERATION: + exception_name = "Float Invalid Operation"; + break; + case EXCEPTION_FLT_OVERFLOW: + exception_name = "Float Overflow"; + break; + case EXCEPTION_FLT_STACK_CHECK: + exception_name = "Float Stack Check"; + break; + case EXCEPTION_FLT_UNDERFLOW: + exception_name = "Float Underflow"; + break; + case EXCEPTION_ILLEGAL_INSTRUCTION: + exception_name = "Illegal Instruction"; + break; + case EXCEPTION_IN_PAGE_ERROR: + exception_name = "In Page Error"; + break; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + exception_name = "Integer Divide by Zero"; + break; + case EXCEPTION_INT_OVERFLOW: + exception_name = "Integer Overflow"; + break; + case EXCEPTION_INVALID_DISPOSITION: + exception_name = "Invalid Disposition"; + break; + case EXCEPTION_NONCONTINUABLE_EXCEPTION: + exception_name = "Noncontinuable Exception"; + break; + case EXCEPTION_PRIV_INSTRUCTION: + exception_name = "Privileged Instruction"; + break; + case EXCEPTION_SINGLE_STEP: + exception_name = "Single Step"; + break; + case EXCEPTION_STACK_OVERFLOW: + exception_name = "Stack Overflow"; + break; + default: + exception_name = "Unknown Exception"; + break; + } + snprintf(buffer, buffer_size, "%s at address 0x%p", exception_name, (void*) crash_address); + } + + static LONG WINAPI UnhandledExceptionFilter(EXCEPTION_POINTERS* exception_pointers) + { + const DWORD code = exception_pointers->ExceptionRecord->ExceptionCode; + const ULONG_PTR crash_address = (code == EXCEPTION_ACCESS_VIOLATION) ? exception_pointers->ExceptionRecord->ExceptionInformation[1] : 0; + + char exception_description[128] = {}; + ExceptionToString(crash_address, code, exception_description, sizeof(exception_description)); + + ZEngine::CrashHandlers::CrashHandler::OnCrash(exception_description, static_cast(exception_pointers)); + return EXCEPTION_EXECUTE_HANDLER; + } + + static void PureCallHandler() + { + ZEngine::CrashHandlers::CrashHandler::OnCrash("CRT Error: Pure Virtual Function Call", nullptr); + } + + static void InvalidParameterHandler(const wchar_t* expression, const wchar_t* function, const wchar_t* file, unsigned int line, uintptr_t pReserved) + { + ZEngine::CrashHandlers::CrashHandler::OnCrash("CRT Error: Invalid Parameter passed to Secure Function", nullptr); + } + + static void SignalAbrtHandler(int signal) + { + ZEngine::CrashHandlers::CrashHandler::OnCrash("CRT Error: SIGABRT (abort requested)", nullptr); + } + + static int OutOfMemoryHandler(size_t size) + { + ZEngine::CrashHandlers::CrashHandler::OnCrash("CRT Error: Out of Memory", nullptr); + return 0; // Return 0 to indicate that the new handler did not handle the allocation failure + } + + static void SafeWriteToFile(HANDLE file, cstring str); + + static void WriteSystemInfo(HANDLE file) + { + // OS version via RtlGetVersion (not affected by compatibility shim, unlike GetVersionEx) + char os_buf[128] = "(unknown)"; + { + using RtlGetVersionFn = NTSTATUS(WINAPI*)(PRTL_OSVERSIONINFOW); + HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + if (ntdll) + { + auto fn = reinterpret_cast(GetProcAddress(ntdll, "RtlGetVersion")); + if (fn) + { + RTL_OSVERSIONINFOW vi = {}; + vi.dwOSVersionInfoSize = sizeof(vi); + if (fn(&vi) == 0) + snprintf(os_buf, sizeof(os_buf), "Windows %lu.%lu (build %lu)", vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber); + } + } + } + + // Architecture + const char* arch = "unknown"; + { + SYSTEM_INFO si = {}; + GetNativeSystemInfo(&si); + switch (si.wProcessorArchitecture) + { + case PROCESSOR_ARCHITECTURE_AMD64: + arch = "x86_64"; + break; + case PROCESSOR_ARCHITECTURE_ARM64: + arch = "arm64"; + break; + case PROCESSOR_ARCHITECTURE_INTEL: + arch = "x86"; + break; + default: + arch = "unknown"; + break; + } + } + + // CPU brand string from registry + char cpu_model[256] = "(unknown)"; + { + HKEY hKey = nullptr; + if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", 0, KEY_READ, &hKey) == ERROR_SUCCESS) + { + DWORD type = 0, size = static_cast(sizeof(cpu_model)); + RegQueryValueExA(hKey, "ProcessorNameString", nullptr, &type, reinterpret_cast(cpu_model), &size); + RegCloseKey(hKey); + } + } + + char line_buf[512]; + snprintf(line_buf, sizeof(line_buf), "OS: %s\n", os_buf); + SafeWriteToFile(file, line_buf); + snprintf(line_buf, sizeof(line_buf), "Arch: %s\n", arch); + SafeWriteToFile(file, line_buf); + snprintf(line_buf, sizeof(line_buf), "CPU: %s\n", cpu_model); + SafeWriteToFile(file, line_buf); + } + + static void SafeWriteToFile(HANDLE file, cstring str) + { + if (file == INVALID_HANDLE_VALUE || !str) + return; + DWORD written; + WriteFile(file, str, static_cast(secure_strlen(str)), &written, nullptr); + } + + static LRESULT WinProcFn(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) + { + switch (msg) + { + case WM_CREATE: + { + auto* cs = reinterpret_cast(lp); + auto* state = reinterpret_cast(cs->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(state)); + + // fonts + state->hFontNormal = CreateFontW(-13, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI"); + state->hFontBold = CreateFontW(-16, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI"); + + HINSTANCE hInst = GetModuleHandleW(nullptr); + const int PAD = 20; + const int W = 720; + const int BTN_H = 28, BTN_W = 160; + int y = PAD; + + // bold header + WCHAR header[256] = {}; + { + WCHAR app_w[128] = {}; + MultiByteToWideChar(CP_UTF8, 0, state->AppName, -1, app_w, 128); + _snwprintf_s(header, _countof(header), _TRUNCATE, L"An application has crashed: %s", app_w); + } + HWND hHeader = CreateWindowExW(0, L"STATIC", header, WS_CHILD | WS_VISIBLE | SS_LEFT, PAD, y, W - PAD * 2, 24, hwnd, (HMENU) IDC_HEADER, hInst, nullptr); + SendMessageW(hHeader, WM_SETFONT, (WPARAM) state->hFontBold, TRUE); + y += 30; + + // description + WCHAR desc[512] = {}; + { + WCHAR sig_w[256] = {}; + MultiByteToWideChar(CP_UTF8, 0, state->Signal, -1, sig_w, 256); + _snwprintf_s(desc, _countof(desc), _TRUNCATE, L"The application encountered an unexpected error and could not continue.\r\n\r\nSignal: %s", sig_w); + } + HWND hDesc = CreateWindowExW(0, L"STATIC", desc, WS_CHILD | WS_VISIBLE | SS_LEFT, PAD, y, W - PAD * 2, 56, hwnd, (HMENU) IDC_DESC, hInst, nullptr); + SendMessageW(hDesc, WM_SETFONT, (WPARAM) state->hFontNormal, TRUE); + y += 64; + + // comment hint + HWND hCommentHint = CreateWindowExW(0, L"STATIC", L"Describe what you were doing when the crash occurred (optional):", WS_CHILD | WS_VISIBLE | SS_LEFT, PAD, y, W - PAD * 2, 18, hwnd, (HMENU) IDC_COMMENT_HINT, hInst, nullptr); + SendMessageW(hCommentHint, WM_SETFONT, (WPARAM) state->hFontNormal, TRUE); + y += 22; + + // editable comment multiline edit + HWND hCommentEdit = CreateWindowExW(0, L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_BORDER | ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN, PAD, y, W - PAD * 2, 90, hwnd, (HMENU) IDC_COMMENT_EDIT, hInst, nullptr); + SendMessageW(hCommentEdit, WM_SETFONT, (WPARAM) state->hFontNormal, TRUE); + SendMessageW(hCommentEdit, EM_SETLIMITTEXT, kMaxCommentLen - 1, 0); + y += 96; + + // log caption + HWND hLogCaption = CreateWindowExW(0, L"STATIC", L"Crash log:", WS_CHILD | WS_VISIBLE | SS_LEFT, PAD, y, W - PAD * 2, 16, hwnd, (HMENU) IDC_LOG_CAPTION, hInst, nullptr); + SendMessageW(hLogCaption, WM_SETFONT, (WPARAM) state->hFontNormal, TRUE); + y += 20; + + // read-only log edit (dark bg / light text) + HWND hLogEdit = CreateWindowExW(0, L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | WS_BORDER | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | ES_AUTOHSCROLL, PAD, y, W - PAD * 2, 200, hwnd, (HMENU) IDC_LOG_EDIT, hInst, nullptr); + { + state->hFontMono = CreateFontW(-12, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, FIXED_PITCH | FF_MODERN, L"Consolas"); + SendMessageW(hLogEdit, WM_SETFONT, (WPARAM) state->hFontMono, TRUE); + } + // load log content + { + HANDLE hLog = CreateFileA(state->LogPath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hLog != INVALID_HANDLE_VALUE) + { + DWORD size = GetFileSize(hLog, nullptr); + if (size != INVALID_FILE_SIZE && size > 0) + { + char* buf = (char*) HeapAlloc(GetProcessHeap(), 0, size + 1); + if (buf) + { + DWORD read = 0; + if (ReadFile(hLog, buf, size, &read, nullptr) && read > 0) + { + buf[read] = '\0'; // Ensure it's null-terminated + + // 1. Find out how many WCHAR characters are needed for the UTF-8 string + int wlen = MultiByteToWideChar(CP_UTF8, 0, buf, -1, nullptr, 0); + if (wlen > 0) + { + WCHAR* wbuf = (WCHAR*) HeapAlloc(GetProcessHeap(), 0, wlen * sizeof(WCHAR)); + if (wbuf) + { + // 2. Perform the conversion into wbuf + MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, wlen); + + // 3. Allocate a destination buffer for line-ending normalization (\n -> \r\n) + // Worst-case scenario is every character is \n, doubling the size + int normalized_capacity = wlen * 2; + WCHAR* normalized_wbuf = (WCHAR*) HeapAlloc(GetProcessHeap(), 0, normalized_capacity * sizeof(WCHAR)); + + if (normalized_wbuf) + { + WCHAR* src = wbuf; + WCHAR* dst = normalized_wbuf; + + while (*src) + { + // If we encounter a lone \n, prepend a \r + if (*src == L'\n' && (src == wbuf || *(src - 1) != L'\r')) + { + *dst++ = L'\r'; + } + *dst++ = *src++; + } + *dst = L'\0'; // Null-terminate the final string + + // Push the normalized text to your edit control + SetWindowTextW(hLogEdit, normalized_wbuf); + + HeapFree(GetProcessHeap(), 0, normalized_wbuf); + } + + // Scroll to bottom + SendMessageW(hLogEdit, EM_SETSEL, (WPARAM) -1, (LPARAM) -1); + SendMessageW(hLogEdit, EM_SCROLLCARET, 0, 0); + + HeapFree(GetProcessHeap(), 0, wbuf); + } + } + } + HeapFree(GetProcessHeap(), 0, buf); + } + } + CloseHandle(hLog); + } + } + y += 206; + + // "Close Without Sending" button + HWND hCloseBtn = CreateWindowExW(0, L"BUTTON", L"Close Without Sending", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, PAD, y, BTN_W, BTN_H, hwnd, (HMENU) IDC_CLOSE_BTN, hInst, nullptr); + SendMessageW(hCloseBtn, WM_SETFONT, (WPARAM) state->hFontNormal, TRUE); + + // "Send and Close" button (default) + HWND hSendBtn = CreateWindowExW(0, L"BUTTON", L"Send and Close", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, W - PAD - BTN_W, y, BTN_W, BTN_H, hwnd, (HMENU) IDC_SEND_BTN, hInst, nullptr); + SendMessageW(hSendBtn, WM_SETFONT, (WPARAM) state->hFontNormal, TRUE); + + (void) hCommentEdit; + (void) hLogEdit; + (void) hCloseBtn; + (void) hSendBtn; + return 0; + } + case WM_CTLCOLORSTATIC: + { + HDC hdcStatic = (HDC) wp; + HWND hwndStatic = (HWND) lp; + + if (GetDlgCtrlID(hwndStatic) == IDC_HEADER) + { + // Make the text background transparent + SetBkMode(hdcStatic, TRANSPARENT); + return (INT_PTR) GetStockObject(HOLLOW_BRUSH); + } + + return 0; + } + case WM_CTLCOLOREDIT: + { + // Dark background for the log read-only edit + HWND hCtrl = reinterpret_cast(lp); + if (GetDlgCtrlID(hCtrl) == IDC_LOG_EDIT) + { + HDC hdc = reinterpret_cast(wp); + SetBkColor(hdc, RGB(30, 30, 30)); + SetTextColor(hdc, RGB(212, 212, 212)); + static HBRUSH hDarkBrush = CreateSolidBrush(RGB(30, 30, 30)); + return reinterpret_cast(hDarkBrush); + } + return DefWindowProcW(hwnd, msg, wp, lp); + } + case WM_COMMAND: + { + auto* state = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + WORD id = LOWORD(wp); + if (id == IDC_CLOSE_BTN) + { + state->DidSend = false; + PostQuitMessage(0); + } + else if (id == IDC_SEND_BTN) + { + state->DidSend = true; + GetDlgItemTextW(hwnd, IDC_COMMENT_EDIT, state->Comment, _countof(state->Comment)); + PostQuitMessage(0); + } + return 0; + } + case WM_CLOSE: + { + auto* state = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + state->DidSend = false; + PostQuitMessage(0); + return 0; + } + case WM_DESTROY: + { + auto* state = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + if (state->hFontBold) + { + DeleteObject(state->hFontBold); + state->hFontBold = nullptr; + } + if (state->hFontNormal) + { + DeleteObject(state->hFontNormal); + state->hFontNormal = nullptr; + } + if (state->hFontMono) + { + DeleteObject(state->hFontMono); + state->hFontMono = nullptr; + } + return 0; + } + default: + return DefWindowProcW(hwnd, msg, wp, lp); + } + } + + static DWORD WINAPI CrashWorkerThreadFn(LPVOID param) + { + auto* params = reinterpret_cast(param); + + if (g_state.PreCrashFn) + { + struct CallbackParams + { + CrashHandler::PreCrashFn fn; + void* ctx; + }; + CallbackParams callback_params = {g_state.PreCrashFn, g_state.PreCrashCtx}; + + HANDLE callback_thread = CreateThread( + nullptr, + 0, + [](LPVOID p) -> DWORD { + auto* cp = static_cast(p); + __try + { + cp->fn(cp->ctx); + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + } + return 0; + }, + &callback_params, + 0, + nullptr); + + if (callback_thread) + { + const DWORD timeout_ms = static_cast(kCallbackTimeoutSec) * 1000; + if (WaitForSingleObject(callback_thread, timeout_ms) == WAIT_TIMEOUT) + { + // Callback did not complete in time. TerminateThread is unsafe in general + // but acceptable here — the process is about to exit anyway. + TerminateThread(callback_thread, 0); + } + CloseHandle(callback_thread); + } + } + + HANDLE log_file_handle = CreateFileA(params->LogPath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (log_file_handle != INVALID_HANDLE_VALUE) + { + char log_buffer[1024] = {}; + SYSTEMTIME current_time; + GetSystemTime(¤t_time); + + SafeWriteToFile(log_file_handle, "==================================================================\n"); + SafeWriteToFile(log_file_handle, "ZEngine Crash Report\n"); + SafeWriteToFile(log_file_handle, "==================================================================\n"); + + snprintf(log_buffer, sizeof(log_buffer), "App: %s %s\n", g_state.AppName, g_state.Version); + SafeWriteToFile(log_file_handle, log_buffer); + snprintf(log_buffer, sizeof(log_buffer), "Date: %04d-%02d-%02d %02d:%02d:%02d UTC\n", current_time.wYear, current_time.wMonth, current_time.wDay, current_time.wHour, current_time.wMinute, current_time.wSecond); + SafeWriteToFile(log_file_handle, log_buffer); + WriteSystemInfo(log_file_handle); + snprintf(log_buffer, sizeof(log_buffer), "Exception: %s\n\n", params->SignalOrException); + SafeWriteToFile(log_file_handle, log_buffer); + + SafeWriteToFile(log_file_handle, "==================================================================\n"); + SafeWriteToFile(log_file_handle, "Stack Trace (most recent call first):\n"); + SafeWriteToFile(log_file_handle, "==================================================================\n"); + + constexpr size_t kMaxStackTrace = 32 * 1024; + char stacktrace_buffer[kMaxStackTrace] = {0}; + + HANDLE process = GetCurrentProcess(); + + SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME); + SymInitialize(process, nullptr, TRUE); + + CONTEXT context = {}; + if (params->ExceptionInfo) + { + context = *params->ExceptionInfo->ContextRecord; + } + else + { + RtlCaptureContext(&context); + } + + STACKFRAME64 frame = {}; + DWORD machine = 0; + +#ifdef _M_AMD64 + machine = IMAGE_FILE_MACHINE_AMD64; + frame.AddrPC.Offset = context.Rip; + frame.AddrFrame.Offset = context.Rbp; + frame.AddrStack.Offset = context.Rsp; +#elif defined(_M_ARM64) + machine = IMAGE_FILE_MACHINE_ARM64; + frame.AddrPC.Offset = context.Pc; + frame.AddrFrame.Offset = context.Fp; + frame.AddrStack.Offset = context.Sp; +#endif + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Mode = AddrModeFlat; + + constexpr size_t kMaxSymName = 256; + uint8_t sym_buffer[sizeof(SYMBOL_INFO) + kMaxSymName] = {}; + auto* sym = reinterpret_cast(sym_buffer); + sym->SizeOfStruct = sizeof(SYMBOL_INFO); + sym->MaxNameLen = kMaxSymName; + + IMAGEHLP_LINE64 line_info = {}; + line_info.SizeOfStruct = sizeof(IMAGEHLP_LINE64); + + size_t offset = 0; + int frame_idx = 0; + + while (StackWalk64(machine, process, params->FaultedThreadHandle, &frame, &context, nullptr, SymFunctionTableAccess64, SymGetModuleBase64, nullptr)) + { + if (frame.AddrPC.Offset == 0) + { + break; + } + + char name_buf[kMaxSymName] = {0}; + DWORD64 sym_disp = 0; + + if (SymFromAddr(process, frame.AddrPC.Offset, &sym_disp, sym)) + { + secure_strncpy(name_buf, sizeof(name_buf), sym->Name, sizeof(name_buf) - 1); + } + else + { + _snprintf_s(name_buf, sizeof(name_buf), _TRUNCATE, ""); + } + + DWORD line_disp = 0; + int written = 0; + + if (SymGetLineFromAddr64(process, frame.AddrPC.Offset, &line_disp, &line_info)) + { + written = _snprintf_s(stacktrace_buffer + offset, kMaxStackTrace - offset, _TRUNCATE, "\t#%-2d %s\n %s:%lu\n", frame_idx, name_buf, line_info.FileName, line_info.LineNumber); + } + else + { + written = _snprintf_s(stacktrace_buffer + offset, kMaxStackTrace - offset, _TRUNCATE, "\t#%-2d %s [0x%016llX]\n", frame_idx, name_buf, (ULONG64) frame.AddrPC.Offset); + } + + if (written > 0) + offset += written; + if (offset >= kMaxStackTrace - 1) + break; + ++frame_idx; + } + SafeWriteToFile(log_file_handle, stacktrace_buffer); + SafeWriteToFile(log_file_handle, "\n"); + + SafeWriteToFile(log_file_handle, "==================================================================\n"); + SafeWriteToFile(log_file_handle, "Modules:\n"); + SafeWriteToFile(log_file_handle, "==================================================================\n"); + EnumerateLoadedModules64( + GetCurrentProcess(), + [](PCSTR module_name, DWORD64 base_address, ULONG size, PVOID user_context) -> BOOL { + HANDLE log_file_handle = reinterpret_cast(user_context); + char log_buffer[512] = {}; + snprintf(log_buffer, sizeof(log_buffer), "\t%s | Base Address: 0x%llX | Size: %lu bytes\n", module_name, base_address, size); + SafeWriteToFile(log_file_handle, log_buffer); + return TRUE; // Continue enumeration + }, + log_file_handle); + + SymCleanup(process); + + SafeWriteToFile(log_file_handle, "==================================================================\n"); + SafeWriteToFile(log_file_handle, "End of Report\n"); + SafeWriteToFile(log_file_handle, "==================================================================\n"); + CloseHandle(log_file_handle); + } + + { + // Attempt to open the dump file; fall back to %TEMP% if the primary path fails. + HANDLE dump_file_handle = CreateFileA(params->DumpPath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + + if (dump_file_handle == INVALID_HANDLE_VALUE) + { + char temp_dir[MAX_PATH] = {}; + char temp_dump_path[MAX_PATH] = {}; + GetTempPathA(MAX_PATH, temp_dir); + snprintf(temp_dump_path, sizeof(temp_dump_path), "%s\\crash_temp_%llu.dmp", temp_dir, GetTickCount64()); + dump_file_handle = CreateFileA(temp_dump_path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + } + + if (dump_file_handle == INVALID_HANDLE_VALUE) + { + fputs("[CrashHandler] FATAL: Failed to create minidump file at both the specified path and temporary path.\n", stderr); + } + else + { + MINIDUMP_EXCEPTION_INFORMATION dump_info = {}; + MINIDUMP_EXCEPTION_INFORMATION* dump_info_ptr = nullptr; + if (params->ExceptionInfo) + { + dump_info.ThreadId = params->FaultedThreadId; + dump_info.ExceptionPointers = params->ExceptionInfo; + dump_info.ClientPointers = FALSE; + dump_info_ptr = &dump_info; + } + +#ifdef _DEBUG + const MINIDUMP_TYPE dump_type = static_cast(MiniDumpNormal); +#elif defined(NDEBUG) || defined(ZENGINE_RELWITHDEBINFO) + const MINIDUMP_TYPE dump_type = static_cast(MiniDumpWithFullMemory | MiniDumpWithFullMemoryInfo | MiniDumpWithHandleData | MiniDumpWithUnloadedModules | MiniDumpWithProcessThreadData); +#else + const MINIDUMP_TYPE dump_type = static_cast(MiniDumpWithDataSegs | MiniDumpWithHandleData | MiniDumpWithUnloadedModules | MiniDumpWithIndirectlyReferencedMemory); +#endif + MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dump_file_handle, dump_type, dump_info_ptr, nullptr, nullptr); + CloseHandle(dump_file_handle); + } + } + + // Native Win32 crash dialog matching the macOS layout. + { + static CrashDlgState s_dlg_state; + s_dlg_state = {}; + s_dlg_state.AppName = g_state.AppName; + s_dlg_state.Signal = params->SignalOrException; + s_dlg_state.LogPath = params->LogPath; + + // Register window class (unique name to avoid conflicts) + const wchar_t* kClassName = L"ZEngineCrashDialog"; + WNDCLASSEXW wc = {}; + wc.cbSize = sizeof(wc); + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = WinProcFn; + HINSTANCE hInst = GetModuleHandleW(nullptr); + wc.hInstance = hInst; + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); + wc.lpszClassName = kClassName; + RegisterClassExW(&wc); + + const int WND_W = 760, WND_H = 560; + int sx = GetSystemMetrics(SM_CXSCREEN); + int sy = GetSystemMetrics(SM_CYSCREEN); + HWND hwnd = CreateWindowExW(WS_EX_APPWINDOW | WS_EX_TOPMOST, kClassName, L"Application Crash", WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU, (sx - WND_W) / 2, (sy - WND_H) / 2, WND_W, WND_H, nullptr, nullptr, hInst, &s_dlg_state); + + if (hwnd) + { + ShowWindow(hwnd, SW_SHOW); + UpdateWindow(hwnd); + + MSG m = {}; + while (GetMessageW(&m, nullptr, 0, 0) > 0) + { + TranslateMessage(&m); + DispatchMessageW(&m); + } + DestroyWindow(hwnd); + } + UnregisterClassW(kClassName, GetModuleHandleW(nullptr)); + + if (s_dlg_state.DidSend) + { + g_state.UserConsentUpload = true; + if (s_dlg_state.Comment[0] != L'\0') + WideCharToMultiByte(CP_UTF8, 0, s_dlg_state.Comment, -1, g_state.UserComment, static_cast(kMaxCommentLen), nullptr, nullptr); + } + + // Append user comment to the log file if provided + if (g_state.UserComment[0] != '\0') + { + HANDLE hAppend = CreateFileA(params->LogPath, FILE_APPEND_DATA, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hAppend != INVALID_HANDLE_VALUE) + { + SafeWriteToFile(hAppend, "\n==================================================================\n"); + SafeWriteToFile(hAppend, "User Comment:\n"); + SafeWriteToFile(hAppend, "==================================================================\n"); + SafeWriteToFile(hAppend, g_state.UserComment); + SafeWriteToFile(hAppend, "\n"); + CloseHandle(hAppend); + } + } + } + + // Upload the crash report if the user consented +#ifdef ZENGINE_CRASH_UPLOAD + if (g_state.UserConsentUpload) + { + // TODO(jeanphilippekernel): Implement crash report upload functionality here. + // cstring upload_url = "https://yourserver.com/upload_crash_report"; + // winrt::Windows::Foundation::Uri uri{winrt::to_hstring(upload_url)}; + + // winrt::Windows::Web::Http::HttpClient http_client; + // http_client.DefaultRequestHeaders().UserAgent().ParseAdd(L"ZEngine/1.0"); + // http_client.PostAsync(uri, winrt::Windows::Web::Http::HttpStringContent(winrt::to_hstring(params->LogPath), winrt::Windows::Web::Http::HttpMediaTypeHeaderValue(L"text/plain"))).get(); + // http_client.PostAsync(uri, winrt::Windows::Web::Http::HttpStringContent(winrt::to_hstring(params->DumpPath), winrt::Windows::Web::Http::HttpMediaTypeHeaderValue(L"application/octet-stream"))).get(); + + // http_client.Close(); + } +#endif // ZENGINE_CRASH_UPLOAD + + return 0; + } + + void CrashHandler::Install(const char* app_name, const char* version, const char* crash_log_dir) + { +#ifdef ZENGINE_CRASH_HANDLER_ENABLED + if (g_state.Installed) + { + return; + } + + StoreMetadata(app_name, version, crash_log_dir); + + _CrtSetReportMode(_CRT_ASSERT, 0); + _set_purecall_handler(PureCallHandler); + _set_invalid_parameter_handler(InvalidParameterHandler); + _set_new_handler(OutOfMemoryHandler); + signal(SIGABRT, SignalAbrtHandler); + + g_previous_exception_filter = SetUnhandledExceptionFilter(UnhandledExceptionFilter); + CreateDirectoryA(crash_log_dir, nullptr); + g_state.Installed = true; +#endif + } + + void CrashHandler::StoreMetadata(const char* app_name, const char* version, const char* crash_log_dir) + { + snprintf(g_state.AppName, kMaxNameLen, "%s", app_name); + snprintf(g_state.Version, kMaxNameLen, "%s", version); + snprintf(g_state.CrashLogDir, kMaxPathLen, "%s", crash_log_dir); + } + + void CrashHandler::SetPreCrashCallback(PreCrashFn fn, void* ctx) + { + g_state.PreCrashFn = fn; + g_state.PreCrashCtx = ctx; + } + + void CrashHandler::Uninstall() + { +#ifdef ZENGINE_CRASH_HANDLER_ENABLED + if (!g_state.Installed) + { + return; + } + + SetUnhandledExceptionFilter(g_previous_exception_filter); + _set_purecall_handler(nullptr); + _set_invalid_parameter_handler(nullptr); + _set_new_handler(nullptr); + signal(SIGABRT, SIG_DFL); + + g_state.Installed = false; +#endif + } + + [[noreturn]] void CrashHandler::OnCrash(const char* signal_or_exception, void* ctx) + { + static std::atomic s_in_crash_handler{false}; + if (s_in_crash_handler.exchange(true, std::memory_order_acq_rel)) + { + // Recursive crash � abort immediately with a minimal message. + fputs("[CrashHandler] FATAL: crash handler reentered. Aborting.\n", stderr); + _Exit(EXIT_FAILURE); + } + + EXCEPTION_POINTERS* exception_info = reinterpret_cast(ctx); + char crash_message[1024] = {0}; + if (exception_info && exception_info->ExceptionRecord) + { + DWORD code = exception_info->ExceptionRecord->ExceptionCode; + // Only reformat for real OS exceptions. Application-defined codes + // (0xE0000000–0xEFFFFFFF) carry a caller-supplied message in + // signal_or_exception already — overwriting it would lose context. + if ((code & 0xF0000000) != 0xE0000000) + { + ULONG_PTR crash_address = (code == EXCEPTION_ACCESS_VIOLATION) ? exception_info->ExceptionRecord->ExceptionInformation[1] : 0; + ExceptionToString(crash_address, code, crash_message, sizeof(crash_message)); + signal_or_exception = crash_message; + } + } + + WorkerThreadParams params; + params.SignalOrException = signal_or_exception; + params.ExceptionInfo = exception_info; + params.FaultedThreadId = GetCurrentThreadId(); + if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), ¶ms.FaultedThreadHandle, THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, 0)) + params.FaultedThreadHandle = GetCurrentThread(); + + { + SYSTEMTIME st = {}; + GetSystemTime(&st); + char t_str[32] = {}; + snprintf(t_str, sizeof(t_str), "%04d-%02d-%02d_%02d-%02d-%02d", (int) st.wYear, (int) st.wMonth, (int) st.wDay, (int) st.wHour, (int) st.wMinute, (int) st.wSecond); + snprintf(params.LogPath, sizeof(params.LogPath), "%s\\crash_%s.log", g_state.CrashLogDir, t_str); + snprintf(params.DumpPath, sizeof(params.DumpPath), "%s\\crash_%s.dmp", g_state.CrashLogDir, t_str); + } + + HANDLE worker_thread = CreateThread(nullptr, kCrashWorkerStackSize, CrashWorkerThreadFn, ¶ms, 0, nullptr); + if (worker_thread) + { + DWORD wait_result = WaitForSingleObject(worker_thread, INFINITE); + CloseHandle(worker_thread); + } + else + { + CrashWorkerThreadFn(¶ms); + } + if (params.FaultedThreadHandle && params.FaultedThreadHandle != GetCurrentThread()) + CloseHandle(params.FaultedThreadHandle); + _Exit(EXIT_FAILURE); + } + + [[noreturn]] void CrashHandler::OnAssertionFailure(const char* file, int line, const char* message) + { + char assertion_message[1024]; + snprintf(assertion_message, sizeof(assertion_message), "Assertion Failure: %s\nFile: %s\nLine: %d", message ? message : "(null)", file ? file : "(null)", line); + __try + { + RaiseException(0xE0000001, 0, 0, nullptr); + } + __except (OnCrash(assertion_message, GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER) + { + } + _Exit(EXIT_FAILURE); + } +} // namespace ZEngine::CrashHandlers \ No newline at end of file diff --git a/ZEngine/ZEngine/ZEngineDef.h b/ZEngine/ZEngine/ZEngineDef.h index d545fd0c..ce2bd7f9 100644 --- a/ZEngine/ZEngine/ZEngineDef.h +++ b/ZEngine/ZEngine/ZEngineDef.h @@ -1,4 +1,5 @@ #pragma once +#include #include #define BIT(x) (1 << (x)) @@ -20,7 +21,7 @@ __builtin_unreachable(); #endif -#ifdef _MSC_VER +#ifdef _WIN32 #define PLATFORM_OS_BACKSLASH '\\' #elif defined(__APPLE__) || defined(__linux__) #define PLATFORM_OS_BACKSLASH '/' @@ -28,14 +29,27 @@ #define PLATFORM_OS_BACKSLASH #endif -#define ZENGINE_VALIDATE_ASSERT(condition, message) \ - { \ - if (!(condition)) \ - { \ - ZENGINE_CORE_CRITICAL(message) \ - ZENGINE_DEBUG_BREAK() \ - } \ - } +#if defined(NDEBUG) || defined(ZENGINE_RELWITHDEBINFO) || defined(ZENGINE_RELEASE) +#define ZENGINE_VALIDATE_ASSERT(cond, msg) \ + do \ + { \ + if (!(cond)) [[unlikely]] \ + { \ + ::ZEngine::CrashHandlers::CrashHandler::OnAssertionFailure(__FILE__, __LINE__, (msg)); \ + } \ + } while (false); +#else +// Debug: use debugger break, no crash handler. +#define ZENGINE_VALIDATE_ASSERT(cond, msg) \ + do \ + { \ + if (!(cond)) \ + { \ + ZENGINE_CORE_CRITICAL(msg) \ + ZENGINE_DEBUG_BREAK() \ + } \ + } while (false); +#endif #define ZENGINE_DESTROY_VULKAN_HANDLE(device, function, handle, ...) \ if (device && handle) \ diff --git a/ZEngine/tests/CMakeLists.txt b/ZEngine/tests/CMakeLists.txt index 1388c0a9..c9a847b7 100644 --- a/ZEngine/tests/CMakeLists.txt +++ b/ZEngine/tests/CMakeLists.txt @@ -1,12 +1,31 @@ -file ( GLOB TEST_SOURCES - Memory/*.cpp - Containers/*.cpp - VFS/*.cpp - Maths/*.cpp - Misc/*.cpp) +file(GLOB TEST_SOURCES + Memory/*.cpp + Containers/*.cpp + VFS/*.cpp + Maths/*.cpp + Misc/*.cpp +) -add_executable(ZEngineTests) +# macOS and Windows are multi-config +if (((APPLE) OR (${CMAKE_SYSTEM_NAME} STREQUAL "Windows"))) + if ((${CMAKE_CONFIGURATION_TYPES} STREQUAL "Release")) + list(APPEND TEST_SOURCES CrashHandlers/CrashHandler_test.cpp) + add_executable(ZEngineCrashDialogDemo CrashHandlers/CrashDialogDemo.cpp) + target_link_libraries(ZEngineCrashDialogDemo PRIVATE zEngineLib) + if(APPLE) + add_custom_command(TARGET ZEngineCrashDialogDemo POST_BUILD + COMMAND codesign --force --sign - $ + COMMENT "Ad-hoc signing ZEngineCrashDialogDemo" + ) + endif() + endif() +elseif((CMAKE_BUILD_TYPE STREQUAL "Release") OR (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) + list(APPEND TEST_SOURCES CrashHandlers/CrashHandler_test.cpp) + add_executable(ZEngineCrashDialogDemo CrashHandlers/CrashDialogDemo.cpp) + target_link_libraries(ZEngineCrashDialogDemo PRIVATE zEngineLib) +endif() +add_executable(ZEngineTests) target_sources(ZEngineTests PRIVATE ${TEST_SOURCES}) target_link_libraries(ZEngineTests PRIVATE diff --git a/ZEngine/tests/CrashHandlers/CrashDialogDemo.cpp b/ZEngine/tests/CrashHandlers/CrashDialogDemo.cpp new file mode 100644 index 00000000..a5a928af --- /dev/null +++ b/ZEngine/tests/CrashHandlers/CrashDialogDemo.cpp @@ -0,0 +1,9 @@ +#include +#include + +int main() +{ + ZEngine::CrashHandlers::CrashHandler::Install("ZEngineDemo", "0.2.0", "/tmp/zengine_crash_demo_logs"); + fprintf(stderr, "[demo] Crash handler installed. Triggering crash...\n"); + ZEngine::CrashHandlers::CrashHandler::OnCrash("GPU device lost: VK_ERROR_DEVICE_LOST"); +} diff --git a/ZEngine/tests/CrashHandlers/CrashHandler_test.cpp b/ZEngine/tests/CrashHandlers/CrashHandler_test.cpp new file mode 100644 index 00000000..09ca0763 --- /dev/null +++ b/ZEngine/tests/CrashHandlers/CrashHandler_test.cpp @@ -0,0 +1,257 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; +using namespace ZEngine::CrashHandlers; + +static const bool kDeathTestStyleSet = []() { + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; +#if defined(_WIN32) + _putenv_s("ZENGINE_CRASH_NO_DIALOG", "1"); +#else + setenv("ZENGINE_CRASH_NO_DIALOG", "1", 1); +#endif + return true; +}(); + +static const char* kTestLogDir = "/tmp/zengine_crash_test_logs"; + +static void CleanLogDir() +{ + std::error_code ec; + fs::remove_all(kTestLogDir, ec); +} + +static std::string FindLatestLog() +{ + std::string latest; + std::error_code ec; + for (auto& entry : fs::directory_iterator(kTestLogDir, ec)) + { + if (entry.path().extension() == ".log") + { + if (latest.empty() || entry.path().string() > latest) + latest = entry.path().string(); + } + } + return latest; +} + +static std::string ReadFile(const std::string& path) +{ + std::ifstream f(path); + return std::string(std::istreambuf_iterator(f), {}); +} + +class CrashHandlerTest : public ::testing::Test +{ +protected: + void SetUp() override + { + CleanLogDir(); + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); + } + + void TearDown() override + { + CrashHandler::Uninstall(); + CleanLogDir(); + } +}; + +class CrashHandlerDeathTest : public ::testing::Test +{ +protected: + void SetUp() override + { + CleanLogDir(); + } + void TearDown() override + { + CleanLogDir(); + } +}; + +TEST_F(CrashHandlerTest, InstallCreatesLogDirectory) +{ + EXPECT_TRUE(fs::exists(kTestLogDir)); +} + +TEST_F(CrashHandlerTest, DoubleInstallIsNoop) +{ + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); + SUCCEED(); +} + +TEST_F(CrashHandlerTest, UninstallLeavesProcessAlive) +{ + CrashHandler::Uninstall(); + SUCCEED(); + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); +} + +TEST_F(CrashHandlerTest, DoubleUninstallIsNoop) +{ + CrashHandler::Uninstall(); + CrashHandler::Uninstall(); + SUCCEED(); + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); +} + +TEST_F(CrashHandlerTest, SetPreCrashCallbackNullIsValid) +{ + CrashHandler::SetPreCrashCallback(nullptr, nullptr); + SUCCEED(); +} + +TEST_F(CrashHandlerDeathTest, PreCrashCallbackFiredBeforeDeath) +{ + ASSERT_DEATH( + { + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); + CrashHandler::SetPreCrashCallback([](void*) { fputs("[test] PreCrashCallback fired\n", stderr); }, nullptr); + CrashHandler::OnCrash("pre-crash callback check"); + }, + "\\[test\\] PreCrashCallback fired"); +} + +TEST_F(CrashHandlerDeathTest, UpdatedCallbackIsUsedNotStaleOne) +{ + ASSERT_DEATH( + { + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); + CrashHandler::SetPreCrashCallback([](void*) { fputs("[test] stale callback\n", stderr); }, nullptr); + CrashHandler::SetPreCrashCallback([](void*) { fputs("[test] updated callback OK\n", stderr); }, nullptr); + CrashHandler::OnCrash("callback update check"); + }, + "\\[test\\] updated callback OK"); +} + +TEST_F(CrashHandlerDeathTest, OnCrashWritesLogFile) +{ + ASSERT_DEATH( + { + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); + CrashHandler::OnCrash("GPU device lost"); + }, + ""); + + EXPECT_FALSE(FindLatestLog().empty()) << "No log file found in " << kTestLogDir; +} + +TEST_F(CrashHandlerDeathTest, OnCrashLogContainsAppMetadata) +{ + ASSERT_DEATH( + { + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); + CrashHandler::OnCrash("metadata check"); + }, + ""); + + std::string log = FindLatestLog(); + ASSERT_FALSE(log.empty()); + std::string body = ReadFile(log); + + EXPECT_NE(body.find("ZEngineTest"), std::string::npos) << "App name missing from log"; + EXPECT_NE(body.find("0.0.1-test"), std::string::npos) << "Version missing from log"; + EXPECT_NE(body.find("metadata check"), std::string::npos) << "Reason missing from log"; +} + +TEST_F(CrashHandlerDeathTest, OnCrashLogContainsStackTrace) +{ + ASSERT_DEATH( + { + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); + CrashHandler::OnCrash("stack trace check"); + }, + ""); + + std::string body = ReadFile(FindLatestLog()); + EXPECT_NE(body.find("Stack Trace"), std::string::npos) << "Stack trace header missing"; + EXPECT_NE(body.find("#0"), std::string::npos) << "No frames in stack trace"; +} + +TEST_F(CrashHandlerDeathTest, OnCrashLogIsProperlyTerminated) +{ + ASSERT_DEATH( + { + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); + CrashHandler::OnCrash("termination check"); + }, + ""); + + std::string body = ReadFile(FindLatestLog()); + EXPECT_NE(body.find("End of Report"), std::string::npos) << "Log not properly terminated"; +} + +TEST_F(CrashHandlerDeathTest, OnAssertionFailureWritesFileAndLine) +{ + ASSERT_DEATH( + { + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); + CrashHandler::OnAssertionFailure("assertion_test.cpp", 42, "x > 0"); + }, + ""); + + std::string body = ReadFile(FindLatestLog()); + EXPECT_NE(body.find("assertion_test.cpp"), std::string::npos) << "File name missing"; + EXPECT_NE(body.find("42"), std::string::npos) << "Line number missing"; + EXPECT_NE(body.find("x > 0"), std::string::npos) << "Condition missing"; +} + +TEST_F(CrashHandlerDeathTest, OnAssertionFailureHandlesNullArgs) +{ + ASSERT_DEATH( + { + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); + CrashHandler::OnAssertionFailure(nullptr, 0, nullptr); + }, + ""); +} + +TEST_F(CrashHandlerDeathTest, SIGSEGVWritesLog) +{ + ASSERT_DEATH( + { + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); + volatile int* p = nullptr; + (void) *p; + }, + ""); + + std::string log = FindLatestLog(); + ASSERT_FALSE(log.empty()) << "No log file found after SIGSEGV"; + EXPECT_NE(ReadFile(log).find("SIGSEGV"), std::string::npos); +} + +TEST_F(CrashHandlerDeathTest, DialogSuppressedInHeadlessMode) +{ + ASSERT_DEATH( + { + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); + CrashHandler::OnCrash("headless mode check"); + }, + ""); + + EXPECT_FALSE(FindLatestLog().empty()) << "Log must still be written in headless mode"; +} + +TEST_F(CrashHandlerDeathTest, SIGABRTWritesLog) +{ + ASSERT_DEATH( + { + CrashHandler::Install("ZEngineTest", "0.0.1-test", kTestLogDir); + std::abort(); + }, + ""); + + std::string log = FindLatestLog(); + ASSERT_FALSE(log.empty()) << "No log file found after SIGABRT"; + EXPECT_NE(ReadFile(log).find("SIGABRT"), std::string::npos); +}