feat : 덤프 남기는 기능 추가 (셋팅값 힙 덤프)
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Release 빌드에서 메모리 덤프 생성용 -->
|
||||
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.553101" Condition="'$(Configuration)' == 'Release'" />
|
||||
<PackageReference Include="LiteNetLib" Version="2.0.2" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
171
ClientTester/EchoClientTester/Utils/CrashDumpHandler.cs
Normal file
171
ClientTester/EchoClientTester/Utils/CrashDumpHandler.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Serilog;
|
||||
|
||||
#if !DEBUG
|
||||
using Microsoft.Diagnostics.NETCore.Client;
|
||||
#endif
|
||||
|
||||
namespace ClientTester.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// 릴리즈 빌드 크래시 덤프 핸들러
|
||||
///
|
||||
/// 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 ← 메모리 덤프 추가
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Release 빌드에서 메모리 덤프 생성용 -->
|
||||
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.553101" Condition="'$(Configuration)' == 'Release'" />
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="Dapper.Contrib" Version="2.0.78" />
|
||||
<PackageReference Include="LiteNetLib" Version="2.0.2" />
|
||||
|
||||
172
MMOTestServer/ServerLib/Utils/CrashDumpHandler.cs
Normal file
172
MMOTestServer/ServerLib/Utils/CrashDumpHandler.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 릴리즈 빌드 크래시 덤프 핸들러
|
||||
///
|
||||
/// 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 ← 메모리 덤프 추가
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user