Add minidump support
This commit is contained in:
parent
65187f401e
commit
92208520d0
213
src/client/component/exception.cpp
Normal file
213
src/client/component/exception.cpp
Normal file
@ -0,0 +1,213 @@
|
||||
#include <std_include.hpp>
|
||||
#include "loader/component_loader.hpp"
|
||||
|
||||
#include "game/game.hpp"
|
||||
|
||||
#include <utils/hook.hpp>
|
||||
#include <utils/io.hpp>
|
||||
#include <utils/string.hpp>
|
||||
#include <utils/thread.hpp>
|
||||
#include <utils/compression.hpp>
|
||||
|
||||
#include <exception/minidump.hpp>
|
||||
|
||||
#include <version.hpp>
|
||||
|
||||
namespace exception
|
||||
{
|
||||
namespace
|
||||
{
|
||||
DWORD main_thread_id{};
|
||||
|
||||
thread_local struct
|
||||
{
|
||||
DWORD code = 0;
|
||||
PVOID address = nullptr;
|
||||
} exception_data{};
|
||||
|
||||
struct
|
||||
{
|
||||
std::chrono::time_point<std::chrono::high_resolution_clock> last_recovery{};
|
||||
std::atomic<int> 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<size_t>(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<LPOSVERSIONINFOA>(&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<void(*)(LPTOP_LEVEL_EXCEPTION_FILTER)>("RtlSetUnhandledExceptionFilter");
|
||||
|
||||
set_filter(exception_filter);
|
||||
utils::hook::jump(set_filter, set_unhandled_exception_filter_stub);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
REGISTER_COMPONENT(exception::component)
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
85
src/common/exception/minidump.cpp
Normal file
85
src/common/exception/minidump.cpp
Normal file
@ -0,0 +1,85 @@
|
||||
#include "minidump.hpp"
|
||||
|
||||
#include <DbgHelp.h>
|
||||
#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<MINIDUMP_TYPE>(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);
|
||||
}
|
||||
}
|
8
src/common/exception/minidump.hpp
Normal file
8
src/common/exception/minidump.hpp
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "../utils/nt.hpp"
|
||||
|
||||
namespace exception
|
||||
{
|
||||
std::string create_minidump(LPEXCEPTION_POINTERS exceptioninfo);
|
||||
}
|
Loading…
Reference in New Issue
Block a user