using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; using Serilog; namespace ServerLib.Utils; /// /// 릴리즈 빌드 크래시 덤프 핸들러 /// Register() 를 Program.cs 최상단에서 한 번 호출. /// 생성 파일 (crashes/ 폴더): /// Debug : crash_YYYY-MM-DD_HH-mm-ss.log /// Release : crash_YYYY-MM-DD_HH-mm-ss.log /// crash_YYYY-MM-DD_HH-mm-ss.dmp ← 메모리 덤프 추가 /// public static class CrashDumpHandler { private const string CRASH_DIR = "crashes"; private static int registered; public static void Register() { // 중복 등록 방지 if (Interlocked.Exchange(ref registered, 1) != 0) { return; } AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; Log.Information("[CrashDump] 핸들러 등록 완료 (CrashDir={Dir})", Path.GetFullPath(CRASH_DIR)); } // ─── 핸들러 ────────────────────────────────────────────────────────── private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) { // 예외 컨텍스트가 살아있는 지금 즉시 캡처 IntPtr exceptionPointers = Marshal.GetExceptionPointers(); Exception? ex = e.ExceptionObject as Exception; string tag = $"[CrashDump] UnhandledException (IsTerminating={e.IsTerminating})"; WriteCrash(tag, ex, exceptionPointers); } private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) { // Task 예외는 파이널라이저 스레드에서 통보되므로 포인터가 없을 수 있음 IntPtr exceptionPointers = Marshal.GetExceptionPointers(); e.SetObserved(); // 프로세스 종료 방지 string tag = "[CrashDump] UnobservedTaskException"; WriteCrash(tag, e.Exception, exceptionPointers); } // ─── 핵심 처리 ─────────────────────────────────────────────────────── private static void WriteCrash(string tag, Exception? ex, IntPtr exceptionPointers) { try { Directory.CreateDirectory(CRASH_DIR); string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); string basePath = Path.Combine(CRASH_DIR, $"crash_{timestamp}"); // 1. 크래시 로그 작성 string logPath = $"{basePath}.log"; WriteCrashLog(logPath, ex); Log.Fatal("{Tag} → {Log}", tag, logPath); #if !DEBUG // 2. 메모리 덤프 작성 (Release only) string dmpPath = $"{basePath}.dmp"; WriteDumpFile(dmpPath, exceptionPointers); Log.Fatal("[CrashDump] 덤프 파일 저장 완료 → {Dmp}", dmpPath); #endif } catch (Exception writeEx) { // 덤프 저장 실패는 무시 (이미 크래시 중이므로) Log.Error(writeEx, "[CrashDump] 덤프 저장 실패"); } finally { Log.CloseAndFlush(); } } private static void WriteCrashLog(string path, Exception? ex) { StringBuilder sb = new(); sb.AppendLine("═══════════════════════════════════════════════════"); sb.AppendLine($" CRASH REPORT {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); sb.AppendLine("═══════════════════════════════════════════════════"); sb.AppendLine(); // 환경 정보 sb.AppendLine("[Environment]"); sb.AppendLine($" OS : {RuntimeInformation.OSDescription}"); sb.AppendLine($" Runtime : {RuntimeInformation.FrameworkDescription}"); sb.AppendLine($" PID : {Environment.ProcessId}"); sb.AppendLine($" WorkDir : {Environment.CurrentDirectory}"); sb.AppendLine($" MachineName: {Environment.MachineName}"); sb.AppendLine(); // 스레드 정보 sb.AppendLine("[Thread]"); sb.AppendLine($" ThreadId : {Environment.CurrentManagedThreadId}"); sb.AppendLine(); // 예외 정보 sb.AppendLine("[Exception]"); if (ex is null) { sb.AppendLine(" (예외 객체 없음)"); } else { AppendException(sb, ex, 0); } File.WriteAllText(path, sb.ToString(), Encoding.UTF8); } private static void AppendException(StringBuilder sb, Exception ex, int depth) { string indent = new(' ', depth * 2); sb.AppendLine($"{indent} Type : {ex.GetType().FullName}"); sb.AppendLine($"{indent} Message : {ex.Message}"); sb.AppendLine($"{indent} Source : {ex.Source}"); sb.AppendLine(); sb.AppendLine($"{indent} StackTrace:"); if (ex.StackTrace is not null) { foreach (string line in ex.StackTrace.Split('\n')) { sb.AppendLine($"{indent} {line.TrimEnd()}"); } } if (ex is AggregateException agg) { foreach (Exception inner in agg.InnerExceptions) { sb.AppendLine(); sb.AppendLine($"{indent} [InnerException]"); AppendException(sb, inner, depth + 1); } } else if (ex.InnerException is not null) { sb.AppendLine(); sb.AppendLine($"{indent} [InnerException]"); AppendException(sb, ex.InnerException, depth + 1); } } #if !DEBUG // ─── Windows P/Invoke ──────────────────────────────────────────────── [StructLayout(LayoutKind.Sequential, Pack = 4)] private struct MinidumpExceptionInformation { public uint ThreadId; public IntPtr ExceptionPointers; public int ClientPointers; // 0 = 서버 자신의 주소 공간 } [DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId(); [DllImport("dbghelp.dll", SetLastError = true)] private static extern bool MiniDumpWriteDump( IntPtr hProcess, uint processId, IntPtr hFile, uint dumpType, IntPtr exceptionParam, IntPtr userStreamParam, IntPtr callbackParam); private const uint MINI_DUMP_WITH_FULL_MEMORY = 0x00000002; private static void WriteDumpFile(string path, IntPtr exceptionPointers) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Log.Warning("[CrashDump] 덤프 생성은 Windows만 지원"); return; } using Process process = Process.GetCurrentProcess(); using FileStream fs = new(path, FileMode.Create, FileAccess.Write, FileShare.None); IntPtr exInfoPtr = IntPtr.Zero; try { if (exceptionPointers != IntPtr.Zero) { MinidumpExceptionInformation exInfo = new() { ThreadId = GetCurrentThreadId(), ExceptionPointers = exceptionPointers, ClientPointers = 0 }; exInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(exInfo)); Marshal.StructureToPtr(exInfo, exInfoPtr, false); } bool success = MiniDumpWriteDump( process.Handle, (uint)process.Id, fs.SafeFileHandle.DangerousGetHandle(), MINI_DUMP_WITH_FULL_MEMORY, exInfoPtr, // 크래시 원인 스레드 & 예외 정보 IntPtr.Zero, IntPtr.Zero); if (!success) { int err = Marshal.GetLastWin32Error(); Log.Error("[CrashDump] MiniDumpWriteDump 실패 (Win32 Error={Err})", err); } } finally { if (exInfoPtr != IntPtr.Zero) { Marshal.FreeHGlobal(exInfoPtr); } } } #endif }