fix : 덤프 남길때 현재 콜스택 저장 기능 추가

This commit is contained in:
qornwh1
2026-03-10 09:03:46 +09:00
parent 275d09001b
commit a3bcbd073e

View File

@@ -6,19 +6,17 @@ using Serilog;
namespace ServerLib.Utils; namespace ServerLib.Utils;
/// <summary> /// <summary>
/// 릴리즈 빌드 크래시 덤프 핸들러 /// 릴리즈 빌드 크래시 덤프 핸들러
/// /// Register() 를 Program.cs 최상단에서 한 번 호출.
/// Register() 를 Program.cs 최상단에서 한 번 호출. /// 생성 파일 (crashes/ 폴더):
/// /// Debug : crash_YYYY-MM-DD_HH-mm-ss.log
/// 생성 파일 (crashes/ 폴더): /// Release : crash_YYYY-MM-DD_HH-mm-ss.log
/// Debug : crash_YYYY-MM-DD_HH-mm-ss.log /// crash_YYYY-MM-DD_HH-mm-ss.dmp ← 메모리 덤프 추가
/// Release : crash_YYYY-MM-DD_HH-mm-ss.log
/// crash_YYYY-MM-DD_HH-mm-ss.dmp ← 메모리 덤프 추가
/// </summary> /// </summary>
public static class CrashDumpHandler public static class CrashDumpHandler
{ {
private const string CRASH_DIR = "crashes"; private const string CRASH_DIR = "crashes";
private static int registered = 0; private static int registered;
public static void Register() public static void Register()
{ {
@@ -29,7 +27,7 @@ public static class CrashDumpHandler
} }
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
Log.Information("[CrashDump] 핸들러 등록 완료 (CrashDir={Dir})", Path.GetFullPath(CRASH_DIR)); Log.Information("[CrashDump] 핸들러 등록 완료 (CrashDir={Dir})", Path.GetFullPath(CRASH_DIR));
} }
@@ -38,31 +36,34 @@ public static class CrashDumpHandler
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{ {
// 예외 컨텍스트가 살아있는 지금 즉시 캡처
IntPtr exceptionPointers = Marshal.GetExceptionPointers();
Exception? ex = e.ExceptionObject as Exception; Exception? ex = e.ExceptionObject as Exception;
bool isTerminating = e.IsTerminating;
string tag = $"[CrashDump] UnhandledException (IsTerminating={isTerminating})"; string tag = $"[CrashDump] UnhandledException (IsTerminating={e.IsTerminating})";
WriteCrash(tag, ex); WriteCrash(tag, ex, exceptionPointers);
} }
private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{ {
// Task 예외는 파이널라이저 스레드에서 통보되므로 포인터가 없을 수 있음
IntPtr exceptionPointers = Marshal.GetExceptionPointers();
e.SetObserved(); // 프로세스 종료 방지 e.SetObserved(); // 프로세스 종료 방지
string tag = "[CrashDump] UnobservedTaskException"; string tag = "[CrashDump] UnobservedTaskException";
WriteCrash(tag, e.Exception); WriteCrash(tag, e.Exception, exceptionPointers);
} }
// ─── 핵심 처리 ─────────────────────────────────────────────────────── // ─── 핵심 처리 ───────────────────────────────────────────────────────
private static void WriteCrash(string tag, Exception? ex) private static void WriteCrash(string tag, Exception? ex, IntPtr exceptionPointers)
{ {
try try
{ {
Directory.CreateDirectory(CRASH_DIR); Directory.CreateDirectory(CRASH_DIR);
string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
string basePath = Path.Combine(CRASH_DIR, $"crash_{timestamp}"); string basePath = Path.Combine(CRASH_DIR, $"crash_{timestamp}");
// 1. 크래시 로그 작성 // 1. 크래시 로그 작성
string logPath = $"{basePath}.log"; string logPath = $"{basePath}.log";
@@ -73,7 +74,7 @@ public static class CrashDumpHandler
#if !DEBUG #if !DEBUG
// 2. 메모리 덤프 작성 (Release only) // 2. 메모리 덤프 작성 (Release only)
string dmpPath = $"{basePath}.dmp"; string dmpPath = $"{basePath}.dmp";
WriteDumpFile(dmpPath); WriteDumpFile(dmpPath, exceptionPointers);
Log.Fatal("[CrashDump] 덤프 파일 저장 완료 → {Dmp}", dmpPath); Log.Fatal("[CrashDump] 덤프 파일 저장 완료 → {Dmp}", dmpPath);
#endif #endif
} }
@@ -118,7 +119,7 @@ public static class CrashDumpHandler
} }
else else
{ {
AppendException(sb, ex, depth: 0); AppendException(sb, ex, 0);
} }
File.WriteAllText(path, sb.ToString(), Encoding.UTF8); File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
@@ -126,7 +127,7 @@ public static class CrashDumpHandler
private static void AppendException(StringBuilder sb, Exception ex, int depth) private static void AppendException(StringBuilder sb, Exception ex, int depth)
{ {
string indent = new string(' ', depth * 2); string indent = new(' ', depth * 2);
sb.AppendLine($"{indent} Type : {ex.GetType().FullName}"); sb.AppendLine($"{indent} Type : {ex.GetType().FullName}");
sb.AppendLine($"{indent} Message : {ex.Message}"); sb.AppendLine($"{indent} Message : {ex.Message}");
sb.AppendLine($"{indent} Source : {ex.Source}"); sb.AppendLine($"{indent} Source : {ex.Source}");
@@ -159,16 +160,27 @@ public static class CrashDumpHandler
} }
#if !DEBUG #if !DEBUG
// Windows MiniDumpWriteDump P/Invoke // ─── 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)] [DllImport("dbghelp.dll", SetLastError = true)]
private static extern bool MiniDumpWriteDump( private static extern bool MiniDumpWriteDump(
IntPtr hProcess, uint processId, IntPtr hFile, IntPtr hProcess, uint processId, IntPtr hFile,
uint dumpType, IntPtr exceptionParam, uint dumpType, IntPtr exceptionParam,
IntPtr userStreamParam, IntPtr callbackParam); IntPtr userStreamParam, IntPtr callbackParam);
private const uint MiniDumpWithFullMemory = 0x00000002; private const uint MINI_DUMP_WITH_FULL_MEMORY = 0x00000002;
private static void WriteDumpFile(string path) private static void WriteDumpFile(string path, IntPtr exceptionPointers)
{ {
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
@@ -177,19 +189,43 @@ public static class CrashDumpHandler
} }
using Process process = Process.GetCurrentProcess(); using Process process = Process.GetCurrentProcess();
using FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); using FileStream fs = new(path, FileMode.Create, FileAccess.Write, FileShare.None);
bool success = MiniDumpWriteDump( IntPtr exInfoPtr = IntPtr.Zero;
process.Handle, try
(uint)process.Id,
fs.SafeFileHandle.DangerousGetHandle(),
MiniDumpWithFullMemory,
IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
if (!success)
{ {
int err = Marshal.GetLastWin32Error(); if (exceptionPointers != IntPtr.Zero)
Log.Error("[CrashDump] MiniDumpWriteDump 실패 (Win32 Error={Err})", err); {
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 #endif