From 5c76d20a16da14212922866339de2ade2ec31eec Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Sat, 3 Oct 2026 08:24:10 -0600 Subject: [PATCH 1/5] feat(crashreporting): Add optional local Crashpad backend --- .../workflows/reusable-build-toolchain.yml | 14 +- CMakeLists.txt | 2 + Core/GameEngine/CMakeLists.txt | 2 + .../Include/Common/CrashReporting.h | 14 ++ .../Source/Common/System/CrashReporting.cpp | 161 ++++++++++++ .../GameEngine/Source/Common/System/Debug.cpp | 36 ++- .../Source/Common/System/MiniDumper.cpp | 9 +- Dependencies/Crashpad/CMakeLists.txt | 74 ++++++ Dependencies/Crashpad/CrashpadBridge.cpp | 237 ++++++++++++++++++ Dependencies/Crashpad/CrashpadBridge.def | 4 + Dependencies/Crashpad/CrashpadBridge.h | 14 ++ Dependencies/Crashpad/CrashpadReports.cpp | 82 ++++++ Dependencies/Crashpad/archive_symbols.py | 57 +++++ Dependencies/Crashpad/getopt-LICENSE | 5 + Dependencies/Crashpad/mini-chromium-LICENSE | 27 ++ Dependencies/Crashpad/tests/CMakeLists.txt | 21 ++ Dependencies/Crashpad/tests/CrashpadTest.cpp | 180 +++++++++++++ Dependencies/Crashpad/tests/SeedDatabase.cpp | 43 ++++ Dependencies/Crashpad/tests/WrongHandler.cpp | 34 +++ Dependencies/Crashpad/tests/validate.py | 138 ++++++++++ Dependencies/Crashpad/vcpkg.json | 5 + Generals/CMakeLists.txt | 1 + .../GameEngine/Source/Common/GameEngine.cpp | 4 + Generals/Code/Main/CMakeLists.txt | 2 + Generals/Code/Main/WinMain.cpp | 9 +- GeneralsMD/CMakeLists.txt | 1 + GeneralsMD/Code/Main/CMakeLists.txt | 2 + GeneralsMD/Code/Main/WinMain.cpp | 9 +- cmake/crashpad-metadata.cmake | 27 ++ cmake/crashpad.cmake | 65 +++++ docs/crashpad-validation.md | 120 +++++++++ docs/crashpad.md | 183 ++++++++++++++ 32 files changed, 1555 insertions(+), 27 deletions(-) create mode 100644 Core/GameEngine/Include/Common/CrashReporting.h create mode 100644 Core/GameEngine/Source/Common/System/CrashReporting.cpp create mode 100644 Dependencies/Crashpad/CMakeLists.txt create mode 100644 Dependencies/Crashpad/CrashpadBridge.cpp create mode 100644 Dependencies/Crashpad/CrashpadBridge.def create mode 100644 Dependencies/Crashpad/CrashpadBridge.h create mode 100644 Dependencies/Crashpad/CrashpadReports.cpp create mode 100644 Dependencies/Crashpad/archive_symbols.py create mode 100644 Dependencies/Crashpad/getopt-LICENSE create mode 100644 Dependencies/Crashpad/mini-chromium-LICENSE create mode 100644 Dependencies/Crashpad/tests/CMakeLists.txt create mode 100644 Dependencies/Crashpad/tests/CrashpadTest.cpp create mode 100644 Dependencies/Crashpad/tests/SeedDatabase.cpp create mode 100644 Dependencies/Crashpad/tests/WrongHandler.cpp create mode 100644 Dependencies/Crashpad/tests/validate.py create mode 100644 Dependencies/Crashpad/vcpkg.json create mode 100644 cmake/crashpad-metadata.cmake create mode 100644 cmake/crashpad.cmake create mode 100644 docs/crashpad-validation.md create mode 100644 docs/crashpad.md diff --git a/.github/workflows/reusable-build-toolchain.yml b/.github/workflows/reusable-build-toolchain.yml index 7e2f84e4ca7..17646c8a88f 100644 --- a/.github/workflows/reusable-build-toolchain.yml +++ b/.github/workflows/reusable-build-toolchain.yml @@ -176,6 +176,18 @@ jobs: Where-Object { $_.Extension -in @(".exe", ".dll", ".pdb") } -Verbose } + # Preserve Crashpad build metadata and dependency notices beside + # the game, matched handler and adapter DLL. + $gameOutput = if ("${{ inputs.preset }}" -like "win32*") { + "$buildDir\${{ inputs.game }}\$configToUse" + } else { + "$buildDir\${{ inputs.game }}" + } + Get-ChildItem -Path $gameOutput -Filter "*-crashpad-build.json" -File | + Copy-Item -Destination $artifactsDir -Force + if (Test-Path "$gameOutput\crashpad-notices") { + Copy-Item -Path "$gameOutput\crashpad-notices" -Destination $artifactsDir -Recurse -Force + } $files | Move-Item -Destination $artifactsDir -Verbose -Force - name: Upload ${{ inputs.game }} ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} Artifact @@ -183,5 +195,5 @@ jobs: with: name: ${{ inputs.game }}-${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} path: build\${{ inputs.preset }}\${{ inputs.game }}\artifacts - retention-days: 30 + retention-days: 90 if-no-files-found: error diff --git a/CMakeLists.txt b/CMakeLists.txt index 39509160788..d414d7df3db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,8 @@ endif() add_subdirectory(resources) +include(cmake/crashpad.cmake) + add_subdirectory(Core) # Add main build targets diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index e29b9f93290..90b9620fada 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -72,6 +72,7 @@ set(GAMEENGINE_SRC Include/Common/MapObject.h Include/Common/MapReaderWriterInfo.h Include/Common/MessageStream.h + Include/Common/CrashReporting.h Include/Common/MiniDumper.h Include/Common/MiniLog.h Include/Common/MiscAudio.h @@ -674,6 +675,7 @@ set(GAMEENGINE_SRC # Source/Common/System/List.cpp Source/Common/System/LocalFile.cpp Source/Common/System/LocalFileSystem.cpp + Source/Common/System/CrashReporting.cpp Source/Common/System/MiniDumper.cpp Source/Common/System/ObjectStatusTypes.cpp # Source/Common/System/QuotedPrintable.cpp diff --git a/Core/GameEngine/Include/Common/CrashReporting.h b/Core/GameEngine/Include/Common/CrashReporting.h new file mode 100644 index 00000000000..7bc9b4ff35b --- /dev/null +++ b/Core/GameEngine/Include/Common/CrashReporting.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#pragma once + +class AsciiString; + +// Keep this interface compatible with VC6 and tools that only use MiniDumper. +namespace CrashReporting +{ + void initialize(const AsciiString& userDirectory, int major, int minor, int build); + void userDirectoryReady(const AsciiString& userDirectory); + const char* backendName(); + void captureFatal(); + void shutdown(); +} diff --git a/Core/GameEngine/Source/Common/System/CrashReporting.cpp b/Core/GameEngine/Source/Common/System/CrashReporting.cpp new file mode 100644 index 00000000000..ba44f9ab81c --- /dev/null +++ b/Core/GameEngine/Source/Common/System/CrashReporting.cpp @@ -0,0 +1,161 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +// TheSuperHackers @feature Codex 01/10/2026 Select optional local Crashpad reporting with legacy MiniDumper fallback. +#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine +#include "Common/CrashReporting.h" +#include "Common/MiniDumper.h" + +#ifdef RTS_USE_CRASHPAD +#include "CrashpadBridge.h" +#include "gitinfo.h" + +namespace +{ + RtsCrashpadCaptureFatalFunction capture; + RtsCrashpadShutdownFunction stop; + bool awaitingUserDirectory; + int savedMajor; + int savedMinor; + int savedBuild; + + bool StartCrashpad(const AsciiString& userDirectory, int major, int minor, int build) + { + wchar_t path[32768]; + const DWORD count = GetModuleFileNameW(nullptr, path, ARRAY_SIZE(path)); + if (!count || count >= ARRAY_SIZE(path)) + { + return false; + } + + wchar_t* leaf = wcsrchr(path, L'\\'); + const wchar_t name[] = L"rts_crashpad.dll"; + if (!leaf || (leaf + 1 - path) + ARRAY_SIZE(name) > ARRAY_SIZE(path)) + { + return false; + } + + memcpy(leaf + 1, name, sizeof(name)); + HMODULE module = LoadLibraryExW(path, nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + if (!module) + { + return false; + } + + RtsCrashpadInitializeFunction start = reinterpret_cast( + GetProcAddress(module, "RtsCrashpadInitialize")); + capture = reinterpret_cast(GetProcAddress(module, "RtsCrashpadCaptureFatal")); + stop = reinterpret_cast(GetProcAddress(module, "RtsCrashpadShutdown")); + char version[64]; + snprintf(version, ARRAY_SIZE(version), "%d.%d.%d", major, minor, build); +#if RTS_ZEROHOUR + const char* game = "Zero Hour"; +#else + const char* game = "Generals"; +#endif + if (start && capture && stop && start(userDirectory.str(), game, version, GitSHA1, GitUncommittedChanges)) + { + return true; + } + + // Crashpad retains process-lifetime pointers even after failed startup. + // Keep the module loaded; its initializer restores the previous filter. + capture = nullptr; + stop = nullptr; + return false; + } +} +#endif + +void CrashReporting::initialize(const AsciiString& userDirectory, int major, int minor, int build) +{ +#ifdef RTS_USE_CRASHPAD + // Generals resolves UserDataLeafName from GameData.ini during engine init. + // Zero Hour already has its registry-derived path at WinMain startup. + awaitingUserDirectory = userDirectory.isEmpty(); + savedMajor = major; + savedMinor = minor; + savedBuild = build; + if (!awaitingUserDirectory && StartCrashpad(userDirectory, major, minor, build)) + { + OutputDebugStringA("Crash reporting: Crashpad (local only)\n"); + DEBUG_LOG(("Crash reporting: Crashpad (local only)\n")); + return; + } + + if (!awaitingUserDirectory) + { + OutputDebugStringA("Crashpad startup failed; trying MiniDumper\n"); + DEBUG_LOG(("Crashpad startup failed; trying MiniDumper\n")); + } +#endif +#ifdef RTS_ENABLE_CRASHDUMP + MiniDumper::initMiniDumper(userDirectory); + DEBUG_LOG(("Crash reporting: %s\n", backendName())); +#endif +} + +void CrashReporting::userDirectoryReady(const AsciiString& userDirectory) +{ +#ifdef RTS_USE_CRASHPAD + // Tools never call initialize(), so this cannot start Crashpad in a tool. + if (awaitingUserDirectory && !userDirectory.isEmpty()) + { + MiniDumper::shutdownMiniDumper(); + initialize(userDirectory, savedMajor, savedMinor, savedBuild); + } +#endif +} + +const char* CrashReporting::backendName() +{ +#ifdef RTS_USE_CRASHPAD + if (capture) + { + return "crashpad"; + } +#endif +#ifdef RTS_ENABLE_CRASHDUMP + if (TheMiniDumper && TheMiniDumper->IsInitialized()) + { + return "minidumper"; + } +#endif + + return "unavailable"; +} + +void CrashReporting::captureFatal() +{ +#ifdef RTS_USE_CRASHPAD + if (capture) + { + capture(); + return; + } +#endif +#ifdef RTS_ENABLE_CRASHDUMP + if (TheMiniDumper && TheMiniDumper->IsInitialized()) + { + // Preserve both minimal and full memory dumps for the legacy backend. + TheMiniDumper->TriggerMiniDump(DumpType_Minimal); + TheMiniDumper->TriggerMiniDump(DumpType_Full); + } + + MiniDumper::shutdownMiniDumper(); +#endif +} + +void CrashReporting::shutdown() +{ +#ifdef RTS_USE_CRASHPAD + awaitingUserDirectory = false; + if (stop) + { + stop(); + stop = nullptr; + capture = nullptr; + } +#endif +#ifdef RTS_ENABLE_CRASHDUMP + MiniDumper::shutdownMiniDumper(); +#endif +} diff --git a/Core/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp index 815ec558ea8..7838afc18bd 100644 --- a/Core/GameEngine/Source/Common/System/Debug.cpp +++ b/Core/GameEngine/Source/Common/System/Debug.cpp @@ -69,9 +69,7 @@ #if defined(DEBUG_STACKTRACE) || defined(IG_DEBUG_STACKTRACE) #include "Common/StackDump.h" #endif -#ifdef RTS_ENABLE_CRASHDUMP -#include "Common/MiniDumper.h" -#endif +#include "Common/CrashReporting.h" // Horrible reference, but we really, really need to know if we are windowed. extern bool DX8Wrapper_IsWindowed; @@ -732,21 +730,21 @@ double SimpleProfiler::getAverageTime() static void TriggerMiniDump() { -#ifdef RTS_ENABLE_CRASHDUMP - if (TheMiniDumper && TheMiniDumper->IsInitialized()) - { - // Create both minimal and full memory dumps - TheMiniDumper->TriggerMiniDump(DumpType_Minimal); - TheMiniDumper->TriggerMiniDump(DumpType_Full); - } - - MiniDumper::shutdownMiniDumper(); -#endif + CrashReporting::captureFatal(); } void ReleaseCrash(const char *reason) { + // We are shutting down, and TheGlobalData has been freed. jba. [4/15/2003] + // Do not consume the one-shot fatal capture when this path will return. + if (TheGlobalData == nullptr) + { + return; + } + + TriggerMiniDump(); + /// do additional reporting on the crash, if possible if (!DX8Wrapper_IsWindowed) { @@ -755,15 +753,9 @@ void ReleaseCrash(const char *reason) } } - TriggerMiniDump(); - char prevbuf[ _MAX_PATH ]; char curbuf[ _MAX_PATH ]; - if (TheGlobalData==nullptr) { - return; // We are shutting down, and TheGlobalData has been freed. jba. [4/15/2003] - } - strlcpy(prevbuf, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(prevbuf)); strlcat(prevbuf, RELEASECRASH_FILE_NAME_PREV, ARRAY_SIZE(prevbuf)); strlcpy(curbuf, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(curbuf)); @@ -833,6 +825,12 @@ void ReleaseCrash(const char *reason) void ReleaseCrashLocalized(const AsciiString& p, const AsciiString& m) { + // TheSuperHackers @bugfix Codex 01/10/2026 Match ReleaseCrash during shutdown instead of dereferencing freed global data. + if (TheGlobalData == nullptr) + { + return; + } + if (!TheGameText) { ReleaseCrash(m.str()); // This won't ever return diff --git a/Core/GameEngine/Source/Common/System/MiniDumper.cpp b/Core/GameEngine/Source/Common/System/MiniDumper.cpp index 53a36ebf627..38a1f794a61 100644 --- a/Core/GameEngine/Source/Common/System/MiniDumper.cpp +++ b/Core/GameEngine/Source/Common/System/MiniDumper.cpp @@ -43,7 +43,14 @@ void MiniDumper::initMiniDumper(const AsciiString& userDirPath) // Use placement new on the process heap so TheMiniDumper is placed outside the MemoryPoolFactory managed area. // If the crash is due to corrupted MemoryPoolFactory structures, try to mitigate the chances of MiniDumper memory also being corrupted - TheMiniDumper = new (::HeapAlloc(::GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, sizeof(MiniDumper))) MiniDumper; + // TheSuperHackers @bugfix Codex 01/10/2026 Reporting initialization must not crash the game when allocation fails. + void* storage = ::HeapAlloc(::GetProcessHeap(), 0, sizeof(MiniDumper)); + if (!storage) + { + return; + } + + TheMiniDumper = new (storage) MiniDumper; TheMiniDumper->Initialize(userDirPath); } diff --git a/Dependencies/Crashpad/CMakeLists.txt b/Dependencies/Crashpad/CMakeLists.txt new file mode 100644 index 00000000000..86edf8c5429 --- /dev/null +++ b/Dependencies/Crashpad/CMakeLists.txt @@ -0,0 +1,74 @@ +# Acquire separately with this directory's manifest. Disabled builds never run +# vcpkg or look for Crashpad. A copied, pinned installed prefix works without a +# vcpkg toolchain in the game build. +set(RTS_CRASHPAD_PACKAGE_ROOT "" CACHE PATH "Pinned x86-windows-static-md Crashpad installed prefix") +set(RTS_CRASHPAD_REVISION "7e0af1d4d45b526f01677e74a56f4a951b70517d") +set(RTS_CRASHPAD_MINI_CHROMIUM_REVISION "dce72d97d1c2e9beb5e206c6a05a702269794ca3") +set(package "${RTS_CRASHPAD_PACKAGE_ROOT}") +if(NOT EXISTS "${package}/share/crashpad/vcpkg.spdx.json") + message(FATAL_ERROR "Set RTS_CRASHPAD_PACKAGE_ROOT to the x86-windows-static-md prefix built with Dependencies/Crashpad/vcpkg.json. See docs/crashpad.md.") +endif() +file(READ "${package}/share/crashpad/vcpkg.spdx.json" provenance) +foreach(required "crashpad:x86-windows-static-md@2024-04-11#7" + "${RTS_CRASHPAD_REVISION}" "${RTS_CRASHPAD_MINI_CHROMIUM_REVISION}") + string(FIND "${provenance}" "${required}" position) + if(position EQUAL -1) + message(FATAL_ERROR "Crashpad package does not match the pinned x86 package: missing ${required}") + endif() +endforeach() + +# The baseline port's config uses these two variables even outside a vcpkg +# toolchain. Keep the workaround scoped to this dependency directory. +get_filename_component(_VCPKG_INSTALLED_DIR "${package}" DIRECTORY) +get_filename_component(VCPKG_TARGET_TRIPLET "${package}" NAME) +list(PREPEND CMAKE_PREFIX_PATH "${package}") +find_package(crashpad CONFIG REQUIRED PATHS "${package}/share/crashpad" NO_DEFAULT_PATH) +set(handler "${package}/tools/crashpad/crashpad_handler.exe") +if(NOT EXISTS "${handler}") + message(FATAL_ERROR "The pinned package is missing its matching crashpad_handler.exe") +endif() +set(RTS_CRASHPAD_HANDLER "${handler}" PARENT_SCOPE) + +# Deploy the redistributable x86 VC runtime from the selected Visual Studio. +# The Windows 10+ Universal CRT is supplied by the OS. Debug builds still need +# a developer installation of the non-redistributable debug CRT. +set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) +include(InstallRequiredSystemLibraries) +if(NOT CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS) + message(FATAL_ERROR "The Visual Studio x86 redistributable runtime was not found") +endif() +set(RTS_CRASHPAD_RUNTIME_FILES "${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS}" PARENT_SCOPE) + +add_library(rts_crashpad SHARED CrashpadBridge.cpp CrashpadBridge.h CrashpadBridge.def) +target_compile_features(rts_crashpad PRIVATE cxx_std_20) +target_link_libraries(rts_crashpad PRIVATE crashpad::crashpad) +set_target_properties(rts_crashpad PROPERTIES + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL" + DISABLE_PRECOMPILE_HEADERS ON) +target_compile_options(rts_crashpad PRIVATE /Zi) +target_link_options(rts_crashpad PRIVATE /DEBUG) +target_compile_definitions(rts_crashpad PRIVATE + "RTS_CRASHPAD_COMPILER=\"${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}\"" + "RTS_CRASHPAD_CONFIGURATION=\"$\"" + "RTS_CRASHPAD_REVISION=\"${RTS_CRASHPAD_REVISION}\"") + +set(notices "${CMAKE_CURRENT_BINARY_DIR}/notices") +file(MAKE_DIRECTORY "${notices}") +configure_file("${package}/share/crashpad/copyright" "${notices}/crashpad-LICENSE" COPYONLY) +configure_file("${package}/share/zlib/copyright" "${notices}/zlib-LICENSE" COPYONLY) +configure_file("${package}/share/crashpad/vcpkg.spdx.json" "${notices}/crashpad-package.spdx.json" COPYONLY) +configure_file("${package}/share/zlib/vcpkg.spdx.json" "${notices}/zlib-package.spdx.json" COPYONLY) +configure_file("${package}/share/crashpad/vcpkg_abi_info.txt" "${notices}/crashpad-package-abi.txt" COPYONLY) +configure_file(getopt-LICENSE "${notices}/getopt-LICENSE" COPYONLY) +configure_file(mini-chromium-LICENSE "${notices}/mini-chromium-LICENSE" COPYONLY) +set(RTS_CRASHPAD_NOTICES "${notices}" PARENT_SCOPE) + +add_executable(rts_crashpad_reports CrashpadReports.cpp) +target_compile_features(rts_crashpad_reports PRIVATE cxx_std_20) +target_link_libraries(rts_crashpad_reports PRIVATE crashpad::crashpad) +set_target_properties(rts_crashpad_reports PROPERTIES + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") + +if(RTS_BUILD_CRASHPAD_TESTS) + add_subdirectory(tests) +endif() diff --git a/Dependencies/Crashpad/CrashpadBridge.cpp b/Dependencies/Crashpad/CrashpadBridge.cpp new file mode 100644 index 00000000000..2ca34779fce --- /dev/null +++ b/Dependencies/Crashpad/CrashpadBridge.cpp @@ -0,0 +1,237 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +// Deliberately compiled outside the game PCH, forced includes and allocator. +#include +#include +#include +#include +#include + +#include "CrashpadBridge.h" +#include "client/crashpad_client.h" +#include "client/crash_report_database.h" +#include "client/prune_crash_reports.h" +#include "client/settings.h" +#include "util/misc/capture_context.h" + +namespace +{ + std::unique_ptr client; + LPTOP_LEVEL_EXCEPTION_FILTER previousFilter; + void (*previousAbortHandler)(int); + HANDLE stopEvent; + HANDLE captureEvent; + HANDLE completedEvent; + HANDLE watchdogThread; + volatile LONG capturedFatal; + bool attempted; + bool active; + + // DumpWithoutCrash waits indefinitely in the pinned Windows client. Keep + // the request on the faulting thread, and bound fatal capture with a thread + // created while the process is healthy. Do not terminate a reporting thread + // and then let the game reuse memory still referenced by the handler. + DWORD WINAPI Watchdog(void*) + { + const HANDLE events[] = {stopEvent, captureEvent}; + if (WaitForMultipleObjects(2, events, FALSE, INFINITE) == WAIT_OBJECT_0 + 1) + { + if (WaitForSingleObject(completedEvent, 15000) != WAIT_OBJECT_0) + { + TerminateProcess(GetCurrentProcess(), 1); + } + } + + return 0; + } + + LONG WINAPI AlreadyCaptured(EXCEPTION_POINTERS*) + { + // A failure in best-effort fatal diagnostics must not create a second + // report that hides the original explicit fatal capture. + TerminateProcess(GetCurrentProcess(), 1); + return EXCEPTION_EXECUTE_HANDLER; + } + + void StopWatchdog() + { + if (watchdogThread) + { + SetEvent(stopEvent); + WaitForSingleObject(watchdogThread, INFINITE); + CloseHandle(watchdogThread); + watchdogThread = nullptr; + } + + if (stopEvent) + { + CloseHandle(stopEvent); + stopEvent = nullptr; + } + + if (captureEvent) + { + CloseHandle(captureEvent); + captureEvent = nullptr; + } + + if (completedEvent) + { + CloseHandle(completedEvent); + completedEvent = nullptr; + } + } + + bool Initialize(const char* userDirectory, const char* game, + const char* version, const char* revision, int dirty) + { + const int count = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + userDirectory, -1, nullptr, 0); + if (count <= 1) + { + return false; + } + + std::vector userPath(count); + if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, userDirectory, + -1, userPath.data(), count)) + { + return false; + } + + const base::FilePath user(userPath.data()); + if (!user.IsAbsolute()) + { + return false; + } + + std::vector executable(32768); + const DWORD length = GetModuleFileNameW(nullptr, executable.data(), + static_cast(executable.size())); + if (!length || length >= executable.size()) + { + return false; + } + + const base::FilePath handler = base::FilePath(executable.data()).DirName() + .Append(L"crashpad_handler.exe"); + if (GetFileAttributesW(handler.value().c_str()) == INVALID_FILE_ATTRIBUTES) + { + return false; + } + + const base::FilePath dumps = user.Append(L"CrashDumps"); + if (!CreateDirectoryW(dumps.value().c_str(), nullptr) && GetLastError() != ERROR_ALREADY_EXISTS) + { + return false; + } + + const base::FilePath databasePath = dumps.Append(L"Crashpad"); + auto database = crashpad::CrashReportDatabase::Initialize(databasePath); + if (!database || !database->GetSettings()->SetUploadsEnabled(false)) + { + return false; + } + + // Prune through the database so active/locked reports stay protected. + // Disable the handler's different default policy below. The size limit + // applies at startup; reports from concurrent runs can exceed it until + // the next startup. + crashpad::BinaryPruneCondition retention(crashpad::BinaryPruneCondition::OR, + new crashpad::AgePruneCondition(30), + new crashpad::DatabaseSizePruneCondition(500 * 1024)); + crashpad::PruneCrashReportDatabase(database.get(), &retention); + + stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + captureEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + completedEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!stopEvent || !captureEvent || !completedEvent) + { + return false; + } + + watchdogThread = CreateThread(nullptr, 0, Watchdog, nullptr, 0, nullptr); + if (!watchdogThread) + { + return false; + } + + const std::map annotations = { + {"game", game}, {"version", version}, {"revision", revision}, + {"dirty", dirty ? "true" : "false"}, {"architecture", "x86"}, + {"compiler", RTS_CRASHPAD_COMPILER}, {"configuration", RTS_CRASHPAD_CONFIGURATION}, + {"backend", "crashpad"}, {"crashpad_revision", RTS_CRASHPAD_REVISION} + }; + client = std::make_unique(); + // Wait for the IPC ping before claiming readiness, but do not block game + // startup forever if a valid executable never services the pipe. On a + // timeout the DLL must stay loaded for the upstream background thread. + // Windows has no automatic restart here. No URL, metrics or attachments. + return client->StartHandler(handler, databasePath, base::FilePath(), "", + annotations, {"--no-periodic-tasks"}, false, true) + && client->WaitForHandlerStart(5000); + } +} + +extern "C" int __cdecl RtsCrashpadInitialize(const char* userDirectory, + const char* game, const char* version, const char* revision, int dirty) +{ + if (attempted) + { + return active; + } + + attempted = true; + previousFilter = SetUnhandledExceptionFilter(nullptr); + SetUnhandledExceptionFilter(previousFilter); + previousAbortHandler = std::signal(SIGABRT, SIG_DFL); + std::signal(SIGABRT, previousAbortHandler); + try + { + active = Initialize(userDirectory, game, version, revision, dirty); + } + catch (...) + { + active = false; + } + + if (!active) + { + RtsCrashpadShutdown(); + } + + return active; +} + +extern "C" void __cdecl RtsCrashpadCaptureFatal() +{ + if (!active || InterlockedCompareExchange(&capturedFatal, 1, 0) != 0) + { + return; + } + + CONTEXT context; + crashpad::CaptureContext(&context); + if (!SetEvent(captureEvent)) + { + TerminateProcess(GetCurrentProcess(), 1); + } + + crashpad::CrashpadClient::DumpWithoutCrash(context); + SetEvent(completedEvent); + SetUnhandledExceptionFilter(AlreadyCaptured); + // Removes the heap-corruption vectored handler too. The client leaves its + // IPC state alive for process lifetime; never unload this DLL. + client.reset(); + std::signal(SIGABRT, SIG_DFL); +} + +extern "C" void __cdecl RtsCrashpadShutdown() +{ + active = false; + SetUnhandledExceptionFilter(previousFilter); + std::signal(SIGABRT, previousAbortHandler); + client.reset(); + StopWatchdog(); + // The handler observes process exit and exits itself. Crashpad's Windows + // API intentionally retains its IPC handles until then. +} diff --git a/Dependencies/Crashpad/CrashpadBridge.def b/Dependencies/Crashpad/CrashpadBridge.def new file mode 100644 index 00000000000..01786e581ae --- /dev/null +++ b/Dependencies/Crashpad/CrashpadBridge.def @@ -0,0 +1,4 @@ +EXPORTS + RtsCrashpadInitialize + RtsCrashpadCaptureFatal + RtsCrashpadShutdown diff --git a/Dependencies/Crashpad/CrashpadBridge.h b/Dependencies/Crashpad/CrashpadBridge.h new file mode 100644 index 00000000000..cd85011db2b --- /dev/null +++ b/Dependencies/Crashpad/CrashpadBridge.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#pragma once + +// C ABI only. No allocation or C++ object crosses the game/DLL boundary. +// The game loads these exports explicitly so a missing DLL permits fallback. +typedef int (__cdecl* RtsCrashpadInitializeFunction)(const char* userDirectory, + const char* game, const char* version, const char* revision, int dirty); +typedef void (__cdecl* RtsCrashpadCaptureFatalFunction)(); +typedef void (__cdecl* RtsCrashpadShutdownFunction)(); + +extern "C" int __cdecl RtsCrashpadInitialize(const char* userDirectory, + const char* game, const char* version, const char* revision, int dirty); +extern "C" void __cdecl RtsCrashpadCaptureFatal(); +extern "C" void __cdecl RtsCrashpadShutdown(); diff --git a/Dependencies/Crashpad/CrashpadReports.cpp b/Dependencies/Crashpad/CrashpadReports.cpp new file mode 100644 index 00000000000..15aecd09d4f --- /dev/null +++ b/Dependencies/Crashpad/CrashpadReports.cpp @@ -0,0 +1,82 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#include +#include +#include +#include "client/crash_report_database.h" +#include "client/prune_crash_reports.h" +#include "client/settings.h" +#include "base/strings/utf_string_conversions.h" + +int wmain(int argc, wchar_t** argv) +{ + if (argc < 3 || argc > 4) + { + std::fputs("Usage: rts_crashpad_reports list|prune|export [export-directory]\n", stderr); + return 2; + } + + // Open an existing database only. Export never enables uploads or marks a + // report as uploaded, and never edits Crashpad's internal files directly. + auto database = crashpad::CrashReportDatabase::InitializeWithoutCreating(base::FilePath(argv[1])); + if (!database) + { + return 1; + } + + if (wcscmp(argv[2], L"prune") == 0 && argc == 3) + { + crashpad::BinaryPruneCondition retention(crashpad::BinaryPruneCondition::OR, + new crashpad::AgePruneCondition(30), + new crashpad::DatabaseSizePruneCondition(500 * 1024)); + std::printf("Deleted %zu reports\n", crashpad::PruneCrashReportDatabase(database.get(), &retention)); + return 0; + } + + const bool exporting = wcscmp(argv[2], L"export") == 0 && argc == 4; + if (!exporting && (wcscmp(argv[2], L"list") != 0 || argc != 3)) + { + return 2; + } + + std::vector pending; + std::vector completed; + if (database->GetPendingReports(&pending) != crashpad::CrashReportDatabase::kNoError + || database->GetCompletedReports(&completed) != crashpad::CrashReportDatabase::kNoError) + { + return 1; + } + + if (exporting && !CreateDirectoryW(argv[3], nullptr) && GetLastError() != ERROR_ALREADY_EXISTS) + { + return 1; + } + + bool uploads = true; + if (!database->GetSettings()->GetUploadsEnabled(&uploads)) + { + return 1; + } + + std::printf("uploads-enabled=%d\n", uploads); + pending.insert(pending.end(), completed.begin(), completed.end()); + for (const auto& report : pending) + { + std::printf("%s %llu\n", report.uuid.ToString().c_str(), + static_cast(report.total_size)); + if (exporting) + { + // These are finalized, immutable report files enumerated by the + // database API. CopyFile holds the source open during the copy; + // concurrent pruning before it opens is reported as an error. + const base::FilePath destination = base::FilePath(argv[3]) + .Append(base::UTF8ToWide(report.uuid.ToString() + ".dmp")); + if (!CopyFileW(report.file_path.value().c_str(), destination.value().c_str(), TRUE)) + { + std::fprintf(stderr, "Export failed: Windows error %lu\n", GetLastError()); + return 1; + } + } + } + + return 0; +} diff --git a/Dependencies/Crashpad/archive_symbols.py b/Dependencies/Crashpad/archive_symbols.py new file mode 100644 index 00000000000..522ac199ef1 --- /dev/null +++ b/Dependencies/Crashpad/archive_symbols.py @@ -0,0 +1,57 @@ +"""Verify binary/PDB identity and archive a Crashpad build for durable storage.""" +import argparse +import hashlib +import json +from pathlib import Path +import re +import subprocess +import zipfile + + +def identity(binary, pdb): + pe = subprocess.check_output(["llvm-readobj", "--coff-debug-directory", str(binary)], text=True) + symbols = subprocess.check_output(["llvm-pdbutil", "dump", "-summary", str(pdb)], text=True) + binary_guid = re.search(r"PDBGUID: (\{[^}]+\})", pe) + binary_age = re.search(r"PDBAge: (\d+)", pe) + pdb_guid = re.search(r"GUID: (\{[^}]+\})", symbols) + pdb_age = re.search(r"Age: (\d+)", symbols) + if not all([binary_guid, binary_age, pdb_guid, pdb_age]): + raise ValueError(f"Cannot read CodeView/PDB identity: {binary.name}") + if (binary_guid[1].lower(), binary_age[1]) != (pdb_guid[1].lower(), pdb_age[1]): + raise ValueError(f"PDB does not match {binary.name}") + return {"binary": binary.name, "pdb": pdb.name, + "guid": pdb_guid[1], "age": int(pdb_age[1])} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("metadata", type=Path, help="The game's *-crashpad-build.json") + parser.add_argument("archive", type=Path, help="New ZIP file, never overwritten") + args = parser.parse_args() + folder = args.metadata.resolve().parent + manifest = json.loads(args.metadata.read_text()) + files = [] + for kind in ["GAME", "PDB", "BRIDGE", "BRIDGE_PDB", "HANDLER"]: + record = manifest[kind] + path = folder / record["file"] + if path.parent != folder: + raise ValueError("Manifest paths must name sibling files") + if hashlib.sha256(path.read_bytes()).hexdigest() != record["sha256"]: + raise ValueError(f"Build metadata hash does not match {path.name}") + files.append(path) + identities = [identity(files[0], files[1]), identity(files[2], files[3])] + files.append(args.metadata.resolve()) + files.append(folder / "rts_crashpad_reports.exe") + for pattern in ["msvcp140*.dll", "vcruntime140*.dll", "concrt140.dll"]: + files.extend(folder.glob(pattern)) + files.extend(path for path in (folder / "crashpad-notices").rglob("*") if path.is_file()) + args.archive.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(args.archive, "x", compression=zipfile.ZIP_DEFLATED) as archive: + for path in files: + archive.write(path, path.relative_to(folder)) + archive.writestr("symbol-identities.json", json.dumps(identities, indent=2)) + print(json.dumps({"archive": str(args.archive), "identities": identities})) + + +if __name__ == "__main__": + main() diff --git a/Dependencies/Crashpad/getopt-LICENSE b/Dependencies/Crashpad/getopt-LICENSE new file mode 100644 index 00000000000..4444b1201e3 --- /dev/null +++ b/Dependencies/Crashpad/getopt-LICENSE @@ -0,0 +1,5 @@ +Copyright (C) 1997 Gregory Pietsch + +[These files] are hereby placed in the public domain without restrictions. Just +give the author credit, don't claim you wrote it or prevent anyone else from +using it. diff --git a/Dependencies/Crashpad/mini-chromium-LICENSE b/Dependencies/Crashpad/mini-chromium-LICENSE new file mode 100644 index 00000000000..179bf344522 --- /dev/null +++ b/Dependencies/Crashpad/mini-chromium-LICENSE @@ -0,0 +1,27 @@ +// Copyright 2006-2008 The Chromium Authors +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Dependencies/Crashpad/tests/CMakeLists.txt b/Dependencies/Crashpad/tests/CMakeLists.txt new file mode 100644 index 00000000000..cf03eafaaaa --- /dev/null +++ b/Dependencies/Crashpad/tests/CMakeLists.txt @@ -0,0 +1,21 @@ +add_executable(rts_crashpad_test CrashpadTest.cpp) +target_compile_features(rts_crashpad_test PRIVATE cxx_std_20) +target_compile_options(rts_crashpad_test PRIVATE /Zi) +target_link_options(rts_crashpad_test PRIVATE /DEBUG) +target_link_libraries(rts_crashpad_test PRIVATE rts_crashpad) +set_target_properties(rts_crashpad_test PROPERTIES + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +add_custom_command(TARGET rts_crashpad_test POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" "${handler}" "$" + VERBATIM) + +add_executable(rts_crashpad_seed SeedDatabase.cpp) +target_compile_features(rts_crashpad_seed PRIVATE cxx_std_20) +target_link_libraries(rts_crashpad_seed PRIVATE crashpad::crashpad) +set_target_properties(rts_crashpad_seed PROPERTIES + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") + +add_executable(rts_crashpad_wrong_handler WrongHandler.cpp) +set_target_properties(rts_crashpad_wrong_handler PROPERTIES MSVC_RUNTIME_LIBRARY MultiThreaded) +add_dependencies(rts_crashpad_test rts_crashpad_wrong_handler) diff --git a/Dependencies/Crashpad/tests/CrashpadTest.cpp b/Dependencies/Crashpad/tests/CrashpadTest.cpp new file mode 100644 index 00000000000..2d9c0ca6b02 --- /dev/null +++ b/Dependencies/Crashpad/tests/CrashpadTest.cpp @@ -0,0 +1,180 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +// Dedicated fault injection only. This executable never starts a game session. +#include +#include +#include +#include +#include +#include "../CrashpadBridge.h" + +namespace +{ + bool forbidAllocation; +} + +void* operator new(size_t size) +{ + if (forbidAllocation) + { + TerminateProcess(GetCurrentProcess(), 90); + } + + void* memory = HeapAlloc(GetProcessHeap(), 0, size ? size : 1); + if (!memory) + { + throw std::bad_alloc(); + } + + return memory; +} + +void operator delete(void* memory) noexcept +{ + if (forbidAllocation) + { + TerminateProcess(GetCurrentProcess(), 91); + } + + HeapFree(GetProcessHeap(), 0, memory); +} + +void operator delete(void* memory, size_t) noexcept +{ + operator delete(memory); +} + +__declspec(noinline) void CrashpadTestFault(void* address) +{ + *static_cast(address) = 42; +} + +DWORD WINAPI FaultingWorker(void* address) +{ + std::printf("fault-thread=%lu\n", GetCurrentThreadId()); + std::fflush(stdout); + CrashpadTestFault(address); + return 0; +} + +__declspec(noinline) unsigned int OverflowStack(unsigned int depth) +{ + if (depth == 0xffffffffu) + { + return depth; + } + + volatile char page[4096]; + page[depth % sizeof(page)] = static_cast(depth); + return OverflowStack(depth + 1) + page[depth % sizeof(page)]; +} + +bool KillOwnHandler() +{ + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + PROCESSENTRY32W process = {}; + process.dwSize = sizeof(process); + bool killed = false; + if (Process32FirstW(snapshot, &process)) + { + do + { + if (process.th32ParentProcessID == GetCurrentProcessId() + && wcscmp(process.szExeFile, L"crashpad_handler.exe") == 0) + { + HANDLE child = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, process.th32ProcessID); + if (child) + { + killed = TerminateProcess(child, 1) != FALSE; + WaitForSingleObject(child, 5000); + CloseHandle(child); + } + } + } while (Process32NextW(snapshot, &process)); + } + + CloseHandle(snapshot); + return killed; +} + +LONG WINAPI PreviousFilter(EXCEPTION_POINTERS*) +{ + return EXCEPTION_EXECUTE_HANDLER; +} + +int wmain(int argc, wchar_t** argv) +{ + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + SetUnhandledExceptionFilter(PreviousFilter); + if (argc != 3) + { + return 2; + } + + char userDirectory[32768 * 4]; + if (!WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, argv[1], -1, + userDirectory, sizeof(userDirectory), nullptr, nullptr)) + { + return 2; + } + + forbidAllocation = true; + const ULONGLONG start = GetTickCount64(); + if (!RtsCrashpadInitialize(userDirectory, "test", "1.0.0", "test-revision", 1)) + { + const bool restored = SetUnhandledExceptionFilter(PreviousFilter) == PreviousFilter; + std::printf("backend=unavailable filter-restored=%d\n", restored); + return restored ? 3 : 4; + } + + std::printf("backend=crashpad startup-ms=%llu fault-thread=%lu\n", + GetTickCount64() - start, GetCurrentThreadId()); + std::fflush(stdout); + void* address = VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_NOACCESS); + if (wcscmp(argv[2], L"main") == 0) + { + CrashpadTestFault(address); + } + else if (wcscmp(argv[2], L"worker") == 0) + { + HANDLE worker = CreateThread(nullptr, 0, FaultingWorker, address, 0, nullptr); + WaitForSingleObject(worker, INFINITE); + } + else if (wcscmp(argv[2], L"stack") == 0) + { + OverflowStack(0); + } + else if (wcscmp(argv[2], L"heap") == 0) + { + RaiseException(0xC0000374, EXCEPTION_NONCONTINUABLE, 0, nullptr); + } + else if (wcscmp(argv[2], L"explicit") == 0 || wcscmp(argv[2], L"duplicate") == 0) + { + RtsCrashpadCaptureFatal(); + RtsCrashpadCaptureFatal(); + if (wcscmp(argv[2], L"duplicate") == 0) + { + CrashpadTestFault(address); + } + } + else if (wcscmp(argv[2], L"dead-handler") == 0) + { + if (!KillOwnHandler()) + { + return 5; + } + + RtsCrashpadCaptureFatal(); + return 6; + } + else if (wcscmp(argv[2], L"wait") == 0) + { + Sleep(1000); + } + else if (wcscmp(argv[2], L"shutdown") != 0) + { + return 2; + } + + RtsCrashpadShutdown(); + return 0; +} diff --git a/Dependencies/Crashpad/tests/SeedDatabase.cpp b/Dependencies/Crashpad/tests/SeedDatabase.cpp new file mode 100644 index 00000000000..5a911541350 --- /dev/null +++ b/Dependencies/Crashpad/tests/SeedDatabase.cpp @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +// Test fixture: three synthetic 200 MiB reports exercise real database pruning. +#include "client/crash_report_database.h" +#include "client/settings.h" + +int wmain(int argc, wchar_t** argv) +{ + if (argc != 2) + { + return 2; + } + + auto database = crashpad::CrashReportDatabase::Initialize(base::FilePath(argv[1])); + if (!database || !database->GetSettings()->SetUploadsEnabled(false)) + { + return 1; + } + + for (int index = 0; index < 3; ++index) + { + std::unique_ptr report; + if (database->PrepareNewCrashReport(&report) != crashpad::CrashReportDatabase::kNoError) + { + return 1; + } + + const char end = 0; + if (report->Writer()->Seek(200 * 1024 * 1024 - 1, SEEK_SET) < 0 + || !report->Writer()->Write(&end, 1)) + { + return 1; + } + + crashpad::UUID uuid; + if (database->FinishedWritingCrashReport(std::move(report), &uuid) + != crashpad::CrashReportDatabase::kNoError) + { + return 1; + } + } + + return 0; +} diff --git a/Dependencies/Crashpad/tests/WrongHandler.cpp b/Dependencies/Crashpad/tests/WrongHandler.cpp new file mode 100644 index 00000000000..cf9d72fb6cb --- /dev/null +++ b/Dependencies/Crashpad/tests/WrongHandler.cpp @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +// A valid executable that deliberately does not service Crashpad's IPC pipe. +#include +#include + +int main() +{ + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + PROCESSENTRY32W process = {}; + process.dwSize = sizeof(process); + HANDLE parent = nullptr; + if (Process32FirstW(snapshot, &process)) + { + do + { + if (process.th32ProcessID == GetCurrentProcessId()) + { + parent = OpenProcess(SYNCHRONIZE, FALSE, process.th32ParentProcessID); + break; + } + } while (Process32NextW(snapshot, &process)); + } + + CloseHandle(snapshot); + if (!parent) + { + return 1; + } + + // Exit with the dedicated test parent so this fixture leaves no process. + WaitForSingleObject(parent, 20000); + CloseHandle(parent); + return 0; +} diff --git a/Dependencies/Crashpad/tests/validate.py b/Dependencies/Crashpad/tests/validate.py new file mode 100644 index 00000000000..d2fe0a42c86 --- /dev/null +++ b/Dependencies/Crashpad/tests/validate.py @@ -0,0 +1,138 @@ +"""Run only the dedicated Crashpad test executable, using fresh data directories.""" +import argparse +import concurrent.futures +import hashlib +import json +from pathlib import Path +import re +import shutil +import struct +import subprocess +import tempfile +import time + + +def exception(path): + data = path.read_bytes() + assert data[:4] == b"MDMP", path + streams, directory = struct.unpack_from(" DESTINATION "${RTS_INSTALL_PREFIX_GENERALS}" OPTIONAL) diff --git a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp index 039321096d0..b317e67f120 100644 --- a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp @@ -27,6 +27,7 @@ // Author: Michael S. Booth, April 2001 #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine +#include "Common/CrashReporting.h" #include "Common/ActionManager.h" #include "Common/AudioAffect.h" @@ -402,6 +403,9 @@ void GameEngine::init() // special-case: parse command-line parameters after loading global data CommandLine::parseCommandLineForEngineInit(); + // Generals obtains its user-data path from the GameData INI files above. + CrashReporting::userDirectoryReady(TheGlobalData->getPath_UserData()); + TheArchiveFileSystem->loadMods(); // doesn't require resets so just create a single instance here. diff --git a/Generals/Code/Main/CMakeLists.txt b/Generals/Code/Main/CMakeLists.txt index 8df79b91bb7..3cc5214fcf8 100644 --- a/Generals/Code/Main/CMakeLists.txt +++ b/Generals/Code/Main/CMakeLists.txt @@ -91,3 +91,5 @@ endif() if(MINGW AND COMMAND add_debug_strip_target) add_debug_strip_target(g_generals) endif() + +rts_enable_crashpad(g_generals g_gameengine) diff --git a/Generals/Code/Main/WinMain.cpp b/Generals/Code/Main/WinMain.cpp index a2a4caa2241..c29dc9b89ef 100644 --- a/Generals/Code/Main/WinMain.cpp +++ b/Generals/Code/Main/WinMain.cpp @@ -67,6 +67,7 @@ #ifdef RTS_ENABLE_CRASHDUMP #include "Common/MiniDumper.h" #endif +#include "Common/CrashReporting.h" // GLOBALS //////////////////////////////////////////////////////////////////// @@ -838,12 +839,13 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, gLoadScreenBitmap = (HBITMAP)LoadImage(hInstance, "Install_Final.bmp", IMAGE_BITMAP, 0, 0, LR_SHARED|LR_LOADFROMFILE); #ifdef RTS_ENABLE_CRASHDUMP - // Initialize minidump facilities - requires TheGlobalData so performed after parseCommandLineForStartup - MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData()); + // Begin reporting with TheGlobalData available; the INIs provide the user-data path later. + CrashReporting::initialize(TheGlobalData->getPath_UserData(), VERSION_MAJOR, VERSION_MINOR, VERSION_BUILDNUM); #endif // register windows class and create application window if(!TheGlobalData->m_headless && initializeAppWindows(hInstance, nCmdShow, TheGlobalData->m_windowed) == false) { + CrashReporting::shutdown(); return exitcode; } @@ -881,6 +883,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, DEBUG_LOG(("Generals is already running...Bail!")); delete TheVersion; TheVersion = nullptr; + CrashReporting::shutdown(); shutdownMemoryManager(); return exitcode; } @@ -911,7 +914,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, } #ifdef RTS_ENABLE_CRASHDUMP - MiniDumper::shutdownMiniDumper(); + CrashReporting::shutdown(); #endif TheAsciiStringCriticalSection = nullptr; TheUnicodeStringCriticalSection = nullptr; diff --git a/GeneralsMD/CMakeLists.txt b/GeneralsMD/CMakeLists.txt index 311a5a0d409..6484aff13be 100644 --- a/GeneralsMD/CMakeLists.txt +++ b/GeneralsMD/CMakeLists.txt @@ -38,6 +38,7 @@ if("${CMAKE_HOST_SYSTEM}" MATCHES "Windows" AND "${CMAKE_SYSTEM}" MATCHES "Windo endif() if(RTS_INSTALL_PREFIX_ZEROHOUR) + rts_install_crashpad(z_generals "${RTS_INSTALL_PREFIX_ZEROHOUR}") install(TARGETS z_generals RUNTIME DESTINATION "${RTS_INSTALL_PREFIX_ZEROHOUR}") install(FILES $ DESTINATION "${RTS_INSTALL_PREFIX_ZEROHOUR}" OPTIONAL) diff --git a/GeneralsMD/Code/Main/CMakeLists.txt b/GeneralsMD/Code/Main/CMakeLists.txt index d1f4dc06683..0ee8660f8e0 100644 --- a/GeneralsMD/Code/Main/CMakeLists.txt +++ b/GeneralsMD/Code/Main/CMakeLists.txt @@ -78,3 +78,5 @@ endif() if(MINGW AND COMMAND add_debug_strip_target) add_debug_strip_target(z_generals) endif() + +rts_enable_crashpad(z_generals z_gameengine) diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index d5040bc1dd7..e2be5260924 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -69,6 +69,7 @@ #ifdef RTS_ENABLE_CRASHDUMP #include "Common/MiniDumper.h" #endif +#include "Common/CrashReporting.h" // GLOBALS //////////////////////////////////////////////////////////////////// @@ -865,13 +866,14 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, #endif #ifdef RTS_ENABLE_CRASHDUMP - // Initialize minidump facilities - requires TheGlobalData so performed after parseCommandLineForStartup - MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData()); + // Initialize reporting after parseCommandLineForStartup provides the user-data path. + CrashReporting::initialize(TheGlobalData->getPath_UserData(), VERSION_MAJOR, VERSION_MINOR, VERSION_BUILDNUM); #endif // register windows class and create application window if(!TheGlobalData->m_headless && initializeAppWindows(hInstance, nCmdShow, TheGlobalData->m_windowed) == false) { + CrashReporting::shutdown(); return exitcode; } @@ -909,6 +911,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, DEBUG_LOG(("Generals is already running...Bail!")); delete TheVersion; TheVersion = nullptr; + CrashReporting::shutdown(); shutdownMemoryManager(); return exitcode; } @@ -940,7 +943,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, } #ifdef RTS_ENABLE_CRASHDUMP - MiniDumper::shutdownMiniDumper(); + CrashReporting::shutdown(); #endif TheUnicodeStringCriticalSection = nullptr; TheDmaCriticalSection = nullptr; diff --git a/cmake/crashpad-metadata.cmake b/cmake/crashpad-metadata.cmake new file mode 100644 index 00000000000..1307cfb818b --- /dev/null +++ b/cmake/crashpad-metadata.cmake @@ -0,0 +1,27 @@ +if(NOT EXISTS "${GAME}" OR NOT EXISTS "${PDB}") + # The pre-link runtime deployment runs before the first executable exists. + return() +endif() +execute_process(COMMAND "${GIT}" -C "${SOURCE}" rev-parse HEAD OUTPUT_VARIABLE revision OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY) +execute_process(COMMAND "${GIT}" -C "${SOURCE}" status --porcelain OUTPUT_VARIABLE status COMMAND_ERROR_IS_FATAL ANY) +set(dirty false) +if(NOT status STREQUAL "") + set(dirty true) +endif() +set(document "{}") +string(JSON document SET "${document}" revision "\"${revision}\"") +string(JSON document SET "${document}" dirty ${dirty}) +string(JSON document SET "${document}" compiler "\"${COMPILER}\"") +string(JSON document SET "${document}" configuration "\"${CONFIG}\"") +string(JSON document SET "${document}" architecture "\"x86\"") +string(JSON document SET "${document}" backend "\"crashpad\"") +string(JSON document SET "${document}" crashpad_revision "\"7e0af1d4d45b526f01677e74a56f4a951b70517d\"") +string(JSON document SET "${document}" vcpkg_baseline "\"b02e341c927f16d991edbd915d8ea43eac52096c\"") +string(JSON document SET "${document}" package_triplet "\"x86-windows-static-md\"") +string(JSON document SET "${document}" handler_configuration "\"Release\"") +foreach(kind GAME PDB BRIDGE BRIDGE_PDB HANDLER) + file(SHA256 "${${kind}}" hash) + get_filename_component(name "${${kind}}" NAME) + string(JSON document SET "${document}" "${kind}" "{\"file\":\"${name}\",\"sha256\":\"${hash}\"}") +endforeach() +file(WRITE "${OUTPUT}" "${document}\n") diff --git a/cmake/crashpad.cmake b/cmake/crashpad.cmake new file mode 100644 index 00000000000..6df3a0e55f9 --- /dev/null +++ b/cmake/crashpad.cmake @@ -0,0 +1,65 @@ +option(RTS_BUILD_OPTION_CRASHPAD "Use optional local-only Crashpad reporting in the games" OFF) +option(RTS_BUILD_CRASHPAD_TESTS "Build the dedicated Crashpad fault-injection executable" OFF) +add_feature_info(Crashpad RTS_BUILD_OPTION_CRASHPAD "Local-only out-of-process game crash reports") + +if(RTS_BUILD_OPTION_CRASHPAD) + if(NOT RTS_CRASHDUMP_ENABLE) + message(FATAL_ERROR "RTS_BUILD_OPTION_CRASHPAD requires RTS_CRASHDUMP_ENABLE=ON") + endif() + if(NOT WIN32 OR NOT MSVC OR IS_VS6_BUILD OR NOT CMAKE_SIZEOF_VOID_P EQUAL 4 + OR MSVC_VERSION LESS 1920) + message(FATAL_ERROR "Crashpad requires modern MSVC or ClangCL targeting Win32 (x86)") + endif() + if(RTS_BUILD_OPTION_ASAN OR RTS_BUILD_CORE_FUZZ) + message(FATAL_ERROR "Keep Crashpad disabled in sanitizer and fuzzing builds") + endif() + add_subdirectory(Dependencies/Crashpad) +elseif(RTS_BUILD_CRASHPAD_TESTS) + message(FATAL_ERROR "RTS_BUILD_CRASHPAD_TESTS requires RTS_BUILD_OPTION_CRASHPAD=ON") +endif() + +function(rts_enable_crashpad game engine) + if(NOT RTS_BUILD_OPTION_CRASHPAD) + return() + endif() + # Tools may link the same engine, but never initialize this optional backend. + # Neither the engine nor tools link the Crashpad client or DLL import library. + target_compile_definitions(${engine} PRIVATE RTS_USE_CRASHPAD=1) + target_include_directories(${engine} PRIVATE "${CMAKE_SOURCE_DIR}/Dependencies/Crashpad") + # Component expressions deliberately avoid a dependency on the executable: + # this runtime target runs before linking, even when only the DLL changed. + set(metadata_command ${CMAKE_COMMAND} + "-DOUTPUT=$/${game}-crashpad-build.json" + "-DGAME=$/$" + "-DPDB=$/$" + "-DBRIDGE=$" "-DBRIDGE_PDB=$" + "-DHANDLER=${RTS_CRASHPAD_HANDLER}" "-DCONFIG=$" + "-DCOMPILER=${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" + "-DSOURCE=${CMAKE_SOURCE_DIR}" "-DGIT=${GIT_EXECUTABLE}" + -P "${CMAKE_SOURCE_DIR}/cmake/crashpad-metadata.cmake") + add_custom_target(${game}_crashpad_runtime + COMMAND ${CMAKE_COMMAND} -E make_directory "$" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" "$" + "${RTS_CRASHPAD_HANDLER}" "$" + ${RTS_CRASHPAD_RUNTIME_FILES} "$" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${RTS_CRASHPAD_NOTICES}" "$/crashpad-notices" + COMMAND ${metadata_command} + DEPENDS rts_crashpad rts_crashpad_reports + VERBATIM) + add_dependencies(${game} ${game}_crashpad_runtime) + add_custom_command(TARGET ${game} POST_BUILD COMMAND ${metadata_command} VERBATIM) +endfunction() + +function(rts_install_crashpad game destination) + if(RTS_BUILD_OPTION_CRASHPAD) + install(FILES "$" "$" + "${RTS_CRASHPAD_HANDLER}" "$" + "$/${game}-crashpad-build.json" + DESTINATION "${destination}") + install(DIRECTORY "${RTS_CRASHPAD_NOTICES}/" DESTINATION "${destination}/crashpad-notices") + install(FILES ${RTS_CRASHPAD_RUNTIME_FILES} DESTINATION "${destination}" + CONFIGURATIONS Release RelWithDebInfo MinSizeRel) + endif() +endfunction() diff --git a/docs/crashpad-validation.md b/docs/crashpad-validation.md new file mode 100644 index 00000000000..caa077effc3 --- /dev/null +++ b/docs/crashpad-validation.md @@ -0,0 +1,120 @@ +# Crashpad validation record + +Local validation on 2026-10-01, based on upstream +`b0c29eba834e7030e5c4d5f86f985e8ff799c92c`. The implementation was an uncommitted +working tree, so generated build metadata correctly reports `dirty: true`. + +Environment: Windows 11 build 26200, Visual Studio 18 2026, ClangCL 22.1.3, +Windows SDK 10.0.28000.0, CMake 4.4.0, Win32 targets. The dependency used the +separate pinned vcpkg manifest and `x86-windows-static-md` for target and host. +Historical builds used Visual C++ 6 SP6, compiler 12.0.8804. + +## Completed checks + +| Check | Result | +| --- | --- | +| Standalone pinned x86 client/handler | One local access-violation report; CDB resolved the intentional faulting function | +| Both ClangCL Release games, Crashpad on | Build and isolated startup passed with the normal game allocator enabled | +| Both ClangCL Release games, Crashpad off | Builds passed without a Crashpad package requirement | +| Both ClangCL Release games, master crash-dump switch off | Builds passed | +| Both VC6 Release games | Builds passed; Crashpad remains disabled | +| Unsupported explicit configurations | x64, VC6, ASan, and Crashpad-on/master-off rejected with the intended configure errors | +| Dedicated Release and Debug adapter tests | Main-thread and worker-thread access violations preserved their exception code and faulting thread ID | +| Stack overflow | Captured `0xc00000fd` in the dedicated test executable | +| Heap-corruption exception | Captured a deliberately raised `0xc0000374`; this was not a real damaged-heap experiment | +| Explicit diagnostic capture | One `0x0517a7ed` report on the requesting thread; repeated capture and a subsequent access violation produced no duplicate | +| Missing/invalid handler and blocked database path | Startup failed cleanly and restored the previous exception filter | +| Valid executable that never answers handler IPC | Readiness timed out in approximately five seconds, restored the previous filter and allowed fallback | +| Handler terminated before explicit capture | No report, game/test process exited with code 1 after approximately 15 seconds | +| Wide paths | Handler and database worked in paths containing spaces, accented Latin characters and CJK characters | +| Concurrent instances | Three processes wrote three reports into the same database | +| Size retention | Startup pruned three synthetic 200 MiB reports to two reports, below the 500 MiB budget | +| Manual export | Exported bytes matched the original report; database uploads remained disabled | +| Isolated game fatal paths | Both `ReleaseCrash` and the localized branch produced one report plus `ReleaseCrashInfo.txt` in both games, without dialogs | +| Legacy fallback in both games | Missing handler selected MiniDumper; explicit fatal capture produced the original minimal/full pair and text report | +| Game symbolization | CDB resolved `CrashpadGameTestFault` and its source line in both game executables using their matching PDBs | +| Install layout | CMake installed handler, adapter/PDB, report helper, VC runtime DLLs, notices and metadata into separate staging directories | +| Symbol archives | Both game archives passed SHA-256 checks and CodeView GUID/age comparisons for game and adapter PDBs | +| Source checks | `git diff --check` and exact-case validation of added includes passed | + +The game fault tests used generated source copies under `build/game-validation`. +They redirected user-data paths into that directory, invoked the unchanged fatal +functions, and injected an access violation in a named test function. The +localized test supplied a small text-provider stub. They did not crash a player +session or modify the retail installation. The normal build trees were restored +to their production sources afterward; no fault-injection mode is compiled into +the shipped games. + +The separate adapter executable rejects calls to its own global allocator while +Crashpad runs. This tests the DLL allocation boundary independently of the game +tests, which report that the normal game allocator is initialized. + +## Existing gameplay crash + +The replay and custom map attached to +[issue #3185](https://github.com/TheSuperHackers/GeneralsGameCode/issues/3185) +reproduced the Zero Hour Rebel Ambush crash twice on the same local build. +Each headless run produced one report and exited with `0xc0000005`, in 1.50 and +1.21 seconds. The exported sample was 464,244 bytes. CDB loaded the matching +private PDB and resolved the null-object read to +`AIGroup::groupDoSpecialPowerAtLocation` at `AIGroup.cpp:2744`, through +`Object::getControllingPlayer`. The fault attempted to read address `0x000001e0`. + +This capture used normal game logic and WinMain sources. Its only additional +test change redirected user data through an environment variable in a generated +copy of `GlobalData.cpp`. No artificial fault was injected. A separate replay +script reproduced the crash again using the archived executable. The original +source configuration was restored afterward. + +The report, exact executable/PDBs, dependency notices, source changes and replay +were packaged locally for developer feedback. Uploads remained disabled. The +sample confirms capture and symbolization; it does not establish better capture +reliability than MiniDumper. Global game state outside the captured memory could +not be inspected, so this is not evidence of full-memory parity. + +## PR preparation checks + +On 2026-10-03 the first repeated Release test run exited with Windows status +`0xc0000142` before the shutdown test entered `main`. The same binaries passed +the complete matrix on retry and 30 further fresh-process startup/shutdown +checks. The loader failure was not reproduced and its cause was not established. +The failed log is retained locally; the retry is not presented as a diagnosis. + +## Replay comparison + +The local ClangCL test runs did not pass retail CRC validation. They produced +the same failures with the feature enabled and disabled, using the same staged +assets and replay files: + +| Game | Replay | Feature on and off | +| --- | --- | --- | +| Generals | `CW_050307_OoE_Silver_CvC_awesome.rep` | CRC mismatch at frame 110 | +| Zero Hour | `!Golden Replay #1.rep`, with the `tansooo` map supplied | CRC mismatch at frame 111 | + +This comparison found no change in those outcomes. It does not establish retail +replay compatibility or resolve the existing local mismatches. + +## Remaining qualification before default enablement + +- Clean-machine testing without Visual Studio or an existing VC runtime, + and OS versions other than the Windows 11 test host. +- Full Debug and Tracy game runs. The debug client/adapter was tested, but that + is not a complete debug-game or profiling-game qualification. +- Multiplayer smoke testing and passing retail replay fixtures. +- Real allocator corruption, handler failure while it has suspended the game, + and scheduler/starvation failures beyond the runnable-process watchdog case. +- Age-limit testing across 30 days. The configured upstream age policy is + present; the automated retention test exercises the size policy. +- Network packet observation, prolonged resident-memory measurements and disk + usage over real play sessions. Local-only settings and an empty upload URL + were verified; no packet-capture claim is made. +- Full-memory dump parity. Keep the legacy backend available for this use. + +Standalone startup samples were generally 15–100 ms on this host; those are not +representative gameplay benchmarks. Report persistence is verified by opening +and inspecting reports, not by treating `DumpWithoutCrash()` returning as proof. + +The pinned [Windows client implementation](https://github.com/chromium/crashpad/blob/7e0af1d4d45b526f01677e74a56f4a951b70517d/client/crashpad_client_win.cc) +defines the blocking and lifetime behavior used by the adapter. See the +[build and deployment guide](crashpad.md) for reproduction commands and the +release-symbol retention procedure. diff --git a/docs/crashpad.md b/docs/crashpad.md new file mode 100644 index 00000000000..880b32b23c1 --- /dev/null +++ b/docs/crashpad.md @@ -0,0 +1,183 @@ +# Optional local Crashpad reports + +`RTS_BUILD_OPTION_CRASHPAD` defaults to `OFF`. It enables a local-only prototype +for the modern MSVC and ClangCL Win32 game builds. WorldBuilder and other tools +keep their existing reporting behavior. VC6, MinGW and non-Windows builds do not +acquire or link Crashpad when the option is off. Explicit unsupported requests, +including `RTS_CRASHDUMP_ENABLE=OFF` and sanitizer/fuzzing builds, fail configure. + +This prototype has been exercised on Windows 11. Its deployment includes the +x86 Visual C++ redistributable DLLs and expects the Windows 10 or newer Universal +CRT. It does not change the OS requirements of default-off builds. Older Windows +versions have not been qualified for the optional backend. Loading the adapter +can fail on an older OS, in which case the game tries MiniDumper. + +## Build the pinned package + +Use the separate dependency manifest so ordinary game builds do not install +Crashpad or change the root vcpkg dependency set. Run these commands from the +repository root in Git Bash, with `VCPKG_ROOT` pointing to vcpkg: + +```bash +"$VCPKG_ROOT/vcpkg.exe" install \ + --x-manifest-root=Dependencies/Crashpad \ + --x-install-root=build/crashpad-package \ + --triplet=x86-windows-static-md --host-triplet=x86-windows-static-md + +cmake -S . -B build/crashpad -G "Visual Studio 18 2026" -A Win32 -T ClangCL \ + -DRTS_BUILD_OPTION_CRASHPAD=ON \ + -DRTS_CRASHPAD_PACKAGE_ROOT="$PWD/build/crashpad-package/x86-windows-static-md" \ + -DCMAKE_MSVC_DEBUG_INFORMATION_FORMAT=Embedded +cmake --build build/crashpad --config Release --target g_generals z_generals --parallel 4 +``` + +Omit `-T ClangCL` to build the games and adapter with MSVC. The package itself +uses vcpkg's MSVC/GN route. The baseline Python helper requires a native triplet, +so both target and host are explicitly x86 here. A copy of this installed prefix +can be supplied to a build that does not otherwise use vcpkg. Configure checks +the package's SPDX provenance, architecture and port version. + +| Component | Pin / configuration | +| --- | --- | +| vcpkg baseline | `b02e341c927f16d991edbd915d8ea43eac52096c` | +| Crashpad port | `2024-04-11#7` | +| Crashpad source | `7e0af1d4d45b526f01677e74a56f4a951b70517d` | +| mini_chromium | `dce72d97d1c2e9beb5e206c6a05a702269794ca3` | +| Client | x86 static libraries, dynamic CRT, Debug `/MDd` and Release `/MD` | +| Handler | Release x86, from the same package and source revision | +| zlib and build helpers | Pinned transitively by the manifest baseline; SPDX and ABI records accompany deployment | + +The baseline port exports `crashpad::crashpad` and packages the handler at +`tools/crashpad/crashpad_handler.exe`. Do not substitute a separately downloaded +handler. Debug builds require the Visual Studio debug CRT and are not release +packages. + +## Startup and ownership + +The game loads `rts_crashpad.dll` by absolute path beside its executable. That DLL +contains the statically linked client and uses its own CRT allocation functions. +It does not inherit the game PCH, forced includes, memory-pool macros or global +`new`/`delete` overrides. Only C functions and borrowed string arguments cross +the boundary. Tools have no import-library dependency on this DLL. + +Zero Hour starts Crashpad at the previous MiniDumper initialization point. +Generals initially uses MiniDumper there because its user-data path is still +empty. After loading the GameData INIs and parsing engine startup options, +Generals replaces that temporary backend using the resolved user-data path. +This differs from the handoff's assumption that both games know the path in +WinMain. Early crashes remain outside Crashpad coverage. + +Initialization converts the game's UTF-8 user path to UTF-16, creates the +database, disables uploads, and starts the handler asynchronously with a +five-second readiness wait. Success requires the upstream IPC ping to complete. +This bounds startup when a valid executable fails to service the handler pipe. +There is no upload URL, metrics directory +or attachment list. Build annotations contain the game, application version, +full Git revision, dirty state, x86 architecture, compiler, CMake configuration +and backend. + +On failure the adapter restores the previous exception filter and abort handler, +removes its heap-corruption handler and stops its watchdog. The game logs one +failure and tries MiniDumper. `CrashReporting::backendName()` reports `crashpad`, +`minidumper` or `unavailable`; startup continues if reporting is unavailable. +Startup selection does not monitor a handler that dies later, and Windows does +not restart the handler in this revision. + +Crashpad owns unhandled exceptions while active. The legacy WinMain filter does +not run first, so Crashpad reports omit the legacy `DumpExceptionInfo` text. +The debug library's `PreStaticInit()` registration runs before WinMain; the +WorldBuilder registration belongs to a separate executable. Neither replaces +Crashpad later in game startup. Normal shutdown restores the original filter. +The DLL stays loaded because upstream retains process-lifetime IPC pointers; +the handler exits when the game process exits. + +Both explicit fatal-error variants capture before best-effort diagnostics and +keep their existing exit paths. The pinned `DumpWithoutCrash()` waits without a +timeout, so the adapter creates a watchdog thread during healthy startup. If an +explicit fatal capture does not return within 15 seconds, that thread terminates +the game with exit code 1. In that case later text diagnostics may be absent. +This bound depends on the game process remaining schedulable; it cannot promise +recovery from arbitrary corruption or a handler that leaves the process +suspended. The diagnostic capture is one-shot, and a subsequent reporting fault +terminates without generating a second report. Headless mode adds no dialogs. +Upstream unhandled-exception handling has its separate 60-second termination +fallback. + +## Reports, export and retention + +The database is `/CrashDumps/Crashpad/`. Crashpad produces one minidump, +not the legacy minimal/full-memory pair. Full-memory parity is unproven. Build +with the option off when an investigation needs the old full dump. + +At startup, Crashpad's database API prunes reports older than 30 days and older +reports beyond a 500 MiB report-size budget. Active/locked reports remain under +Crashpad's control. The handler's different default pruning policy is disabled. +The budget excludes database metadata and is not a hard disk quota; concurrent +runs and newly written reports can exceed it until the next startup or explicit +prune. The legacy filename cleanup does not enter this subdirectory. + +Use the installed helper instead of manipulating database metadata: + +```bash +rts_crashpad_reports.exe "C:/path/to/user-data/CrashDumps/Crashpad" list +rts_crashpad_reports.exe "C:/path/to/user-data/CrashDumps/Crashpad" export "C:/path/to/new-export" +rts_crashpad_reports.exe "C:/path/to/user-data/CrashDumps/Crashpad" prune +``` + +Export enumerates finalized reports through the database API and copies them +without marking them uploaded. It refuses to overwrite an existing destination +file and reports concurrent-pruning failures. Dumps contain process memory even +without attachments; manual sharing is a separate user action. + +## Deployment and symbols + +Each enabled game output and CMake install includes the matching handler, +adapter DLL/PDB, report helper, redistributable VC runtime DLLs, dependency +notices and `*-crashpad-build.json`. Set `RTS_INSTALL_PREFIX_GENERALS` and +`RTS_INSTALL_PREFIX_ZEROHOUR` to explicit staging paths when testing installation. +No build-tree path is used to locate the runtime handler or database. + +Keep the exact executable, game PDB, adapter DLL/PDB, handler and metadata for +every distributed build. With LLVM tools on `PATH`, verify CodeView GUID/age +against each PDB and create an archive: + +```bash +python Dependencies/Crashpad/archive_symbols.py \ + build/crashpad/Generals/Release/g_generals-crashpad-build.json \ + build/symbols/generals-crashpad.zip +``` + +Use `GeneralsMD/Release/z_generals-crashpad-build.json` for Zero Hour. The helper +also verifies the build's recorded SHA-256 hashes and refuses to overwrite an +archive. Keep the source commit with the archive; preserve the source snapshot +as well for a build marked dirty. Open an exported dump in WinDbg/CDB or Visual +Studio with the archived PDBs and verify a known function and source line. + +CI collection includes the notices and metadata and retains artifacts for 90 +days. That is temporary storage. Attach the verified symbol archive to the +corresponding release, or copy it to the project's durable symbol archive, +before distributing a Crashpad-enabled release. No reporting server or release +upload is configured by this change. + +## Validation + +Enable `RTS_BUILD_CRASHPAD_TESTS=ON` in the Crashpad build tree, then run: + +```bash +cmake --build build/crashpad --config Release \ + --target rts_crashpad_test rts_crashpad_seed rts_crashpad_reports --parallel 4 +python Dependencies/Crashpad/tests/validate.py \ + build/crashpad/Dependencies/Crashpad/tests/Release/rts_crashpad_test.exe \ + build/crashpad-validation +``` + +The script runs only the dedicated test executable in fresh directories under +`build/`. It checks original exception codes and thread IDs, one report per +event, fatal timeout after handler death, startup failure/filter restoration, +paths with spaces and non-ASCII characters, concurrent instances, disabled +uploads, export identity, and pruning of a 600 MiB synthetic database. The +executable forbids use of its global allocator during adapter operations. + +See [the validation record](crashpad-validation.md) for local build and runtime +results and the remaining qualification work. Keep the option off by default +until those limitations and full-memory requirements have been resolved. From 56e944e5f3acb8347225512a47ee536cec80ed15 Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Sat, 3 Oct 2026 18:09:26 -0600 Subject: [PATCH 2/5] docs(crashpad): Keep local validation results out of the PR --- docs/crashpad-validation.md | 120 ------------------------------------ docs/crashpad.md | 5 +- 2 files changed, 2 insertions(+), 123 deletions(-) delete mode 100644 docs/crashpad-validation.md diff --git a/docs/crashpad-validation.md b/docs/crashpad-validation.md deleted file mode 100644 index caa077effc3..00000000000 --- a/docs/crashpad-validation.md +++ /dev/null @@ -1,120 +0,0 @@ -# Crashpad validation record - -Local validation on 2026-10-01, based on upstream -`b0c29eba834e7030e5c4d5f86f985e8ff799c92c`. The implementation was an uncommitted -working tree, so generated build metadata correctly reports `dirty: true`. - -Environment: Windows 11 build 26200, Visual Studio 18 2026, ClangCL 22.1.3, -Windows SDK 10.0.28000.0, CMake 4.4.0, Win32 targets. The dependency used the -separate pinned vcpkg manifest and `x86-windows-static-md` for target and host. -Historical builds used Visual C++ 6 SP6, compiler 12.0.8804. - -## Completed checks - -| Check | Result | -| --- | --- | -| Standalone pinned x86 client/handler | One local access-violation report; CDB resolved the intentional faulting function | -| Both ClangCL Release games, Crashpad on | Build and isolated startup passed with the normal game allocator enabled | -| Both ClangCL Release games, Crashpad off | Builds passed without a Crashpad package requirement | -| Both ClangCL Release games, master crash-dump switch off | Builds passed | -| Both VC6 Release games | Builds passed; Crashpad remains disabled | -| Unsupported explicit configurations | x64, VC6, ASan, and Crashpad-on/master-off rejected with the intended configure errors | -| Dedicated Release and Debug adapter tests | Main-thread and worker-thread access violations preserved their exception code and faulting thread ID | -| Stack overflow | Captured `0xc00000fd` in the dedicated test executable | -| Heap-corruption exception | Captured a deliberately raised `0xc0000374`; this was not a real damaged-heap experiment | -| Explicit diagnostic capture | One `0x0517a7ed` report on the requesting thread; repeated capture and a subsequent access violation produced no duplicate | -| Missing/invalid handler and blocked database path | Startup failed cleanly and restored the previous exception filter | -| Valid executable that never answers handler IPC | Readiness timed out in approximately five seconds, restored the previous filter and allowed fallback | -| Handler terminated before explicit capture | No report, game/test process exited with code 1 after approximately 15 seconds | -| Wide paths | Handler and database worked in paths containing spaces, accented Latin characters and CJK characters | -| Concurrent instances | Three processes wrote three reports into the same database | -| Size retention | Startup pruned three synthetic 200 MiB reports to two reports, below the 500 MiB budget | -| Manual export | Exported bytes matched the original report; database uploads remained disabled | -| Isolated game fatal paths | Both `ReleaseCrash` and the localized branch produced one report plus `ReleaseCrashInfo.txt` in both games, without dialogs | -| Legacy fallback in both games | Missing handler selected MiniDumper; explicit fatal capture produced the original minimal/full pair and text report | -| Game symbolization | CDB resolved `CrashpadGameTestFault` and its source line in both game executables using their matching PDBs | -| Install layout | CMake installed handler, adapter/PDB, report helper, VC runtime DLLs, notices and metadata into separate staging directories | -| Symbol archives | Both game archives passed SHA-256 checks and CodeView GUID/age comparisons for game and adapter PDBs | -| Source checks | `git diff --check` and exact-case validation of added includes passed | - -The game fault tests used generated source copies under `build/game-validation`. -They redirected user-data paths into that directory, invoked the unchanged fatal -functions, and injected an access violation in a named test function. The -localized test supplied a small text-provider stub. They did not crash a player -session or modify the retail installation. The normal build trees were restored -to their production sources afterward; no fault-injection mode is compiled into -the shipped games. - -The separate adapter executable rejects calls to its own global allocator while -Crashpad runs. This tests the DLL allocation boundary independently of the game -tests, which report that the normal game allocator is initialized. - -## Existing gameplay crash - -The replay and custom map attached to -[issue #3185](https://github.com/TheSuperHackers/GeneralsGameCode/issues/3185) -reproduced the Zero Hour Rebel Ambush crash twice on the same local build. -Each headless run produced one report and exited with `0xc0000005`, in 1.50 and -1.21 seconds. The exported sample was 464,244 bytes. CDB loaded the matching -private PDB and resolved the null-object read to -`AIGroup::groupDoSpecialPowerAtLocation` at `AIGroup.cpp:2744`, through -`Object::getControllingPlayer`. The fault attempted to read address `0x000001e0`. - -This capture used normal game logic and WinMain sources. Its only additional -test change redirected user data through an environment variable in a generated -copy of `GlobalData.cpp`. No artificial fault was injected. A separate replay -script reproduced the crash again using the archived executable. The original -source configuration was restored afterward. - -The report, exact executable/PDBs, dependency notices, source changes and replay -were packaged locally for developer feedback. Uploads remained disabled. The -sample confirms capture and symbolization; it does not establish better capture -reliability than MiniDumper. Global game state outside the captured memory could -not be inspected, so this is not evidence of full-memory parity. - -## PR preparation checks - -On 2026-10-03 the first repeated Release test run exited with Windows status -`0xc0000142` before the shutdown test entered `main`. The same binaries passed -the complete matrix on retry and 30 further fresh-process startup/shutdown -checks. The loader failure was not reproduced and its cause was not established. -The failed log is retained locally; the retry is not presented as a diagnosis. - -## Replay comparison - -The local ClangCL test runs did not pass retail CRC validation. They produced -the same failures with the feature enabled and disabled, using the same staged -assets and replay files: - -| Game | Replay | Feature on and off | -| --- | --- | --- | -| Generals | `CW_050307_OoE_Silver_CvC_awesome.rep` | CRC mismatch at frame 110 | -| Zero Hour | `!Golden Replay #1.rep`, with the `tansooo` map supplied | CRC mismatch at frame 111 | - -This comparison found no change in those outcomes. It does not establish retail -replay compatibility or resolve the existing local mismatches. - -## Remaining qualification before default enablement - -- Clean-machine testing without Visual Studio or an existing VC runtime, - and OS versions other than the Windows 11 test host. -- Full Debug and Tracy game runs. The debug client/adapter was tested, but that - is not a complete debug-game or profiling-game qualification. -- Multiplayer smoke testing and passing retail replay fixtures. -- Real allocator corruption, handler failure while it has suspended the game, - and scheduler/starvation failures beyond the runnable-process watchdog case. -- Age-limit testing across 30 days. The configured upstream age policy is - present; the automated retention test exercises the size policy. -- Network packet observation, prolonged resident-memory measurements and disk - usage over real play sessions. Local-only settings and an empty upload URL - were verified; no packet-capture claim is made. -- Full-memory dump parity. Keep the legacy backend available for this use. - -Standalone startup samples were generally 15–100 ms on this host; those are not -representative gameplay benchmarks. Report persistence is verified by opening -and inspecting reports, not by treating `DumpWithoutCrash()` returning as proof. - -The pinned [Windows client implementation](https://github.com/chromium/crashpad/blob/7e0af1d4d45b526f01677e74a56f4a951b70517d/client/crashpad_client_win.cc) -defines the blocking and lifetime behavior used by the adapter. See the -[build and deployment guide](crashpad.md) for reproduction commands and the -release-symbol retention procedure. diff --git a/docs/crashpad.md b/docs/crashpad.md index 880b32b23c1..55c31bdf5de 100644 --- a/docs/crashpad.md +++ b/docs/crashpad.md @@ -178,6 +178,5 @@ paths with spaces and non-ASCII characters, concurrent instances, disabled uploads, export identity, and pruning of a 600 MiB synthetic database. The executable forbids use of its global allocator during adapter operations. -See [the validation record](crashpad-validation.md) for local build and runtime -results and the remaining qualification work. Keep the option off by default -until those limitations and full-memory requirements have been resolved. +Keep the option off by default until the intended deployment environments and +full-memory requirements have been qualified. From 5efe45421c93bb94e5f12e4abd59c14ec5427405 Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Sat, 3 Oct 2026 21:33:25 -0600 Subject: [PATCH 3/5] ci(vcpkg): Select the root dependency manifest explicitly --- .github/workflows/reusable-build-toolchain.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/reusable-build-toolchain.yml b/.github/workflows/reusable-build-toolchain.yml index 17646c8a88f..cd7088cbafb 100644 --- a/.github/workflows/reusable-build-toolchain.yml +++ b/.github/workflows/reusable-build-toolchain.yml @@ -120,6 +120,8 @@ jobs: if: contains(inputs.preset, 'vcpkg') uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 with: + # The optional Crashpad package has its own separate manifest. + vcpkgJsonGlob: 'vcpkg.json' runVcpkgInstall: false doNotCache: true From ec660df48fdb56a24081c53bedd19b5f261d03ab Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Sat, 3 Oct 2026 22:52:33 -0600 Subject: [PATCH 4/5] bugfix(crashreporting): Preserve fatal dumps during shutdown --- Core/GameEngine/Source/Common/System/Debug.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Core/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp index 7838afc18bd..27e212f2c5d 100644 --- a/Core/GameEngine/Source/Common/System/Debug.cpp +++ b/Core/GameEngine/Source/Common/System/Debug.cpp @@ -736,15 +736,14 @@ static void TriggerMiniDump() void ReleaseCrash(const char *reason) { + TriggerMiniDump(); + // We are shutting down, and TheGlobalData has been freed. jba. [4/15/2003] - // Do not consume the one-shot fatal capture when this path will return. if (TheGlobalData == nullptr) { return; } - TriggerMiniDump(); - /// do additional reporting on the crash, if possible if (!DX8Wrapper_IsWindowed) { @@ -828,6 +827,7 @@ void ReleaseCrashLocalized(const AsciiString& p, const AsciiString& m) // TheSuperHackers @bugfix Codex 01/10/2026 Match ReleaseCrash during shutdown instead of dereferencing freed global data. if (TheGlobalData == nullptr) { + TriggerMiniDump(); return; } From 160053ceb9d561d28c77e839c3ca201869fa67ca Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Sun, 4 Oct 2026 09:14:28 -0600 Subject: [PATCH 5/5] refactor(crashreporting): Simplify optional Crashpad integration --- .../workflows/reusable-build-toolchain.yml | 7 +- .../Include/Common/CrashReporting.h | 19 +- .../Source/Common/System/CrashReporting.cpp | 19 +- Dependencies/Crashpad/CMakeLists.txt | 10 - Dependencies/Crashpad/CrashpadBridge.cpp | 19 +- Dependencies/Crashpad/CrashpadBridge.h | 19 +- Dependencies/Crashpad/CrashpadReports.cpp | 82 ------- Dependencies/Crashpad/archive_symbols.py | 57 ----- Dependencies/Crashpad/tests/CMakeLists.txt | 21 -- Dependencies/Crashpad/tests/CrashpadTest.cpp | 180 -------------- Dependencies/Crashpad/tests/SeedDatabase.cpp | 43 ---- Dependencies/Crashpad/tests/WrongHandler.cpp | 34 --- Dependencies/Crashpad/tests/validate.py | 138 ----------- cmake/crashpad-metadata.cmake | 27 --- cmake/crashpad.cmake | 24 +- docs/crashpad.md | 227 +++++------------- 16 files changed, 142 insertions(+), 784 deletions(-) delete mode 100644 Dependencies/Crashpad/CrashpadReports.cpp delete mode 100644 Dependencies/Crashpad/archive_symbols.py delete mode 100644 Dependencies/Crashpad/tests/CMakeLists.txt delete mode 100644 Dependencies/Crashpad/tests/CrashpadTest.cpp delete mode 100644 Dependencies/Crashpad/tests/SeedDatabase.cpp delete mode 100644 Dependencies/Crashpad/tests/WrongHandler.cpp delete mode 100644 Dependencies/Crashpad/tests/validate.py delete mode 100644 cmake/crashpad-metadata.cmake diff --git a/.github/workflows/reusable-build-toolchain.yml b/.github/workflows/reusable-build-toolchain.yml index cd7088cbafb..bb7db873219 100644 --- a/.github/workflows/reusable-build-toolchain.yml +++ b/.github/workflows/reusable-build-toolchain.yml @@ -178,15 +178,12 @@ jobs: Where-Object { $_.Extension -in @(".exe", ".dll", ".pdb") } -Verbose } - # Preserve Crashpad build metadata and dependency notices beside - # the game, matched handler and adapter DLL. + # Preserve dependency notices alongside the optional Crashpad runtime. $gameOutput = if ("${{ inputs.preset }}" -like "win32*") { "$buildDir\${{ inputs.game }}\$configToUse" } else { "$buildDir\${{ inputs.game }}" } - Get-ChildItem -Path $gameOutput -Filter "*-crashpad-build.json" -File | - Copy-Item -Destination $artifactsDir -Force if (Test-Path "$gameOutput\crashpad-notices") { Copy-Item -Path "$gameOutput\crashpad-notices" -Destination $artifactsDir -Recurse -Force } @@ -197,5 +194,5 @@ jobs: with: name: ${{ inputs.game }}-${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} path: build\${{ inputs.preset }}\${{ inputs.game }}\artifacts - retention-days: 90 + retention-days: 30 if-no-files-found: error diff --git a/Core/GameEngine/Include/Common/CrashReporting.h b/Core/GameEngine/Include/Common/CrashReporting.h index 7bc9b4ff35b..d40cdb95755 100644 --- a/Core/GameEngine/Include/Common/CrashReporting.h +++ b/Core/GameEngine/Include/Common/CrashReporting.h @@ -1,4 +1,21 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + #pragma once class AsciiString; diff --git a/Core/GameEngine/Source/Common/System/CrashReporting.cpp b/Core/GameEngine/Source/Common/System/CrashReporting.cpp index ba44f9ab81c..377f2e56bfe 100644 --- a/Core/GameEngine/Source/Common/System/CrashReporting.cpp +++ b/Core/GameEngine/Source/Common/System/CrashReporting.cpp @@ -1,4 +1,21 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + // TheSuperHackers @feature Codex 01/10/2026 Select optional local Crashpad reporting with legacy MiniDumper fallback. #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine #include "Common/CrashReporting.h" diff --git a/Dependencies/Crashpad/CMakeLists.txt b/Dependencies/Crashpad/CMakeLists.txt index 86edf8c5429..77cf22030da 100644 --- a/Dependencies/Crashpad/CMakeLists.txt +++ b/Dependencies/Crashpad/CMakeLists.txt @@ -62,13 +62,3 @@ configure_file("${package}/share/crashpad/vcpkg_abi_info.txt" "${notices}/crashp configure_file(getopt-LICENSE "${notices}/getopt-LICENSE" COPYONLY) configure_file(mini-chromium-LICENSE "${notices}/mini-chromium-LICENSE" COPYONLY) set(RTS_CRASHPAD_NOTICES "${notices}" PARENT_SCOPE) - -add_executable(rts_crashpad_reports CrashpadReports.cpp) -target_compile_features(rts_crashpad_reports PRIVATE cxx_std_20) -target_link_libraries(rts_crashpad_reports PRIVATE crashpad::crashpad) -set_target_properties(rts_crashpad_reports PROPERTIES - MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") - -if(RTS_BUILD_CRASHPAD_TESTS) - add_subdirectory(tests) -endif() diff --git a/Dependencies/Crashpad/CrashpadBridge.cpp b/Dependencies/Crashpad/CrashpadBridge.cpp index 2ca34779fce..fe6620c8d81 100644 --- a/Dependencies/Crashpad/CrashpadBridge.cpp +++ b/Dependencies/Crashpad/CrashpadBridge.cpp @@ -1,4 +1,21 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + // Deliberately compiled outside the game PCH, forced includes and allocator. #include #include diff --git a/Dependencies/Crashpad/CrashpadBridge.h b/Dependencies/Crashpad/CrashpadBridge.h index cd85011db2b..88b23577793 100644 --- a/Dependencies/Crashpad/CrashpadBridge.h +++ b/Dependencies/Crashpad/CrashpadBridge.h @@ -1,4 +1,21 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + #pragma once // C ABI only. No allocation or C++ object crosses the game/DLL boundary. diff --git a/Dependencies/Crashpad/CrashpadReports.cpp b/Dependencies/Crashpad/CrashpadReports.cpp deleted file mode 100644 index 15aecd09d4f..00000000000 --- a/Dependencies/Crashpad/CrashpadReports.cpp +++ /dev/null @@ -1,82 +0,0 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later */ -#include -#include -#include -#include "client/crash_report_database.h" -#include "client/prune_crash_reports.h" -#include "client/settings.h" -#include "base/strings/utf_string_conversions.h" - -int wmain(int argc, wchar_t** argv) -{ - if (argc < 3 || argc > 4) - { - std::fputs("Usage: rts_crashpad_reports list|prune|export [export-directory]\n", stderr); - return 2; - } - - // Open an existing database only. Export never enables uploads or marks a - // report as uploaded, and never edits Crashpad's internal files directly. - auto database = crashpad::CrashReportDatabase::InitializeWithoutCreating(base::FilePath(argv[1])); - if (!database) - { - return 1; - } - - if (wcscmp(argv[2], L"prune") == 0 && argc == 3) - { - crashpad::BinaryPruneCondition retention(crashpad::BinaryPruneCondition::OR, - new crashpad::AgePruneCondition(30), - new crashpad::DatabaseSizePruneCondition(500 * 1024)); - std::printf("Deleted %zu reports\n", crashpad::PruneCrashReportDatabase(database.get(), &retention)); - return 0; - } - - const bool exporting = wcscmp(argv[2], L"export") == 0 && argc == 4; - if (!exporting && (wcscmp(argv[2], L"list") != 0 || argc != 3)) - { - return 2; - } - - std::vector pending; - std::vector completed; - if (database->GetPendingReports(&pending) != crashpad::CrashReportDatabase::kNoError - || database->GetCompletedReports(&completed) != crashpad::CrashReportDatabase::kNoError) - { - return 1; - } - - if (exporting && !CreateDirectoryW(argv[3], nullptr) && GetLastError() != ERROR_ALREADY_EXISTS) - { - return 1; - } - - bool uploads = true; - if (!database->GetSettings()->GetUploadsEnabled(&uploads)) - { - return 1; - } - - std::printf("uploads-enabled=%d\n", uploads); - pending.insert(pending.end(), completed.begin(), completed.end()); - for (const auto& report : pending) - { - std::printf("%s %llu\n", report.uuid.ToString().c_str(), - static_cast(report.total_size)); - if (exporting) - { - // These are finalized, immutable report files enumerated by the - // database API. CopyFile holds the source open during the copy; - // concurrent pruning before it opens is reported as an error. - const base::FilePath destination = base::FilePath(argv[3]) - .Append(base::UTF8ToWide(report.uuid.ToString() + ".dmp")); - if (!CopyFileW(report.file_path.value().c_str(), destination.value().c_str(), TRUE)) - { - std::fprintf(stderr, "Export failed: Windows error %lu\n", GetLastError()); - return 1; - } - } - } - - return 0; -} diff --git a/Dependencies/Crashpad/archive_symbols.py b/Dependencies/Crashpad/archive_symbols.py deleted file mode 100644 index 522ac199ef1..00000000000 --- a/Dependencies/Crashpad/archive_symbols.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Verify binary/PDB identity and archive a Crashpad build for durable storage.""" -import argparse -import hashlib -import json -from pathlib import Path -import re -import subprocess -import zipfile - - -def identity(binary, pdb): - pe = subprocess.check_output(["llvm-readobj", "--coff-debug-directory", str(binary)], text=True) - symbols = subprocess.check_output(["llvm-pdbutil", "dump", "-summary", str(pdb)], text=True) - binary_guid = re.search(r"PDBGUID: (\{[^}]+\})", pe) - binary_age = re.search(r"PDBAge: (\d+)", pe) - pdb_guid = re.search(r"GUID: (\{[^}]+\})", symbols) - pdb_age = re.search(r"Age: (\d+)", symbols) - if not all([binary_guid, binary_age, pdb_guid, pdb_age]): - raise ValueError(f"Cannot read CodeView/PDB identity: {binary.name}") - if (binary_guid[1].lower(), binary_age[1]) != (pdb_guid[1].lower(), pdb_age[1]): - raise ValueError(f"PDB does not match {binary.name}") - return {"binary": binary.name, "pdb": pdb.name, - "guid": pdb_guid[1], "age": int(pdb_age[1])} - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("metadata", type=Path, help="The game's *-crashpad-build.json") - parser.add_argument("archive", type=Path, help="New ZIP file, never overwritten") - args = parser.parse_args() - folder = args.metadata.resolve().parent - manifest = json.loads(args.metadata.read_text()) - files = [] - for kind in ["GAME", "PDB", "BRIDGE", "BRIDGE_PDB", "HANDLER"]: - record = manifest[kind] - path = folder / record["file"] - if path.parent != folder: - raise ValueError("Manifest paths must name sibling files") - if hashlib.sha256(path.read_bytes()).hexdigest() != record["sha256"]: - raise ValueError(f"Build metadata hash does not match {path.name}") - files.append(path) - identities = [identity(files[0], files[1]), identity(files[2], files[3])] - files.append(args.metadata.resolve()) - files.append(folder / "rts_crashpad_reports.exe") - for pattern in ["msvcp140*.dll", "vcruntime140*.dll", "concrt140.dll"]: - files.extend(folder.glob(pattern)) - files.extend(path for path in (folder / "crashpad-notices").rglob("*") if path.is_file()) - args.archive.parent.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(args.archive, "x", compression=zipfile.ZIP_DEFLATED) as archive: - for path in files: - archive.write(path, path.relative_to(folder)) - archive.writestr("symbol-identities.json", json.dumps(identities, indent=2)) - print(json.dumps({"archive": str(args.archive), "identities": identities})) - - -if __name__ == "__main__": - main() diff --git a/Dependencies/Crashpad/tests/CMakeLists.txt b/Dependencies/Crashpad/tests/CMakeLists.txt deleted file mode 100644 index cf03eafaaaa..00000000000 --- a/Dependencies/Crashpad/tests/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -add_executable(rts_crashpad_test CrashpadTest.cpp) -target_compile_features(rts_crashpad_test PRIVATE cxx_std_20) -target_compile_options(rts_crashpad_test PRIVATE /Zi) -target_link_options(rts_crashpad_test PRIVATE /DEBUG) -target_link_libraries(rts_crashpad_test PRIVATE rts_crashpad) -set_target_properties(rts_crashpad_test PROPERTIES - MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") -add_custom_command(TARGET rts_crashpad_test POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "$" "${handler}" "$" - VERBATIM) - -add_executable(rts_crashpad_seed SeedDatabase.cpp) -target_compile_features(rts_crashpad_seed PRIVATE cxx_std_20) -target_link_libraries(rts_crashpad_seed PRIVATE crashpad::crashpad) -set_target_properties(rts_crashpad_seed PROPERTIES - MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") - -add_executable(rts_crashpad_wrong_handler WrongHandler.cpp) -set_target_properties(rts_crashpad_wrong_handler PROPERTIES MSVC_RUNTIME_LIBRARY MultiThreaded) -add_dependencies(rts_crashpad_test rts_crashpad_wrong_handler) diff --git a/Dependencies/Crashpad/tests/CrashpadTest.cpp b/Dependencies/Crashpad/tests/CrashpadTest.cpp deleted file mode 100644 index 2d9c0ca6b02..00000000000 --- a/Dependencies/Crashpad/tests/CrashpadTest.cpp +++ /dev/null @@ -1,180 +0,0 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later */ -// Dedicated fault injection only. This executable never starts a game session. -#include -#include -#include -#include -#include -#include "../CrashpadBridge.h" - -namespace -{ - bool forbidAllocation; -} - -void* operator new(size_t size) -{ - if (forbidAllocation) - { - TerminateProcess(GetCurrentProcess(), 90); - } - - void* memory = HeapAlloc(GetProcessHeap(), 0, size ? size : 1); - if (!memory) - { - throw std::bad_alloc(); - } - - return memory; -} - -void operator delete(void* memory) noexcept -{ - if (forbidAllocation) - { - TerminateProcess(GetCurrentProcess(), 91); - } - - HeapFree(GetProcessHeap(), 0, memory); -} - -void operator delete(void* memory, size_t) noexcept -{ - operator delete(memory); -} - -__declspec(noinline) void CrashpadTestFault(void* address) -{ - *static_cast(address) = 42; -} - -DWORD WINAPI FaultingWorker(void* address) -{ - std::printf("fault-thread=%lu\n", GetCurrentThreadId()); - std::fflush(stdout); - CrashpadTestFault(address); - return 0; -} - -__declspec(noinline) unsigned int OverflowStack(unsigned int depth) -{ - if (depth == 0xffffffffu) - { - return depth; - } - - volatile char page[4096]; - page[depth % sizeof(page)] = static_cast(depth); - return OverflowStack(depth + 1) + page[depth % sizeof(page)]; -} - -bool KillOwnHandler() -{ - HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - PROCESSENTRY32W process = {}; - process.dwSize = sizeof(process); - bool killed = false; - if (Process32FirstW(snapshot, &process)) - { - do - { - if (process.th32ParentProcessID == GetCurrentProcessId() - && wcscmp(process.szExeFile, L"crashpad_handler.exe") == 0) - { - HANDLE child = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, process.th32ProcessID); - if (child) - { - killed = TerminateProcess(child, 1) != FALSE; - WaitForSingleObject(child, 5000); - CloseHandle(child); - } - } - } while (Process32NextW(snapshot, &process)); - } - - CloseHandle(snapshot); - return killed; -} - -LONG WINAPI PreviousFilter(EXCEPTION_POINTERS*) -{ - return EXCEPTION_EXECUTE_HANDLER; -} - -int wmain(int argc, wchar_t** argv) -{ - SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); - SetUnhandledExceptionFilter(PreviousFilter); - if (argc != 3) - { - return 2; - } - - char userDirectory[32768 * 4]; - if (!WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, argv[1], -1, - userDirectory, sizeof(userDirectory), nullptr, nullptr)) - { - return 2; - } - - forbidAllocation = true; - const ULONGLONG start = GetTickCount64(); - if (!RtsCrashpadInitialize(userDirectory, "test", "1.0.0", "test-revision", 1)) - { - const bool restored = SetUnhandledExceptionFilter(PreviousFilter) == PreviousFilter; - std::printf("backend=unavailable filter-restored=%d\n", restored); - return restored ? 3 : 4; - } - - std::printf("backend=crashpad startup-ms=%llu fault-thread=%lu\n", - GetTickCount64() - start, GetCurrentThreadId()); - std::fflush(stdout); - void* address = VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_NOACCESS); - if (wcscmp(argv[2], L"main") == 0) - { - CrashpadTestFault(address); - } - else if (wcscmp(argv[2], L"worker") == 0) - { - HANDLE worker = CreateThread(nullptr, 0, FaultingWorker, address, 0, nullptr); - WaitForSingleObject(worker, INFINITE); - } - else if (wcscmp(argv[2], L"stack") == 0) - { - OverflowStack(0); - } - else if (wcscmp(argv[2], L"heap") == 0) - { - RaiseException(0xC0000374, EXCEPTION_NONCONTINUABLE, 0, nullptr); - } - else if (wcscmp(argv[2], L"explicit") == 0 || wcscmp(argv[2], L"duplicate") == 0) - { - RtsCrashpadCaptureFatal(); - RtsCrashpadCaptureFatal(); - if (wcscmp(argv[2], L"duplicate") == 0) - { - CrashpadTestFault(address); - } - } - else if (wcscmp(argv[2], L"dead-handler") == 0) - { - if (!KillOwnHandler()) - { - return 5; - } - - RtsCrashpadCaptureFatal(); - return 6; - } - else if (wcscmp(argv[2], L"wait") == 0) - { - Sleep(1000); - } - else if (wcscmp(argv[2], L"shutdown") != 0) - { - return 2; - } - - RtsCrashpadShutdown(); - return 0; -} diff --git a/Dependencies/Crashpad/tests/SeedDatabase.cpp b/Dependencies/Crashpad/tests/SeedDatabase.cpp deleted file mode 100644 index 5a911541350..00000000000 --- a/Dependencies/Crashpad/tests/SeedDatabase.cpp +++ /dev/null @@ -1,43 +0,0 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later */ -// Test fixture: three synthetic 200 MiB reports exercise real database pruning. -#include "client/crash_report_database.h" -#include "client/settings.h" - -int wmain(int argc, wchar_t** argv) -{ - if (argc != 2) - { - return 2; - } - - auto database = crashpad::CrashReportDatabase::Initialize(base::FilePath(argv[1])); - if (!database || !database->GetSettings()->SetUploadsEnabled(false)) - { - return 1; - } - - for (int index = 0; index < 3; ++index) - { - std::unique_ptr report; - if (database->PrepareNewCrashReport(&report) != crashpad::CrashReportDatabase::kNoError) - { - return 1; - } - - const char end = 0; - if (report->Writer()->Seek(200 * 1024 * 1024 - 1, SEEK_SET) < 0 - || !report->Writer()->Write(&end, 1)) - { - return 1; - } - - crashpad::UUID uuid; - if (database->FinishedWritingCrashReport(std::move(report), &uuid) - != crashpad::CrashReportDatabase::kNoError) - { - return 1; - } - } - - return 0; -} diff --git a/Dependencies/Crashpad/tests/WrongHandler.cpp b/Dependencies/Crashpad/tests/WrongHandler.cpp deleted file mode 100644 index cf9d72fb6cb..00000000000 --- a/Dependencies/Crashpad/tests/WrongHandler.cpp +++ /dev/null @@ -1,34 +0,0 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later */ -// A valid executable that deliberately does not service Crashpad's IPC pipe. -#include -#include - -int main() -{ - HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - PROCESSENTRY32W process = {}; - process.dwSize = sizeof(process); - HANDLE parent = nullptr; - if (Process32FirstW(snapshot, &process)) - { - do - { - if (process.th32ProcessID == GetCurrentProcessId()) - { - parent = OpenProcess(SYNCHRONIZE, FALSE, process.th32ParentProcessID); - break; - } - } while (Process32NextW(snapshot, &process)); - } - - CloseHandle(snapshot); - if (!parent) - { - return 1; - } - - // Exit with the dedicated test parent so this fixture leaves no process. - WaitForSingleObject(parent, 20000); - CloseHandle(parent); - return 0; -} diff --git a/Dependencies/Crashpad/tests/validate.py b/Dependencies/Crashpad/tests/validate.py deleted file mode 100644 index d2fe0a42c86..00000000000 --- a/Dependencies/Crashpad/tests/validate.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Run only the dedicated Crashpad test executable, using fresh data directories.""" -import argparse -import concurrent.futures -import hashlib -import json -from pathlib import Path -import re -import shutil -import struct -import subprocess -import tempfile -import time - - -def exception(path): - data = path.read_bytes() - assert data[:4] == b"MDMP", path - streams, directory = struct.unpack_from("/${game}-crashpad-build.json" - "-DGAME=$/$" - "-DPDB=$/$" - "-DBRIDGE=$" "-DBRIDGE_PDB=$" - "-DHANDLER=${RTS_CRASHPAD_HANDLER}" "-DCONFIG=$" - "-DCOMPILER=${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" - "-DSOURCE=${CMAKE_SOURCE_DIR}" "-DGIT=${GIT_EXECUTABLE}" - -P "${CMAKE_SOURCE_DIR}/cmake/crashpad-metadata.cmake") + # Refresh runtime files before linking, even when only the DLL changed. add_custom_target(${game}_crashpad_runtime COMMAND ${CMAKE_COMMAND} -E make_directory "$" COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "$" - "${RTS_CRASHPAD_HANDLER}" "$" + "${RTS_CRASHPAD_HANDLER}" ${RTS_CRASHPAD_RUNTIME_FILES} "$" COMMAND ${CMAKE_COMMAND} -E copy_directory "${RTS_CRASHPAD_NOTICES}" "$/crashpad-notices" - COMMAND ${metadata_command} - DEPENDS rts_crashpad rts_crashpad_reports + DEPENDS rts_crashpad VERBATIM) add_dependencies(${game} ${game}_crashpad_runtime) - add_custom_command(TARGET ${game} POST_BUILD COMMAND ${metadata_command} VERBATIM) endfunction() function(rts_install_crashpad game destination) if(RTS_BUILD_OPTION_CRASHPAD) install(FILES "$" "$" - "${RTS_CRASHPAD_HANDLER}" "$" - "$/${game}-crashpad-build.json" + "${RTS_CRASHPAD_HANDLER}" DESTINATION "${destination}") install(DIRECTORY "${RTS_CRASHPAD_NOTICES}/" DESTINATION "${destination}/crashpad-notices") install(FILES ${RTS_CRASHPAD_RUNTIME_FILES} DESTINATION "${destination}" diff --git a/docs/crashpad.md b/docs/crashpad.md index 55c31bdf5de..85a1c1c5868 100644 --- a/docs/crashpad.md +++ b/docs/crashpad.md @@ -1,22 +1,21 @@ # Optional local Crashpad reports -`RTS_BUILD_OPTION_CRASHPAD` defaults to `OFF`. It enables a local-only prototype -for the modern MSVC and ClangCL Win32 game builds. WorldBuilder and other tools -keep their existing reporting behavior. VC6, MinGW and non-Windows builds do not -acquire or link Crashpad when the option is off. Explicit unsupported requests, -including `RTS_CRASHDUMP_ENABLE=OFF` and sanitizer/fuzzing builds, fail configure. +`RTS_BUILD_OPTION_CRASHPAD` defaults to `OFF`. It enables local crash reporting +through a separate handler process for modern MSVC and ClangCL Win32 game builds. +WorldBuilder and other tools keep their existing reporting behavior. Disabled +builds do not acquire or link Crashpad. Unsupported configurations, including +VC6, non-Windows, x64, sanitizer/fuzzing builds and `RTS_CRASHDUMP_ENABLE=OFF`, +reject an explicit request to enable it. -This prototype has been exercised on Windows 11. Its deployment includes the -x86 Visual C++ redistributable DLLs and expects the Windows 10 or newer Universal -CRT. It does not change the OS requirements of default-off builds. Older Windows -versions have not been qualified for the optional backend. Loading the adapter -can fail on an older OS, in which case the game tries MiniDumper. +The optional backend expects Windows 10 or newer for the Universal CRT. It does +not change the OS requirements of default builds. A failure to load or start +Crashpad falls back to MiniDumper; the game can start if neither is available. -## Build the pinned package +## Building -Use the separate dependency manifest so ordinary game builds do not install -Crashpad or change the root vcpkg dependency set. Run these commands from the -repository root in Git Bash, with `VCPKG_ROOT` pointing to vcpkg: +Use the separate pinned dependency manifest so ordinary builds keep their +existing dependencies. From the repository root in Git Bash, with `VCPKG_ROOT` +pointing to vcpkg: ```bash "$VCPKG_ROOT/vcpkg.exe" install \ @@ -31,152 +30,54 @@ cmake -S . -B build/crashpad -G "Visual Studio 18 2026" -A Win32 -T ClangCL \ cmake --build build/crashpad --config Release --target g_generals z_generals --parallel 4 ``` -Omit `-T ClangCL` to build the games and adapter with MSVC. The package itself -uses vcpkg's MSVC/GN route. The baseline Python helper requires a native triplet, -so both target and host are explicitly x86 here. A copy of this installed prefix -can be supplied to a build that does not otherwise use vcpkg. Configure checks -the package's SPDX provenance, architecture and port version. - -| Component | Pin / configuration | -| --- | --- | -| vcpkg baseline | `b02e341c927f16d991edbd915d8ea43eac52096c` | -| Crashpad port | `2024-04-11#7` | -| Crashpad source | `7e0af1d4d45b526f01677e74a56f4a951b70517d` | -| mini_chromium | `dce72d97d1c2e9beb5e206c6a05a702269794ca3` | -| Client | x86 static libraries, dynamic CRT, Debug `/MDd` and Release `/MD` | -| Handler | Release x86, from the same package and source revision | -| zlib and build helpers | Pinned transitively by the manifest baseline; SPDX and ABI records accompany deployment | - -The baseline port exports `crashpad::crashpad` and packages the handler at -`tools/crashpad/crashpad_handler.exe`. Do not substitute a separately downloaded -handler. Debug builds require the Visual Studio debug CRT and are not release -packages. - -## Startup and ownership - -The game loads `rts_crashpad.dll` by absolute path beside its executable. That DLL -contains the statically linked client and uses its own CRT allocation functions. -It does not inherit the game PCH, forced includes, memory-pool macros or global -`new`/`delete` overrides. Only C functions and borrowed string arguments cross -the boundary. Tools have no import-library dependency on this DLL. - -Zero Hour starts Crashpad at the previous MiniDumper initialization point. -Generals initially uses MiniDumper there because its user-data path is still -empty. After loading the GameData INIs and parsing engine startup options, -Generals replaces that temporary backend using the resolved user-data path. -This differs from the handoff's assumption that both games know the path in -WinMain. Early crashes remain outside Crashpad coverage. - -Initialization converts the game's UTF-8 user path to UTF-16, creates the -database, disables uploads, and starts the handler asynchronously with a -five-second readiness wait. Success requires the upstream IPC ping to complete. -This bounds startup when a valid executable fails to service the handler pipe. -There is no upload URL, metrics directory -or attachment list. Build annotations contain the game, application version, -full Git revision, dirty state, x86 architecture, compiler, CMake configuration -and backend. - -On failure the adapter restores the previous exception filter and abort handler, -removes its heap-corruption handler and stops its watchdog. The game logs one -failure and tries MiniDumper. `CrashReporting::backendName()` reports `crashpad`, -`minidumper` or `unavailable`; startup continues if reporting is unavailable. -Startup selection does not monitor a handler that dies later, and Windows does -not restart the handler in this revision. - -Crashpad owns unhandled exceptions while active. The legacy WinMain filter does -not run first, so Crashpad reports omit the legacy `DumpExceptionInfo` text. -The debug library's `PreStaticInit()` registration runs before WinMain; the -WorldBuilder registration belongs to a separate executable. Neither replaces -Crashpad later in game startup. Normal shutdown restores the original filter. -The DLL stays loaded because upstream retains process-lifetime IPC pointers; -the handler exits when the game process exits. - -Both explicit fatal-error variants capture before best-effort diagnostics and -keep their existing exit paths. The pinned `DumpWithoutCrash()` waits without a -timeout, so the adapter creates a watchdog thread during healthy startup. If an -explicit fatal capture does not return within 15 seconds, that thread terminates -the game with exit code 1. In that case later text diagnostics may be absent. -This bound depends on the game process remaining schedulable; it cannot promise -recovery from arbitrary corruption or a handler that leaves the process -suspended. The diagnostic capture is one-shot, and a subsequent reporting fault -terminates without generating a second report. Headless mode adds no dialogs. -Upstream unhandled-exception handling has its separate 60-second termination -fallback. - -## Reports, export and retention - -The database is `/CrashDumps/Crashpad/`. Crashpad produces one minidump, -not the legacy minimal/full-memory pair. Full-memory parity is unproven. Build -with the option off when an investigation needs the old full dump. - -At startup, Crashpad's database API prunes reports older than 30 days and older -reports beyond a 500 MiB report-size budget. Active/locked reports remain under -Crashpad's control. The handler's different default pruning policy is disabled. -The budget excludes database metadata and is not a hard disk quota; concurrent -runs and newly written reports can exceed it until the next startup or explicit -prune. The legacy filename cleanup does not enter this subdirectory. - -Use the installed helper instead of manipulating database metadata: - -```bash -rts_crashpad_reports.exe "C:/path/to/user-data/CrashDumps/Crashpad" list -rts_crashpad_reports.exe "C:/path/to/user-data/CrashDumps/Crashpad" export "C:/path/to/new-export" -rts_crashpad_reports.exe "C:/path/to/user-data/CrashDumps/Crashpad" prune -``` - -Export enumerates finalized reports through the database API and copies them -without marking them uploaded. It refuses to overwrite an existing destination -file and reports concurrent-pruning failures. Dumps contain process memory even -without attachments; manual sharing is a separate user action. - -## Deployment and symbols - -Each enabled game output and CMake install includes the matching handler, -adapter DLL/PDB, report helper, redistributable VC runtime DLLs, dependency -notices and `*-crashpad-build.json`. Set `RTS_INSTALL_PREFIX_GENERALS` and -`RTS_INSTALL_PREFIX_ZEROHOUR` to explicit staging paths when testing installation. -No build-tree path is used to locate the runtime handler or database. - -Keep the exact executable, game PDB, adapter DLL/PDB, handler and metadata for -every distributed build. With LLVM tools on `PATH`, verify CodeView GUID/age -against each PDB and create an archive: - -```bash -python Dependencies/Crashpad/archive_symbols.py \ - build/crashpad/Generals/Release/g_generals-crashpad-build.json \ - build/symbols/generals-crashpad.zip -``` - -Use `GeneralsMD/Release/z_generals-crashpad-build.json` for Zero Hour. The helper -also verifies the build's recorded SHA-256 hashes and refuses to overwrite an -archive. Keep the source commit with the archive; preserve the source snapshot -as well for a build marked dirty. Open an exported dump in WinDbg/CDB or Visual -Studio with the archived PDBs and verify a known function and source line. - -CI collection includes the notices and metadata and retains artifacts for 90 -days. That is temporary storage. Attach the verified symbol archive to the -corresponding release, or copy it to the project's durable symbol archive, -before distributing a Crashpad-enabled release. No reporting server or release -upload is configured by this change. - -## Validation - -Enable `RTS_BUILD_CRASHPAD_TESTS=ON` in the Crashpad build tree, then run: - -```bash -cmake --build build/crashpad --config Release \ - --target rts_crashpad_test rts_crashpad_seed rts_crashpad_reports --parallel 4 -python Dependencies/Crashpad/tests/validate.py \ - build/crashpad/Dependencies/Crashpad/tests/Release/rts_crashpad_test.exe \ - build/crashpad-validation -``` - -The script runs only the dedicated test executable in fresh directories under -`build/`. It checks original exception codes and thread IDs, one report per -event, fatal timeout after handler death, startup failure/filter restoration, -paths with spaces and non-ASCII characters, concurrent instances, disabled -uploads, export identity, and pruning of a 600 MiB synthetic database. The -executable forbids use of its global allocator during adapter operations. - -Keep the option off by default until the intended deployment environments and -full-memory requirements have been qualified. +Omit `-T ClangCL` to build the games and adapter with MSVC. The dependency uses +vcpkg's MSVC/GN build. Both triplets must be native x86 for the pinned port's +Python helper. Configure checks the package's SPDX record against the pinned +port version, architecture and source revisions. Use the handler from that same +package. Debug builds need the Visual Studio debug CRT. + +## Capture and fallback + +The game loads `rts_crashpad.dll` by absolute path beside its executable. The DLL +isolates Crashpad's C++ requirements and CRT allocations from the game's memory +allocator. Zero Hour initializes it in WinMain. Generals uses MiniDumper until +its user-data path is available after GameData loading and startup options. + +Startup waits up to five seconds for handler readiness. Failure restores the +previous exception handlers and selects MiniDumper. A handler that dies later +is not restarted and does not trigger automatic fallback. The DLL stays loaded +because upstream retains process-lifetime IPC pointers; the handler exits when +the game process exits. + +Crashpad handles unhandled exceptions and both explicit fatal-error paths. +Explicit fatal errors capture once, before legacy diagnostics. A watchdog +terminates the game if that capture stalls for 15 seconds, provided the process +remains schedulable. Headless mode adds no dialogs. Crashpad reports omit the +legacy unhandled-exception text and do not provide the legacy full-memory dump. + +## Reports and deployment + +Reports stay in `/CrashDumps/Crashpad/`. Uploads are disabled, with no +upload URL or attachments. Each report is a binary minidump with build annotations +including the game version, Git revision, dirty state, compiler and configuration. +Crashpad does not generate a readable diagnosis of the crash. + +At startup, the database API prunes reports older than 30 days and older reports +beyond a 500 MiB report-size budget. Active reports remain under Crashpad's +control. This is not a hard disk quota; new reports can exceed it until the next +startup. The handler's default pruning policy is disabled. + +After the game exits, copy the desired `.dmp` from the database's `reports` +directory to share it. Keep the original database files in place. Dumps contain +process memory; sharing is a separate user action. + +Enabled game outputs and CMake installs include the matching handler, adapter +DLL/PDB, redistributable VC runtime DLLs and dependency notices. Set +`RTS_INSTALL_PREFIX_GENERALS` and `RTS_INSTALL_PREFIX_ZEROHOUR` to explicit staging +paths when testing installation. Runtime paths do not depend on the build tree. + +Keep the exact game executable/PDB, adapter DLL/PDB and handler for each +distributed build, together with its source revision. Preserve the source +snapshot for dirty builds. Open the dump as a crash dump in WinDbg or Visual +Studio and load those matching PDBs to inspect the exception and call stack. +CI artifact retention remains 30 days; preserve release symbols separately.