// Copyright (c) 2022 Sentry. All Rights Reserved. #include "SentrySubsystemDesktop.h" #include "SentryEventDesktop.h" #include "SentryBreadcrumbDesktop.h" #include "SentryUserDesktop.h" #include "SentryUserFeedbackDesktop.h" #include "SentryScopeDesktop.h" #include "SentryTransactionDesktop.h" #include "SentryTransactionContextDesktop.h" #include "SentryIdDesktop.h" #include "SentryDefines.h" #include "SentrySettings.h" #include "SentryEvent.h" #include "SentryModule.h" #include "SentryBeforeSendHandler.h" #include "SentryTraceSampler.h" #include "Utils/SentryFileUtils.h" #include "Utils/SentryLogUtils.h" #include "Utils/SentryScreenshotUtils.h" #include "Infrastructure/SentryConvertorsDesktop.h" #include "CrashReporter/SentryCrashReporter.h" #include "CrashReporter/SentryCrashContext.h" #include "Transport/SentryTransport.h" #include "Misc/Paths.h" #include "Misc/ScopeLock.h" #include "Misc/CoreDelegates.h" #include "HAL/FileManager.h" #include "Misc/EngineVersionComparison.h" #include "GenericPlatform/GenericPlatformOutputDevices.h" #include "GenericPlatform/GenericPlatformCrashContext.h" #include "HAL/ExceptionHandling.h" #include "UObject/GarbageCollection.h" #include "UObject/UObjectThreadContext.h" #if PLATFORM_WINDOWS #include "Windows/WindowsPlatformMisc.h" #include "Windows/WindowsPlatformCrashContext.h" #endif extern CORE_API bool GIsGPUCrashed; #if USE_SENTRY_NATIVE void PrintVerboseLog(sentry_level_t level, const char *message, va_list args, void *userdata) { char buffer[512]; vsnprintf(buffer, 512, message, args); FString MessageBuf = FString(buffer); // The WER (Windows Error Reporting) module (crashpad_wer.dll) can't be distributed along with other Sentry binaries // within the plugin package due to some UE Marketplace restrictions. Its absence doesn't affect crash capturing // and the corresponding warning can be disregarded if(MessageBuf.Equals(TEXT("crashpad WER handler module not found"))) { return; } #if !NO_LOGGING const FName SentryCategoryName(LogSentrySdk.GetCategoryName()); #else const FName SentryCategoryName(TEXT("LogSentrySdk")); #endif GLog->CategorizedLogf(SentryCategoryName, SentryConvertorsDesktop::SentryLevelToLogVerbosity(level), TEXT("%s"), *MessageBuf); } void PrintCrashLog(const sentry_ucontext_t *uctx) { #if PLATFORM_WINDOWS && !UE_VERSION_OLDER_THAN(5, 0, 0) SentryConvertorsDesktop::SentryCrashContextToString(uctx, GErrorExceptionDescription, UE_ARRAY_COUNT(GErrorExceptionDescription)); const SIZE_T StackTraceSize = 65535; ANSICHAR* StackTrace = (ANSICHAR*)GMalloc->Malloc(StackTraceSize); StackTrace[0] = 0; // Currently raw crash data stored in `uctx` can be utilized for stalk walking on Windows only void* ProgramCounter = uctx->exception_ptrs.ExceptionRecord->ExceptionAddress; FPlatformStackWalk::StackWalkAndDump(StackTrace, StackTraceSize, ProgramCounter); FCString::Strncat(GErrorHist, GErrorExceptionDescription, UE_ARRAY_COUNT(GErrorHist)); FCString::Strncat(GErrorHist, TEXT("\r\n\r\n"), UE_ARRAY_COUNT(GErrorHist)); FCString::Strncat(GErrorHist, ANSI_TO_TCHAR(StackTrace), UE_ARRAY_COUNT(GErrorHist)); #if !NO_LOGGING FDebug::LogFormattedMessageWithCallstack(LogSentrySdk.GetCategoryName(), __FILE__, __LINE__, TEXT("=== Critical error: ==="), GErrorHist, ELogVerbosity::Error); #endif #if !UE_VERSION_OLDER_THAN(5, 1, 0) GLog->Panic(); #endif GMalloc->Free(StackTrace); #endif } sentry_value_t HandleBeforeSend(sentry_value_t event, void *hint, void *closure) { SentrySubsystemDesktop* SentrySubsystem = static_cast(closure); TSharedPtr eventDesktop = MakeShareable(new SentryEventDesktop(event)); SentrySubsystem->GetCurrentScope()->Apply(eventDesktop); FGCScopeGuard GCScopeGuard; USentryEvent* EventToProcess = NewObject(); EventToProcess->InitWithNativeImpl(eventDesktop); USentryEvent* ProcessedEvent = EventToProcess; if(!FUObjectThreadContext::Get().IsRoutingPostLoad) { // Executing UFUNCTION is allowed only when not post-loading ProcessedEvent = SentrySubsystem->GetBeforeSendHandler()->HandleBeforeSend(EventToProcess, nullptr); } return ProcessedEvent ? event : sentry_value_new_null(); } sentry_value_t HandleBeforeCrash(const sentry_ucontext_t *uctx, sentry_value_t event, void *closure) { #if PLATFORM_WINDOWS // Ensures that error message and corresponding callstack flushed to a log file (if available) // before it's attached to the captured crash event and uploaded to Sentry. PrintCrashLog(uctx); #endif SentrySubsystemDesktop* SentrySubsystem = static_cast(closure); SentrySubsystem->TryCaptureScreenshot(); if (GIsGPUCrashed) { IFileManager::Get().Copy(*SentrySubsystem->GetGpuDumpBackupPath(), *SentryFileUtils::GetGpuDumpPath()); } FSentryCrashContext::Get()->Apply(SentrySubsystem->GetCurrentScope()); TSharedPtr eventDesktop = MakeShareable(new SentryEventDesktop(event, true)); SentrySubsystem->GetCurrentScope()->Apply(eventDesktop); if(!IsGarbageCollecting()) { USentryEvent* EventToProcess = NewObject(); EventToProcess->InitWithNativeImpl(eventDesktop); USentryEvent* ProcessedEvent = EventToProcess; if(!FUObjectThreadContext::Get().IsRoutingPostLoad) { // Executing UFUNCTION is allowed only when not post-loading ProcessedEvent = SentrySubsystem->GetBeforeSendHandler()->HandleBeforeSend(EventToProcess, nullptr); } return ProcessedEvent ? event : sentry_value_new_null(); } else { // If crash occured during garbage collection we can't just obtain a GC lock like with normal events // since there is no guarantee it will be ever freed. In this case crash event will be reported // without calling a `beforeSend` handler. return event; } } SentrySubsystemDesktop::SentrySubsystemDesktop() : beforeSend(nullptr) , crashReporter(MakeShareable(new SentryCrashReporter)) , isEnabled(false) , isStackTraceEnabled(true) , isScreenshotAttachmentEnabled(false) { } void SentrySubsystemDesktop::InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryTraceSampler* traceSampler) { beforeSend = beforeSendHandler; scopeStack.Push(MakeShareable(new SentryScopeDesktop())); sentry_options_t* options = sentry_options_new(); if(settings->EnableAutoLogAttachment) { const FString LogFilePath = FGenericPlatformOutputDevices::GetAbsoluteLogFilename(); #if PLATFORM_WINDOWS sentry_options_add_attachmentw(options, *FPaths::ConvertRelativePathToFull(LogFilePath)); #elif PLATFORM_LINUX sentry_options_add_attachment(options, TCHAR_TO_UTF8(*FPaths::ConvertRelativePathToFull(LogFilePath))); #endif } switch (settings->DatabaseLocation) { case ESentryDatabaseLocation::ProjectDirectory: databaseParentPath = FPaths::ProjectDir(); break; case ESentryDatabaseLocation::ProjectUserDirectory: databaseParentPath = FPaths::ProjectUserDir(); break; } if(databaseParentPath.IsEmpty()) { UE_LOG(LogSentrySdk, Warning, TEXT("Unknown Sentry database location. Falling back to FPaths::ProjectUserDir().")); databaseParentPath = FPaths::ProjectUserDir(); } if(settings->AttachScreenshot) { isScreenshotAttachmentEnabled = true; #if PLATFORM_WINDOWS sentry_options_add_attachmentw(options, *GetScreenshotPath()); #elif PLATFORM_LINUX sentry_options_add_attachment(options, TCHAR_TO_UTF8(*GetScreenshotPath())); #endif } if (settings->AttachGpuDump) { #if PLATFORM_WINDOWS sentry_options_add_attachmentw(options, *GetGpuDumpBackupPath()); #elif PLATFORM_LINUX sentry_options_add_attachment(options, TCHAR_TO_UTF8(*GetGpuDumpBackupPath())); #endif } if(settings->UseProxy) { sentry_options_set_http_proxy(options, TCHAR_TO_ANSI(*settings->ProxyUrl)); } if(settings->EnableTracing && settings->SamplingType == ESentryTracesSamplingType::UniformSampleRate) { sentry_options_set_traces_sample_rate(options, settings->TracesSampleRate); } if(settings->EnableTracing && settings->SamplingType == ESentryTracesSamplingType::TracesSampler) { UE_LOG(LogSentrySdk, Warning, TEXT("The Native SDK doesn't currently support sampling functions")); } #if PLATFORM_WINDOWS if(!FSentryModule::Get().IsMarketplaceVersion()) { const FString HandlerPath = GetHandlerPath(); if(!FPaths::FileExists(HandlerPath)) { UE_LOG(LogSentrySdk, Log, TEXT("Crashpad executable couldn't be found so Breakpad will be used instead. " "Please make sure that the plugin was rebuilt to avoid initialization failure.")); } sentry_options_set_handler_pathw(options, *HandlerPath); } #elif PLATFORM_LINUX sentry_options_set_handler_path(options, TCHAR_TO_UTF8(*GetHandlerPath())); #endif #if PLATFORM_WINDOWS sentry_options_set_database_pathw(options, *GetDatabasePath()); #elif PLATFORM_LINUX sentry_options_set_database_path(options, TCHAR_TO_UTF8(*GetDatabasePath())); #endif sentry_options_set_release(options, TCHAR_TO_ANSI(settings->OverrideReleaseName ? *settings->Release : *settings->GetFormattedReleaseName())); sentry_options_set_dsn(options, TCHAR_TO_ANSI(*settings->Dsn)); sentry_options_set_environment(options, TCHAR_TO_ANSI(*settings->Environment)); sentry_options_set_logger(options, PrintVerboseLog, nullptr); sentry_options_set_debug(options, settings->Debug); sentry_options_set_auto_session_tracking(options, settings->EnableAutoSessionTracking); sentry_options_set_sample_rate(options, settings->SampleRate); sentry_options_set_max_breadcrumbs(options, settings->MaxBreadcrumbs); sentry_options_set_before_send(options, HandleBeforeSend, this); sentry_options_set_on_crash(options, HandleBeforeCrash, this); sentry_options_set_shutdown_timeout(options, 3000); #if PLATFORM_LINUX sentry_options_set_transport(options, FSentryTransport::Create()); #endif int initResult = sentry_init(options); UE_LOG(LogSentrySdk, Log, TEXT("Sentry initialization completed with result %d (0 on success)."), initResult); isEnabled = initResult == 0 ? true : false; sentry_clear_crashed_last_run(); #if PLATFORM_WINDOWS && !UE_VERSION_OLDER_THAN(5, 2, 0) FPlatformMisc::SetCrashHandlingType(settings->EnableAutoCrashCapturing ? ECrashHandlingType::Disabled : ECrashHandlingType::Default); #endif isStackTraceEnabled = settings->AttachStacktrace; crashReporter->SetRelease(settings->Release); crashReporter->SetEnvironment(settings->Environment); } void SentrySubsystemDesktop::Close() { isEnabled = false; sentry_close(); scopeStack.Empty(); } bool SentrySubsystemDesktop::IsEnabled() { return isEnabled; } ESentryCrashedLastRun SentrySubsystemDesktop::IsCrashedLastRun() { ESentryCrashedLastRun unrealIsCrashed = ESentryCrashedLastRun::NotEvaluated; switch (sentry_get_crashed_last_run()) { case -1: unrealIsCrashed = ESentryCrashedLastRun::NotEvaluated; break; case 0: unrealIsCrashed = ESentryCrashedLastRun::NotCrashed; break; case 1: unrealIsCrashed = ESentryCrashedLastRun::Crashed; break; default: UE_LOG(LogSentrySdk, Warning, TEXT("Unknown IsCrashedLastRun result. NotEvaluated will be returned.")); } return unrealIsCrashed; } void SentrySubsystemDesktop::AddBreadcrumb(TSharedPtr breadcrumb) { GetCurrentScope()->AddBreadcrumb(breadcrumb); } void SentrySubsystemDesktop::AddBreadcrumbWithParams(const FString& Message, const FString& Category, const FString& Type, const TMap& Data, ESentryLevel Level) { TSharedPtr breadcrumbDesktop = MakeShareable(new SentryBreadcrumbDesktop()); breadcrumbDesktop->SetMessage(Message); breadcrumbDesktop->SetCategory(Category); breadcrumbDesktop->SetType(Type); breadcrumbDesktop->SetData(Data); breadcrumbDesktop->SetLevel(Level); GetCurrentScope()->AddBreadcrumb(breadcrumbDesktop); } void SentrySubsystemDesktop::ClearBreadcrumbs() { GetCurrentScope()->ClearBreadcrumbs(); } TSharedPtr SentrySubsystemDesktop::CaptureMessage(const FString& message, ESentryLevel level) { sentry_value_t sentryEvent = sentry_value_new_message_event(SentryConvertorsDesktop::SentryLevelToNative(level), nullptr, TCHAR_TO_UTF8(*message)); if(isStackTraceEnabled) { sentry_value_set_stacktrace(sentryEvent, nullptr, 0); } sentry_uuid_t id = sentry_capture_event(sentryEvent); return MakeShareable(new SentryIdDesktop(id)); } TSharedPtr SentrySubsystemDesktop::CaptureMessageWithScope(const FString& message, const FSentryScopeDelegate& onScopeConfigure, ESentryLevel level) { FScopeLock Lock(&CriticalSection); TSharedPtr NewLocalScope = MakeShareable(new SentryScopeDesktop(*GetCurrentScope())); onScopeConfigure.ExecuteIfBound(NewLocalScope); scopeStack.Push(NewLocalScope); TSharedPtr Id = CaptureMessage(message, level); scopeStack.Pop(); return Id; } TSharedPtr SentrySubsystemDesktop::CaptureEvent(TSharedPtr event) { TSharedPtr eventDesktop = StaticCastSharedPtr(event); sentry_value_t nativeEvent = eventDesktop->GetNativeObject(); if(isStackTraceEnabled) { sentry_value_set_stacktrace(nativeEvent, nullptr, 0); } sentry_uuid_t id = sentry_capture_event(nativeEvent); return MakeShareable(new SentryIdDesktop(id)); } TSharedPtr SentrySubsystemDesktop::CaptureEventWithScope(TSharedPtr event, const FSentryScopeDelegate& onScopeConfigure) { FScopeLock Lock(&CriticalSection); TSharedPtr NewLocalScope = MakeShareable(new SentryScopeDesktop(*GetCurrentScope())); onScopeConfigure.ExecuteIfBound(NewLocalScope); scopeStack.Push(NewLocalScope); TSharedPtr Id = CaptureEvent(event); scopeStack.Pop(); return Id; } TSharedPtr SentrySubsystemDesktop::CaptureException(const FString& type, const FString& message, int32 framesToSkip) { sentry_value_t exceptionEvent = sentry_value_new_event(); auto StackFrames = FGenericPlatformStackWalk::GetStack(framesToSkip); sentry_value_set_by_key(exceptionEvent, "stacktrace", SentryConvertorsDesktop::CallstackToNative(StackFrames)); sentry_value_t nativeException = sentry_value_new_exception(TCHAR_TO_ANSI(*type), TCHAR_TO_ANSI(*message)); sentry_event_add_exception(exceptionEvent, nativeException); sentry_uuid_t id = sentry_capture_event(exceptionEvent); return MakeShareable(new SentryIdDesktop(id)); } TSharedPtr SentrySubsystemDesktop::CaptureAssertion(const FString& type, const FString& message) { #if PLATFORM_WINDOWS int32 framesToSkip = 7; #else int32 framesToSkip = 5; #endif SentryLogUtils::LogStackTrace(*message, ELogVerbosity::Error, framesToSkip); return CaptureException(type, message, framesToSkip); } TSharedPtr SentrySubsystemDesktop::CaptureEnsure(const FString& type, const FString& message) { #if PLATFORM_WINDOWS && !UE_VERSION_OLDER_THAN(5, 3, 0) int32 framesToSkip = 8; #else int32 framesToSkip = 7; #endif return CaptureException(type, message, framesToSkip); } void SentrySubsystemDesktop::CaptureUserFeedback(TSharedPtr userFeedback) { TSharedPtr userFeedbackDesktop = StaticCastSharedPtr(userFeedback); sentry_capture_user_feedback(userFeedbackDesktop->GetNativeObject()); } void SentrySubsystemDesktop::SetUser(TSharedPtr user) { TSharedPtr userDesktop = StaticCastSharedPtr(user); sentry_set_user(userDesktop->GetNativeObject()); crashReporter->SetUser(userDesktop); } void SentrySubsystemDesktop::RemoveUser() { sentry_remove_user(); crashReporter->RemoveUser(); } void SentrySubsystemDesktop::ConfigureScope(const FSentryScopeDelegate& onConfigureScope) { onConfigureScope.ExecuteIfBound(GetCurrentScope()); } void SentrySubsystemDesktop::SetContext(const FString& key, const TMap& values) { GetCurrentScope()->SetContext(key, values); crashReporter->SetContext(key, values); } void SentrySubsystemDesktop::SetTag(const FString& key, const FString& value) { GetCurrentScope()->SetTagValue(key, value); crashReporter->SetTag(key, value); } void SentrySubsystemDesktop::RemoveTag(const FString& key) { GetCurrentScope()->RemoveTag(key); crashReporter->RemoveTag(key); } void SentrySubsystemDesktop::SetLevel(ESentryLevel level) { GetCurrentScope()->SetLevel(level); } void SentrySubsystemDesktop::StartSession() { sentry_start_session(); } void SentrySubsystemDesktop::EndSession() { sentry_end_session(); } TSharedPtr SentrySubsystemDesktop::StartTransaction(const FString& name, const FString& operation) { sentry_transaction_context_t* transactionContext = sentry_transaction_context_new(TCHAR_TO_ANSI(*name), TCHAR_TO_ANSI(*operation)); sentry_transaction_t* nativeTransaction = sentry_transaction_start(transactionContext, sentry_value_new_null()); return MakeShareable(new SentryTransactionDesktop(nativeTransaction)); } TSharedPtr SentrySubsystemDesktop::StartTransactionWithContext(TSharedPtr context) { TSharedPtr transactionContextDesktop = StaticCastSharedPtr(context); sentry_transaction_t* nativeTransaction = sentry_transaction_start(transactionContextDesktop->GetNativeObject(), sentry_value_new_null()); return MakeShareable(new SentryTransactionDesktop(nativeTransaction)); } TSharedPtr SentrySubsystemDesktop::StartTransactionWithContextAndTimestamp(TSharedPtr context, int64 timestamp) { TSharedPtr transactionContextDesktop = StaticCastSharedPtr(context); sentry_transaction_t* nativeTransaction = sentry_transaction_start_ts(transactionContextDesktop->GetNativeObject(), sentry_value_new_null(), timestamp); return MakeShareable(new SentryTransactionDesktop(nativeTransaction)); } TSharedPtr SentrySubsystemDesktop::StartTransactionWithContextAndOptions(TSharedPtr context, const TMap& options) { UE_LOG(LogSentrySdk, Log, TEXT("Transaction options currently not supported on desktop.")); return StartTransactionWithContext(context); } TSharedPtr SentrySubsystemDesktop::ContinueTrace(const FString& sentryTrace, const TArray& baggageHeaders) { sentry_transaction_context_t* nativeTransactionContext = sentry_transaction_context_new("", "default"); sentry_transaction_context_update_from_header(nativeTransactionContext, "sentry-trace", TCHAR_TO_ANSI(*sentryTrace)); // currently `sentry-native` doesn't have API for `sentry_transaction_context_t` to set `baggageHeaders` TSharedPtr transactionContextDesktop = MakeShareable(new SentryTransactionContextDesktop(nativeTransactionContext)); return transactionContextDesktop; } USentryBeforeSendHandler* SentrySubsystemDesktop::GetBeforeSendHandler() { return beforeSend; } void SentrySubsystemDesktop::TryCaptureScreenshot() const { if(!isScreenshotAttachmentEnabled) { UE_LOG(LogSentrySdk, Log, TEXT("Screenshot attachment is disabled in plugin settings.")); return; } SentryScreenshotUtils::CaptureScreenshot(GetScreenshotPath()); } FString SentrySubsystemDesktop::GetGpuDumpBackupPath() const { static const FString DateTimeString = FDateTime::Now().ToString(); const FString GpuDumpPath = FPaths::Combine(GetDatabasePath(), TEXT("gpudumps"), *FString::Printf(TEXT("UEAftermath-%s.nv-gpudmp"), *DateTimeString));; const FString GpuDumpFullPath = FPaths::ConvertRelativePathToFull(GpuDumpPath); return GpuDumpFullPath; } TSharedPtr SentrySubsystemDesktop::GetCurrentScope() { if(scopeStack.Num() == 0) { UE_LOG(LogSentrySdk, Warning, TEXT("Scope stack is empty.")); return nullptr; } return scopeStack.Top(); } FString SentrySubsystemDesktop::GetHandlerPath() const { #if PLATFORM_WINDOWS const FString HandlerExecutableName = TEXT("crashpad_handler.exe"); #elif PLATFORM_LINUX const FString HandlerExecutableName = TEXT("crashpad_handler"); #endif const FString HandlerPath = FPaths::Combine(FSentryModule::Get().GetBinariesPath(), HandlerExecutableName); const FString HandlerFullPath = FPaths::ConvertRelativePathToFull(HandlerPath); return HandlerFullPath; } FString SentrySubsystemDesktop::GetDatabasePath() const { const FString DatabasePath = FPaths::Combine(databaseParentPath, TEXT(".sentry-native")); const FString DatabaseFullPath = FPaths::ConvertRelativePathToFull(DatabasePath); return DatabaseFullPath; } FString SentrySubsystemDesktop::GetScreenshotPath() const { const FString ScreenshotPath = FPaths::Combine(GetDatabasePath(), TEXT("screenshots"), TEXT("crash_screenshot.png")); const FString ScreenshotFullPath = FPaths::ConvertRelativePathToFull(ScreenshotPath); return ScreenshotFullPath; } #endif