using System.Text; using Microsoft.Diagnostics.Runtime; namespace CrashReporter; /// /// Extracts managed (.NET) stack frames from a minidump using ClrMD. /// This allows us to see C# method names in crash reports instead of <unknown>. /// static class ManagedStackExtractor { /// /// Attempts to extract managed stack traces from a minidump file on disk. /// Returns a formatted string containing all managed thread stacks, or null if extraction fails. /// public static string? ExtractManagedStacks( string minidumpPath ) { try { if ( !File.Exists( minidumpPath ) ) { Console.WriteLine( $"Minidump file not found: {minidumpPath}" ); return null; } using var dataTarget = DataTarget.LoadDump( minidumpPath ); return ExtractFromDataTarget( dataTarget, Path.GetFileName( minidumpPath ) ); } catch ( Exception ex ) { Console.WriteLine( $"Failed to extract managed stacks from file: {ex.Message}" ); return null; } } static string? ExtractFromDataTarget( DataTarget dataTarget, string sourceName ) { if ( dataTarget.ClrVersions.Length == 0 ) { Console.WriteLine( "No CLR found in minidump" ); return null; } var sb = new StringBuilder(); sb.AppendLine( "=== Managed Stack Traces ===" ); sb.AppendLine( $"Source: {sourceName}" ); sb.AppendLine( $"Extracted: {DateTime.UtcNow:O}" ); sb.AppendLine(); foreach ( var clrVersion in dataTarget.ClrVersions ) { sb.AppendLine( $"CLR Version: {clrVersion.Version}" ); sb.AppendLine(); ClrRuntime? runtime = null; try { runtime = clrVersion.CreateRuntime(); } catch ( Exception ex ) { sb.AppendLine( $"ERROR: Failed to create runtime: {ex.Message}" ); sb.AppendLine(); continue; } using ( runtime ) { // Get all threads with managed frames var threadsWithManagedCode = runtime.Threads .Where( t => t.EnumerateStackTrace().Any( f => f.Method != null ) ) .ToList(); if ( threadsWithManagedCode.Count == 0 ) { sb.AppendLine( "No threads with managed frames found." ); continue; } sb.AppendLine( $"Threads with managed code: {threadsWithManagedCode.Count}" ); sb.AppendLine(); foreach ( var thread in threadsWithManagedCode ) { sb.AppendLine( $"--- Thread {thread.OSThreadId} (Managed ID: {thread.ManagedThreadId}) ---" ); if ( thread.CurrentException != null ) { var ex = thread.CurrentException; sb.AppendLine( $" ** EXCEPTION: {ex.Type?.Name}: {ex.Message}" ); // Print the exception's stack trace if available foreach ( var frame in ex.StackTrace ) { var method = frame.Method; if ( method != null ) { var typeName = method.Type?.Name ?? ""; var methodName = method.Name ?? ""; var signature = GetMethodSignature( method ); sb.AppendLine( $" at {typeName}.{methodName}{signature}" ); } else { sb.AppendLine( $" at 0x{frame.InstructionPointer:X16} " ); } } } sb.AppendLine( "" ); var frames = thread.EnumerateStackTrace().ToList(); var frameIndex = 0; foreach ( var frame in frames ) { var method = frame.Method; if ( method == null ) { // Native frame - show instruction pointer for correlation sb.AppendLine( $" [{frameIndex,2}] 0x{frame.InstructionPointer:X16} " ); } else { var typeName = method.Type?.Name ?? ""; var methodName = method.Name ?? ""; var signature = GetMethodSignature( method ); sb.AppendLine( $" [{frameIndex,2}] 0x{frame.InstructionPointer:X16} {typeName}.{methodName}{signature}" ); } frameIndex++; } sb.AppendLine(); } // Also dump any unhandled exceptions var threadsWithExceptions = runtime.Threads .Where( t => t.CurrentException != null ) .ToList(); if ( threadsWithExceptions.Count > 0 ) { sb.AppendLine( "=== Threads with Exceptions ===" ); foreach ( var thread in threadsWithExceptions ) { var ex = thread.CurrentException!; sb.AppendLine( $"Thread {thread.OSThreadId:X}: {ex.Type?.Name}" ); sb.AppendLine( $" Message: {ex.Message}" ); sb.AppendLine( $" HResult: 0x{ex.HResult:X8}" ); // Walk the exception chain var inner = ex.Inner; var depth = 1; while ( inner != null && depth < 10 ) { sb.AppendLine( $" Inner[{depth}]: {inner.Type?.Name}: {inner.Message}" ); inner = inner.Inner; depth++; } sb.AppendLine(); } } } } return sb.ToString(); } /// /// Finds the most recent minidump file in the game directory that was written within the last 2 minutes. /// public static string? FindRecentMinidump( string gameDir ) { try { var cutoffTime = DateTime.UtcNow.AddSeconds( -120 ); var dumpFiles = Directory.GetFiles( gameDir, "*.mdmp" ) .Select( f => new FileInfo( f ) ) .Where( f => f.LastWriteTimeUtc >= cutoffTime ) .OrderByDescending( f => f.LastWriteTimeUtc ) .ToList(); if ( dumpFiles.Count == 0 ) { return null; } var latest = dumpFiles[0]; Console.WriteLine( $"Found minidump: {latest.Name} ({latest.Length:N0} bytes)" ); return latest.FullName; } catch ( Exception ex ) { Console.WriteLine( $"Error finding minidump: {ex.Message}" ); return null; } } static string GetMethodSignature( ClrMethod method ) { try { var sig = method.Signature; if ( string.IsNullOrEmpty( sig ) ) return "()"; // The signature includes the full method, extract just the parameters var parenIndex = sig.IndexOf( '(' ); if ( parenIndex >= 0 ) { return sig[parenIndex..]; } return "()"; } catch { return "()"; } } }