diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index f713e0a0ab..31b7287fc3 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -70,6 +70,8 @@ set(GAMEENGINE_SRC # Include/Common/MapObject.h # Include/Common/MapReaderWriterInfo.h # Include/Common/MessageStream.h + Include/Common/MiniDumper.h + Include/Common/MiniDumper_compat.h # Include/Common/MiniLog.h Include/Common/MiscAudio.h # Include/Common/MissionStats.h @@ -657,6 +659,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 @@ -1170,6 +1173,7 @@ target_include_directories(corei_gameengine_private INTERFACE target_link_libraries(corei_gameengine_private INTERFACE corei_always + $<$,$>:shlwapi.lib> ) target_compile_definitions(corei_gameengine_private INTERFACE diff --git a/Core/GameEngine/Include/Common/GameMemory.h b/Core/GameEngine/Include/Common/GameMemory.h index 65b1732e52..9a9d8063ee 100644 --- a/Core/GameEngine/Include/Common/GameMemory.h +++ b/Core/GameEngine/Include/Common/GameMemory.h @@ -226,6 +226,9 @@ class MemoryPool; class MemoryPoolFactory; class DynamicMemoryAllocator; class BlockCheckpointInfo; +#ifdef RTS_ENABLE_CRASHDUMP +class AllocationRangeIterator; +#endif // TYPE DEFINES /////////////////////////////////////////////////////////////// @@ -282,6 +285,14 @@ class Checkpointable }; #endif +#ifdef RTS_ENABLE_CRASHDUMP +struct MemoryPoolAllocatedRange +{ + char* allocationAddr; + size_t allocationSize; +}; +#endif + // ---------------------------------------------------------------------------- /** A MemoryPool provides a way to efficiently allocate objects of the same (or similar) @@ -387,6 +398,9 @@ class MemoryPool /// return true iff this block was allocated by this pool. Bool debugIsBlockInPool(void *pBlock); #endif +#ifdef RTS_ENABLE_CRASHDUMP + friend class AllocationRangeIterator; +#endif }; // ---------------------------------------------------------------------------- @@ -477,6 +491,10 @@ class DynamicMemoryAllocator Bool debugIsPoolInDma(MemoryPool *pool); #endif // MEMORYPOOL_DEBUG +#ifdef RTS_ENABLE_CRASHDUMP + Int getRawBlockCount() const; + void fillAllocationRangeForRawBlockN(const Int n, MemoryPoolAllocatedRange& allocationRange) const; +#endif }; // ---------------------------------------------------------------------------- @@ -484,6 +502,69 @@ class DynamicMemoryAllocator enum { MAX_SPECIAL_USED = 256 }; #endif +#ifdef RTS_ENABLE_CRASHDUMP +class AllocationRangeIterator +{ + typedef const MemoryPoolAllocatedRange value_type; + typedef const MemoryPoolAllocatedRange* pointer; + typedef const MemoryPoolAllocatedRange& reference; + +public: + + AllocationRangeIterator(const MemoryPoolFactory* factory); + AllocationRangeIterator(MemoryPool& pool, MemoryPoolBlob& blob) + { + m_currentPool = &pool; + m_currentBlobInPool = &blob; + m_factory = NULL; + m_range = MemoryPoolAllocatedRange(); + }; + + AllocationRangeIterator(MemoryPool* pool, MemoryPoolBlob* blob) + { + m_currentPool = pool; + m_currentBlobInPool = blob; + m_factory = NULL; + m_range = MemoryPoolAllocatedRange(); + }; + + AllocationRangeIterator() + { + m_currentPool = NULL; + m_currentBlobInPool = NULL; + m_factory = NULL; + m_range = MemoryPoolAllocatedRange(); + }; + + reference operator*() { UpdateRange(); return m_range; } + pointer operator->() { UpdateRange(); return &m_range; } + + // Prefix increment + AllocationRangeIterator& operator++() { MoveToNextBlob(); return *this; } + + // Postfix increment + AllocationRangeIterator operator++(int) { AllocationRangeIterator tmp = *this; ++(*this); return tmp; } + + friend const bool operator== (const AllocationRangeIterator& a, const AllocationRangeIterator& b) + { + return a.m_currentBlobInPool == b.m_currentBlobInPool; + }; + + friend const bool operator!= (const AllocationRangeIterator& a, const AllocationRangeIterator& b) + { + return a.m_currentBlobInPool != b.m_currentBlobInPool; + }; + +private: + const MemoryPoolFactory* m_factory; + MemoryPool* m_currentPool; + MemoryPoolBlob* m_currentBlobInPool; + MemoryPoolAllocatedRange m_range; + void UpdateRange(); + void MoveToNextBlob(); +}; +#endif + // ---------------------------------------------------------------------------- /** The class that manages all the MemoryPools and DynamicMemoryAllocators. @@ -576,6 +657,21 @@ class MemoryPoolFactory void debugResetCheckpoints(); #endif +#ifdef RTS_ENABLE_CRASHDUMP + AllocationRangeIterator cbegin() const + { + return AllocationRangeIterator(this); + } + + AllocationRangeIterator cend() const + { + return AllocationRangeIterator(NULL, NULL); + } + + Int getMemoryPoolCount() const; + MemoryPool* getMemoryPoolN(const Int n) const; + friend class AllocationRangeIterator; +#endif }; // how many bytes are we allowed to 'waste' per pool allocation before the debug code starts yelling at us... diff --git a/Core/GameEngine/Include/Common/MiniDumper.h b/Core/GameEngine/Include/Common/MiniDumper.h new file mode 100644 index 0000000000..5a7b21a56b --- /dev/null +++ b/Core/GameEngine/Include/Common/MiniDumper.h @@ -0,0 +1,129 @@ +/* +** 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 +#include "Common/MiniDumper_compat.h" + +enum DumpType CPP_11(: Int) +{ + // Smallest dump type with call stacks and some supporting variables + DUMP_TYPE_MINIMAL, + // Large dump including all memory regions allocated by the GameMemory implementaion + DUMP_TYPE_GAMEMEMORY, + // Largest dump size including complete memory contents of the process + DUMP_TYPE_FULL, +}; + +enum MiniDumperExitCode CPP_11(: Int) +{ + DUMPER_EXIT_SUCCESS = 0x0, + DUMPER_EXIT_FAILURE_WAIT = 0x37DA1040, + DUMPER_EXIT_FAILURE_PARAM = 0x4EA527BB, + DUMPER_EXIT_FORCED_TERMINATE = 0x158B1154, +}; + +class MiniDumper +{ +public: + MiniDumper(); + Bool IsInitialized() const; + void TriggerMiniDump(DumpType dumpType); + void TriggerMiniDumpForException(struct _EXCEPTION_POINTERS* e_info, DumpType dumpType); + static void initMiniDumper(const AsciiString& userDirPath); + static void shutdownMiniDumper(); + static LONG WINAPI DumpingExceptionFilter(struct _EXCEPTION_POINTERS* e_info); + +private: + void Initialize(const AsciiString& userDirPath); + void ShutDown(); + void CreateMiniDump(DumpType dumpType); + BOOL DumpMemoryObjects(ULONG64& memoryBase, ULONG& memorySize); + void CleanupResources(); + Bool IsDumpThreadStillRunning() const; + + // Callbacks from dbghelp + static BOOL CALLBACK MiniDumpCallback(PVOID CallbackParam, PMINIDUMP_CALLBACK_INPUT CallbackInput, PMINIDUMP_CALLBACK_OUTPUT CallbackOutput); + BOOL CallbackInternal(const MINIDUMP_CALLBACK_INPUT& input, MINIDUMP_CALLBACK_OUTPUT& output); + + // 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 std::string& fileWildcard, 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; + DumpType m_requestedDumpType; + + // Path buffers + Char m_dumpDir[MAX_PATH]; + Char m_dumpFile[MAX_PATH]; + Char m_sysDbgHelpPath[MAX_PATH]; + WideChar m_executablePath[MAX_PATH]; + + // Module handles + HMODULE m_dbgHlp; + + // Event handles + HANDLE m_dumpRequested; + HANDLE m_dumpComplete; + HANDLE m_quitting; + + // Thread handles + HANDLE m_dumpThread; + DWORD m_dumpThreadId; + +#ifndef DISABLE_GAMEMEMORY + // Internal memory dumping progress state + int m_dumpObjectsState; + int m_dumpObjectsSubState; + int m_dmaRawBlockIndex; + + AllocationRangeIterator m_rangeIter; +#endif + + // Function pointer to MiniDumpWriteDump in dbghelp.dll + 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 + ); + + MiniDumpWriteDump_t m_pMiniDumpWriteDump; +}; + +extern MiniDumper* TheMiniDumper; +#endif diff --git a/Core/GameEngine/Include/Common/MiniDumper_compat.h b/Core/GameEngine/Include/Common/MiniDumper_compat.h new file mode 100644 index 0000000000..ab28931418 --- /dev/null +++ b/Core/GameEngine/Include/Common/MiniDumper_compat.h @@ -0,0 +1,254 @@ +#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 +#pragma pack(push, 4) + +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 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; + +#pragma pack(pop) +#endif +#endif diff --git a/Core/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp index 88a65e228f..2c2eac9bb1 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; @@ -729,6 +732,16 @@ void ReleaseCrash(const char *reason) } } +#ifdef RTS_ENABLE_CRASHDUMP + if (TheMiniDumper && TheMiniDumper->IsInitialized()) + { + // Do dumps both with and without extended info + TheMiniDumper->TriggerMiniDump(DUMP_TYPE_MINIMAL); + TheMiniDumper->TriggerMiniDump(DUMP_TYPE_GAMEMEMORY); + MiniDumper::shutdownMiniDumper(); + } +#endif + char prevbuf[ _MAX_PATH ]; char curbuf[ _MAX_PATH ]; @@ -794,6 +807,16 @@ void ReleaseCrashLocalized(const AsciiString& p, const AsciiString& m) return; } +#ifdef RTS_ENABLE_CRASHDUMP + if (TheMiniDumper && TheMiniDumper->IsInitialized()) + { + // Do dumps both with and without extended info + TheMiniDumper->TriggerMiniDump(DUMP_TYPE_MINIMAL); + TheMiniDumper->TriggerMiniDump(DUMP_TYPE_GAMEMEMORY); + MiniDumper::shutdownMiniDumper(); + } +#endif + UnicodeString prompt = TheGameText->fetch(p); UnicodeString mesg = TheGameText->fetch(m); diff --git a/Core/GameEngine/Source/Common/System/GameMemory.cpp b/Core/GameEngine/Source/Common/System/GameMemory.cpp index 81d70b9d55..8d6929d09f 100644 --- a/Core/GameEngine/Source/Common/System/GameMemory.cpp +++ b/Core/GameEngine/Source/Common/System/GameMemory.cpp @@ -408,9 +408,11 @@ class MemoryPoolSingleBlock #ifdef MEMORYPOOL_BOUNDINGWALL Int m_wallPattern; ///< unique seed value for the bounding-walls for this block #endif +#if defined(MEMORYPOOL_DEBUG) || defined(RTS_ENABLE_CRASHDUMP) + Int m_logicalSize; ///< logical size of block (not including overhead, walls, etc.) +#endif #ifdef MEMORYPOOL_DEBUG const char *m_debugLiteralTagString; ///< ptr to the tagstring for this block. - Int m_logicalSize; ///< logical size of block (not including overhead, walls, etc.) Int m_wastedSize; ///< if allocated via DMA, the "wasted" bytes Short m_magicCookie; ///< magic value used to verify that the block is one of ours (as opposed to random pointer) Short m_debugFlags; ///< misc flags @@ -442,10 +444,12 @@ class MemoryPoolSingleBlock MemoryPoolSingleBlock *getNextRawBlock(); void setNextRawBlock(MemoryPoolSingleBlock *b); +#if defined(MEMORYPOOL_DEBUG) || defined(RTS_ENABLE_CRASHDUMP) + Int debugGetLogicalSize(); +#endif #ifdef MEMORYPOOL_DEBUG void debugIgnoreLeaksForThisBlock(); const char *debugGetLiteralTagString(); - Int debugGetLogicalSize(); Int debugGetWastedSize(); void debugSetWastedSize(Int waste); void debugVerifyBlock(); @@ -501,6 +505,9 @@ class MemoryPoolBlob #ifdef MEMORYPOOL_CHECKPOINTING void debugResetCheckpoints(); #endif +#ifdef RTS_ENABLE_CRASHDUMP + void fillAllocatedRange(MemoryPoolAllocatedRange& range); +#endif }; @@ -640,7 +647,7 @@ inline const char *MemoryPoolSingleBlock::debugGetLiteralTagString() } #endif -#ifdef MEMORYPOOL_DEBUG +#if defined(MEMORYPOOL_DEBUG) || defined(RTS_ENABLE_CRASHDUMP) /** accessor */ @@ -875,6 +882,8 @@ void MemoryPoolSingleBlock::initBlock(Int logicalSize, MemoryPoolBlob *owningBlo } #endif } +#elif defined(RTS_ENABLE_CRASHDUMP) + m_logicalSize = logicalSize; #endif // MEMORYPOOL_DEBUG #ifdef MEMORYPOOL_CHECKPOINTING @@ -1382,6 +1391,14 @@ void MemoryPoolBlob::debugResetCheckpoints() } #endif +#ifdef RTS_ENABLE_CRASHDUMP +void MemoryPoolBlob::fillAllocatedRange(MemoryPoolAllocatedRange& range) +{ + range.allocationAddr = m_blockData; + range.allocationSize = m_totalBlocksInBlob * MemoryPoolSingleBlock::calcRawBlockSize(m_owningPool->getAllocationSize()); +} +#endif + //----------------------------------------------------------------------------- // METHODS for Checkpointable //----------------------------------------------------------------------------- @@ -2573,6 +2590,29 @@ void DynamicMemoryAllocator::debugDmaInfoReport( FILE *fp ) } #endif +#ifdef RTS_ENABLE_CRASHDUMP +Int DynamicMemoryAllocator::getRawBlockCount() const +{ + Int count = 0; + for (MemoryPoolSingleBlock* block = m_rawBlocks; block; block = block->getNextRawBlock()) + { + ++count; + } + + return count; +} +void DynamicMemoryAllocator::fillAllocationRangeForRawBlockN(const Int n, MemoryPoolAllocatedRange& allocationRange) const +{ + MemoryPoolSingleBlock* block = m_rawBlocks; + for (int i = 0; i < n; ++i) + { + block = block->getNextRawBlock(); + } + allocationRange.allocationAddr = reinterpret_cast(block); + allocationRange.allocationSize = block->calcRawBlockSize(block->debugGetLogicalSize()); +} +#endif + //----------------------------------------------------------------------------- // METHODS for MemoryPoolFactory //----------------------------------------------------------------------------- @@ -3232,6 +3272,71 @@ void MemoryPoolFactory::debugMemoryReport(Int flags, Int startCheckpoint, Int en } #endif +#ifdef RTS_ENABLE_CRASHDUMP +Int MemoryPoolFactory::getMemoryPoolCount() const +{ + Int count = 0; + MemoryPool* current = m_firstPoolInFactory; + while (current != NULL) + { + ++count; + current = current->getNextPoolInList(); + } + + return count; +} + +MemoryPool* MemoryPoolFactory::getMemoryPoolN(const Int n) const +{ + Int count = 0; + MemoryPool* current = m_firstPoolInFactory; + while (count < n && current != NULL) + { + ++count; + current = current->getNextPoolInList(); + } + + return current; +} + +AllocationRangeIterator::AllocationRangeIterator(const MemoryPoolFactory* factory) +{ + m_factory = factory; + m_currentPool = factory->m_firstPoolInFactory; + m_currentBlobInPool = m_currentPool->m_firstBlob; + m_range = MemoryPoolAllocatedRange(); +} + +void AllocationRangeIterator::UpdateRange() +{ + m_currentBlobInPool->fillAllocatedRange(m_range); +} + +void AllocationRangeIterator::MoveToNextBlob() +{ + // Advances to the next blob, advancing to the next MemoryPool if needed. + m_currentBlobInPool = m_currentBlobInPool->getNextInList(); + if (m_currentBlobInPool != NULL) + { + return; + } + do + { + m_currentPool = m_currentPool->getNextPoolInList(); + } while (m_currentPool != NULL && m_currentPool->m_firstBlob == NULL); + + if (m_currentPool != NULL) + { + m_currentBlobInPool = m_currentPool->m_firstBlob; + } + else + { + m_currentBlobInPool = NULL; + } +} + +#endif + //----------------------------------------------------------------------------- // GLOBAL FUNCTIONS //----------------------------------------------------------------------------- diff --git a/Core/GameEngine/Source/Common/System/MiniDumper.cpp b/Core/GameEngine/Source/Common/System/MiniDumper.cpp new file mode 100644 index 0000000000..cee264a893 --- /dev/null +++ b/Core/GameEngine/Source/Common/System/MiniDumper.cpp @@ -0,0 +1,725 @@ +/* +** 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 "Common/GameMemory.h" +#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 }; + +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_requestedDumpType = DUMP_TYPE_MINIMAL; + m_dbgHlp = NULL; + m_pMiniDumpWriteDump = NULL; + m_dumpRequested = NULL; + m_dumpComplete = NULL; + m_quitting = NULL; + m_dumpThread = NULL; + m_dumpThreadId = 0; +#ifndef DISABLE_GAMEMEMORY + m_dumpObjectsState = 0; + m_dumpObjectsSubState = 0; + m_dmaRawBlockIndex = 0; +#endif + memset(m_dumpDir, 0, ARRAY_SIZE(m_dumpDir)); + memset(m_dumpFile, 0, ARRAY_SIZE(m_dumpFile)); + memset(m_sysDbgHelpPath, 0, ARRAY_SIZE(m_sysDbgHelpPath)); + memset(m_executablePath, 0, ARRAY_SIZE(m_executablePath)); +}; + +LONG WINAPI MiniDumper::DumpingExceptionFilter(struct _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(struct _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; +#ifdef DISABLE_GAMEMEMORY + if (m_requestedDumpType == DUMP_TYPE_GAMEMEMORY) + { + // Dump the whole process if the game memory implementation is turned off + m_requestedDumpType = DUMP_TYPE_FULL; + } +#endif + + 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) +{ + // Find the full path to the dbghelp.dll file in the system32 dir + ::GetSystemDirectory(m_sysDbgHelpPath, MAX_PATH); + strlcat(m_sysDbgHelpPath, "\\dbghelp.dll", MAX_PATH); + + // We want to only use the dbghelp.dll from the OS installation, as the one bundled with the game does not support MiniDump functionality + Bool loadedDbgHelp = false; + HMODULE m_dbgHlp = ::GetModuleHandle(m_sysDbgHelpPath); + if (m_dbgHlp == NULL) + { + // Load the dbghelp library from the system folder + m_dbgHlp = ::LoadLibrary(m_sysDbgHelpPath); + if (m_dbgHlp == NULL) + { + DEBUG_LOG(("MiniDumper::Initialize: Unable to load system-provided dbghelp.dll from '%s': error=%u", m_sysDbgHelpPath, ::GetLastError())); + return; + } + + loadedDbgHelp = true; + } + + m_pMiniDumpWriteDump = reinterpret_cast(::GetProcAddress(m_dbgHlp, "MiniDumpWriteDump")); + if (m_pMiniDumpWriteDump == NULL) + { + if (loadedDbgHelp) + { + ::FreeLibrary(m_dbgHlp); + m_dbgHlp = NULL; + } + + DEBUG_LOG(("MiniDumper::Initialize: Could not get address of proc MiniDumpWriteDump from '%s'!", m_sysDbgHelpPath)); + 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())); + CleanupResources(); + 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())); + CleanupResources(); + return; + } + + if (!::ResumeThread(m_dumpThread)) + { + DEBUG_LOG(("MiniDumper::Initialize: Unable to resume thread: error=%u", ::GetLastError())); + CleanupResources(); + 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 (::GetExitCodeThread(m_dumpThread, &exitCode) && exitCode == STILL_ACTIVE) + { + return true; + } + + return false; +} + +Bool MiniDumper::InitializeDumpDirectory(const AsciiString& userDirPath) +{ + constexpr Int MaxExtendedFileCount = 2; + constexpr 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 and 2 extended) + KeepNewestFiles(m_dumpDir, "CrashX*", MaxExtendedFileCount); + KeepNewestFiles(m_dumpDir, "CrashM*", MaxMiniFileCount); + + return true; +} + +void MiniDumper::CleanupResources() +{ + if (m_dumpThread != NULL) + { + DEBUG_ASSERTCRASH(!IsDumpThreadStillRunning(), ("MiniDumper::CleanupResources() called while Dump thread still active")); + ::CloseHandle(m_dumpThread); + m_dumpThread = NULL; + } + + if (m_dumpComplete != NULL) + { + ::CloseHandle(m_dumpComplete); + m_dumpComplete = NULL; + } + + if (m_dumpRequested != NULL) + { + ::CloseHandle(m_dumpRequested); + m_dumpRequested = NULL; + } + + if (m_quitting != NULL) + { + ::CloseHandle(m_quitting); + m_quitting = NULL; + } + + if (m_dbgHlp != NULL) + { + ::FreeModule(m_dbgHlp); + m_dbgHlp = NULL; + } +} + +void MiniDumper::ShutDown() +{ + if (!m_miniDumpInitialized) + { + return; + } + + ::SetEvent(m_quitting); + DWORD waitRet = ::WaitForSingleObject(m_dumpThread, 3000); + if (waitRet != WAIT_OBJECT_0) + { + if (waitRet == WAIT_TIMEOUT) + { + DEBUG_LOG(("MiniDumper::ShutDown: Waiting for dumping thread to exit timed out, killing thread", waitRet)); + ::TerminateThread(m_dumpThread, DUMPER_EXIT_FORCED_TERMINATE); + } + else if (waitRet == WAIT_FAILED) + { + DEBUG_LOG(("MiniDumper::ShutDown: Waiting for minidump triggering failed: status=%u, error=%u", waitRet, ::GetLastError())); + } + else + { + DEBUG_LOG(("MiniDumper::ShutDown: Waiting for minidump triggering failed: status=%u", waitRet)); + } + + return; + } + + CleanupResources(); + m_miniDumpInitialized = false; +} + +DWORD MiniDumper::ThreadProcInternal() +{ + while (true) + { + HANDLE waitEvents[2] = { m_dumpRequested, m_quitting }; + DWORD event = ::WaitForMultipleObjects(ARRAY_SIZE(waitEvents), waitEvents, FALSE, INFINITE); + if (event == WAIT_OBJECT_0 + 0) + { + // A dump is requested (m_dumpRequested) + ::ResetEvent(m_dumpComplete); + CreateMiniDump(m_requestedDumpType); + ::ResetEvent(m_dumpRequested); + ::SetEvent(m_dumpComplete); + } + else if (event == WAIT_OBJECT_0 + 1) + { + // Quit (m_quitting) + return DUMPER_EXIT_SUCCESS; + } + else + { + if (event == WAIT_FAILED) + { + DEBUG_LOG(("MiniDumper::ThreadProcInternal: Waiting for events failed: status=%u, error=%u", event, ::GetLastError())); + } + else + { + DEBUG_LOG(("MiniDumper::ThreadProcInternal: Waiting for events failed: status=%u", event)); + } + + return DUMPER_EXIT_FAILURE_WAIT; + } + } +} + +DWORD WINAPI MiniDumper::MiniDumpThreadProc(LPVOID lpParam) +{ + if (lpParam == NULL) + { + DEBUG_LOG(("MiniDumper::MiniDumpThreadProc: The provided parameter was NULL, exiting thread.")); + return DUMPER_EXIT_FAILURE_PARAM; + } + + 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 + Char product = 'G'; +#elif RTS_ZEROHOUR + Char product = 'Z'; +#endif + Char dumpTypeSpecifier = dumpType == DUMP_TYPE_MINIMAL ? 'M' : 'X'; + DWORD currentProcessId = ::GetCurrentProcessId(); + + // m_dumpDir is stored with trailing backslash in Initialize + snprintf(m_dumpFile, ARRAY_SIZE(m_dumpFile), "%sCrash%c%c-%04d%02d%02d-%02d%02d%02d-%s-pid%ld.dmp", + m_dumpDir, 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; + } + + PMINIDUMP_CALLBACK_INFORMATION callbackInfoPtr = NULL; + MINIDUMP_CALLBACK_INFORMATION callBackInfo = { 0 }; + if (dumpType == DUMP_TYPE_GAMEMEMORY) + { + callBackInfo.CallbackRoutine = MiniDumpCallback; + callBackInfo.CallbackParam = this; + callbackInfoPtr = &callBackInfo; + } + + int dumpTypeFlags = MiniDumpWithIndirectlyReferencedMemory | MiniDumpScanMemory; + if (dumpType != DUMP_TYPE_MINIMAL) + { + dumpTypeFlags |= MiniDumpWithDataSegs | MiniDumpWithHandleData | MiniDumpWithThreadInfo | MiniDumpWithFullMemoryInfo; + if (dumpType == DUMP_TYPE_FULL) + { + dumpTypeFlags |= MiniDumpWithFullMemory; + } + } + + MINIDUMP_TYPE miniDumpType = static_cast(dumpTypeFlags); + BOOL success = m_pMiniDumpWriteDump( + ::GetCurrentProcess(), + currentProcessId, + dumpFile, + miniDumpType, + exceptionInfoPtr, + NULL, + callbackInfoPtr); + + 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); +} + +BOOL CALLBACK MiniDumper::MiniDumpCallback(PVOID CallbackParam, PMINIDUMP_CALLBACK_INPUT CallbackInput, PMINIDUMP_CALLBACK_OUTPUT CallbackOutput) +{ + if (CallbackParam == NULL || CallbackInput == NULL || CallbackOutput == NULL) + { + DEBUG_LOG(("MiniDumper::MiniDumpCallback: Required parameters were null; CallbackParam=%p, CallbackInput=%p, CallbackOutput=%p.", + CallbackParam, CallbackInput, CallbackOutput)); + return false; + } + + MiniDumper* dumper = static_cast(CallbackParam); + return dumper->CallbackInternal(*CallbackInput, *CallbackOutput); +} + +// This is where the memory regions and things are being filtered +BOOL MiniDumper::CallbackInternal(const MINIDUMP_CALLBACK_INPUT& input, MINIDUMP_CALLBACK_OUTPUT& output) +{ + BOOL retVal = TRUE; + switch (input.CallbackType) + { + case IncludeModuleCallback: + retVal = TRUE; + break; + case ModuleCallback: + { + // Only include data segments for the game and ntdll modules to keep dump size low + if (output.ModuleWriteFlags & ModuleWriteDataSeg) + { + if (::StrCmpIW(input.Module.FullPath, m_executablePath) != 0 && !::StrStrIW(input.Module.FullPath, L"ntdll.dll")) + { + // Exclude data segments for the module + output.ModuleWriteFlags &= (~ModuleWriteDataSeg); + } + } + + retVal = TRUE; + break; + } + case IncludeThreadCallback: + // We want all threads except the dumping thread + if (input.IncludeThread.ThreadId == m_dumpThreadId) + { + retVal = FALSE; + } + break; + case ThreadCallback: + retVal = TRUE; + break; + case ThreadExCallback: + retVal = TRUE; + break; + case MemoryCallback: + { +#ifndef DISABLE_GAMEMEMORY + do + { + // DumpMemoryObjects will return false once it's completed, signalling the end of memory callbacks + retVal = DumpMemoryObjects(output.MemoryBase, output.MemorySize); + } while ((output.MemoryBase == NULL || output.MemorySize == NULL) && retVal == TRUE); +#else + retVal = FALSE; +#endif + break; + } + case ReadMemoryFailureCallback: + { + DEBUG_LOG(("MiniDumper::CallbackInternal: ReadMemoryFailure with MemoryBase=%llu, MemorySize=%lu: error=%u", + input.ReadMemoryFailure.Offset, input.ReadMemoryFailure.Bytes, input.ReadMemoryFailure.FailureStatus)); + retVal = TRUE; + break; + } + case CancelCallback: + output.Cancel = FALSE; + output.CheckCancel = FALSE; + retVal = TRUE; + break; + } + + return retVal; +} + +#ifndef DISABLE_GAMEMEMORY +BOOL MiniDumper::DumpMemoryObjects(ULONG64& memoryBase, ULONG& memorySize) +{ + BOOL moreToDo = TRUE; + // m_dumpObjectsState is used to keep track of the current "phase" of the memory dumping process + // m_dumpObjectsSubState is used to keep track of the progress within each phase, and is reset when advancing on to the next phase + switch (m_dumpObjectsState) + { + case 0: + { + // Dump all the MemoryPool instances in TheMemoryPoolFactory + // This only dumps the metadata, not the actual MemoryPool contents (done in the next phase). + if (TheMemoryPoolFactory == NULL) + { + ++m_dumpObjectsState; + break; + } + + Int poolCount = TheMemoryPoolFactory->getMemoryPoolCount(); + //m_dumpObjectsSubState contains the index in TheMemoryPoolFactory of the MemoryPool that is being processed + if (m_dumpObjectsSubState < poolCount) + { + MemoryPool* pool = TheMemoryPoolFactory->getMemoryPoolN(m_dumpObjectsSubState); + if (pool != NULL) + { + memoryBase = reinterpret_cast(pool); + memorySize = sizeof(MemoryPool); + ++m_dumpObjectsSubState; + } + else + { + m_dumpObjectsSubState = poolCount; + } + } + + if (m_dumpObjectsSubState == poolCount) + { + m_dumpObjectsSubState = 0; + ++m_dumpObjectsState; + } + break; + } + case 1: + { + // Iterate through all the allocations of memory pools and containing blobs that has been done via the memory pool factory + // and include all of the storage space allocated for objects + if (TheMemoryPoolFactory == NULL) + { + ++m_dumpObjectsState; + break; + } + + //m_dumpObjectsSubState is used to track if the iterator needs to be initialized, otherwise just a counter of the number of items dumped + if (m_dumpObjectsSubState == 0) + { + m_rangeIter = TheMemoryPoolFactory->cbegin(); + ++m_dumpObjectsSubState; + } + + // m_RangeIter should != cend() at this point before advancing, unless the memory pool factory is corrupted (or has 0 entries) + memoryBase = reinterpret_cast(m_rangeIter->allocationAddr); + memorySize = m_rangeIter->allocationSize; + ++m_dumpObjectsSubState; + ++m_rangeIter; + + if (m_rangeIter == TheMemoryPoolFactory->cend()) + { + ++m_dumpObjectsState; + m_dumpObjectsSubState = 0; + } + break; + } + case 2: + { + // Iterate through all the direct allocations ("raw blocks") done by DMAs, as these are done outside of the + // memory pool factory allocations dumped in the previous phase. + if (TheDynamicMemoryAllocator == NULL) + { + ++m_dumpObjectsState; + break; + } + + DynamicMemoryAllocator* allocator = TheDynamicMemoryAllocator; + + //m_dumpObjectsSubState is used to track the index of the allocator we are currently traversing + for (int i = 0; i < m_dumpObjectsSubState; ++i) + { + allocator = allocator->getNextDmaInList(); + } + + MemoryPoolAllocatedRange rawBlockRange = {0}; + int rawBlocksInDma = allocator->getRawBlockCount(); + if (m_dmaRawBlockIndex < rawBlocksInDma) + { + // Dump this block + allocator->fillAllocationRangeForRawBlockN(m_dmaRawBlockIndex, rawBlockRange); + memoryBase = reinterpret_cast(rawBlockRange.allocationAddr); + memorySize = rawBlockRange.allocationSize; + ++m_dmaRawBlockIndex; + } + + if (rawBlocksInDma == m_dmaRawBlockIndex) + { + // Advance to the next DMA + ++m_dumpObjectsSubState; + m_dmaRawBlockIndex = 0; + if (allocator->getNextDmaInList() == NULL) + { + // Done iterating through all the DMAs + m_dumpObjectsSubState = 0; + ++m_dumpObjectsState; + } + } + break; + } + default: + // Done, set "no more stuff" values + m_dumpObjectsState = 0; + m_dumpObjectsSubState = 0; + m_dmaRawBlockIndex = 0; + memoryBase = 0; + memorySize = 0; + moreToDo = FALSE; + break; + } + + return moreToDo; +} +#endif + +// 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 std::string& fileWildcard, const Int keepCount) +{ + // directory already contains trailing backslash + std::string searchPath = directory + fileWildcard; + 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/Generals/Code/Main/WinMain.cpp b/Generals/Code/Main/WinMain.cpp index 3eacc83fdf..88c13cb6ef 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,15 @@ 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()) + { + // Do dumps both with and without extended info + TheMiniDumper->TriggerMiniDumpForException(e_info, DUMP_TYPE_MINIMAL); + TheMiniDumper->TriggerMiniDumpForException(e_info, DUMP_TYPE_GAMEMEMORY); + MiniDumper::shutdownMiniDumper(); + } +#endif return EXCEPTION_EXECUTE_HANDLER; } @@ -807,6 +819,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; @@ -874,6 +890,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 2fd5e3f804..c43bef83d5 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,15 @@ 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()) + { + // Do dumps both with and without extended info + TheMiniDumper->TriggerMiniDumpForException(e_info, DUMP_TYPE_MINIMAL); + TheMiniDumper->TriggerMiniDumpForException(e_info, DUMP_TYPE_GAMEMEMORY); + MiniDumper::shutdownMiniDumper(); + } +#endif return EXCEPTION_EXECUTE_HANDLER; } @@ -853,6 +865,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) @@ -922,6 +938,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 72a28d1c94..09d8732e8b 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(WARNING "Crash Dumps will be significantly less useful in VC6 builds without full debug info enabled") + endif() +endif()