Skip to content

Commit f5cd759

Browse files
Add support for printing crash stack traces to the game log (Microsoft) (#1177)
* Add new plugin setting allowing to opt-in logging during crash handling * Add crash stacktrace logger for Windows * Fix printing native logs during memory-related crash handling * Fix issue with stacktrace logging in packaged dev build * Add memory corruption test util * Fix UE4 compat issues * Clean up includes * Format code * Clean up * Add integration tests for more crash types (stack overflow, memory corruption) * Format code * Update changelog * Update snapshot * Fix issue with logging during running tests in UE 4.27 on Linux * Disable auto init in sample app when running integration tests * Make sleep before crash Android-only step * Refactor desktop tests * Fix PR suggestions * Fix else-if chain in sample app * Add null check for exception address * Rename test args * Transfer ownership of crashed thread handle to logger * Format code * Fix clear * Enable logging during crash handling for tests * Add more checks * Format code * Test generic crash logger * Format code * Fix * Test * Add unified logger implementation fox Windows and Xbox * Test ci * Fix build errors * Update package snapshot * Revert ci trigger * Update changelog * Test sleep for init-only mode * Change max stacktrace depth (test) * Remove dsn input arg parsing from sample * Update readme --------- Co-authored-by: Sentry Github Bot <[email protected]>
1 parent 1a8b189 commit f5cd759

File tree

14 files changed

+199
-134
lines changed

14 files changed

+199
-134
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
### Features
66

7-
- Add support for printing crash stack traces to the game log (Windows) ([#1170](https://github.com/getsentry/sentry-unreal/pull/1170))
7+
- Add support for printing crash stack traces to the game log (Windows, Xbox) ([#1177](https://github.com/getsentry/sentry-unreal/pull/1177))
88

99
### Fixes
1010

integration-test/Integration.Desktop.Tests.ps1

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,9 @@ Describe "Sentry Unreal Desktop Integration Tests (<Platform>)" -ForEach $TestTa
134134
)
135135

136136
# Override default project settings
137-
$appArgs += '-ini:Engine:[/Script/Sentry.SentrySettings]:InitAutomatically=False' # Prevents double initialization
138-
$appArgs += '-ini:Engine:[/Script/Sentry.SentrySettings]:EnableOnCrashLogging=True' # Enables crash logging
139-
$appArgs += '-ini:Engine:[/Script/Sentry.SentrySettings]:EnableAutoLogAttachment=True' # Enables log attachment
137+
$appArgs += "-ini:Engine:[/Script/Sentry.SentrySettings]:Dsn=$script:DSN" # Prevents double initialization
138+
$appArgs += "-ini:Engine:[/Script/Sentry.SentrySettings]:EnableOnCrashLogging=True" # Enables crash logging
139+
$appArgs += "-ini:Engine:[/Script/Sentry.SentrySettings]:EnableAutoLogAttachment=True" # Enables log attachment
140140

141141
# $crashTypeArg triggers specific crash type scenario in the sample app
142142
$script:CrashResult = Invoke-DeviceApp -ExecutablePath $script:AppPath -Arguments ((@($crashTypeArg) + $appArgs) -join ' ')
@@ -240,7 +240,7 @@ Describe "Sentry Unreal Desktop Integration Tests (<Platform>)" -ForEach $TestTa
240240
)
241241

242242
# Override default project settings to avoid double initialization
243-
$appArgs += '-ini:Engine:[/Script/Sentry.SentrySettings]:InitAutomatically=False'
243+
$appArgs += "-ini:Engine:[/Script/Sentry.SentrySettings]:Dsn=$script:DSN"
244244

245245
# -message-capture triggers integration test message scenario in the sample app
246246
$script:MessageResult = Invoke-DeviceApp -ExecutablePath $script:AppPath -Arguments ((@('-message-capture') + $appArgs) -join ' ')

plugin-dev/Source/Sentry/Private/Windows/Infrastructure/WindowsSentryConverters.cpp renamed to plugin-dev/Source/Sentry/Private/Microsoft/Infrastructure/MicrosoftSentryConverters.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// Copyright (c) 2025 Sentry. All Rights Reserved.
22

3-
#include "WindowsSentryConverters.h"
3+
#include "MicrosoftSentryConverters.h"
44

55
#if USE_SENTRY_NATIVE
66

7-
/* static */ void FWindowsSentryConverters::SentryCrashContextToString(const sentry_ucontext_t* crashContext, TCHAR* outErrorString, int32 errorStringBufSize)
7+
/* static */ void FMicrosoftSentryConverters::SentryCrashContextToString(const sentry_ucontext_t* crashContext, TCHAR* outErrorString, int32 errorStringBufSize)
88
{
99
EXCEPTION_RECORD* ExceptionRecord = crashContext->exception_ptrs.ExceptionRecord;
1010
if (!ExceptionRecord)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) 2025 Sentry. All Rights Reserved.
2+
3+
#pragma once
4+
5+
#if USE_SENTRY_NATIVE
6+
7+
#include "CoreMinimal.h"
8+
9+
#include "GenericPlatform/Convenience/GenericPlatformSentryInclude.h"
10+
11+
/**
12+
* Utility class for converting Sentry crash context to human-readable strings.
13+
* Shared across all Microsoft platforms (Windows, Xbox).
14+
*/
15+
class FMicrosoftSentryConverters
16+
{
17+
public:
18+
/**
19+
* Converts Sentry crash context to a human-readable exception string.
20+
*
21+
* @param crashContext - Sentry crash context containing exception information
22+
* @param outErrorString - Output buffer for the error string
23+
* @param errorStringBufSize - Size of the output buffer
24+
*/
25+
static void SentryCrashContextToString(const sentry_ucontext_t* crashContext, TCHAR* outErrorString, int32 errorStringBufSize);
26+
};
27+
28+
#endif // USE_SENTRY_NATIVE

plugin-dev/Source/Sentry/Private/Windows/WindowsCrashLogger.cpp renamed to plugin-dev/Source/Sentry/Private/Microsoft/MicrosoftSentryCrashLogger.cpp

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
// Copyright (c) 2025 Sentry. All Rights Reserved.
22

3-
#include "WindowsCrashLogger.h"
3+
#include "MicrosoftSentryCrashLogger.h"
44

55
#if USE_SENTRY_NATIVE
66

77
#include "SentryDefines.h"
88

9-
#include "Windows/Infrastructure/WindowsSentryConverters.h"
9+
#include "Infrastructure/MicrosoftSentryConverters.h"
1010

1111
#include "CoreGlobals.h"
12+
#include "HAL/PlatformStackWalk.h"
1213
#include "Misc/EngineVersionComparison.h"
1314
#include "Misc/OutputDeviceRedirector.h"
14-
#include "Windows/WindowsPlatformStackWalk.h"
1515

16-
FWindowsCrashLogger::FWindowsCrashLogger()
16+
#include "Microsoft/AllowMicrosoftPlatformTypes.h"
17+
18+
FMicrosoftSentryCrashLogger::FMicrosoftSentryCrashLogger()
1719
: CrashLoggingThread(nullptr)
1820
, CrashLoggingThreadId(0)
1921
, CrashEvent(nullptr)
@@ -23,9 +25,9 @@ FWindowsCrashLogger::FWindowsCrashLogger()
2325
, SharedCrashedThreadHandle(nullptr)
2426
{
2527
// Create synchronization events
26-
CrashEvent = CreateEvent(nullptr, Windows::FALSE, Windows::FALSE, nullptr); // Auto-reset event
27-
CrashCompletedEvent = CreateEvent(nullptr, Windows::FALSE, Windows::FALSE, nullptr); // Auto-reset event
28-
StopThreadEvent = CreateEvent(nullptr, Windows::TRUE, Windows::FALSE, nullptr); // Manual-reset event
28+
CrashEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); // Auto-reset event
29+
CrashCompletedEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); // Auto-reset event
30+
StopThreadEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); // Manual-reset event
2931

3032
if (!CrashEvent || !CrashCompletedEvent || !StopThreadEvent)
3133
{
@@ -55,7 +57,7 @@ FWindowsCrashLogger::FWindowsCrashLogger()
5557
}
5658
}
5759

