using System.Runtime.InteropServices; using System.Text; using Serilog; namespace ServerLib.Utils; /// /// 크래시 핸들러 (Windows / Linux 공통) /// Register() 를 Program.cs 최상단에서 한 번 호출. /// /// 덤프 생성은 CLR 환경변수로 처리 (스택 언와인드 전에 찍힘): /// DOTNET_DbgEnableMiniDump=1 /// DOTNET_DbgMiniDumpType=4 /// DOTNET_DbgMiniDumpName=crashes/crash_%p_%t.dmp /// /// 생성 파일 (crashes/ 폴더): /// crash_YYYY-MM-DD_HH-mm-ss.log ← 항상 생성 /// crash_%p_%t.dmp ← CLR이 직접 생성 (정확한 크래시 위치) /// 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) { Exception? ex = e.ExceptionObject as Exception; string tag = $"[CrashDump] UnhandledException (IsTerminating={e.IsTerminating})"; WriteCrash(tag, ex); } private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) { e.SetObserved(); // 프로세스 종료 방지 string tag = "[CrashDump] UnobservedTaskException"; WriteCrash(tag, e.Exception); } private static void WriteCrash(string tag, Exception? ex) { try { Directory.CreateDirectory(CRASH_DIR); string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); string logPath = Path.Combine(CRASH_DIR, $"crash_{timestamp}.log"); WriteCrashLog(logPath, ex); Log.Fatal("{Tag} → {Log}", tag, logPath); } 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); } } }