diff --git a/MMOTestServer/MMOserver/Dockerfile b/MMOTestServer/MMOserver/Dockerfile index 15ae802..6717832 100644 --- a/MMOTestServer/MMOserver/Dockerfile +++ b/MMOTestServer/MMOserver/Dockerfile @@ -5,7 +5,9 @@ WORKDIR /app FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src +# ServerLib 의존성 포함해서 restore (캐시 레이어 최적화) COPY ["MMOserver/MMOserver.csproj", "MMOserver/"] +COPY ["ServerLib/ServerLib.csproj", "ServerLib/"] RUN dotnet restore "MMOserver/MMOserver.csproj" COPY . . WORKDIR "/src/MMOserver" diff --git a/MMOTestServer/MMOserver/MMOserver.csproj b/MMOTestServer/MMOserver/MMOserver.csproj index 3f9edfb..bc0803a 100644 --- a/MMOTestServer/MMOserver/MMOserver.csproj +++ b/MMOTestServer/MMOserver/MMOserver.csproj @@ -9,6 +9,11 @@ 13 + + portable + true + + diff --git a/MMOTestServer/ServerLib/Utils/CrashDumpHandler.cs b/MMOTestServer/ServerLib/Utils/CrashDumpHandler.cs index 8ee6c3f..b14f8e6 100644 --- a/MMOTestServer/ServerLib/Utils/CrashDumpHandler.cs +++ b/MMOTestServer/ServerLib/Utils/CrashDumpHandler.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; using Serilog; @@ -6,12 +5,17 @@ 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/ 폴더): -/// 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 ← 메모리 덤프 추가 +/// crash_YYYY-MM-DD_HH-mm-ss.log ← 항상 생성 +/// crash_%p_%t.dmp ← CLR이 직접 생성 (정확한 크래시 위치) /// public static class CrashDumpHandler { @@ -27,7 +31,7 @@ public static class CrashDumpHandler } AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; - TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; + TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; Log.Information("[CrashDump] 핸들러 등록 완료 (CrashDir={Dir})", Path.GetFullPath(CRASH_DIR)); } @@ -36,52 +40,35 @@ public static class CrashDumpHandler 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); + WriteCrash(tag, ex); } private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) { - // Task 예외는 파이널라이저 스레드에서 통보되므로 포인터가 없을 수 있음 - IntPtr exceptionPointers = Marshal.GetExceptionPointers(); e.SetObserved(); // 프로세스 종료 방지 - string tag = "[CrashDump] UnobservedTaskException"; - WriteCrash(tag, e.Exception, exceptionPointers); + WriteCrash(tag, e.Exception); } // ─── 핵심 처리 ─────────────────────────────────────────────────────── - private static void WriteCrash(string tag, Exception? ex, IntPtr exceptionPointers) + 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}"); + string logPath = Path.Combine(CRASH_DIR, $"crash_{timestamp}.log"); - // 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] 덤프 저장 실패"); + Log.Error(writeEx, "[CrashDump] 로그 저장 실패"); } finally { @@ -97,21 +84,18 @@ public static class CrashDumpHandler 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($" 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) { @@ -158,75 +142,4 @@ public static class CrashDumpHandler 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 } diff --git a/MMOTestServer/compose.yaml b/MMOTestServer/compose.yaml index 5a308a7..0491508 100644 --- a/MMOTestServer/compose.yaml +++ b/MMOTestServer/compose.yaml @@ -7,4 +7,10 @@ ports: - "9050:9050/udp" # LiteNetLib UDP 포트 - "9500:9500/udp" # LiteNetLib UDP 포트 + environment: + - DOTNET_DbgEnableMiniDump=1 # 크래시 시 덤프 자동 생성 + - DOTNET_DbgMiniDumpType=4 # 4 = Full dump + - DOTNET_DbgMiniDumpName=/app/crashes/crash_%p_%t.dmp + volumes: + - ./crashes:/app/crashes # 덤프/로그 로컬 마운트 restart: unless-stopped