diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index faebd73f6cc..95748f544ad 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/MiniDumper.h # Include/Common/MiniLog.h Include/Common/MiscAudio.h # Include/Common/MissionStats.h @@ -660,6 +661,7 @@ set(GAMEENGINE_SRC # Source/Common/System/List.cpp Source/Common/System/LocalFile.cpp Source/Common/System/LocalFileSystem.cpp + Source/Common/System/MiniDumper.cpp Source/Common/System/ObjectStatusTypes.cpp # Source/Common/System/QuotedPrintable.cpp Source/Common/System/Radar.cpp diff --git a/Core/GameEngine/Include/Common/MiniDumper.h b/Core/GameEngine/Include/Common/MiniDumper.h new file mode 100644 index 00000000000..44c03e96ef1 --- /dev/null +++ b/Core/GameEngine/Include/Common/MiniDumper.h @@ -0,0 +1,97 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 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 + +#ifdef RTS_ENABLE_CRASHDUMP +#include "DbgHelpLoader.h" + +enum DumpType CPP_11(: Char) +{ + // Smallest dump type with call stacks and some supporting variables + DumpType_Minimal = 'M', + // Largest dump size including complete memory contents of the process + DumpType_Full = 'F', +}; + +class MiniDumper +{ + enum MiniDumperExitCode CPP_11(: Int) + { + MiniDumperExitCode_Success = 0x0, + MiniDumperExitCode_FailureWait = 0x37DA1040, + MiniDumperExitCode_FailureParam = 0x4EA527BB, + MiniDumperExitCode_ForcedTerminate = 0x158B1154, + }; + +public: + MiniDumper(); + Bool IsInitialized() const; + void TriggerMiniDump(DumpType dumpType); + void TriggerMiniDumpForException(_EXCEPTION_POINTERS* e_info, DumpType dumpType); + static void initMiniDumper(const AsciiString& userDirPath); + static void shutdownMiniDumper(); + static LONG WINAPI DumpingExceptionFilter(_EXCEPTION_POINTERS* e_info); + +private: + void Initialize(const AsciiString& userDirPath); + void ShutDown(); + void CreateMiniDump(DumpType dumpType); + void CleanupResources(); + Bool IsDumpThreadStillRunning() const; + void ShutdownDumpThread(); + + // Thread procs + static DWORD WINAPI MiniDumpThreadProc(LPVOID lpParam); + DWORD ThreadProcInternal(); + + // Dump file directory bookkeeping + Bool InitializeDumpDirectory(const AsciiString& userDirPath); + static void KeepNewestFiles(const std::string& directory, const DumpType dumpType, const Int keepCount); + + // Struct to hold file information + struct FileInfo + { + std::string name; + FILETIME lastWriteTime; + }; + + static bool CompareByLastWriteTime(const FileInfo& a, const FileInfo& b); + +private: + Bool m_miniDumpInitialized; + Bool m_loadedDbgHelp; + DumpType m_requestedDumpType; + + // Path buffers + Char m_dumpDir[MAX_PATH]; + Char m_dumpFile[MAX_PATH]; + WideChar m_executablePath[MAX_PATH]; + + // Event handles + HANDLE m_dumpRequested; + HANDLE m_dumpComplete; + HANDLE m_quitting; + + // Thread handles + HANDLE m_dumpThread; + DWORD m_dumpThreadId; +}; + +extern MiniDumper* TheMiniDumper; +#endif diff --git a/Core/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp index 385dd856161..33ddbff4606 100644 --- a/Core/GameEngine/Source/Common/System/Debug.cpp +++ b/Core/GameEngine/Source/Common/System/Debug.cpp @@ -70,6 +70,9 @@ #if defined(DEBUG_STACKTRACE) || defined(IG_DEBUG_STACKTRACE) #include "Common/StackDump.h" #endif +#ifdef RTS_ENABLE_CRASHDUMP +#include "Common/MiniDumper.h" +#endif // Horrible reference, but we really, really need to know if we are windowed. extern bool DX8Wrapper_IsWindowed; @@ -727,6 +730,22 @@ 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 +} + + void ReleaseCrash(const char *reason) { /// do additional reporting on the crash, if possible @@ -737,6 +756,8 @@ void ReleaseCrash(const char *reason) } } + TriggerMiniDump(); + char prevbuf[ _MAX_PATH ]; char curbuf[ _MAX_PATH ]; @@ -813,6 +834,8 @@ void ReleaseCrashLocalized(const AsciiString& p, const AsciiString& m) return; } + TriggerMiniDump(); + UnicodeString prompt = TheGameText->fetch(p); UnicodeString mesg = TheGameText->fetch(m); diff --git a/Core/GameEngine/Source/Common/System/MiniDumper.cpp b/Core/GameEngine/Source/Common/System/MiniDumper.cpp new file mode 100644 index 00000000000..21ab80e578a --- /dev/null +++ b/Core/GameEngine/Source/Common/System/MiniDumper.cpp @@ -0,0 +1,461 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 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 . +*/ + +#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine + +#ifdef RTS_ENABLE_CRASHDUMP +#include "Common/MiniDumper.h" +#include +#include "gitinfo.h" + +// Globals for storing the pointer to the exception +_EXCEPTION_POINTERS* g_dumpException = NULL; +DWORD g_dumpExceptionThreadId = 0; + +MiniDumper* TheMiniDumper = NULL; + +// Globals containing state about the current exception that's used for context in the mini dump. +// These are populated by MiniDumper::DumpingExceptionFilter to store a copy of the exception in case it goes out of scope +_EXCEPTION_POINTERS g_exceptionPointers = { 0 }; +EXCEPTION_RECORD g_exceptionRecord = { 0 }; +CONTEXT g_exceptionContext = { 0 }; + +constexpr const char* DumpFileNamePrefix = "Crash"; + +void MiniDumper::initMiniDumper(const AsciiString& userDirPath) +{ + DEBUG_ASSERTCRASH(TheMiniDumper == NULL, ("MiniDumper::initMiniDumper called on already created instance")); + + // 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; + TheMiniDumper->Initialize(userDirPath); +} + +void MiniDumper::shutdownMiniDumper() +{ + if (TheMiniDumper) + { + TheMiniDumper->ShutDown(); + TheMiniDumper->~MiniDumper(); + ::HeapFree(::GetProcessHeap(), NULL, TheMiniDumper); + TheMiniDumper = NULL; + } +} + +MiniDumper::MiniDumper() +{ + m_miniDumpInitialized = false; + m_loadedDbgHelp = false; + m_requestedDumpType = DumpType_Minimal; + m_dumpRequested = NULL; + m_dumpComplete = NULL; + m_quitting = NULL; + m_dumpThread = NULL; + m_dumpThreadId = 0; + m_dumpDir[0] = 0; + m_dumpFile[0] = 0; + m_executablePath[0] = 0; +}; + +LONG WINAPI MiniDumper::DumpingExceptionFilter(_EXCEPTION_POINTERS* e_info) +{ + // Store the exception info in the global variables for later use by the dumping thread + g_exceptionRecord = *(e_info->ExceptionRecord); + g_exceptionContext = *(e_info->ContextRecord); + g_exceptionPointers.ContextRecord = &g_exceptionContext; + g_exceptionPointers.ExceptionRecord = &g_exceptionRecord; + g_dumpException = &g_exceptionPointers; + + return EXCEPTION_EXECUTE_HANDLER; +} + +void MiniDumper::TriggerMiniDump(DumpType dumpType) +{ + if (!m_miniDumpInitialized) + { + DEBUG_LOG(("MiniDumper::TriggerMiniDump: Attempted to use an uninitialized instance.")); + return; + } + + __try + { + // Use DebugBreak to raise an exception that can be caught in the __except block + ::DebugBreak(); + } + __except (DumpingExceptionFilter(GetExceptionInformation())) + { + TriggerMiniDumpForException(g_dumpException, dumpType); + } +} + +void MiniDumper::TriggerMiniDumpForException(_EXCEPTION_POINTERS* e_info, DumpType dumpType) +{ + if (!m_miniDumpInitialized) + { + DEBUG_LOG(("MiniDumper::TriggerMiniDumpForException: Attempted to use an uninitialized instance.")); + return; + } + + g_dumpException = e_info; + g_dumpExceptionThreadId = ::GetCurrentThreadId(); + m_requestedDumpType = dumpType; + + DEBUG_ASSERTCRASH(IsDumpThreadStillRunning(), ("MiniDumper::TriggerMiniDumpForException: Dumping thread has exited.")); + ::SetEvent(m_dumpRequested); + DWORD wait = ::WaitForSingleObject(m_dumpComplete, INFINITE); + if (wait != WAIT_OBJECT_0) + { + if (wait == WAIT_FAILED) + { + DEBUG_LOG(("MiniDumper::TriggerMiniDumpForException: Waiting for minidump triggering failed: status=%u, error=%u", wait, ::GetLastError())); + } + else + { + DEBUG_LOG(("MiniDumper::TriggerMiniDumpForException: Waiting for minidump triggering failed: status=%u", wait)); + } + } + + ::ResetEvent(m_dumpComplete); +} + +void MiniDumper::Initialize(const AsciiString& userDirPath) +{ + m_loadedDbgHelp = DbgHelpLoader::load(); + + // We want to only use the dbghelp.dll from the OS installation, as the one bundled with the game does not support MiniDump functionality + if (!(m_loadedDbgHelp && DbgHelpLoader::isLoadedFromSystem())) + { + DEBUG_LOG(("MiniDumper::Initialize: Unable to load system-provided dbghelp.dll, minidump functionality disabled.")); + return; + } + + DWORD executableSize = ::GetModuleFileNameW(NULL, m_executablePath, ARRAY_SIZE(m_executablePath)); + if (executableSize == 0 || executableSize == ARRAY_SIZE(m_executablePath)) + { + DEBUG_LOG(("MiniDumper::Initialize: Could not get executable file name. Returned value=%u", executableSize)); + return; + } + + // Create & store dump folder + if (!InitializeDumpDirectory(userDirPath)) + { + return; + } + + m_dumpRequested = CreateEvent(NULL, TRUE, FALSE, NULL); + m_dumpComplete = CreateEvent(NULL, TRUE, FALSE, NULL); + m_quitting = CreateEvent(NULL, TRUE, FALSE, NULL); + + if (m_dumpRequested == NULL || m_dumpComplete == NULL || m_quitting == NULL) + { + // Something went wrong with the creation of the events.. + DEBUG_LOG(("MiniDumper::Initialize: Unable to create events: error=%u", ::GetLastError())); + return; + } + + m_dumpThread = ::CreateThread(NULL, 0, MiniDumpThreadProc, this, CREATE_SUSPENDED, &m_dumpThreadId); + if (!m_dumpThread) + { + DEBUG_LOG(("MiniDumper::Initialize: Unable to create thread: error=%u", ::GetLastError())); + return; + } + + if (::ResumeThread(m_dumpThread) != 1) + { + DEBUG_LOG(("MiniDumper::Initialize: Unable to resume thread: error=%u", ::GetLastError())); + return; + } + + DEBUG_LOG(("MiniDumper::Initialize: Configured to store crash dumps in '%s'", m_dumpDir)); + m_miniDumpInitialized = true; +} + +Bool MiniDumper::IsInitialized() const +{ + return m_miniDumpInitialized; +} + +Bool MiniDumper::IsDumpThreadStillRunning() const +{ + DWORD exitCode; + if (m_dumpThread != NULL && ::GetExitCodeThread(m_dumpThread, &exitCode) && exitCode == STILL_ACTIVE) + { + return true; + } + + return false; +} + +Bool MiniDumper::InitializeDumpDirectory(const AsciiString& userDirPath) +{ + constexpr const Int MaxFullFileCount = 2; + constexpr const Int MaxMiniFileCount = 10; + + strlcpy(m_dumpDir, userDirPath.str(), ARRAY_SIZE(m_dumpDir)); + strlcat(m_dumpDir, "CrashDumps\\", ARRAY_SIZE(m_dumpDir)); + if (::_access(m_dumpDir, 0) != 0) + { + if (!::CreateDirectory(m_dumpDir, NULL)) + { + DEBUG_LOG(("MiniDumper::Initialize: Unable to create path for crash dumps at '%s': error=%u", m_dumpDir, ::GetLastError())); + return false; + } + } + + // Clean up old files (we keep a maximum of 10 small, 2 full) + KeepNewestFiles(m_dumpDir, DumpType_Full, MaxFullFileCount); + KeepNewestFiles(m_dumpDir, DumpType_Minimal, MaxMiniFileCount); + + return true; +} + +void MiniDumper::ShutdownDumpThread() +{ + if (IsDumpThreadStillRunning()) + { + DEBUG_ASSERTCRASH(m_quitting != NULL, ("MiniDumper::ShutdownDumpThread: Dump thread still running despite m_quitting being NULL")); + ::SetEvent(m_quitting); + + DWORD waitRet = ::WaitForSingleObject(m_dumpThread, 3000); + switch (waitRet) + { + case WAIT_OBJECT_0: + // Wait for thread exit was successful + break; + case WAIT_TIMEOUT: + DEBUG_LOG(("MiniDumper::ShutdownDumpThread: Waiting for dumping thread to exit timed out, killing thread", waitRet)); + ::TerminateThread(m_dumpThread, MiniDumperExitCode_ForcedTerminate); + break; + case WAIT_FAILED: + DEBUG_LOG(("MiniDumper::ShutdownDumpThread: Waiting for minidump triggering failed: status=%u, error=%u", waitRet, ::GetLastError())); + break; + default: + DEBUG_LOG(("MiniDumper::ShutdownDumpThread: Waiting for minidump triggering failed: status=%u", waitRet)); + break; + } + } +} + +void MiniDumper::ShutDown() +{ + ShutdownDumpThread(); + + if (m_dumpThread != NULL) + { + DEBUG_ASSERTCRASH(!IsDumpThreadStillRunning(), ("MiniDumper::ShutDown: ShutdownDumpThread() was unable to stop Dump thread")); + ::CloseHandle(m_dumpThread); + m_dumpThread = NULL; + } + + if (m_quitting != NULL) + { + ::CloseHandle(m_quitting); + m_quitting = NULL; + } + + if (m_dumpComplete != NULL) + { + ::CloseHandle(m_dumpComplete); + m_dumpComplete = NULL; + } + + if (m_dumpRequested != NULL) + { + ::CloseHandle(m_dumpRequested); + m_dumpRequested = NULL; + } + + if (m_loadedDbgHelp) + { + DbgHelpLoader::unload(); + m_loadedDbgHelp = false; + } + + m_miniDumpInitialized = false; +} + +DWORD MiniDumper::ThreadProcInternal() +{ + while (true) + { + HANDLE waitEvents[2] = { m_dumpRequested, m_quitting }; + DWORD event = ::WaitForMultipleObjects(ARRAY_SIZE(waitEvents), waitEvents, FALSE, INFINITE); + switch (event) + { + case WAIT_OBJECT_0 + 0: + // A dump is requested (m_dumpRequested) + ::ResetEvent(m_dumpComplete); + CreateMiniDump(m_requestedDumpType); + ::ResetEvent(m_dumpRequested); + ::SetEvent(m_dumpComplete); + break; + case WAIT_OBJECT_0 + 1: + // Quit (m_quitting) + return MiniDumperExitCode_Success; + case WAIT_FAILED: + DEBUG_LOG(("MiniDumper::ThreadProcInternal: Waiting for events failed: status=%u, error=%u", event, ::GetLastError())); + return MiniDumperExitCode_FailureWait; + default: + DEBUG_LOG(("MiniDumper::ThreadProcInternal: Waiting for events failed: status=%u", event)); + return MiniDumperExitCode_FailureWait; + } + } +} + +DWORD WINAPI MiniDumper::MiniDumpThreadProc(LPVOID lpParam) +{ + if (lpParam == NULL) + { + DEBUG_LOG(("MiniDumper::MiniDumpThreadProc: The provided parameter was NULL, exiting thread.")); + return MiniDumperExitCode_FailureParam; + } + + MiniDumper* dumper = static_cast(lpParam); + return dumper->ThreadProcInternal(); +} + + +void MiniDumper::CreateMiniDump(DumpType dumpType) +{ + // Create a unique dump file name, using the path from m_dumpDir, in m_dumpFile + SYSTEMTIME sysTime; + ::GetLocalTime(&sysTime); +#if RTS_GENERALS + const Char product = 'G'; +#elif RTS_ZEROHOUR + const Char product = 'Z'; +#endif + Char dumpTypeSpecifier = static_cast(dumpType); + DWORD currentProcessId = ::GetCurrentProcessId(); + + // m_dumpDir is stored with trailing backslash in Initialize + snprintf(m_dumpFile, ARRAY_SIZE(m_dumpFile), "%s%s%c%c-%04d%02d%02d-%02d%02d%02d-%s-pid%ld.dmp", + m_dumpDir, DumpFileNamePrefix, dumpTypeSpecifier, product, sysTime.wYear, sysTime.wMonth, + sysTime.wDay, sysTime.wHour, sysTime.wMinute, sysTime.wSecond, + GitShortSHA1, currentProcessId); + + HANDLE dumpFile = ::CreateFile(m_dumpFile, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (dumpFile == NULL || dumpFile == INVALID_HANDLE_VALUE) + { + DEBUG_LOG(("MiniDumper::CreateMiniDump: Unable to create dump file '%s': error=%u", m_dumpFile, ::GetLastError())); + return; + } + + PMINIDUMP_EXCEPTION_INFORMATION exceptionInfoPtr = NULL; + MINIDUMP_EXCEPTION_INFORMATION exceptionInfo = { 0 }; + if (g_dumpException != NULL) + { + exceptionInfo.ExceptionPointers = g_dumpException; + exceptionInfo.ThreadId = g_dumpExceptionThreadId; + exceptionInfo.ClientPointers = FALSE; + exceptionInfoPtr = &exceptionInfo; + } + + int dumpTypeFlags = MiniDumpNormal; + switch (dumpType) + { + case DumpType_Full: + dumpTypeFlags |= MiniDumpWithFullMemory | MiniDumpWithDataSegs | MiniDumpWithHandleData | + MiniDumpWithThreadInfo | MiniDumpWithFullMemoryInfo | MiniDumpWithPrivateReadWriteMemory; + FALLTHROUGH; + case DumpType_Minimal: + dumpTypeFlags |= MiniDumpWithIndirectlyReferencedMemory | MiniDumpScanMemory; + break; + } + + MINIDUMP_TYPE miniDumpType = static_cast(dumpTypeFlags); + BOOL success = DbgHelpLoader::miniDumpWriteDump( + ::GetCurrentProcess(), + currentProcessId, + dumpFile, + miniDumpType, + exceptionInfoPtr, + NULL, + NULL); + + if (!success) + { + DEBUG_LOG(("MiniDumper::CreateMiniDump: Unable to write minidump file '%s': error=%u", m_dumpFile, ::GetLastError())); + } + else + { + DEBUG_LOG(("MiniDumper::CreateMiniDump: Successfully wrote minidump file to '%s'", m_dumpFile)); + } + + ::CloseHandle(dumpFile); +} + +// Comparator for sorting files by last modified time (newest first) +bool MiniDumper::CompareByLastWriteTime(const FileInfo& a, const FileInfo& b) +{ + return ::CompareFileTime(&a.lastWriteTime, &b.lastWriteTime) > 0; +} + +void MiniDumper::KeepNewestFiles(const std::string& directory, const DumpType dumpType, const Int keepCount) +{ + // directory already contains trailing backslash + std::string searchPath = directory + DumpFileNamePrefix + static_cast(dumpType) + "*"; + WIN32_FIND_DATA findData; + HANDLE hFind = ::FindFirstFile(searchPath.c_str(), &findData); + + if (hFind == INVALID_HANDLE_VALUE) + { + if (::GetLastError() != ERROR_FILE_NOT_FOUND) + { + DEBUG_LOG(("MiniDumper::KeepNewestFiles: Unable to find files in directory '%s': error=%u", searchPath.c_str(), ::GetLastError())); + } + + return; + } + + std::vector files; + do + { + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + continue; + } + + // Store file info + FileInfo fileInfo; + fileInfo.name = directory + findData.cFileName; + fileInfo.lastWriteTime = findData.ftLastWriteTime; + files.push_back(fileInfo); + + } while (::FindNextFile(hFind, &findData)); + + ::FindClose(hFind); + + // Sort files by last modified time in descending order + std::sort(files.begin(), files.end(), CompareByLastWriteTime); + + // Delete files beyond the newest keepCount + for (size_t i = keepCount; i < files.size(); ++i) + { + if (::DeleteFile(files[i].name.c_str())) + { + DEBUG_LOG(("MiniDumper::KeepNewestFiles: Deleted old dump file '%s'.", files[i].name.c_str())); + } + else + { + DEBUG_LOG(("MiniDumper::KeepNewestFiles: Failed to delete file '%s': error=%u", files[i].name.c_str(), ::GetLastError())); + } + } +} +#endif diff --git a/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt b/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt index 3eec2f42f8e..997a6c02420 100644 --- a/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt +++ b/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt @@ -36,6 +36,7 @@ set(WWLIB_SRC DbgHelpGuard.h DbgHelpLoader.cpp DbgHelpLoader.h + DbgHelpLoader_minidump.h Except.cpp Except.h FastAllocator.cpp diff --git a/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.cpp b/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.cpp index 96898d5097f..368bcd03ca7 100644 --- a/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.cpp +++ b/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.cpp @@ -33,6 +33,9 @@ DbgHelpLoader::DbgHelpLoader() , m_symSetOptions(NULL) , m_symFunctionTableAccess(NULL) , m_stackWalk(NULL) +#ifdef RTS_ENABLE_CRASHDUMP + , m_miniDumpWriteDump(NULL) +#endif , m_dllModule(HMODULE(0)) , m_referenceCount(0) , m_failed(false) @@ -118,6 +121,9 @@ bool DbgHelpLoader::load() Inst->m_symSetOptions = reinterpret_cast(::GetProcAddress(Inst->m_dllModule, "SymSetOptions")); Inst->m_symFunctionTableAccess = reinterpret_cast(::GetProcAddress(Inst->m_dllModule, "SymFunctionTableAccess")); Inst->m_stackWalk = reinterpret_cast(::GetProcAddress(Inst->m_dllModule, "StackWalk")); +#ifdef RTS_ENABLE_CRASHDUMP + Inst->m_miniDumpWriteDump = reinterpret_cast(::GetProcAddress(Inst->m_dllModule, "MiniDumpWriteDump")); +#endif if (Inst->m_symInitialize == NULL || Inst->m_symCleanup == NULL) { @@ -171,6 +177,9 @@ void DbgHelpLoader::freeResources() Inst->m_symSetOptions = NULL; Inst->m_symFunctionTableAccess = NULL; Inst->m_stackWalk = NULL; +#ifdef RTS_ENABLE_CRASHDUMP + Inst->m_miniDumpWriteDump = NULL; +#endif Inst->m_loadedFromSystem = false; } @@ -332,3 +341,22 @@ BOOL DbgHelpLoader::stackWalk( return FALSE; } + +#ifdef RTS_ENABLE_CRASHDUMP +BOOL DbgHelpLoader::miniDumpWriteDump( + HANDLE hProcess, + DWORD ProcessId, + HANDLE hFile, + MINIDUMP_TYPE DumpType, + PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, + PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, + PMINIDUMP_CALLBACK_INFORMATION CallbackParam) +{ + CriticalSectionClass::LockClass lock(CriticalSection); + + if (Inst != NULL && Inst->m_miniDumpWriteDump) + return Inst->m_miniDumpWriteDump(hProcess, ProcessId, hFile, DumpType, ExceptionParam, UserStreamParam, CallbackParam); + + return FALSE; +} +#endif diff --git a/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.h b/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.h index c2167db075e..3dda58f2068 100644 --- a/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.h +++ b/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.h @@ -23,6 +23,9 @@ #include #include // Must be included after Windows.h #include +#ifdef RTS_ENABLE_CRASHDUMP +#include +#endif #include "mutex.h" #include "SystemAllocator.h" @@ -109,6 +112,17 @@ class DbgHelpLoader PGET_MODULE_BASE_ROUTINE GetModuleBaseRoutine, PTRANSLATE_ADDRESS_ROUTINE TranslateAddress); +#ifdef RTS_ENABLE_CRASHDUMP + static BOOL WINAPI miniDumpWriteDump( + HANDLE hProcess, + DWORD ProcessId, + HANDLE hFile, + MINIDUMP_TYPE DumpType, + PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, + PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, + PMINIDUMP_CALLBACK_INFORMATION CallbackParam); +#endif + private: static void freeResources(); @@ -167,6 +181,17 @@ class DbgHelpLoader PGET_MODULE_BASE_ROUTINE GetModuleBaseRoutine, PTRANSLATE_ADDRESS_ROUTINE TranslateAddress); +#ifdef RTS_ENABLE_CRASHDUMP + typedef BOOL(WINAPI* MiniDumpWriteDump_t)( + HANDLE hProcess, + DWORD ProcessId, + HANDLE hFile, + MINIDUMP_TYPE DumpType, + PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, + PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, + PMINIDUMP_CALLBACK_INFORMATION CallbackParam); +#endif + SymInitialize_t m_symInitialize; SymCleanup_t m_symCleanup; SymLoadModule_t m_symLoadModule; @@ -177,13 +202,15 @@ class DbgHelpLoader SymSetOptions_t m_symSetOptions; SymFunctionTableAccess_t m_symFunctionTableAccess; StackWalk_t m_stackWalk; +#ifdef RTS_ENABLE_CRASHDUMP + MiniDumpWriteDump_t m_miniDumpWriteDump; +#endif typedef std::set, stl::system_allocator > Processes; Processes m_initializedProcesses; HMODULE m_dllModule; int m_referenceCount; - CriticalSectionClass m_criticalSection; bool m_failed; bool m_loadedFromSystem; }; diff --git a/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader_minidump.h b/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader_minidump.h new file mode 100644 index 00000000000..272fa832f52 --- /dev/null +++ b/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader_minidump.h @@ -0,0 +1,265 @@ +#pragma once + +#ifdef RTS_ENABLE_CRASHDUMP + +// Backported defines from minidumpapiset.h for VC6. +// minidumpapiset.h is Copyright (C) Microsoft Corporation. All rights reserved. + +#if defined(_MSC_VER) && _MSC_VER < 1300 +#include + +typedef enum _MINIDUMP_CALLBACK_TYPE { + ModuleCallback, + ThreadCallback, + ThreadExCallback, + IncludeThreadCallback, + IncludeModuleCallback, + MemoryCallback, + CancelCallback, + WriteKernelMinidumpCallback, + KernelMinidumpStatusCallback, + RemoveMemoryCallback, + IncludeVmRegionCallback, + IoStartCallback, + IoWriteAllCallback, + IoFinishCallback, + ReadMemoryFailureCallback, + SecondaryFlagsCallback, + IsProcessSnapshotCallback, + VmStartCallback, + VmQueryCallback, + VmPreReadCallback, + VmPostReadCallback +} MINIDUMP_CALLBACK_TYPE; + +typedef struct _MINIDUMP_THREAD_CALLBACK { + ULONG ThreadId; + HANDLE ThreadHandle; +#if defined(_ARM64_) + ULONG Pad; +#endif + CONTEXT Context; + ULONG SizeOfContext; + ULONG64 StackBase; + ULONG64 StackEnd; +} MINIDUMP_THREAD_CALLBACK, * PMINIDUMP_THREAD_CALLBACK; + +typedef struct _MINIDUMP_THREAD_EX_CALLBACK { + ULONG ThreadId; + HANDLE ThreadHandle; +#if defined(_ARM64_) + ULONG Pad; +#endif + CONTEXT Context; + ULONG SizeOfContext; + ULONG64 StackBase; + ULONG64 StackEnd; + ULONG64 BackingStoreBase; + ULONG64 BackingStoreEnd; +} MINIDUMP_THREAD_EX_CALLBACK, * PMINIDUMP_THREAD_EX_CALLBACK; + +typedef struct _MINIDUMP_MODULE_CALLBACK { + PWCHAR FullPath; + ULONG64 BaseOfImage; + ULONG SizeOfImage; + ULONG CheckSum; + ULONG TimeDateStamp; + VS_FIXEDFILEINFO VersionInfo; + PVOID CvRecord; + ULONG SizeOfCvRecord; + PVOID MiscRecord; + ULONG SizeOfMiscRecord; +} MINIDUMP_MODULE_CALLBACK, * PMINIDUMP_MODULE_CALLBACK; + +typedef struct _MINIDUMP_INCLUDE_THREAD_CALLBACK { + ULONG ThreadId; +} MINIDUMP_INCLUDE_THREAD_CALLBACK, * PMINIDUMP_INCLUDE_THREAD_CALLBACK; + +typedef enum _THREAD_WRITE_FLAGS { + ThreadWriteThread = 0x0001, + ThreadWriteStack = 0x0002, + ThreadWriteContext = 0x0004, + ThreadWriteBackingStore = 0x0008, + ThreadWriteInstructionWindow = 0x0010, + ThreadWriteThreadData = 0x0020, + ThreadWriteThreadInfo = 0x0040, +} THREAD_WRITE_FLAGS; + +typedef struct _MINIDUMP_INCLUDE_MODULE_CALLBACK { + ULONG64 BaseOfImage; +} MINIDUMP_INCLUDE_MODULE_CALLBACK, * PMINIDUMP_INCLUDE_MODULE_CALLBACK; + +typedef struct _MINIDUMP_IO_CALLBACK { + HANDLE Handle; + ULONG64 Offset; + PVOID Buffer; + ULONG BufferBytes; +} MINIDUMP_IO_CALLBACK, * PMINIDUMP_IO_CALLBACK; + +typedef struct _MINIDUMP_READ_MEMORY_FAILURE_CALLBACK +{ + ULONG64 Offset; + ULONG Bytes; + HRESULT FailureStatus; +} MINIDUMP_READ_MEMORY_FAILURE_CALLBACK, +* PMINIDUMP_READ_MEMORY_FAILURE_CALLBACK; + +typedef struct _MINIDUMP_VM_QUERY_CALLBACK +{ + ULONG64 Offset; +} MINIDUMP_VM_QUERY_CALLBACK, * PMINIDUMP_VM_QUERY_CALLBACK; + +typedef struct _MINIDUMP_VM_PRE_READ_CALLBACK +{ + ULONG64 Offset; + PVOID Buffer; + ULONG Size; +} MINIDUMP_VM_PRE_READ_CALLBACK, * PMINIDUMP_VM_PRE_READ_CALLBACK; + +typedef struct _MINIDUMP_VM_POST_READ_CALLBACK +{ + ULONG64 Offset; + PVOID Buffer; + ULONG Size; + ULONG Completed; + HRESULT Status; +} MINIDUMP_VM_POST_READ_CALLBACK, * PMINIDUMP_VM_POST_READ_CALLBACK; + +typedef struct _MINIDUMP_MEMORY_INFO { + ULONG64 BaseAddress; + ULONG64 AllocationBase; + ULONG32 AllocationProtect; + ULONG32 __alignment1; + ULONG64 RegionSize; + ULONG32 State; + ULONG32 Protect; + ULONG32 Type; + ULONG32 __alignment2; +} MINIDUMP_MEMORY_INFO, * PMINIDUMP_MEMORY_INFO; + +typedef struct _MINIDUMP_CALLBACK_INPUT { + ULONG ProcessId; + HANDLE ProcessHandle; + ULONG CallbackType; + union { + HRESULT Status; + MINIDUMP_THREAD_CALLBACK Thread; + MINIDUMP_THREAD_EX_CALLBACK ThreadEx; + MINIDUMP_MODULE_CALLBACK Module; + MINIDUMP_INCLUDE_THREAD_CALLBACK IncludeThread; + MINIDUMP_INCLUDE_MODULE_CALLBACK IncludeModule; + MINIDUMP_IO_CALLBACK Io; + MINIDUMP_READ_MEMORY_FAILURE_CALLBACK ReadMemoryFailure; + ULONG SecondaryFlags; + MINIDUMP_VM_QUERY_CALLBACK VmQuery; + MINIDUMP_VM_PRE_READ_CALLBACK VmPreRead; + MINIDUMP_VM_POST_READ_CALLBACK VmPostRead; + }; +} MINIDUMP_CALLBACK_INPUT, * PMINIDUMP_CALLBACK_INPUT; + +typedef struct _MINIDUMP_CALLBACK_OUTPUT { + union { + ULONG ModuleWriteFlags; + ULONG ThreadWriteFlags; + ULONG SecondaryFlags; + struct { + ULONG64 MemoryBase; + ULONG MemorySize; + }; + struct { + BOOL CheckCancel; + BOOL Cancel; + }; + HANDLE Handle; + struct { + MINIDUMP_MEMORY_INFO VmRegion; + BOOL Continue; + }; + struct { + HRESULT VmQueryStatus; + MINIDUMP_MEMORY_INFO VmQueryResult; + }; + struct { + HRESULT VmReadStatus; + ULONG VmReadBytesCompleted; + }; + HRESULT Status; + }; +} MINIDUMP_CALLBACK_OUTPUT, * PMINIDUMP_CALLBACK_OUTPUT; + +typedef struct _MINIDUMP_EXCEPTION_INFORMATION { + DWORD ThreadId; + PEXCEPTION_POINTERS ExceptionPointers; + BOOL ClientPointers; +} MINIDUMP_EXCEPTION_INFORMATION, * PMINIDUMP_EXCEPTION_INFORMATION; + +typedef struct _MINIDUMP_USER_STREAM { + ULONG32 Type; + ULONG BufferSize; + PVOID Buffer; + +} MINIDUMP_USER_STREAM, * PMINIDUMP_USER_STREAM; + +typedef struct _MINIDUMP_USER_STREAM_INFORMATION { + ULONG UserStreamCount; + PMINIDUMP_USER_STREAM UserStreamArray; +} MINIDUMP_USER_STREAM_INFORMATION, * PMINIDUMP_USER_STREAM_INFORMATION; + +typedef +BOOL +(WINAPI* MINIDUMP_CALLBACK_ROUTINE) ( + PVOID CallbackParam, + PMINIDUMP_CALLBACK_INPUT CallbackInput, + PMINIDUMP_CALLBACK_OUTPUT CallbackOutput + ); + +typedef struct _MINIDUMP_CALLBACK_INFORMATION { + MINIDUMP_CALLBACK_ROUTINE CallbackRoutine; + PVOID CallbackParam; +} MINIDUMP_CALLBACK_INFORMATION, * PMINIDUMP_CALLBACK_INFORMATION; + +typedef enum _MINIDUMP_TYPE { + MiniDumpNormal = 0x00000000, + MiniDumpWithDataSegs = 0x00000001, + MiniDumpWithFullMemory = 0x00000002, + MiniDumpWithHandleData = 0x00000004, + MiniDumpFilterMemory = 0x00000008, + MiniDumpScanMemory = 0x00000010, + MiniDumpWithUnloadedModules = 0x00000020, + MiniDumpWithIndirectlyReferencedMemory = 0x00000040, + MiniDumpFilterModulePaths = 0x00000080, + MiniDumpWithProcessThreadData = 0x00000100, + MiniDumpWithPrivateReadWriteMemory = 0x00000200, + MiniDumpWithoutOptionalData = 0x00000400, + MiniDumpWithFullMemoryInfo = 0x00000800, + MiniDumpWithThreadInfo = 0x00001000, + MiniDumpWithCodeSegs = 0x00002000, + MiniDumpWithoutAuxiliaryState = 0x00004000, + MiniDumpWithFullAuxiliaryState = 0x00008000, + MiniDumpWithPrivateWriteCopyMemory = 0x00010000, + MiniDumpIgnoreInaccessibleMemory = 0x00020000, + MiniDumpWithTokenInformation = 0x00040000, + MiniDumpWithModuleHeaders = 0x00080000, + MiniDumpFilterTriage = 0x00100000, + MiniDumpWithAvxXStateContext = 0x00200000, + MiniDumpWithIptTrace = 0x00400000, + MiniDumpScanInaccessiblePartialPages = 0x00800000, + MiniDumpFilterWriteCombinedMemory = 0x01000000, + MiniDumpValidTypeFlags = 0x01ffffff, + MiniDumpNoIgnoreInaccessibleMemory = 0x02000000, + MiniDumpValidTypeFlagsEx = 0x03ffffff, +} MINIDUMP_TYPE; + +typedef enum _MODULE_WRITE_FLAGS { + ModuleWriteModule = 0x0001, + ModuleWriteDataSeg = 0x0002, + ModuleWriteMiscRecord = 0x0004, + ModuleWriteCvRecord = 0x0008, + ModuleReferencedByMemory = 0x0010, + ModuleWriteTlsData = 0x0020, + ModuleWriteCodeSegs = 0x0040, +} MODULE_WRITE_FLAGS; + +#include +#endif +#endif diff --git a/Generals/Code/Main/WinMain.cpp b/Generals/Code/Main/WinMain.cpp index 303d6da1cec..47412511028 100644 --- a/Generals/Code/Main/WinMain.cpp +++ b/Generals/Code/Main/WinMain.cpp @@ -64,6 +64,9 @@ #include "BuildVersion.h" #include "GeneratedVersion.h" #include "resource.h" +#ifdef RTS_ENABLE_CRASHDUMP +#include "Common/MiniDumper.h" +#endif // GLOBALS //////////////////////////////////////////////////////////////////// @@ -741,6 +744,16 @@ static CriticalSection critSec1, critSec2, critSec3, critSec4, critSec5; static LONG WINAPI UnHandledExceptionFilter( struct _EXCEPTION_POINTERS* e_info ) { DumpExceptionInfo( e_info->ExceptionRecord->ExceptionCode, e_info ); +#ifdef RTS_ENABLE_CRASHDUMP + if (TheMiniDumper && TheMiniDumper->IsInitialized()) + { + // Create both minimal and full memory dumps + TheMiniDumper->TriggerMiniDumpForException(e_info, DumpType_Minimal); + TheMiniDumper->TriggerMiniDumpForException(e_info, DumpType_Full); + } + + MiniDumper::shutdownMiniDumper(); +#endif return EXCEPTION_EXECUTE_HANDLER; } @@ -801,6 +814,10 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, CommandLine::parseCommandLineForStartup(); +#ifdef RTS_ENABLE_CRASHDUMP + // Initialize minidump facilities - requires TheGlobalData so performed after parseCommandLineForStartup + MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData()); +#endif // register windows class and create application window if(!TheGlobalData->m_headless && initializeAppWindows(hInstance, nCmdShow, TheGlobalData->m_windowed) == false) return exitcode; @@ -868,6 +885,9 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, } +#ifdef RTS_ENABLE_CRASHDUMP + MiniDumper::shutdownMiniDumper(); +#endif TheAsciiStringCriticalSection = NULL; TheUnicodeStringCriticalSection = NULL; TheDmaCriticalSection = NULL; diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index 6017276941f..a92a799932f 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -67,6 +67,9 @@ #include "resource.h" #include +#ifdef RTS_ENABLE_CRASHDUMP +#include "Common/MiniDumper.h" +#endif // GLOBALS //////////////////////////////////////////////////////////////////// @@ -763,6 +766,16 @@ static CriticalSection critSec1, critSec2, critSec3, critSec4, critSec5; static LONG WINAPI UnHandledExceptionFilter( struct _EXCEPTION_POINTERS* e_info ) { DumpExceptionInfo( e_info->ExceptionRecord->ExceptionCode, e_info ); +#ifdef RTS_ENABLE_CRASHDUMP + if (TheMiniDumper && TheMiniDumper->IsInitialized()) + { + // Create both minimal and full memory dumps + TheMiniDumper->TriggerMiniDumpForException(e_info, DumpType_Minimal); + TheMiniDumper->TriggerMiniDumpForException(e_info, DumpType_Full); + } + + MiniDumper::shutdownMiniDumper(); +#endif return EXCEPTION_EXECUTE_HANDLER; } @@ -847,6 +860,10 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, #endif CommandLine::parseCommandLineForStartup(); +#ifdef RTS_ENABLE_CRASHDUMP + // Initialize minidump facilities - requires TheGlobalData so performed after parseCommandLineForStartup + MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData()); +#endif // register windows class and create application window if(!TheGlobalData->m_headless && initializeAppWindows(hInstance, nCmdShow, TheGlobalData->m_windowed) == false) @@ -916,6 +933,9 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, } +#ifdef RTS_ENABLE_CRASHDUMP + MiniDumper::shutdownMiniDumper(); +#endif TheUnicodeStringCriticalSection = NULL; TheDmaCriticalSection = NULL; TheMemoryPoolCriticalSection = NULL; diff --git a/cmake/config-memory.cmake b/cmake/config-memory.cmake index 72a28d1c94e..5daa6f110a4 100644 --- a/cmake/config-memory.cmake +++ b/cmake/config-memory.cmake @@ -20,6 +20,9 @@ option(RTS_MEMORYPOOL_DEBUG_INTENSE_VERIFY "Enables intensive verifications afte option(RTS_MEMORYPOOL_DEBUG_CHECK_BLOCK_OWNERSHIP "Enables debug to verify that a block actually belongs to the pool it is called with. This is great for debugging, but can be realllly slow, so is OFF by default." OFF) option(RTS_MEMORYPOOL_DEBUG_INTENSE_DMA_BOOKKEEPING "Prints statistics for memory usage of Memory Pools." OFF) +# Memory dump options +option(RTS_CRASHDUMP_ENABLE "Enables writing crash dumps on unhandled exceptions or release crash failures." ON) + # Game Memory features add_feature_info(GameMemoryEnable RTS_GAMEMEMORY_ENABLE "Build with the original game memory implementation") @@ -37,6 +40,8 @@ add_feature_info(MemoryPoolDebugIntenseVerify RTS_MEMORYPOOL_DEBUG_INTENSE_VERIF add_feature_info(MemoryPoolDebugCheckBlockOwnership RTS_MEMORYPOOL_DEBUG_CHECK_BLOCK_OWNERSHIP "Build with Memory Pool block ownership checks") add_feature_info(MemoryPoolDebugIntenseDmaBookkeeping RTS_MEMORYPOOL_DEBUG_INTENSE_DMA_BOOKKEEPING "Build with Memory Pool intense DMA bookkeeping") +# Memory dump features +add_feature_info(CrashDumpEnable RTS_CRASHDUMP_ENABLE "Build with Crash Dumps") # Game Memory features if(NOT RTS_GAMEMEMORY_ENABLE) @@ -87,3 +92,10 @@ else() target_compile_definitions(core_config INTERFACE INTENSE_DMA_BOOKKEEPING=1) endif() endif() + +if(RTS_CRASHDUMP_ENABLE) + target_compile_definitions(core_config INTERFACE RTS_ENABLE_CRASHDUMP=1) + if (IS_VS6_BUILD AND NOT RTS_BUILD_OPTION_VC6_FULL_DEBUG) + message(STATUS "Crash Dumps will be significantly less useful in VC6 builds without full debug info enabled") + endif() +endif()