diff --git a/ClientTester/EchoClientTester/EchoClientTester.csproj b/ClientTester/EchoClientTester/EchoClientTester.csproj index 6c44a04..ae21309 100644 --- a/ClientTester/EchoClientTester/EchoClientTester.csproj +++ b/ClientTester/EchoClientTester/EchoClientTester.csproj @@ -9,6 +9,8 @@ + + diff --git a/ClientTester/EchoClientTester/Program.cs b/ClientTester/EchoClientTester/Program.cs index 167e9ed..7f11316 100644 --- a/ClientTester/EchoClientTester/Program.cs +++ b/ClientTester/EchoClientTester/Program.cs @@ -1,5 +1,6 @@ using ClientTester.DummyService; using ClientTester.EchoDummyService; +using ClientTester.Utils; using Serilog; class EcoClientTester @@ -79,6 +80,9 @@ class EcoClientTester private static async Task Main(string[] args) { + // 크래시 덤프 핸들러 (Release: .log + .dmp / Debug: .log) + CrashDumpHandler.Register(); + // .MinimumLevel.Warning() // Warning 이상만 출력 배포시 string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); diff --git a/ClientTester/EchoClientTester/Utils/CrashDumpHandler.cs b/ClientTester/EchoClientTester/Utils/CrashDumpHandler.cs new file mode 100644 index 0000000..37fa618 --- /dev/null +++ b/ClientTester/EchoClientTester/Utils/CrashDumpHandler.cs @@ -0,0 +1,171 @@ +using System.Runtime.InteropServices; +using System.Text; +using Serilog; + +#if !DEBUG +using Microsoft.Diagnostics.NETCore.Client; +#endif + +namespace ClientTester.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 = 0; + + 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; + bool isTerminating = e.IsTerminating; + + string tag = $"[CrashDump] UnhandledException (IsTerminating={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 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); + 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, depth: 0); + } + + File.WriteAllText(path, sb.ToString(), Encoding.UTF8); + } + + private static void AppendException(StringBuilder sb, Exception ex, int depth) + { + string indent = new string(' ', 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 + private static void WriteDumpFile(string path) + { + DiagnosticsClient client = new DiagnosticsClient(Environment.ProcessId); + client.WriteDump(DumpType.WithHeap, path, logDumpGeneration: false); + } +#endif +} diff --git a/MMOTestServer/MMOserver/Program.cs b/MMOTestServer/MMOserver/Program.cs index 8590bd5..cfc08b5 100644 --- a/MMOTestServer/MMOserver/Program.cs +++ b/MMOTestServer/MMOserver/Program.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Configuration; using MMOserver.Game; using MMOserver.RDB; using Serilog; +using ServerLib.Utils; namespace MMOserver; @@ -9,6 +10,9 @@ class Program { private static async Task Main() { + // 크래시 덤프 핸들러 (Release: .log + .dmp / Debug: .log) + CrashDumpHandler.Register(); + // .MinimumLevel.Warning() // Warning 이상만 출력 배포시 IConfigurationRoot config = new ConfigurationBuilder() diff --git a/MMOTestServer/ServerLib/ServerLib.csproj b/MMOTestServer/ServerLib/ServerLib.csproj index c8ed13a..fba7671 100644 --- a/MMOTestServer/ServerLib/ServerLib.csproj +++ b/MMOTestServer/ServerLib/ServerLib.csproj @@ -8,6 +8,8 @@ + + diff --git a/MMOTestServer/ServerLib/Utils/CrashDumpHandler.cs b/MMOTestServer/ServerLib/Utils/CrashDumpHandler.cs new file mode 100644 index 0000000..d6353e4 --- /dev/null +++ b/MMOTestServer/ServerLib/Utils/CrashDumpHandler.cs @@ -0,0 +1,172 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using Serilog; + +#if !DEBUG +using Microsoft.Diagnostics.NETCore.Client; +#endif + +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 = 0; + + 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; + bool isTerminating = e.IsTerminating; + + string tag = $"[CrashDump] UnhandledException (IsTerminating={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 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); + 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, depth: 0); + } + + File.WriteAllText(path, sb.ToString(), Encoding.UTF8); + } + + private static void AppendException(StringBuilder sb, Exception ex, int depth) + { + string indent = new string(' ', 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 + private static void WriteDumpFile(string path) + { + DiagnosticsClient client = new DiagnosticsClient(Environment.ProcessId); + client.WriteDump(DumpType.WithHeap, path, logDumpGeneration: false); + } +#endif +}