From b15189807145bb37f5cb035373b24e47dcdbabbb Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sun, 1 Nov 2026 16:41:03 +0100 Subject: [PATCH] Add small crash reports with crash details, CPU preflight check and eu07_starter --- CMakeLists.txt | 68 ++++ EU07.cpp | 60 +-- starter/eu07_starter.cpp | 536 ++++++++++++++++++++++++++ utilities/Globals.cpp | 7 + utilities/Globals.h | 1 + utilities/Logs.cpp | 5 + utilities/cpu_features.h | 180 +++++++++ utilities/cpu_preflight.cpp | 155 ++++++++ utilities/crashdump.cpp | 679 +++++++++++++++++++++++++++++++++ utilities/crashdump.h | 22 ++ utilities/crashdump_channel.h | 53 +++ utilities/crashdump_writer.cpp | 98 +++++ utilities/crashreporter.cpp | 3 + utilities/crashreporter.h | 5 +- 14 files changed, 1818 insertions(+), 54 deletions(-) create mode 100644 starter/eu07_starter.cpp create mode 100644 utilities/cpu_features.h create mode 100644 utilities/cpu_preflight.cpp create mode 100644 utilities/crashdump.cpp create mode 100644 utilities/crashdump.h create mode 100644 utilities/crashdump_channel.h create mode 100644 utilities/crashdump_writer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4104efa7..4c218735 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,6 +86,7 @@ option(GENERATE_PDB "Generate executable with program debugging symbols" ON) option(ENABLE_MCC "Enable multicore compilation" ON) option(WITHDUMPGEN "Enable generating DMP files on crash" ON) option(ENABLE_AVX2 "Enable AVX2 instruction set (requires AVX2-capable CPU, Haswell/2013 or newer)" ON) +option(BUNDLE_VC_RUNTIME "Copy the Visual C++ runtime libraries next to the executable (MSVC), so it does not depend on the installed redistributable" ON) option(ADD_VERSION_SUFFIX "Add date and commit hash to the executable name" ON) set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) @@ -152,6 +153,8 @@ set(SOURCES "vehicle/Gauge.cpp" "utilities/Globals.cpp" "utilities/Logs.cpp" +"utilities/crashdump.cpp" +"utilities/crashdump_writer.cpp" "model/MdlMngr.cpp" "world/MemCell.cpp" "model/Model3d.cpp" @@ -912,6 +915,20 @@ if(WIN32 AND WITH_PYTHON) ) endif() +# the Visual C++ runtime the simulator is built with, next to it: older runtimes installed in the system crash the programs of newer +# compilers (std::mutex of Visual Studio 2022 17.10 and newer with a runtime older than 14.40), missing ones keep them from starting +if (MSVC AND BUNDLE_VC_RUNTIME) + set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) + set(CMAKE_INSTALL_UCRT_LIBRARIES FALSE) + include(InstallRequiredSystemLibraries) + if (CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS) + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS} "$") + else() + message(WARNING "BUNDLE_VC_RUNTIME: the Visual C++ runtime libraries were not found, they are not copied") + endif() +endif() + # yaml-cpp, for the descriptions of scenery templates edited in the scenery editor and for the heightmap terrain add_subdirectory("ref/yaml-cpp" "${CMAKE_CURRENT_BINARY_DIR}/yaml-cpp" EXCLUDE_FROM_ALL) target_link_libraries(eu07_settings INTERFACE yaml-cpp) @@ -942,13 +959,64 @@ if(ENABLE_MCC AND MSVC) add_compile_options(/MP) endif() +set(EU07_AVX2_TARGET OFF) if(ENABLE_AVX2) + # AVX2 belongs to the x86-64 processors: the builds for ARM (Apple silicon, Linux aarch64, Windows on ARM) and the universal macOS ones go without it + if (MSVC) + if ("${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}" STREQUAL "x64") + set(EU07_AVX2_TARGET ON) + endif() + elseif (APPLE AND CMAKE_OSX_ARCHITECTURES) + if ("${CMAKE_OSX_ARCHITECTURES}" STREQUAL "x86_64") + set(EU07_AVX2_TARGET ON) + endif() + elseif ("${CMAKE_SYSTEM_PROCESSOR}" MATCHES "^(x86_64|AMD64|amd64|x64)$") + set(EU07_AVX2_TARGET ON) + endif() + if (NOT EU07_AVX2_TARGET) + message(STATUS "AVX2 instruction set not available for the target processor, disabled.") + endif() +endif() + +if(EU07_AVX2_TARGET) message(STATUS "Enabling AVX2 instruction set.") if (MSVC) target_compile_options(eu07_settings INTERFACE /arch:AVX2) else() target_compile_options(eu07_settings INTERFACE -mavx2) endif() + + # the check of the processor before any code built with AVX2 runs, built without it (it does not use eu07_settings) + add_library(eu07_preflight OBJECT "utilities/cpu_preflight.cpp") + target_include_directories(eu07_preflight PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") + set_target_properties(eu07_preflight PROPERTIES FOLDER "libraries" INTERPROCEDURAL_OPTIMIZATION OFF) + if (MSVC) + # it runs before the C runtime starts, the buffer security checks need the runtime + target_compile_options(eu07_preflight PRIVATE /GS-) + endif() + target_link_libraries(${PROJECT_NAME} eu07_preflight) + target_compile_definitions(${PROJECT_NAME} PRIVATE EU07_CPU_PREFLIGHT) +endif() + +# eu07_starter, the starter of the simulator: checks the processor and the libraries before the start, explains the failures +# of the start the system reports with a bare error code, and writes the crash dumps of the simulator from outside of it +if (MSVC) + file(CONFIGURE OUTPUT "${CMAKE_BINARY_DIR}/eu07_starter.rc" CONTENT "IDI_ICON1 ICON \"${CMAKE_CURRENT_SOURCE_DIR}/eu07.ico\"\n" @ONLY) + add_executable(eu07_starter WIN32 "starter/eu07_starter.cpp" "utilities/crashdump_writer.cpp" "${CMAKE_BINARY_DIR}/eu07_starter.rc") + target_include_directories(eu07_starter PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") + target_compile_definitions(eu07_starter PRIVATE UNICODE _UNICODE "EU07_SIMULATOR_EXE=\"$\"") + if (EU07_AVX2_TARGET) + target_compile_definitions(eu07_starter PRIVATE EU07_REQUIRES_AVX2) + endif() + # the static C runtime: the starter runs where the Visual C++ runtime is missing, to say so + set_target_properties(eu07_starter PROPERTIES + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/pdb" + FOLDER "tools") + target_compile_options(eu07_starter PRIVATE $,/MTd,/MT>) + target_link_libraries(eu07_starter PRIVATE dbghelp version shell32 user32) + add_dependencies(${PROJECT_NAME} eu07_starter) endif() if(WITH_TESTS) diff --git a/EU07.cpp b/EU07.cpp index d3540235..e1e03061 100644 --- a/EU07.cpp +++ b/EU07.cpp @@ -20,67 +20,25 @@ Stele, firleju, szociu, hunter, ZiomalCl, OLI_EU and others #include "application/application.h" #include "utilities/Logs.h" +#include "utilities/crashdump.h" #include -#ifdef WITHDUMPGEN -#ifdef _WIN32 -#include -#include -#endif -#endif #ifdef _MSC_VER +#ifdef EU07_CPU_PREFLIGHT +// checks the processor before the C runtime starts, see utilities/cpu_preflight.cpp +#pragma comment(linker, "/subsystem:windows /ENTRY:eu07_preflight_entry") +#else #pragma comment(linker, "/subsystem:windows /ENTRY:mainCRTStartup") #endif +#endif void export_e3d_standalone(std::string in, std::string out, int flags, bool dynamic); int export_btf_standalone(std::string in, std::string out); int generate_nodebank_previews(int Argc, char *Argv[]); -#include #include -#include -#include #include -#ifdef _WIN32 -#pragma comment(lib, "Dbghelp.lib") - -LONG WINAPI CrashHandler(EXCEPTION_POINTERS *ExceptionInfo) -{ - // Get current local time - SYSTEMTIME st; - GetLocalTime(&st); - - // Format: crash_YYYY-MM-DD_HH-MM-SS.dmp - std::ostringstream oss; - oss << "crash_" << std::setw(4) << std::setfill('0') << st.wYear << "-" << std::setw(2) << std::setfill('0') << st.wMonth << "-" << std::setw(2) << std::setfill('0') << st.wDay << "_" - << std::setw(2) << std::setfill('0') << st.wHour << "-" << std::setw(2) << std::setfill('0') << st.wMinute << "-" << std::setw(2) << std::setfill('0') << st.wSecond << ".dmp"; - - std::string filename = oss.str(); - - HANDLE hFile = CreateFileA(filename.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); - if (hFile != INVALID_HANDLE_VALUE) - { - MINIDUMP_EXCEPTION_INFORMATION dumpInfo; - dumpInfo.ThreadId = GetCurrentThreadId(); - dumpInfo.ExceptionPointers = ExceptionInfo; - dumpInfo.ClientPointers = FALSE; - - // Wybrana kombinacja flag - MINIDUMP_TYPE dumpType = MINIDUMP_TYPE(MiniDumpWithFullMemory | MiniDumpWithHandleData | MiniDumpWithThreadInfo | MiniDumpWithUnloadedModules | MiniDumpWithIndirectlyReferencedMemory | - MiniDumpWithFullMemoryInfo | MiniDumpWithTokenInformation); - - MessageBoxA(nullptr, "Simulator crash occured :(\n", "Simulator crashed :(", MB_ICONERROR); - MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, dumpType, &dumpInfo, nullptr, nullptr); - - CloseHandle(hFile); - } - - return EXCEPTION_EXECUTE_HANDLER; -} - -#endif - // lets the log service write what is still queued and stops it; does nothing once Application.exit() has done that static void stop_log_service() { @@ -92,11 +50,7 @@ static void stop_log_service() int main(int argc, char *argv[]) { -#ifdef WITHDUMPGEN -#ifdef _WIN32 - SetUnhandledExceptionFilter(CrashHandler); -#endif -#endif + crashdump_install(); // init start timestamp Global.startTimestamp = std::chrono::steady_clock::now(); diff --git a/starter/eu07_starter.cpp b/starter/eu07_starter.cpp new file mode 100644 index 00000000..c756b51a --- /dev/null +++ b/starter/eu07_starter.cpp @@ -0,0 +1,536 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +// eu07_starter: starts the simulator next to it, after it checks what the system would otherwise report with a bare error code +// or not at all: the processor (AVX2), the libraries the simulator links (the Visual C++ runtime above all) and their versions. +// it explains the failures of the start, and writes the minidump of a crashed simulator from outside of it (utilities/crashdump.cpp). +// built with the static C runtime and without AVX2, so it runs where the simulator cannot + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "utilities/cpu_features.h" +#include "utilities/crashdump_channel.h" + +#ifndef EU07_SIMULATOR_EXE +#error "EU07_SIMULATOR_EXE, the file name of the simulator, is set by CMakeLists.txt" +#endif + +namespace +{ + +wchar_t const *const title{L"MaSzyna"}; +wchar_t const *const redistributable_url{L"https://aka.ms/vs/17/release/vc_redist.x64.exe"}; + +std::wstring widen(char const *Text) +{ + auto const length{::MultiByteToWideChar(CP_UTF8, 0, Text, -1, nullptr, 0)}; + std::wstring result(length > 0 ? length - 1 : 0, L'\0'); + if (length > 1) + { + ::MultiByteToWideChar(CP_UTF8, 0, Text, -1, result.data(), length); + } + return result; +} + +std::wstring lowercase(std::wstring Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](wchar_t const Character) { return static_cast(std::towlower(Character)); }); + return Text; +} + +bool starts_with(std::wstring const &Text, wchar_t const *Prefix) +{ + return Text.compare(0, std::wcslen(Prefix), Prefix) == 0; +} + +bool file_exists(std::wstring const &Path) +{ + auto const attributes{::GetFileAttributesW(Path.c_str())}; + return (attributes != INVALID_FILE_ATTRIBUTES) && ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0); +} + +std::wstring system_directory() +{ + wchar_t path[MAX_PATH]; + auto const length{::GetSystemDirectoryW(path, MAX_PATH)}; + return (length > 0) && (length < MAX_PATH) ? std::wstring(path, length) : std::wstring(); +} + +std::wstring windows_directory() +{ + wchar_t path[MAX_PATH]; + auto const length{::GetWindowsDirectoryW(path, MAX_PATH)}; + return (length > 0) && (length < MAX_PATH) ? std::wstring(path, length) : std::wstring(); +} + +int show(std::wstring const &Text, UINT const Type) +{ + return ::MessageBoxW(nullptr, Text.c_str(), title, Type | MB_SETFOREGROUND); +} + +// asks to open the download page of the Visual C++ runtime, true when the user wants it +bool offer_redistributable(std::wstring const &Text, bool const CanContinue) +{ + auto const answer{show(Text + L"\n\nOpen the download page of the Microsoft Visual C++ Redistributable (x64) now?" + (CanContinue ? L"\n(No starts the simulator anyway.)" : L""), + MB_YESNO | MB_ICONWARNING)}; + if (answer == IDYES) + { + ::ShellExecuteW(nullptr, L"open", redistributable_url, nullptr, nullptr, SW_SHOWNORMAL); + return true; + } + return false; +} + +// the libraries an image links, from its import directory, read from the image mapped as the loader maps it, without running it +struct image_imports +{ + bool valid{false}; + bool same_machine{false}; + std::vector libraries; +}; + +image_imports read_imports(std::wstring const &Path) +{ + image_imports result; + auto const file{::CreateFileW(Path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; + if (file == INVALID_HANDLE_VALUE) + { + return result; + } + auto const mapping{::CreateFileMappingW(file, nullptr, PAGE_READONLY | SEC_IMAGE, 0, 0, nullptr)}; + ::CloseHandle(file); + if (mapping == nullptr) + { + return result; + } + auto const *view{static_cast(::MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0))}; + ::CloseHandle(mapping); + if (view == nullptr) + { + return result; + } + + MEMORY_BASIC_INFORMATION region{}; + ::VirtualQuery(view, ®ion, sizeof(region)); + auto const *dos{reinterpret_cast(view)}; + if (dos->e_magic == IMAGE_DOS_SIGNATURE) + { + auto const *nt{reinterpret_cast(view + dos->e_lfanew)}; + if (nt->Signature == IMAGE_NT_SIGNATURE) + { + result.valid = true; +#if defined(_M_ARM64) + result.same_machine = (nt->FileHeader.Machine == IMAGE_FILE_MACHINE_ARM64); +#else + result.same_machine = (nt->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64); +#endif + if (result.same_machine && (nt->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC)) + { + auto const size{nt->OptionalHeader.SizeOfImage}; + auto const &directory{nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]}; + if ((directory.VirtualAddress != 0) && (directory.VirtualAddress < size)) + { + for (auto const *descriptor{reinterpret_cast(view + directory.VirtualAddress)}; + (reinterpret_cast(descriptor + 1) <= view + size) && (descriptor->Name != 0); ++descriptor) + { + if (descriptor->Name >= size) + { + break; + } + auto const *name{reinterpret_cast(view + descriptor->Name)}; + auto const length{strnlen(name, std::min(size - descriptor->Name, MAX_PATH))}; + result.libraries.push_back(widen(std::string(name, length).c_str())); + } + } + } + } + } + ::UnmapViewOfFile(view); + return result; +} + +struct library_check +{ + std::wstring application_directory; + std::vector visited; + std::vector missing; + std::vector foreign; // built for another processor, a 32 bit library mostly + bool needs_universal_runtime{false}; +}; + +bool is_runtime_library(std::wstring const &Name) +{ + return starts_with(Name, L"msvcp140") || starts_with(Name, L"vcruntime140") || starts_with(Name, L"concrt140") || (Name == L"ucrtbase.dll"); +} + +// where the loader finds a library of the simulator: next to it first, then in the system. KnownDLLs are system libraries anyway +std::wstring locate(library_check const &Check, std::wstring const &Name, bool &Local) +{ + Local = false; + auto path{Check.application_directory + L"\\" + Name}; + if (file_exists(path)) + { + Local = true; + return path; + } + for (auto const &directory : {system_directory(), windows_directory()}) + { + if (directory.empty()) + { + continue; + } + path = directory + L"\\" + Name; + if (file_exists(path)) + { + return path; + } + } + wchar_t found[MAX_PATH]; + auto const length{::SearchPathW(nullptr, Name.c_str(), nullptr, MAX_PATH, found, nullptr)}; + return (length > 0) && (length < MAX_PATH) ? std::wstring(found, length) : std::wstring(); +} + +// the libraries of the image and of the libraries next to the simulator, which the installation brings. the system ones are taken as they are +void check_libraries(library_check &Check, std::wstring const &Image, int const Depth) +{ + auto const imports{read_imports(Image)}; + for (auto const &library : imports.libraries) + { + auto const name{lowercase(library)}; + if (std::find(Check.visited.begin(), Check.visited.end(), name) != Check.visited.end()) + { + continue; + } + Check.visited.push_back(name); + // the api sets the system resolves on its own; the C runtime ones lead to the universal C runtime + if (starts_with(name, L"api-ms-") || starts_with(name, L"ext-ms-")) + { + if (starts_with(name, L"api-ms-win-crt-")) + { + Check.needs_universal_runtime = true; + } + continue; + } + bool local{false}; + auto const path{locate(Check, name, local)}; + if (path.empty()) + { + Check.missing.push_back(library); + continue; + } + if (local) + { + auto const nested{read_imports(path)}; + if (nested.valid && (false == nested.same_machine)) + { + Check.foreign.push_back(library); + continue; + } + if (Depth < 8) + { + check_libraries(Check, path, Depth + 1); + } + } + } +} + +// the version of a library as major, minor and build, zeros when it has none +void file_version(std::wstring const &Path, unsigned int &Major, unsigned int &Minor, unsigned int &Build) +{ + Major = Minor = Build = 0; + DWORD handle{0}; + auto const size{::GetFileVersionInfoSizeW(Path.c_str(), &handle)}; + if (size == 0) + { + return; + } + std::vector data(size); + if (::GetFileVersionInfoW(Path.c_str(), 0, size, data.data()) == FALSE) + { + return; + } + VS_FIXEDFILEINFO *info{nullptr}; + UINT length{0}; + if ((::VerQueryValueW(data.data(), L"\\", reinterpret_cast(&info), &length) == FALSE) || (info == nullptr)) + { + return; + } + Major = HIWORD(info->dwFileVersionMS); + Minor = LOWORD(info->dwFileVersionMS); + Build = HIWORD(info->dwFileVersionLS); +} + +std::wstring join(std::vector const &Items) +{ + std::wstring result; + for (auto const &item : Items) + { + result += L"\n " + item; + } + return result; +} + +// false when the simulator should not be started +bool check_system(std::wstring const &Simulator, std::wstring const &Directory) +{ +#ifdef EU07_REQUIRES_AVX2 + auto const missingfeatures{cpu_features::missing(cpu_features::required_for_avx2)}; + if (missingfeatures != 0) + { + std::wstring features; + for (unsigned int feature = 1; feature <= cpu_features::os_avx; feature <<= 1) + { + if ((missingfeatures & feature) != 0) + { + features += (features.empty() ? L"" : L", ") + widen(cpu_features::name(feature)); + } + } + char brand[49]; + cpu_features::brand(brand); + show(L"This processor does not support the AVX2 instruction set the simulator is built for. The simulator won't run.\n\nProcessor: " + widen(brand) + L"\nMissing: " + features + + L"\n\nIt needs a processor with AVX2: Intel Core of the 4th generation (Haswell, 2013), AMD of the Excavator generation (2015) or newer.", + MB_OK | MB_ICONERROR); + return false; + } +#endif + + auto const imports{read_imports(Simulator)}; + if (imports.valid && (false == imports.same_machine)) + { + show(L"The simulator " + Simulator + L" is built for another processor than this starter.", MB_OK | MB_ICONERROR); + return false; + } + + library_check check; + check.application_directory = Directory; + check_libraries(check, Simulator, 0); + if (check.needs_universal_runtime) + { + bool local{false}; + if (locate(check, L"ucrtbase.dll", local).empty()) + { + check.missing.push_back(L"ucrtbase.dll (Universal C Runtime, update KB2999226 on Windows 7 and 8.1)"); + } + } + + if (false == check.foreign.empty()) + { + show(L"These libraries next to the simulator are built for another processor (32 bit?):" + join(check.foreign) + + L"\n\nReplace them with the 64 bit ones of the installation: on Steam, verify the integrity of the game files.", + MB_OK | MB_ICONERROR); + return false; + } + if (false == check.missing.empty()) + { + bool runtime{false}; + for (auto const &library : check.missing) + { + runtime = runtime || is_runtime_library(lowercase(library)); + } + std::wstring const text{L"The simulator can't start, these libraries are missing:" + join(check.missing)}; + if (runtime) + { + offer_redistributable(text + L"\n\nThey belong to the Microsoft Visual C++ Redistributable.", false); + } + else + { + show(text + L"\n\nThe installation is incomplete: on Steam, verify the integrity of the game files, otherwise install the simulator again.", MB_OK | MB_ICONERROR); + } + return false; + } + +#if defined(_MSC_VER) && (_MSC_VER >= 1940) + // the programs of Visual Studio 2022 17.10 and newer crash in std::mutex with the runtime older than 14.40 it replaces + bool local{false}; + auto const runtime{locate(check, L"msvcp140.dll", local)}; + unsigned int major, minor, build; + file_version(runtime, major, minor, build); + if ((major == 14) && (minor < 40)) + { + if (offer_redistributable(L"The Microsoft Visual C++ runtime (" + runtime + L", version 14." + std::to_wstring(minor) + L"." + std::to_wstring(build) + + L") is older than the simulator needs (14.40 or newer). The simulator will likely crash at start.", + true)) + { + return false; + } + } +#endif + return true; +} + +// the explanation of an exit code of a simulator which did not report a crash itself, empty for an ordinary exit +std::wstring explain_exit(DWORD const Code) +{ + switch (Code) + { + case 0xC0000135: // STATUS_DLL_NOT_FOUND + return L"A library the simulator needs is missing (0xC0000135). Install the Microsoft Visual C++ Redistributable (x64); if it does not help, verify the integrity of the game files."; + case 0xC0000139: // STATUS_ENTRYPOINT_NOT_FOUND + return L"A library the simulator needs is too old or of another version (0xC0000139). Install the latest Microsoft Visual C++ Redistributable (x64), and verify the integrity of the game files."; + case 0xC000007B: // STATUS_INVALID_IMAGE_FORMAT + return L"A library of the simulator is damaged or built for another processor (0xC000007B). Verify the integrity of the game files."; + case 0xC0000142: // STATUS_DLL_INIT_FAILED + return L"A library of the simulator failed to start (0xC0000142)."; + case 0xC000001D: // STATUS_ILLEGAL_INSTRUCTION + return L"The simulator used an instruction this processor does not have (0xC000001D). It needs a processor with AVX2."; + case 0xC0000409: // STATUS_STACK_BUFFER_OVERRUN, also a fast fail of the runtime + return L"The simulator was stopped by a fatal error check (0xC0000409), no crash report could be written."; + case 0xC000013A: // STATUS_CONTROL_C_EXIT + return {}; + default: + break; + } + if ((Code & 0xF0000000) == 0xC0000000) + { + wchar_t text[160]; + std::swprintf(text, 160, L"The simulator ended with the error 0x%08lX before its crash report could be written.", static_cast(Code)); + return text; + } + return {}; +} + +// the arguments of the starter, passed on to the simulator +wchar_t const *arguments() +{ + auto const *line{::GetCommandLineW()}; + if (*line == L'"') + { + ++line; + while ((*line != L'\0') && (*line != L'"')) + { + ++line; + } + if (*line == L'"') + { + ++line; + } + } + else + { + while ((*line != L'\0') && (*line != L' ') && (*line != L'\t')) + { + ++line; + } + } + while ((*line == L' ') || (*line == L'\t')) + { + ++line; + } + return line; +} + +} // namespace + +int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) +{ + wchar_t path[MAX_PATH * 2]; + auto const length{::GetModuleFileNameW(nullptr, path, MAX_PATH * 2)}; + if ((length == 0) || (length >= MAX_PATH * 2)) + { + return 1; + } + std::wstring directory(path, length); + directory.erase(directory.find_last_of(L"\\/")); + auto const simulator{directory + L"\\" + widen(EU07_SIMULATOR_EXE)}; + + if (false == file_exists(simulator)) + { + show(L"The simulator was not found:\n" + simulator, MB_OK | MB_ICONERROR); + return 1; + } + if (false == check_system(simulator, directory)) + { + return 1; + } + + // the shared block of the crash report, its handles are inherited by the simulator + SECURITY_ATTRIBUTES inheritable{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; + auto const mapping{::CreateFileMappingW(INVALID_HANDLE_VALUE, &inheritable, PAGE_READWRITE, 0, sizeof(crashdump::channel_data), nullptr)}; + auto *channel{mapping != nullptr ? static_cast(::MapViewOfFile(mapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, sizeof(crashdump::channel_data))) : nullptr}; + auto const request{::CreateEventW(&inheritable, FALSE, FALSE, nullptr)}; + auto const done{::CreateEventW(&inheritable, FALSE, FALSE, nullptr)}; + bool const reporting{(channel != nullptr) && (request != nullptr) && (done != nullptr)}; + if (reporting) + { + channel->magic = crashdump::channel_magic; + channel->version = crashdump::channel_version; + wchar_t value[128]; + std::swprintf(value, 128, L"%llx:%llx:%llx", static_cast(reinterpret_cast(mapping)), static_cast(reinterpret_cast(request)), + static_cast(reinterpret_cast(done))); + ::SetEnvironmentVariableW(crashdump::channel_variable, value); + } + + std::wstring commandline{L"\"" + simulator + L"\""}; + auto const *passed{arguments()}; + if (*passed != L'\0') + { + commandline += L" "; + commandline += passed; + } + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + if (::CreateProcessW(simulator.c_str(), commandline.data(), nullptr, nullptr, reporting ? TRUE : FALSE, 0, nullptr, nullptr, &startup, &process) == FALSE) + { + auto const error{::GetLastError()}; + wchar_t text[160]; + std::swprintf(text, 160, L"\n\nThe system refused to start it (error %lu)", static_cast(error)); + show(L"The simulator could not be started:\n" + simulator + text + + (error == ERROR_BAD_EXE_FORMAT ? L": it is not a program for this version of Windows (it needs 64 bit Windows)." : L"."), + MB_OK | MB_ICONERROR); + return 1; + } + ::CloseHandle(process.hThread); + + bool reported{false}; + HANDLE const waited[]{process.hProcess, request}; + while (true) + { + auto const result{::WaitForMultipleObjects(reporting ? 2 : 1, waited, FALSE, INFINITE)}; + if (result == WAIT_OBJECT_0 + 1) + { + // the simulator crashed and waits: its minidump, written from here as the crashed process may be unable to + if (channel->process_id == process.dwProcessId) + { + crashdump_write_minidump(process.hProcess, *channel, true); + reported = true; + } + ::SetEvent(done); + continue; + } + break; + } + + DWORD code{0}; + ::GetExitCodeProcess(process.hProcess, &code); + ::CloseHandle(process.hProcess); + if ((false == reported) && (code != cpu_features::exit_unsupported)) + { + auto const explanation{explain_exit(code)}; + if (false == explanation.empty()) + { + show(explanation, MB_OK | MB_ICONERROR); + } + } + return static_cast(code); +} diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index b1fcee94..65ef8ab1 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1220,6 +1220,12 @@ bool global_settings::ConfigParseDebug(cParser& Parser, const std::string& token return true; } + if (token == "crashdump.full") + { + ParseOne(Parser, crashdump_full); + return true; + } + if (token == "showsystemconsole") { ParseOne(Parser, ShowSystemConsole); @@ -2158,6 +2164,7 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "debug.memorystats", debug_memorystats ); export_as_text( Output, "debuglog", iWriteLogEnabled ); export_as_text( Output, "multiplelogs", MultipleLogs ); + export_as_text( Output, "crashdump.full", crashdump_full ); export_as_text( Output, "showsystemconsole", ShowSystemConsole ); export_as_text( Output, "logs.filter", DisabledLogTypes ); Output diff --git a/utilities/Globals.h b/utilities/Globals.h index fd90b6ff..6dd83f42 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -98,6 +98,7 @@ struct global_settings { bool priorityLoadText3D{false}; // ladowanie T3D priorytetowo int iWriteLogEnabled{ 3 }; // maska bitowa: 1-zapis do pliku, 2-okienko, 4-nazwy torów bool MultipleLogs{ false }; + bool crashdump_full{ false }; // crash dump with the whole memory of the process instead of the small one bool ShowSystemConsole{ false }; // toggle visibility of the OS console window with log output unsigned int DisabledLogTypes{ 0 }; bool ParserLogIncludes{ true }; diff --git a/utilities/Logs.cpp b/utilities/Logs.cpp index da14373a..ba9cac2a 100644 --- a/utilities/Logs.cpp +++ b/utilities/Logs.cpp @@ -16,6 +16,7 @@ http://mozilla.org/MPL/2.0/. #include "winheaders.h" #include "utilities/utilities.h" #include "application/uilayer.h" +#include "utilities/crashdump.h" #include std::ofstream output; // standardowy "log.txt", można go wyłączyć @@ -102,6 +103,7 @@ void LogService() { std::string filename = Global.MultipleLogs ? "logs/log (" + filename_scenery() + ") " + filename_date() + ".txt" : "log.txt"; output.open(filename, std::ios::trunc); + crashdump_set_log_file(crashdump_log_file::log, filename); } output << msg << "\n"; output.flush(); @@ -139,6 +141,7 @@ void LogService() { std::string filename = Global.MultipleLogs ? "logs/errors (" + filename_scenery() + ") " + filename_date() + ".txt" : "errors.txt"; errors.open(filename, std::ios::trunc); + crashdump_set_log_file(crashdump_log_file::errors, filename); errors << "EU07.EXE " + Global.asVersion << "\n"; } @@ -176,6 +179,8 @@ void WriteLog(std::string_view str, logtype type, bool isError) const auto message = FormatLogMessage(str); std::scoped_lock lock(logMutex); + // at once rather than by the log service, which may not get to it before a crash + crashdump_note_log(message.data(), message.size()); InfoStack.emplace_back(message, isError); } diff --git a/utilities/cpu_features.h b/utilities/cpu_features.h new file mode 100644 index 00000000..835bef27 --- /dev/null +++ b/utilities/cpu_features.h @@ -0,0 +1,180 @@ +#pragma once + +// the processor features the simulator is built to use (ENABLE_AVX2), checked before any code built with them runs. +// header only and free of the C runtime: the simulator checks them from its entry point, before the runtime starts + +#if defined(_M_X64) || defined(__x86_64__) +#define EU07_CPU_FEATURES_X64 1 +#ifdef _MSC_VER +#include +#else +#include +#endif +#endif + +namespace cpu_features +{ + +enum feature : unsigned int +{ + avx2 = 1 << 0, + fma3 = 1 << 1, + bmi1 = 1 << 2, + bmi2 = 1 << 3, + lzcnt = 1 << 4, + // the processor has AVX, but the system does not save its registers, so it is turned off (old systems, some virtual machines) + os_avx = 1 << 5, +}; + +// the exit code of the simulator stopped by the check of the processor, after it told the user (eu07_starter stays silent then) +constexpr unsigned int exit_unsupported{0xE0450010}; + +// msvc builds with /arch:AVX2 use the bit manipulation instructions of the same processor generation as well, +// gcc and clang with -mavx2 only AVX2 itself. every processor with AVX2 (Intel Haswell, AMD Excavator and newer) has them all +#ifdef _MSC_VER +constexpr unsigned int required_for_avx2{avx2 | fma3 | bmi1 | bmi2 | lzcnt | os_avx}; +#else +constexpr unsigned int required_for_avx2{avx2 | os_avx}; +#endif + +#ifdef EU07_CPU_FEATURES_X64 + +inline void cpuid(int const Leaf, int const Subleaf, int Registers[4]) +{ +#ifdef _MSC_VER + __cpuidex(Registers, Leaf, Subleaf); +#else + unsigned int a, b, c, d; + __cpuid_count(Leaf, Subleaf, a, b, c, d); + Registers[0] = static_cast(a); + Registers[1] = static_cast(b); + Registers[2] = static_cast(c); + Registers[3] = static_cast(d); +#endif +} + +// the register state the system saves on a thread switch; xgetbv may be used only when cpuid reports OSXSAVE +inline unsigned long long xcr0() +{ +#ifdef _MSC_VER + return _xgetbv(0); +#else + unsigned int low, high; + __asm__ volatile("xgetbv" : "=a"(low), "=d"(high) : "c"(0)); + return (static_cast(high) << 32) | low; +#endif +} + +// the features of the processor, a mask of the feature values +inline unsigned int present() +{ + int registers[4]; + cpuid(0, 0, registers); + int const maxleaf{registers[0]}; + cpuid(0x80000000, 0, registers); + unsigned int const maxextendedleaf{static_cast(registers[0])}; + + unsigned int result{0}; + cpuid(1, 0, registers); + bool const hasavx{(registers[2] & (1 << 28)) != 0}; + bool const hasosxsave{(registers[2] & (1 << 27)) != 0}; + if ((registers[2] & (1 << 12)) != 0) + { + result |= fma3; + } + // bits 1 and 2: the SSE and AVX registers + if (hasavx && hasosxsave && ((xcr0() & 0x6) == 0x6)) + { + result |= os_avx; + } + if (maxleaf >= 7) + { + cpuid(7, 0, registers); + if ((registers[1] & (1 << 5)) != 0) + { + result |= avx2; + } + if ((registers[1] & (1 << 3)) != 0) + { + result |= bmi1; + } + if ((registers[1] & (1 << 8)) != 0) + { + result |= bmi2; + } + } + if (maxextendedleaf >= 0x80000001) + { + cpuid(0x80000001, 0, registers); + if ((registers[2] & (1 << 5)) != 0) + { + result |= lzcnt; + } + } + return result; +} + +// the name of the processor, up to 48 characters +inline void brand(char Output[49]) +{ + int registers[4]; + cpuid(0x80000000, 0, registers); + if (static_cast(registers[0]) < 0x80000004) + { + Output[0] = '\0'; + return; + } + for (int leaf = 0; leaf < 3; ++leaf) + { + cpuid(0x80000002 + leaf, 0, registers); + for (int index = 0; index < 16; ++index) + { + Output[leaf * 16 + index] = reinterpret_cast(registers)[index]; + } + } + Output[48] = '\0'; +} + +#else + +inline unsigned int present() +{ + return ~0u; +} + +inline void brand(char Output[49]) +{ + Output[0] = '\0'; +} + +#endif + +// the required features the processor does not have +inline unsigned int missing(unsigned int const Required) +{ + return Required & ~present(); +} + +// the name of a single feature +inline char const *name(unsigned int const Feature) +{ + switch (Feature) + { + case avx2: + return "AVX2"; + case fma3: + return "FMA3"; + case bmi1: + return "BMI1"; + case bmi2: + return "BMI2"; + case lzcnt: + return "LZCNT"; + case os_avx: + return "AVX support of the operating system"; + default: + return "?"; + } +} + +} // namespace cpu_features diff --git a/utilities/cpu_preflight.cpp b/utilities/cpu_preflight.cpp new file mode 100644 index 00000000..0bb60237 --- /dev/null +++ b/utilities/cpu_preflight.cpp @@ -0,0 +1,155 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +// the check of the processor of a build with ENABLE_AVX2, before any code built with AVX2 runs. the start of the C runtime runs +// the constructors of the global objects, already built with it: on a processor without AVX2 the simulator would end there +// without a word. this file is built without AVX2 and without the precompiled header (eu07_preflight in CMakeLists.txt) + +#include "utilities/cpu_features.h" + +#ifdef _WIN32 + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include + +// the entry point of the C runtime, started by the one below +extern "C" int mainCRTStartup(void *); + +namespace +{ + +#ifdef _MSC_VER +// the runtime checks need the C runtime, which is not started yet +#pragma runtime_checks("", off) +#endif + +void append(wchar_t *Output, int const Capacity, int &Length, char const *Text) +{ + while ((*Text != '\0') && (Length < Capacity - 1)) + { + Output[Length++] = static_cast(*Text++); + } + Output[Length] = L'\0'; +} + +#ifdef _MSC_VER +__declspec(noinline) __declspec(safebuffers) +#endif +void report_missing(unsigned int const Missing) +{ + wchar_t text[512]; + int length{0}; + append(text, 512, length, "This processor does not support the AVX2 instruction set the simulator is built for. The simulator won't run.\n\nMissing: "); + bool first{true}; + for (unsigned int feature = 1; feature <= cpu_features::os_avx; feature <<= 1) + { + if ((Missing & feature) != 0) + { + append(text, 512, length, first ? "" : ", "); + append(text, 512, length, cpu_features::name(feature)); + first = false; + } + } + append(text, 512, length, "\n\nIt needs a processor with AVX2: Intel Core of the 4th generation (Haswell, 2013), AMD of the Excavator generation (2015) or newer."); + ::MessageBoxW(nullptr, text, L"MaSzyna", MB_ICONERROR | MB_OK); +} + +} // namespace + +// the entry point of the simulator (/ENTRY), runs before the C runtime starts. without a buffer security check, the security cookie +// is set up by the runtime started from here +extern "C" +#ifdef _MSC_VER + __declspec(safebuffers) +#endif + int eu07_preflight_entry(void *Peb) +{ + auto const missing{cpu_features::missing(cpu_features::required_for_avx2)}; + if (missing != 0) + { + report_missing(missing); + ::ExitProcess(cpu_features::exit_unsupported); + } + return mainCRTStartup(Peb); +} + +#ifdef _MSC_VER +#pragma runtime_checks("", restore) +#endif + +#else + +#include +#include +#include +#include +#include + +extern char **environ; + +namespace +{ + +bool show_dialog(char const *Program, char const *const *Arguments) +{ + pid_t process; + if (posix_spawnp(&process, Program, nullptr, nullptr, const_cast(Arguments), environ) != 0) + { + return false; + } + int status{0}; + return (waitpid(process, &status, 0) == process) && WIFEXITED(status) && (WEXITSTATUS(status) != 127); +} + +// before the constructors of the other global objects of the simulator +__attribute__((constructor(101))) void eu07_preflight() +{ + auto const missing{cpu_features::missing(cpu_features::required_for_avx2)}; + if (missing == 0) + { + return; + } + char text[512]; + int length{std::snprintf(text, sizeof(text), "This processor does not support the AVX2 instruction set the simulator is built for. The simulator won't run.\nMissing:")}; + for (unsigned int feature = 1; feature <= cpu_features::os_avx; feature <<= 1) + { + if (((missing & feature) != 0) && (length > 0) && (length < static_cast(sizeof(text)))) + { + length += std::snprintf(text + length, sizeof(text) - length, " %s", cpu_features::name(feature)); + } + } + std::fprintf(stderr, "%s\n", text); + // started from the desktop or steam, the console is not seen +#ifdef __APPLE__ + char script[640]; + std::snprintf(script, sizeof(script), "display alert \"MaSzyna\" message \"%s\" as critical", text); + char const *const osascript[]{"osascript", "-e", script, nullptr}; + show_dialog("osascript", osascript); +#else + if ((std::getenv("DISPLAY") != nullptr) || (std::getenv("WAYLAND_DISPLAY") != nullptr)) + { + char const *const zenity[]{"zenity", "--error", "--title=MaSzyna", "--text", text, nullptr}; + char const *const kdialog[]{"kdialog", "--title", "MaSzyna", "--error", text, nullptr}; + if (false == show_dialog("zenity", zenity)) + { + show_dialog("kdialog", kdialog); + } + } +#endif + std::_Exit(1); +} + +} // namespace + +#endif diff --git a/utilities/crashdump.cpp b/utilities/crashdump.cpp new file mode 100644 index 00000000..2d38467e --- /dev/null +++ b/utilities/crashdump.cpp @@ -0,0 +1,679 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "utilities/crashdump.h" + +#if defined(_WIN32) && defined(WITHDUMPGEN) + +#include +#include +#include +#include +#include +#include + +#include "utilities/Globals.h" +#include "utilities/cpu_features.h" +#include "utilities/crashdump_channel.h" +#include "version_info.h" + +#ifdef _MSC_VER +#include +#pragma comment(lib, "Dbghelp.lib") +#pragma comment(lib, "psapi.lib") +#endif + +namespace +{ + +// the crashes reported without an exception of the system +constexpr DWORD code_abort{0xE0450001}; +constexpr DWORD code_purecall{0xE0450002}; +constexpr DWORD code_invalid_parameter{0xE0450003}; +// the reports kept in crashdumps/, the older ones are removed at the start +constexpr std::size_t reports_kept{10}; +// "crash_YYYY-MM-DD_HH-MM-SS", the start of the names of the files of a report +constexpr std::size_t report_stamp_length{25}; + +// the named values, written by the simulator as it goes and read without a lock when it crashes +struct info_entry +{ + char name[48]; + char value[464]; +}; +info_entry info_entries[48]; +std::atomic info_used{0}; +std::mutex info_mutex; + +// the recent lines of the log, written under the lock of the log +char log_ring[24 * 1024]; +std::size_t log_written{0}; +wchar_t log_paths[2][MAX_PATH * 2]{}; + +// taken at the installation, when it is still safe to +wchar_t report_directory[MAX_PATH * 2]{}; +wchar_t executable_stem[MAX_PATH]{}; +char executable_path[MAX_PATH * 3]{}; +char processor_name[49]{}; +char system_version[96]{}; +SYSTEMTIME start_time{}; +DWORD main_thread_id{0}; + +// the report. with eu07_starter the block is shared with it and the starter writes the minidump +crashdump::channel_data local_channel{}; +crashdump::channel_data *channel{&local_channel}; +HANDLE starter_request{nullptr}; +HANDLE starter_done{nullptr}; + +// the report is written by a thread started beforehand: the crashed one may have no stack left, and a thread started +// after a crash may wait forever for the lock of the loader the crashed one holds +HANDLE report_request{nullptr}; +HANDLE report_written{nullptr}; +HANDLE report_done{nullptr}; +HANDLE reporter_thread{nullptr}; +DWORD reporter_thread_id{0}; +volatile LONG crash_started{0}; +EXCEPTION_POINTERS *crash_exception{nullptr}; +DWORD crash_thread_id{0}; + +struct text_buffer +{ + char *data; + std::size_t capacity; + std::size_t length; +}; + +void append(text_buffer &Buffer, char const *Format, ...) +{ + if (Buffer.length + 1 >= Buffer.capacity) + { + return; + } + va_list arguments; + va_start(arguments, Format); + auto const written{std::vsnprintf(Buffer.data + Buffer.length, Buffer.capacity - Buffer.length, Format, arguments)}; + va_end(arguments); + if (written > 0) + { + Buffer.length = std::min(Buffer.length + static_cast(written), Buffer.capacity - 1); + } +} + +void append_wide(text_buffer &Buffer, wchar_t const *Text) +{ + if (Buffer.length + 1 >= Buffer.capacity) + { + return; + } + auto const written{::WideCharToMultiByte(CP_UTF8, 0, Text, -1, Buffer.data + Buffer.length, static_cast(Buffer.capacity - Buffer.length), nullptr, nullptr)}; + if (written > 0) + { + // the count includes the terminating zero + Buffer.length += static_cast(written) - 1; + } + Buffer.data[Buffer.length] = '\0'; +} + +char const *exception_name(DWORD const Code) +{ + switch (Code) + { + case EXCEPTION_ACCESS_VIOLATION: + return "access violation"; + case EXCEPTION_STACK_OVERFLOW: + return "stack overflow"; + case EXCEPTION_ILLEGAL_INSTRUCTION: + return "illegal instruction"; + case EXCEPTION_PRIV_INSTRUCTION: + return "privileged instruction"; + case EXCEPTION_IN_PAGE_ERROR: + return "in page error"; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + return "integer division by zero"; + case EXCEPTION_INT_OVERFLOW: + return "integer overflow"; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + case EXCEPTION_FLT_INVALID_OPERATION: + case EXCEPTION_FLT_OVERFLOW: + case EXCEPTION_FLT_UNDERFLOW: + case EXCEPTION_FLT_INEXACT_RESULT: + case EXCEPTION_FLT_DENORMAL_OPERAND: + case EXCEPTION_FLT_STACK_CHECK: + return "floating point exception"; + case EXCEPTION_DATATYPE_MISALIGNMENT: + return "datatype misalignment"; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + return "array bounds exceeded"; + case EXCEPTION_BREAKPOINT: + return "breakpoint"; + case 0xC0000374: + return "heap corruption"; + case 0xC0000409: + return "stack buffer overrun"; + case 0xE06D7363: + return "unhandled C++ exception"; + case code_abort: + return "abort() called"; + case code_purecall: + return "pure virtual function call"; + case code_invalid_parameter: + return "invalid parameter passed to a C runtime function"; + default: + return "unknown"; + } +} + +#if defined(_MSC_VER) && defined(_WIN64) +// the type and the message of an unhandled C++ exception, read from the structures msvc throws it with (image relative on x64). +// guarded, the exception may come from anywhere +void describe_cpp_exception(EXCEPTION_RECORD const &Record, text_buffer &Buffer) +{ + if ((Record.ExceptionCode != 0xE06D7363) || (Record.NumberParameters < 4) || (Record.ExceptionInformation[0] != 0x19930520)) + { + return; + } + __try + { + auto const object{Record.ExceptionInformation[1]}; + auto const throwinfo{Record.ExceptionInformation[2]}; + auto const imagebase{Record.ExceptionInformation[3]}; + if ((throwinfo == 0) || (imagebase == 0)) + { + return; + } + // ThrowInfo: attributes, unwind, forward compat, catchable type array + auto const *types{reinterpret_cast(imagebase + reinterpret_cast(throwinfo)[3])}; + auto const count{types[0]}; + for (int index = 0; (index < count) && (index < 32); ++index) + { + // CatchableType: properties, type descriptor, displacement (mdisp, pdisp, vdisp), size, copy function + auto const *type{reinterpret_cast(imagebase + types[1 + index])}; + // TypeDescriptor: vftable, spare, decorated name + auto const *name{reinterpret_cast(imagebase + type[1]) + 2 * sizeof(void *)}; + if (index == 0) + { + append(Buffer, " C++ exception type: %.200s\n", name); + } + if ((std::strcmp(name, ".?AVexception@std@@") == 0) && (type[3] == -1)) + { + auto const *exception{reinterpret_cast(object + type[2])}; + append(Buffer, " C++ exception message: %.1000s\n", exception->what()); + break; + } + } + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + append(Buffer, " (the C++ exception could not be read)\n"); + } +} +#endif + +// the module and the offset in it. through the memory map rather than the module list of the loader, whose lock the crashed thread may hold +void describe_address(void const *Address, text_buffer &Buffer) +{ + MEMORY_BASIC_INFORMATION region{}; + wchar_t path[MAX_PATH * 2]; + if ((::VirtualQuery(Address, ®ion, sizeof(region)) == 0) || (region.Type != MEM_IMAGE) || (::GetMappedFileNameW(::GetCurrentProcess(), region.AllocationBase, path, MAX_PATH * 2) == 0)) + { + append(Buffer, " at 0x%p, outside of the modules\n", Address); + return; + } + path[MAX_PATH * 2 - 1] = L'\0'; + auto const *name{std::wcsrchr(path, L'\\')}; + append(Buffer, " at 0x%p, ", Address); + append_wide(Buffer, name != nullptr ? name + 1 : path); + append(Buffer, "+0x%llx\n", static_cast(reinterpret_cast(Address) - reinterpret_cast(region.AllocationBase))); +} + +void describe_crash(crashdump::channel_data &Data, SYSTEMTIME const &Now) +{ + text_buffer buffer{Data.comment, crashdump::comment_capacity, 0}; + Data.comment[0] = '\0'; + + append(buffer, "MaSzyna crash report\n\n"); + append(buffer, "version: %s\n", Global.asVersion.c_str()); + append(buffer, "build: %s, committed at %s\n", GIT_HASH, SRC_DATE); + append(buffer, "executable: %s\n", executable_path); + append(buffer, "started: %04u-%02u-%02u %02u:%02u:%02u\n", start_time.wYear, start_time.wMonth, start_time.wDay, start_time.wHour, start_time.wMinute, start_time.wSecond); + append(buffer, "crashed: %04u-%02u-%02u %02u:%02u:%02u\n", Now.wYear, Now.wMonth, Now.wDay, Now.wHour, Now.wMinute, Now.wSecond); + append(buffer, "processor: %s\n", processor_name); + append(buffer, "system: %s\n\n", system_version); + + if ((crash_exception != nullptr) && (crash_exception->ExceptionRecord != nullptr)) + { + auto const &record{*crash_exception->ExceptionRecord}; + append(buffer, "exception: 0x%08lX, %s\n", static_cast(record.ExceptionCode), exception_name(record.ExceptionCode)); + describe_address(record.ExceptionAddress, buffer); + if (((record.ExceptionCode == EXCEPTION_ACCESS_VIOLATION) || (record.ExceptionCode == EXCEPTION_IN_PAGE_ERROR)) && (record.NumberParameters >= 2)) + { + auto const operation{record.ExceptionInformation[0]}; + append(buffer, " %s of address 0x%p\n", operation == 0 ? "read" : (operation == 8 ? "execution" : "write"), reinterpret_cast(record.ExceptionInformation[1])); + } +#if defined(_MSC_VER) && defined(_WIN64) + describe_cpp_exception(record, buffer); +#endif + } + append(buffer, "thread: %lu%s\n\n", static_cast(crash_thread_id), crash_thread_id == main_thread_id ? " (main)" : ""); + + PROCESS_MEMORY_COUNTERS_EX memory{}; + memory.cb = sizeof(memory); + if (::GetProcessMemoryInfo(::GetCurrentProcess(), reinterpret_cast(&memory), sizeof(memory)) != FALSE) + { + append(buffer, "process memory: %llu MB private, %llu MB working set, %llu MB peak working set\n", static_cast(memory.PrivateUsage >> 20), + static_cast(memory.WorkingSetSize >> 20), static_cast(memory.PeakWorkingSetSize >> 20)); + } + MEMORYSTATUSEX system{}; + system.dwLength = sizeof(system); + if (::GlobalMemoryStatusEx(&system) != FALSE) + { + append(buffer, "system memory: %llu MB of %llu MB free, %llu MB of %llu MB of the page file free\n", static_cast(system.ullAvailPhys >> 20), + static_cast(system.ullTotalPhys >> 20), static_cast(system.ullAvailPageFile >> 20), + static_cast(system.ullTotalPageFile >> 20)); + } + append(buffer, "dump: %s\n\n", Data.full_memory != 0 ? "full memory" : "small"); + + append(buffer, "info:\n"); + auto const infocount{std::min(info_used.load(), std::size(info_entries))}; + for (std::size_t index = 0; index < infocount; ++index) + { + append(buffer, " %.47s: %.463s\n", info_entries[index].name, info_entries[index].value); + } + + append(buffer, "\nrecent log:\n"); + auto const written{log_written}; + auto const capacity{std::size(log_ring)}; + if (written <= capacity) + { + append(buffer, "%.*s", static_cast(written), log_ring); + } + else + { + // the ring went around: from the oldest whole line on + auto const start{written % capacity}; + auto skip{start}; + while ((skip < capacity) && (log_ring[skip] != '\n')) + { + ++skip; + } + if (skip < capacity) + { + append(buffer, "%.*s", static_cast(capacity - skip - 1), log_ring + skip + 1); + } + append(buffer, "%.*s", static_cast(start), log_ring); + } + + Data.comment_length = static_cast(buffer.length); +} + +void write_file(wchar_t const *Path, char const *Text, std::size_t const Length) +{ + auto const file{::CreateFileW(Path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)}; + if (file == INVALID_HANDLE_VALUE) + { + return; + } + DWORD written{0}; + ::WriteFile(file, Text, static_cast(Length), &written, nullptr); + ::CloseHandle(file); +} + +void write_report() +{ + auto &data{*channel}; + SYSTEMTIME now; + ::GetLocalTime(&now); + + // crashdumps\crash__