feat : 덤프 남기는 기능 추가 (셋팅값 힙 덤프)
This commit is contained in:
@@ -9,6 +9,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<!-- Release 빌드에서 메모리 덤프 생성용 -->
|
||||||
|
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.553101" Condition="'$(Configuration)' == 'Release'" />
|
||||||
<PackageReference Include="LiteNetLib" Version="2.0.2" />
|
<PackageReference Include="LiteNetLib" Version="2.0.2" />
|
||||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using ClientTester.DummyService;
|
using ClientTester.DummyService;
|
||||||
using ClientTester.EchoDummyService;
|
using ClientTester.EchoDummyService;
|
||||||
|
using ClientTester.Utils;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
class EcoClientTester
|
class EcoClientTester
|
||||||
@@ -79,6 +80,9 @@ class EcoClientTester
|
|||||||
|
|
||||||
private static async Task Main(string[] args)
|
private static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
|
// 크래시 덤프 핸들러 (Release: .log + .dmp / Debug: .log)
|
||||||
|
CrashDumpHandler.Register();
|
||||||
|
|
||||||
// .MinimumLevel.Warning() // Warning 이상만 출력 배포시
|
// .MinimumLevel.Warning() // Warning 이상만 출력 배포시
|
||||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
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.Game;
|
||||||
using MMOserver.RDB;
|
using MMOserver.RDB;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using ServerLib.Utils;
|
||||||
|
|
||||||
namespace MMOserver;
|
namespace MMOserver;
|
||||||
|
|
||||||
@@ -9,6 +10,9 @@ class Program
|
|||||||
{
|
{
|
||||||
private static async Task Main()
|
private static async Task Main()
|
||||||
{
|
{
|
||||||
|
// 크래시 덤프 핸들러 (Release: .log + .dmp / Debug: .log)
|
||||||
|
CrashDumpHandler.Register();
|
||||||
|
|
||||||
// .MinimumLevel.Warning() // Warning 이상만 출력 배포시
|
// .MinimumLevel.Warning() // Warning 이상만 출력 배포시
|
||||||
|
|
||||||
IConfigurationRoot config = new ConfigurationBuilder()
|
IConfigurationRoot config = new ConfigurationBuilder()
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<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" Version="2.1.66" />
|
||||||
<PackageReference Include="Dapper.Contrib" Version="2.0.78" />
|
<PackageReference Include="Dapper.Contrib" Version="2.0.78" />
|
||||||
<PackageReference Include="LiteNetLib" Version="2.0.2" />
|
<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