58-
FWindowsCrashLogger::~FWindowsCrashLogger()
60+
FMicrosoftSentryCrashLogger::~FMicrosoftSentryCrashLogger()
5961
{
6062
if (StopThreadEvent)
6163
{
@@ -89,7 +91,7 @@ FWindowsCrashLogger::~FWindowsCrashLogger()
8991
}
9092
}
9193

92-
bool FWindowsCrashLogger::LogCrash(const sentry_ucontext_t* CrashContext, HANDLE CrashedThreadHandle, DWORD TimeoutMs)
94+
bool FMicrosoftSentryCrashLogger::LogCrash(const sentry_ucontext_t* CrashContext, HANDLE CrashedThreadHandle, DWORD TimeoutMs)
9395
{
9496
if (!CrashLoggingThread || !CrashEvent || !CrashCompletedEvent)
9597
{
@@ -116,9 +118,9 @@ bool FWindowsCrashLogger::LogCrash(const sentry_ucontext_t* CrashContext, HANDLE
116118
return (WaitResult == WAIT_OBJECT_0);
117119
}
118120

119-
DWORD WINAPI FWindowsCrashLogger::CrashLoggingThreadProc(LPVOID Parameter)
121+
DWORD WINAPI FMicrosoftSentryCrashLogger::CrashLoggingThreadProc(LPVOID Parameter)
120122
{
121-
FWindowsCrashLogger* Logger = static_cast<FWindowsCrashLogger*>(Parameter);
123+
FMicrosoftSentryCrashLogger* Logger = static_cast<FMicrosoftSentryCrashLogger*>(Parameter);
122124
if (!Logger)
123125
{
124126
return 1;
@@ -129,7 +131,7 @@ DWORD WINAPI FWindowsCrashLogger::CrashLoggingThreadProc(LPVOID Parameter)
129131
while (true)
130132
{
131133
// Wait for either a crash event or stop event
132-
DWORD WaitResult = WaitForMultipleObjects(2, Events, Windows::FALSE, INFINITE);
134+
DWORD WaitResult = WaitForMultipleObjects(2, Events, FALSE, INFINITE);
133135

134136
if (WaitResult == WAIT_OBJECT_0)
135137
{
@@ -154,7 +156,7 @@ DWORD WINAPI FWindowsCrashLogger::CrashLoggingThreadProc(LPVOID Parameter)
154156
return 0;
155157
}
156158

157-
void FWindowsCrashLogger::PerformCrashLogging()
159+
void FMicrosoftSentryCrashLogger::PerformCrashLogging()
158160
{
159161
// Perform stack walking and fill GErrorHist
160162
// This happens in a separate thread to avoid stack overflow issues
@@ -183,7 +185,7 @@ void FWindowsCrashLogger::PerformCrashLogging()
183185
SharedCrashedThreadHandle = nullptr;
184186
}
185187

186-
void* FWindowsCrashLogger::GetExceptionAddress(const sentry_ucontext_t* CrashContext)
188+
void* FMicrosoftSentryCrashLogger::GetExceptionAddress(const sentry_ucontext_t* CrashContext)
187189
{
188190
if (CrashContext && CrashContext->exception_ptrs.ExceptionRecord)
189191
{
@@ -193,10 +195,10 @@ void* FWindowsCrashLogger::GetExceptionAddress(const sentry_ucontext_t* CrashCon
193195
return nullptr;
194196
}
195197

196-
void FWindowsCrashLogger::WriteToErrorBuffers(const sentry_ucontext_t* CrashContext, HANDLE CrashedThreadHandle)
198+
void FMicrosoftSentryCrashLogger::WriteToErrorBuffers(const sentry_ucontext_t* CrashContext, HANDLE CrashedThreadHandle)
197199
{
198200
// Step 1: Write exception description to GErrorExceptionDescription
199-
FWindowsSentryConverters::SentryCrashContextToString(
201+
FMicrosoftSentryConverters::SentryCrashContextToString(
200202
CrashContext,
201203
GErrorExceptionDescription,
202204
UE_ARRAY_COUNT(GErrorExceptionDescription));
@@ -219,24 +221,19 @@ void FWindowsCrashLogger::WriteToErrorBuffers(const sentry_ucontext_t* CrashCont
219221

220222
if (CrashedThreadHandle && CrashContext->exception_ptrs.ContextRecord)
221223
{
222-
// Create thread context wrapper for safe cross-thread stack walking
223-
void* ContextWrapper = FWindowsPlatformStackWalk::MakeThreadContextWrapper(
224+
// Create platform-specific context wrapper for cross-thread stack walking
225+
FPlatformStackWalk StackWalker;
226+
void* ContextWrapper = StackWalker.MakeThreadContextWrapper(
224227
CrashContext->exception_ptrs.ContextRecord,
225228
CrashedThreadHandle);
226229

227230
if (ContextWrapper)
228231
{
229-
// Perform stack walking using the crashed thread's context
230-
void* ProgramCounter = GetExceptionAddress(CrashContext);
231-
232-
#if !UE_VERSION_OLDER_THAN(5, 0, 0)
233-
FPlatformStackWalk::StackWalkAndDump(StackTrace, StackTraceSize, ProgramCounter, ContextWrapper);
234-
#else
235-
FPlatformStackWalk::StackWalkAndDump(StackTrace, StackTraceSize, 0, ContextWrapper);
236-
#endif
232+
// Platform-specific stack walking (Xbox overrides this)
233+
PerformStackWalk(StackTrace, StackTraceSize, ContextWrapper, CrashContext, CrashedThreadHandle);
237234

238-
// Release the context wrapper
239-
FWindowsPlatformStackWalk::ReleaseThreadContextWrapper(ContextWrapper);
235+
// Release the platform-specific context wrapper
236+
StackWalker.ReleaseThreadContextWrapper(ContextWrapper);
240237
}
241238
}
242239

@@ -248,4 +245,36 @@ void FWindowsCrashLogger::WriteToErrorBuffers(const sentry_ucontext_t* CrashCont
248245
#endif
249246
}
250247

248+
void FMicrosoftSentryCrashLogger::PerformStackWalk(ANSICHAR* StackTrace, SIZE_T StackTraceSize, void* ContextWrapper, const sentry_ucontext_t* CrashContext, HANDLE CrashedThreadHandle)
249+
{
250+
// Unified implementation for all Microsoft platforms (Windows, Xbox)
251+
// Use CaptureThreadStackBackTrace which properly handles context wrappers on all platforms
252+
253+
const uint32 MaxDepth = 64;
254+
uint64 BackTrace[MaxDepth];
255+
256+
// Get the thread ID from the thread handle
257+
DWORD CrashedThreadId = GetThreadId(CrashedThreadHandle);
258+
259+
// CaptureThreadStackBackTrace properly determines for which thread stack walking should be performed
260+
#if !UE_VERSION_OLDER_THAN(5, 0, 0)
261+
uint32 Depth = FPlatformStackWalk::CaptureThreadStackBackTrace(CrashedThreadId, BackTrace, MaxDepth, ContextWrapper);
262+
#else
263+
uint32 Depth = FPlatformStackWalk::CaptureThreadStackBackTrace(CrashedThreadId, BackTrace, MaxDepth);
264+
#endif
265+
266+
// Format the captured addresses into human-readable strings
267+
for (uint32 i = 0; i < Depth; i++)
268+
{
269+
FPlatformStackWalk::ProgramCounterToHumanReadableString(i, BackTrace[i], StackTrace, StackTraceSize, nullptr);
270+
#if !UE_VERSION_OLDER_THAN(5, 6, 0)
271+
FCStringAnsi::StrncatTruncateDest(StackTrace, (int32)StackTraceSize, LINE_TERMINATOR_ANSI);
272+
#else
273+
FCStringAnsi::Strncat(StackTrace, LINE_TERMINATOR_ANSI, (int32)StackTraceSize);
274+
#endif
275+
}
276+
}
277+
278+
#include "Microsoft/HideMicrosoftPlatformTypes.h"
279+
251280
#endif // USE_SENTRY_NATIVE

plugin-dev/Source/Sentry/Private/Windows/WindowsCrashLogger.h renamed to plugin-dev/Source/Sentry/Private/Microsoft/MicrosoftSentryCrashLogger.h

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,22 @@
99
#include "GenericPlatform/Convenience/GenericPlatformSentryInclude.h"
1010

1111
/**
12-
* Class that manages a dedicated thread for safe crash logging on Windows.
12+
* Base class for crash logging on Microsoft platforms (Windows, Xbox).
1313
*
1414
* This class creates a separate thread that performs stack walking and logging
1515
* during crash handling. By using a separate thread, we:
1616
* - Avoid stack overflow issues (thread has its own stack)
1717
* - Can safely walk the crashed thread's stack
1818
* - Write crash information to game log file without risking secondary crashes
1919
*
20-
* The implementation mirrors Unreal's own Crash Reporter design (see WindowsPlatformCrashContext.cpp).
20+
* Platform-specific implementations override context wrapper management functions
21+
* to handle differences in stack walking APIs between platforms.
2122
*/
22-
class FWindowsCrashLogger
23+
class FMicrosoftSentryCrashLogger
2324
{
2425
public:
25-
FWindowsCrashLogger();
26-
~FWindowsCrashLogger();
26+
FMicrosoftSentryCrashLogger();
27+
virtual ~FMicrosoftSentryCrashLogger();
2728

2829
/**
2930
* Logs crash information from a separate thread.
@@ -43,6 +44,20 @@ class FWindowsCrashLogger
4344
*/
4445
bool IsThreadRunning() const { return CrashLoggingThread != nullptr; }
4546

47+
protected:
48+
/**
49+
* Performs stack walking on the crashed thread.
50+
* Uses FPlatformStackWalk::CaptureThreadStackBackTrace which properly handles context wrappers
51+
* on all Microsoft platforms (Windows, Xbox).
52+
*
53+
* @param StackTrace - Output buffer for the stack trace string
54+
* @param StackTraceSize - Size of the output buffer
55+
* @param ContextWrapper - Context wrapper for cross-thread stack walking
56+
* @param CrashContext - The crash context
57+
* @param CrashedThreadHandle - Handle to the crashed thread
58+
*/
59+
void PerformStackWalk(ANSICHAR* StackTrace, SIZE_T StackTraceSize, void* ContextWrapper, const sentry_ucontext_t* CrashContext, HANDLE CrashedThreadHandle);
60+
4661
private:
4762
/**
4863
* Thread procedure that waits for crash events and performs logging.

plugin-dev/Source/Sentry/Private/Microsoft/MicrosoftSentrySubsystem.cpp

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,41 @@
44

55
#if USE_SENTRY_NATIVE
66

7-
#include "Misc/Paths.h"
8-
97
#include "SentryDefines.h"
108
#include "SentryModule.h"
119
#include "SentrySettings.h"
1210

11+
#include "Microsoft/MicrosoftSentryCrashLogger.h"
12+
1313
#include "GenericPlatform/GenericPlatformSentryAttachment.h"
1414

1515
#include "GenericPlatform/GenericPlatformOutputDevices.h"
1616
#include "Misc/EngineVersionComparison.h"
17+
#include "Misc/Paths.h"
18+
19+
#include "Microsoft/AllowMicrosoftPlatformTypes.h"
1720

1821
#if !UE_VERSION_OLDER_THAN(5, 2, 0)
1922
#include "GenericPlatform/GenericPlatformMisc.h"
2023
#endif
2124

2225
void FMicrosoftSentrySubsystem::InitWithSettings(const USentrySettings* Settings, USentryBeforeSendHandler* BeforeSendHandler, USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler, USentryBeforeLogHandler* BeforeLogHandler, USentryTraceSampler* TraceSampler)
2326
{
27+
// Initialize crash logger if enabled
28+
if (Settings->EnableOnCrashLogging)
29+
{
30+
CrashLogger = MakeUnique<FMicrosoftSentryCrashLogger>();
31+
if (CrashLogger->IsThreadRunning())
32+
{
33+
UE_LOG(LogSentrySdk, Log, TEXT("Crash logging enabled - stack traces will be written to game log during crashes"));
34+
}
35+
else
36+
{
37+
UE_LOG(LogSentrySdk, Warning, TEXT("Crash logging requested but thread failed to initialize"));
38+
CrashLogger.Reset();
39+
}
40+
}
41+
2442
FGenericPlatformSentrySubsystem::InitWithSettings(Settings, BeforeSendHandler, BeforeBreadcrumbHandler, BeforeLogHandler, TraceSampler);
2543

2644
#if !UE_VERSION_OLDER_THAN(5, 2, 0)
@@ -84,4 +102,47 @@ void FMicrosoftSentrySubsystem::AddByteAttachment(TSharedPtr<ISentryAttachment>
84102
attachments.Add(platformAttachment);
85103
}
86104

105+
sentry_value_t FMicrosoftSentrySubsystem::OnCrash(const sentry_ucontext_t* uctx, sentry_value_t event, void* closure)
106+
{
107+
// If crash logging is enabled, log the crash callstack to game log
108+
if (CrashLogger)
109+
{
110+
// Get a pseudo-handle to the current thread (the crashed thread)
111+
// We need a real handle for cross-thread stack walking
112+
HANDLE CurrentThreadPseudoHandle = GetCurrentThread();
113+
HANDLE CrashedThreadHandle = nullptr;
114+
115+
// Duplicate the pseudo-handle to get a real handle
116+
if (DuplicateHandle(
117+
GetCurrentProcess(), // Source process
118+
CurrentThreadPseudoHandle, // Source handle (pseudo)
119+
GetCurrentProcess(), // Target process
120+
&CrashedThreadHandle, // Target handle (real)
121+
0, // Desired access (ignored when using DUPLICATE_SAME_ACCESS)
122+
FALSE, // Inherit handle
123+
DUPLICATE_SAME_ACCESS // Options
124+
))
125+
{
126+
// Log the crash (with timeout to prevent hanging)
127+
// This waits for the logging thread to complete stack walking, fill GErrorHist and dump callstack to logs
128+
// Logging thread is responsible for closing CrashedThreadHandle when done
129+
CrashLogger->LogCrash(uctx, CrashedThreadHandle, 5000);
130+
}
131+
}
132+
133+
return FGenericPlatformSentrySubsystem::OnCrash(uctx, event, closure);
134+
}
135+
136+
void FMicrosoftSentrySubsystem::Close()
137+
{
138+
if (CrashLogger)
139+
{
140+
CrashLogger.Reset();
141+
}
142+
143+
FGenericPlatformSentrySubsystem::Close();
144+
}
145+
146+
#include "Microsoft/HideMicrosoftPlatformTypes.h"
147+
87148
#endif // USE_SENTRY_NATIVE

0 commit comments

Comments
 (0)