Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/reusable-build-toolchain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -176,12 +178,24 @@ 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
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
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
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ endif()

add_subdirectory(resources)

include(cmake/crashpad.cmake)

add_subdirectory(Core)

# Add main build targets
Expand Down
2 changes: 2 additions & 0 deletions Core/GameEngine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions Core/GameEngine/Include/Common/CrashReporting.h
Original file line number Diff line number Diff line change
@@ -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();
}
161 changes: 161 additions & 0 deletions Core/GameEngine/Source/Common/System/CrashReporting.cpp
Original file line number Diff line number Diff line change
@@ -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<RtsCrashpadInitializeFunction>(
GetProcAddress(module, "RtsCrashpadInitialize"));
capture = reinterpret_cast<RtsCrashpadCaptureFatalFunction>(GetProcAddress(module, "RtsCrashpadCaptureFatal"));
stop = reinterpret_cast<RtsCrashpadShutdownFunction>(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
}
36 changes: 17 additions & 19 deletions Core/GameEngine/Source/Common/System/Debug.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -732,21 +730,20 @@ 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)
{
TriggerMiniDump();

// We are shutting down, and TheGlobalData has been freed. jba. [4/15/2003]
if (TheGlobalData == nullptr)
{
return;
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

/// do additional reporting on the crash, if possible

if (!DX8Wrapper_IsWindowed) {
Expand All @@ -755,15 +752,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));
Expand Down Expand Up @@ -833,6 +824,13 @@ 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)
{
TriggerMiniDump();
return;
}

if (!TheGameText) {
ReleaseCrash(m.str());
// This won't ever return
Expand Down
9 changes: 8 additions & 1 deletion Core/GameEngine/Source/Common/System/MiniDumper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
74 changes: 74 additions & 0 deletions Dependencies/Crashpad/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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$<$<CONFIG:Debug>: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=\"$<CONFIG>\""
"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$<$<CONFIG:Debug>:Debug>DLL")

if(RTS_BUILD_CRASHPAD_TESTS)
add_subdirectory(tests)
endif()
Loading
Loading