From 92208520d03917860cce09958aba8de693e11ea7 Mon Sep 17 00:00:00 2001 From: momo5502 Date: Sat, 17 Sep 2022 12:24:04 +0200 Subject: [PATCH] Add minidump support --- src/client/component/exception.cpp | 213 +++++++++++++++++++++++++++++ src/client/game/structs.hpp | 12 ++ src/common/exception/minidump.cpp | 85 ++++++++++++ src/common/exception/minidump.hpp | 8 ++ 4 files changed, 318 insertions(+) create mode 100644 src/client/component/exception.cpp create mode 100644 src/common/exception/minidump.cpp create mode 100644 src/common/exception/minidump.hpp diff --git a/src/client/component/exception.cpp b/src/client/component/exception.cpp new file mode 100644 index 00000000..6d01ea12 --- /dev/null +++ b/src/client/component/exception.cpp @@ -0,0 +1,213 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include +#include +#include +#include +#include + +#include + +#include + +namespace exception +{ + namespace + { + DWORD main_thread_id{}; + + thread_local struct + { + DWORD code = 0; + PVOID address = nullptr; + } exception_data{}; + + struct + { + std::chrono::time_point last_recovery{}; + std::atomic recovery_counts = {0}; + } recovery_data{}; + + bool is_game_thread() + { + return main_thread_id == GetCurrentThreadId(); + } + + bool is_exception_interval_too_short() + { + const auto delta = std::chrono::high_resolution_clock::now() - recovery_data.last_recovery; + return delta < 1min; + } + + bool too_many_exceptions_occured() + { + return recovery_data.recovery_counts >= 3; + } + + volatile bool& is_initialized() + { + static volatile bool initialized = false; + return initialized; + } + + bool is_recoverable() + { + return is_initialized() + && is_game_thread() + && !is_exception_interval_too_short() + && !too_many_exceptions_occured(); + } + + void show_mouse_cursor() + { + while (ShowCursor(TRUE) < 0); + } + + void display_error_dialog() + { + const std::string error_str = utils::string::va("Fatal error (0x%08X) at 0x%p.\n" + "A minidump has been written.\n\n", + exception_data.code, exception_data.address); + + utils::thread::suspend_other_threads(); + show_mouse_cursor(); + + MessageBoxA(nullptr, error_str.data(), "S1x ERROR", MB_ICONERROR); + TerminateProcess(GetCurrentProcess(), exception_data.code); + } + + void reset_state() + { + if (is_recoverable()) + { + recovery_data.last_recovery = std::chrono::high_resolution_clock::now(); + ++recovery_data.recovery_counts; + + game::Com_Error(game::ERR_DROP, "Fatal error (0x%08X) at 0x%p.\nA minidump has been written.\n\n" + "BOIII has tried to recover your game, but it might not run stable anymore.\n\n" + "Make sure to update your graphics card drivers and install operating system updates!\n" + "Closing or restarting Steam might also help.", + exception_data.code, exception_data.address); + } + else + { + display_error_dialog(); + } + } + + size_t get_reset_state_stub() + { + static auto* stub = utils::hook::assemble([](utils::hook::assembler& a) + { + a.sub(rsp, 0x10); + a.or_(rsp, 0x8); + a.jmp(reset_state); + }); + + return reinterpret_cast(stub); + } + + std::string get_timestamp() + { + tm ltime{}; + char timestamp[MAX_PATH] = {0}; + const auto time = _time64(nullptr); + + _localtime64_s(<ime, &time); + strftime(timestamp, sizeof(timestamp) - 1, "%Y-%m-%d-%H-%M-%S", <ime); + + return timestamp; + } + + std::string generate_crash_info(const LPEXCEPTION_POINTERS exceptioninfo) + { + std::string info{}; + const auto line = [&info](const std::string& text) + { + info.append(text); + info.append("\r\n"); + }; + + line("BOIII Crash Dump"); + line(""); + line("Version: "s + VERSION); + line("Timestamp: "s + get_timestamp()); + line(utils::string::va("Exception: 0x%08X", exceptioninfo->ExceptionRecord->ExceptionCode)); + line(utils::string::va("Address: 0x%llX", exceptioninfo->ExceptionRecord->ExceptionAddress)); + +#pragma warning(push) +#pragma warning(disable: 4996) + OSVERSIONINFOEXA version_info; + ZeroMemory(&version_info, sizeof(version_info)); + version_info.dwOSVersionInfoSize = sizeof(version_info); + GetVersionExA(reinterpret_cast(&version_info)); +#pragma warning(pop) + + line(utils::string::va("OS Version: %u.%u", version_info.dwMajorVersion, version_info.dwMinorVersion)); + + return info; + } + + void write_minidump(const LPEXCEPTION_POINTERS exceptioninfo) + { + const std::string crash_name = utils::string::va("minidumps/boiii-crash-%s.zip", + get_timestamp().data()); + + utils::compression::zip::archive zip_file{}; + zip_file.add("crash.dmp", create_minidump(exceptioninfo)); + zip_file.add("info.txt", generate_crash_info(exceptioninfo)); + zip_file.write(crash_name, "BOIII Crash Dump"); + } + + bool is_harmless_error(const LPEXCEPTION_POINTERS exceptioninfo) + { + const auto code = exceptioninfo->ExceptionRecord->ExceptionCode; + return code == STATUS_INTEGER_OVERFLOW || code == STATUS_FLOAT_OVERFLOW || code == STATUS_SINGLE_STEP; + } + + LONG WINAPI exception_filter(const LPEXCEPTION_POINTERS exceptioninfo) + { + if (is_harmless_error(exceptioninfo)) + { + return EXCEPTION_CONTINUE_EXECUTION; + } + + write_minidump(exceptioninfo); + + exception_data.code = exceptioninfo->ExceptionRecord->ExceptionCode; + exception_data.address = exceptioninfo->ExceptionRecord->ExceptionAddress; + exceptioninfo->ContextRecord->Rip = get_reset_state_stub(); + + return EXCEPTION_CONTINUE_EXECUTION; + } + + void WINAPI set_unhandled_exception_filter_stub(LPTOP_LEVEL_EXCEPTION_FILTER) + { + // Don't register anything here... + } + } + + class component final : public component_interface + { + public: + component() + { + main_thread_id = GetCurrentThreadId(); + SetUnhandledExceptionFilter(exception_filter); + } + + void pre_start() override + { + const utils::nt::library ntdll("ntdll.dll"); + auto* set_filter = ntdll.get_proc("RtlSetUnhandledExceptionFilter"); + + set_filter(exception_filter); + utils::hook::jump(set_filter, set_unhandled_exception_filter_stub); + } + }; +} + +REGISTER_COMPONENT(exception::component) diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index a2a6ec10..e0abd046 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -416,4 +416,16 @@ namespace game ITEM_TEXTSTYLE_MONOSPACE = 128, ITEM_TEXTSTYLE_MONOSPACESHADOWED = 132, }; + + enum errorParm + { + ERR_FATAL = 0, + ERR_DROP = 1, + ERR_SERVERDISCONNECT = 2, + ERR_DISCONNECT = 3, + ERR_SCRIPT = 4, + ERR_SCRIPT_DROP = 5, + ERR_LOCALIZATION = 6, + ERR_MAPLOADERRORSUMMARY = 7, + }; } diff --git a/src/common/exception/minidump.cpp b/src/common/exception/minidump.cpp new file mode 100644 index 00000000..a55d2909 --- /dev/null +++ b/src/common/exception/minidump.cpp @@ -0,0 +1,85 @@ +#include "minidump.hpp" + +#include +#pragma comment(lib, "dbghelp.lib") + +namespace exception +{ + namespace + { + constexpr MINIDUMP_TYPE get_minidump_type() + { + constexpr auto type = MiniDumpIgnoreInaccessibleMemory // + | MiniDumpWithHandleData // + | MiniDumpScanMemory // + | MiniDumpWithProcessThreadData // + | MiniDumpWithFullMemoryInfo // + | MiniDumpWithThreadInfo // + | MiniDumpWithUnloadedModules; + + return static_cast(type); + } + + std::string get_temp_filename() + { + char filename[MAX_PATH] = {0}; + char pathname[MAX_PATH] = {0}; + + GetTempPathA(sizeof(pathname), pathname); + GetTempFileNameA(pathname, "boiii-", 0, filename); + return filename; + } + + HANDLE write_dump_to_temp_file(const LPEXCEPTION_POINTERS exceptioninfo) + { + MINIDUMP_EXCEPTION_INFORMATION minidump_exception_info = {GetCurrentThreadId(), exceptioninfo, FALSE}; + + auto* const file_handle = CreateFileA(get_temp_filename().data(), GENERIC_WRITE | GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, + FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, + nullptr); + + if (!MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), file_handle, get_minidump_type(), + &minidump_exception_info, + nullptr, + nullptr)) + { + MessageBoxA(nullptr, "There was an error creating the minidump! Hit OK to close the program.", + "Minidump Error", MB_OK | MB_ICONERROR); + TerminateProcess(GetCurrentProcess(), 123); + } + + return file_handle; + } + + std::string read_file(const HANDLE file_handle) + { + FlushFileBuffers(file_handle); + SetFilePointer(file_handle, 0, nullptr, FILE_BEGIN); + + std::string buffer{}; + + DWORD bytes_read = 0; + char temp_bytes[0x2000]; + + do + { + if (!ReadFile(file_handle, temp_bytes, sizeof(temp_bytes), &bytes_read, nullptr)) + { + return {}; + } + + buffer.append(temp_bytes, bytes_read); + } + while (bytes_read == sizeof(temp_bytes)); + + return buffer; + } + } + + std::string create_minidump(const LPEXCEPTION_POINTERS exceptioninfo) + { + const utils::nt::handle file_handle = write_dump_to_temp_file(exceptioninfo); + return read_file(file_handle); + } +} diff --git a/src/common/exception/minidump.hpp b/src/common/exception/minidump.hpp new file mode 100644 index 00000000..42b3a46a --- /dev/null +++ b/src/common/exception/minidump.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include "../utils/nt.hpp" + +namespace exception +{ + std::string create_minidump(LPEXCEPTION_POINTERS exceptioninfo); +}