Files
a301_mmo_game_server/MMOTestServer/ServerLib/Utils/CrashDumpHandler.cs

146 lines
5.2 KiB
C#

using System.Runtime.InteropServices;
using System.Text;
using Serilog;
namespace ServerLib.Utils;
/// <summary>
/// 크래시 핸들러 (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이 직접 생성 (정확한 크래시 위치)
/// </summary>
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);
}
}
}