-
-
Save bobbyg603/ec9ce09f0f0b4ca9a8efbb91f5a4b943 to your computer and use it in GitHub Desktop.
BugSplat Unreal Support Response
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright Epic Games, Inc. All Rights Reserved. | |
| using UnrealBuildTool; | |
| public class CrashReportClient : ModuleRules | |
| { | |
| public CrashReportClient(ReadOnlyTargetRules Target) : base(Target) | |
| { | |
| PublicIncludePathModuleNames.Add("Launch"); | |
| PrivateDependencyModuleNames.AddRange( | |
| new string[] | |
| { | |
| "Core", | |
| "CoreUObject", | |
| "ApplicationCore", | |
| "CrashReportCore", | |
| "HTTP", | |
| "Json", | |
| "Projects", | |
| "PakFile", | |
| "XmlParser", | |
| "Analytics", | |
| "AnalyticsET", | |
| "DesktopPlatform", | |
| "LauncherPlatform", | |
| "InputCore", | |
| + "WebBrowser", // BUGSPLAT BG support response | |
| "Slate", | |
| "SlateCore", | |
| "StandaloneRenderer", | |
| "MessageLog", | |
| } | |
| ); | |
| if (Target.Configuration != UnrealTargetConfiguration.Shipping) | |
| { | |
| PrivateIncludePathModuleNames.AddRange( | |
| new string[] { | |
| "SlateReflector", | |
| } | |
| ); | |
| DynamicallyLoadedModuleNames.AddRange( | |
| new string[] { | |
| "SlateReflector", | |
| } | |
| ); | |
| } | |
| PrivateDefinitions.AddRange( | |
| new string[] | |
| { | |
| "CRASH_REPORT_WITH_MTBF=1", | |
| } | |
| ); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright Epic Games, Inc. All Rights Reserved. | |
| #include "CrashReportClient.h" | |
| #include "Misc/CommandLine.h" | |
| #include "Internationalization/Internationalization.h" | |
| #include "Containers/Ticker.h" | |
| #include "CrashReportCoreConfig.h" | |
| #include "Templates/UniquePtr.h" | |
| #include "Async/TaskGraphInterfaces.h" | |
| #include "ILauncherPlatform.h" | |
| #include "LauncherPlatformModule.h" | |
| #include "HAL/PlatformApplicationMisc.h" | |
| #define LOCTEXT_NAMESPACE "CrashReportClient" | |
| struct FCrashReportUtil | |
| { | |
| /** Formats processed diagnostic text by adding additional information about machine and user. */ | |
| static FText FormatDiagnosticText( const FText& DiagnosticText ) | |
| { | |
| TStringBuilder<512> Accounts; | |
| if (const FString LoginId = FPrimaryCrashProperties::Get()->LoginId.AsString(); !LoginId.IsEmpty()) | |
| { | |
| Accounts.Appendf(TEXT("LoginId:%s\n"), *LoginId); | |
| } | |
| if (const FString EpicAccountId= FPrimaryCrashProperties::Get()->EpicAccountId.AsString(); !EpicAccountId.IsEmpty()) | |
| { | |
| Accounts.Appendf(TEXT("EpicAccountId:%s\n"), *EpicAccountId); | |
| } | |
| return FText::Format(LOCTEXT("CrashReportClientCallstackPattern", "{0}\n{1}"), FText::FromString(Accounts.ToString()), DiagnosticText); | |
| } | |
| }; | |
| #if !CRASH_REPORT_UNATTENDED_ONLY | |
| #include "PlatformHttp.h" | |
| #include "Framework/Application/SlateApplication.h" | |
| FCrashReportClient::FCrashReportClient(const FPlatformErrorReport& InErrorReport, bool bImplicitSend) | |
| : DiagnosticText( LOCTEXT("ProcessingReport", "Processing crash report ...") ) | |
| , DiagnoseReportTask(nullptr) | |
| , ErrorReport( InErrorReport ) | |
| , ReceiverUploader(FCrashReportCoreConfig::Get().GetReceiverAddress()) | |
| , DataRouterUploader(FCrashReportCoreConfig::Get().GetDataRouterURL()) | |
| , bShouldWindowBeHidden(false) | |
| , bSendData(bImplicitSend) | |
| , bIsSuccesfullRestart(false) | |
| , bIsUploadComplete(false) | |
| { | |
| if (FPrimaryCrashProperties::Get()->IsValid()) | |
| { | |
| bool bUsePrimaryData = false; | |
| if (FPrimaryCrashProperties::Get()->HasProcessedData()) | |
| { | |
| bUsePrimaryData = true; | |
| } | |
| else | |
| { | |
| if (!ErrorReport.TryReadDiagnosticsFile() && !FParse::Param( FCommandLine::Get(), TEXT( "no-local-diagnosis" ) )) | |
| { | |
| DiagnoseReportTask = new FAsyncTask<FDiagnoseReportWorker>( this ); | |
| DiagnoseReportTask->StartBackgroundTask(); | |
| StartTicker(); | |
| } | |
| else | |
| { | |
| bUsePrimaryData = true; | |
| } | |
| } | |
| if (bUsePrimaryData) | |
| { | |
| const FString CallstackString = FPrimaryCrashProperties::Get()->CallStack.AsString(); | |
| const FString ReportString = FString::Printf( TEXT( "%s\n\n%s" ), *FPrimaryCrashProperties::Get()->ErrorMessage.AsString(), *CallstackString ); | |
| DiagnosticText = FText::FromString( ReportString ); | |
| FormattedDiagnosticText = FCrashReportUtil::FormatDiagnosticText( FText::FromString( ReportString ) ); | |
| } | |
| } | |
| if (bSendData) | |
| { | |
| StartTicker(); | |
| } | |
| } | |
| FCrashReportClient::~FCrashReportClient() | |
| { | |
| if (TickHandle.IsValid()) | |
| { | |
| FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); | |
| TickHandle.Reset(); | |
| } | |
| StopBackgroundThread(); | |
| } | |
| void FCrashReportClient::StopBackgroundThread() | |
| { | |
| if (DiagnoseReportTask) | |
| { | |
| DiagnoseReportTask->EnsureCompletion(); | |
| delete DiagnoseReportTask; | |
| DiagnoseReportTask = nullptr; | |
| } | |
| } | |
| FReply FCrashReportClient::CloseWithoutSending() | |
| { | |
| bSendData = false; | |
| bShouldWindowBeHidden = true; | |
| StartTicker(); | |
| return FReply::Handled(); | |
| } | |
| FReply FCrashReportClient::Close() | |
| { | |
| bShouldWindowBeHidden = true; | |
| return FReply::Handled(); | |
| } | |
| #if PLATFORM_WINDOWS | |
| extern void CopyDiagnosticFilesToClipboard(TConstArrayView<FString> Files); | |
| #endif | |
| #if PLATFORM_WINDOWS | |
| FReply FCrashReportClient::CopyFilesToClipboard() | |
| { | |
| TArray<FString> Files = FPlatformErrorReport(ErrorReport.GetReportDirectory()).GetFilesToUpload(); | |
| CopyDiagnosticFilesToClipboard(Files); | |
| return FReply::Handled(); | |
| } | |
| #endif | |
| +// BUGSPLAT BG support response | |
| +bool FCrashReportClient::IsSendEnabled() const | |
| +{ | |
| + return !bShouldWindowBeHidden && !bSendData; | |
| +} | |
| +EVisibility FCrashReportClient::IsWebBrowserThrobberVisible() const | |
| +{ | |
| + return DataRouterUploader.IsEnabled() && DataRouterUploader.IsUploadCalled() && !DataRouterUploader.IsFinished() ? EVisibility::Visible : EVisibility::Hidden; | |
| +} | |
| +FReply FCrashReportClient::SubmitKeepVisible() | |
| +{ | |
| + bSendData = true; | |
| + StoreCommentAndUpload(); | |
| + bShouldWindowBeHidden = false; | |
| + return FReply::Handled(); | |
| +} | |
| +// BUGSPLAT BG | |
| + | |
| FReply FCrashReportClient::Submit() | |
| { | |
| bSendData = true; | |
| StoreCommentAndUpload(); | |
| bShouldWindowBeHidden = true; | |
| return FReply::Handled(); | |
| } | |
| FReply FCrashReportClient::SubmitAndRestart() | |
| { | |
| Submit(); | |
| // Check for processes that were started from the Launcher using -EpicPortal on the command line | |
| bool bRunFromLauncher = FParse::Param(*FPrimaryCrashProperties::Get()->RestartCommandLine, TEXT("EPICPORTAL")); | |
| const FString CrashedAppPath = ErrorReport.FindCrashedAppPath(); | |
| bool bLauncherRestarted = false; | |
| if (bRunFromLauncher) | |
| { | |
| // Hacky check to see if this is the editor. Not attempting to relaunch the editor using the Launcher because there is no way to pass the project via OpenLauncher() | |
| if (!FPaths::GetCleanFilename(CrashedAppPath).StartsWith(TEXT("UnrealEditor"))) | |
| { | |
| // We'll restart Launcher-run processes by having the installed Launcher handle it | |
| ILauncherPlatform* LauncherPlatform = FLauncherPlatformModule::Get(); | |
| if (LauncherPlatform != nullptr) | |
| { | |
| // Split the path so we can format it as a URI | |
| TArray<FString> PathArray; | |
| CrashedAppPath.Replace(TEXT("//"), TEXT("/")).ParseIntoArray(PathArray, TEXT("/"), false); // WER saves this out on Windows with double slashes as the separator for some reason. | |
| FString CrashedAppPathUri; | |
| // Exclude the last item (the filename). The Launcher currently expects an installed application folder. | |
| for (int32 ItemIndex = 0; ItemIndex < PathArray.Num() - 1; ItemIndex++) | |
| { | |
| FString& PathItem = PathArray[ItemIndex]; | |
| CrashedAppPathUri += FPlatformHttp::UrlEncode(PathItem); | |
| CrashedAppPathUri += TEXT("/"); | |
| } | |
| CrashedAppPathUri.RemoveAt(CrashedAppPathUri.Len() - 1); | |
| // Re-run the application via the Launcher | |
| FOpenLauncherOptions OpenOptions(FString::Printf(TEXT("apps/%s?action=launch"), *CrashedAppPathUri)); | |
| OpenOptions.bSilent = true; | |
| if (LauncherPlatform->OpenLauncher(OpenOptions)) | |
| { | |
| bLauncherRestarted = true; | |
| bIsSuccesfullRestart = true; | |
| } | |
| } | |
| } | |
| } | |
| if (!bLauncherRestarted) | |
| { | |
| // Launcher didn't restart the process so start it ourselves | |
| const FString CommandLineArguments = FPrimaryCrashProperties::Get()->RestartCommandLine; | |
| FPlatformProcess::CreateProc(*CrashedAppPath, *CommandLineArguments, true, false, false, NULL, 0, NULL, NULL); | |
| bIsSuccesfullRestart = true; | |
| } | |
| return FReply::Handled(); | |
| } | |
| FReply FCrashReportClient::CopyCallstack() | |
| { | |
| FPlatformApplicationMisc::ClipboardCopy(*DiagnosticText.ToString()); | |
| return FReply::Handled(); | |
| } | |
| FText FCrashReportClient::GetDiagnosticText() const | |
| { | |
| return FormattedDiagnosticText; | |
| } | |
| void FCrashReportClient::UserCommentChanged(const FText& Comment, ETextCommit::Type CommitType) | |
| { | |
| UserComment = Comment; | |
| // Implement Shift+Enter to commit shortcut | |
| if (CommitType == ETextCommit::OnEnter && FSlateApplication::Get().GetModifierKeys().IsShiftDown()) | |
| { | |
| Submit(); | |
| } | |
| } | |
| void FCrashReportClient::RequestCloseWindow(const TSharedRef<SWindow>& Window) | |
| { | |
| // We may still processing minidump etc. so start the main ticker. | |
| StartTicker(); | |
| bShouldWindowBeHidden = true; | |
| } | |
| bool FCrashReportClient::AreCallstackWidgetsEnabled() const | |
| { | |
| return !IsProcessingCallstack(); | |
| } | |
| EVisibility FCrashReportClient::IsThrobberVisible() const | |
| { | |
| return IsProcessingCallstack() ? EVisibility::Visible : EVisibility::Hidden; | |
| } | |
| void FCrashReportClient::AllowToBeContacted_OnCheckStateChanged( ECheckBoxState NewRadioState ) | |
| { | |
| FCrashReportCoreConfig::Get().SetAllowToBeContacted( NewRadioState == ECheckBoxState::Checked ); | |
| // Refresh PII based on the bAllowToBeContacted flag. | |
| FPrimaryCrashProperties::Get()->UpdateIDs(); | |
| // Save updated properties. | |
| FPrimaryCrashProperties::Get()->Save(); | |
| // Update diagnostics text. | |
| FormattedDiagnosticText = FCrashReportUtil::FormatDiagnosticText( DiagnosticText ); | |
| } | |
| void FCrashReportClient::SendLogFile_OnCheckStateChanged( ECheckBoxState NewRadioState ) | |
| { | |
| FCrashReportCoreConfig::Get().SetSendLogFile( NewRadioState == ECheckBoxState::Checked ); | |
| } | |
| void FCrashReportClient::StartTicker() | |
| { | |
| if (!TickHandle.IsValid()) | |
| { | |
| TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this, &FCrashReportClient::Tick), 1.f); | |
| } | |
| } | |
| void FCrashReportClient::StoreCommentAndUpload() | |
| { | |
| // Write user's comment | |
| ErrorReport.SetUserComment( UserComment ); | |
| StartTicker(); | |
| } | |
| bool FCrashReportClient::Tick(float UnusedDeltaTime) | |
| { | |
| QUICK_SCOPE_CYCLE_COUNTER(STAT_FCrashReportClient_Tick); | |
| // We are waiting for diagnose report task to complete. | |
| if( IsProcessingCallstack() ) | |
| { | |
| return true; | |
| } | |
| if (DiagnoseReportTask) | |
| { | |
| check(DiagnoseReportTask->IsWorkDone()); // Expected when IsProcessingCallstack() returns false. | |
| StopBackgroundThread(); // Free the DiagnoseReportTask to avoid reentering this condition. | |
| FinalizeDiagnoseReportWorker(); // Update the Text displaying call stack information (on game thread as they are visualized in UI) | |
| check(DiagnoseReportTask == nullptr); // Expected after StopBackgroundThread() call. | |
| } | |
| + // Before going further, wait for the an action, either Submit(), CloseWithoutSending() or RequestCloseWindow(). | |
| + if (!bShouldWindowBeHidden && !bSendData) // BUGSPLAT BG support response | |
| + { | |
| + return true; | |
| + } | |
| + | |
| // Implicit send will begin uploading immediately and continue after the window is hidden | |
| if( bSendData ) | |
| { | |
| if (!FCrashUploadBase::IsInitialized()) | |
| { | |
| FCrashUploadBase::StaticInitialize( ErrorReport ); | |
| } | |
| if (ReceiverUploader.IsEnabled()) | |
| { | |
| if (!ReceiverUploader.IsUploadCalled()) | |
| { | |
| // Can be called only when we have all files. | |
| ReceiverUploader.BeginUpload(ErrorReport); | |
| } | |
| // IsWorkDone will always return true here (since ReceiverUploader can't finish until the diagnosis has been sent), but it | |
| // has the side effect of joining the worker thread. | |
| if (!ReceiverUploader.IsFinished()) | |
| { | |
| // More ticks, please | |
| return true; | |
| } | |
| } | |
| if (DataRouterUploader.IsEnabled()) | |
| { | |
| if (!DataRouterUploader.IsUploadCalled()) | |
| { | |
| // Can be called only when we have all files. | |
| DataRouterUploader.BeginUpload(ErrorReport); | |
| } | |
| // IsWorkDone will always return true here (since DataRouterUploader can't finish until the diagnosis has been sent), but it | |
| // has the side effect of joining the worker thread. | |
| if (!DataRouterUploader.IsFinished()) | |
| { | |
| // More ticks, please | |
| return true; | |
| } | |
| } | |
| } | |
| - if (!bShouldWindowBeHidden) | |
| + // BUGSPLAT BG support response | |
| + if (FCrashUploadBase::IsInitialized()) | |
| { | |
| - return true; | |
| + FCrashUploadBase::StaticShutdown(); | |
| } | |
| - if (FCrashUploadBase::IsInitialized()) | |
| + if (!bShouldWindowBeHidden) | |
| { | |
| - FCrashUploadBase::StaticShutdown(); | |
| + // Crash report window is still visible at this point. If DataRouterUploader is enabled, display the support URL to the user | |
| + if (DataRouterUploader.IsEnabled()) | |
| + { | |
| + CrashSupportURLAvailableDelegate.Broadcast(DataRouterUploader.GetBugsplatSupportResponseURL()); | |
| + } | |
| } | |
| + // BUGSPLAT BG | |
| bIsUploadComplete = true; | |
| return false; | |
| } | |
| FString FCrashReportClient::GetCrashDirectory() const | |
| { | |
| return ErrorReport.GetReportDirectory(); | |
| } | |
| void FCrashReportClient::FinalizeDiagnoseReportWorker() | |
| { | |
| // Update properties for the crash. | |
| ErrorReport.SetPrimaryCrashProperties( *FPrimaryCrashProperties::Get() ); | |
| FString CallstackString = FPrimaryCrashProperties::Get()->CallStack.AsString(); | |
| if (CallstackString.IsEmpty()) | |
| { | |
| if (FPrimaryCrashProperties::Get()->PCallStackHash.IsEmpty()) | |
| { | |
| DiagnosticText = LOCTEXT( "NoCallstack", "The system failed to capture the callstack for this crash." ); | |
| } | |
| else | |
| { | |
| DiagnosticText = LOCTEXT( "NoDebuggingSymbols", "You do not have any debugging symbols required to display the callstack for this crash." ); | |
| } | |
| } | |
| else | |
| { | |
| const FString ReportString = FString::Printf( TEXT( "%s\n\n%s" ), *FPrimaryCrashProperties::Get()->ErrorMessage.AsString(), *CallstackString ); | |
| DiagnosticText = FText::FromString( ReportString ); | |
| } | |
| FormattedDiagnosticText = FCrashReportUtil::FormatDiagnosticText( DiagnosticText ); | |
| } | |
| bool FCrashReportClient::IsProcessingCallstack() const | |
| { | |
| return DiagnoseReportTask && !DiagnoseReportTask->IsWorkDone(); | |
| } | |
| FDiagnoseReportWorker::FDiagnoseReportWorker( FCrashReportClient* InCrashReportClient ) | |
| : CrashReportClient( InCrashReportClient ) | |
| {} | |
| void FDiagnoseReportWorker::DoWork() | |
| { | |
| CrashReportClient->ErrorReport.DiagnoseReport(); | |
| } | |
| #endif // !CRASH_REPORT_UNATTENDED_ONLY | |
| #undef LOCTEXT_NAMESPACE |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright Epic Games, Inc. All Rights Reserved. | |
| #pragma once | |
| #include "CoreMinimal.h" | |
| #include "Containers/UnrealString.h" | |
| #include "Internationalization/Text.h" | |
| #include "Stats/Stats.h" | |
| #include "Async/AsyncWork.h" | |
| #include "CrashReportClientApp.h" | |
| #include "CrashUpload.h" | |
| #include "Containers/Ticker.h" | |
| #if !CRASH_REPORT_UNATTENDED_ONLY | |
| #include "Input/Reply.h" | |
| #include "Layout/Visibility.h" | |
| class SWindow; | |
| enum class ECheckBoxState : uint8; | |
| class FCrashReportClient; | |
| +DECLARE_MULTICAST_DELEGATE_OneParam(FCrashSupportURLAvailableDelegate, const FString&); // BUGSPLAT BG support response | |
| /** | |
| * Helper task class to process a crash report in the background | |
| */ | |
| class FDiagnoseReportWorker : public FNonAbandonableTask | |
| { | |
| public: | |
| /** Pointer to the crash report client, used to store the results. */ | |
| FCrashReportClient* CrashReportClient; | |
| /** Initialization constructor. */ | |
| FDiagnoseReportWorker( FCrashReportClient* InCrashReportClient ); | |
| /** | |
| * Do platform-specific work to get information about the crash. | |
| */ | |
| void DoWork(); | |
| FORCEINLINE TStatId GetStatId() const | |
| { | |
| return TStatId(); | |
| } | |
| /** | |
| * @return The name to display in external event viewers | |
| */ | |
| static const TCHAR* Name() | |
| { | |
| return TEXT( "FDiagnoseCrashWorker" ); | |
| } | |
| }; | |
| /** | |
| * Main implementation of the crash report client application | |
| */ | |
| class FCrashReportClient : public TSharedFromThis<FCrashReportClient> | |
| { | |
| friend class FDiagnoseReportWorker; | |
| public: | |
| /** | |
| * Constructor: sets up background diagnosis | |
| * @param ErrorReport Error report to upload | |
| */ | |
| FCrashReportClient( const FPlatformErrorReport& InErrorReport, bool bImplicitSend ); | |
| /** Destructor. */ | |
| virtual ~FCrashReportClient(); | |
| /** Stops processing work in the background. */ | |
| void StopBackgroundThread(); | |
| /** Closes the crash report client without sending any data. Except the startup analytics. */ | |
| FReply CloseWithoutSending(); | |
| + // BUGSPLAT BG support response | |
| + bool IsSendEnabled() const; | |
| + /** Whether the web browser section throbber should be visible. */ | |
| + EVisibility IsWebBrowserThrobberVisible() const; | |
| + /** | |
| + * Respond to the user pressing Submit | |
| + * @return Whether the request was handled | |
| + */ | |
| + FReply SubmitKeepVisible(); | |
| + // BUGSPLAT | |
| + | |
| /** Closes the crash report client allowing data to finish being sent. */ | |
| FReply Close(); | |
| #if PLATFORM_WINDOWS | |
| /** Copy report files to the clipboard for user to immediately request help with. */ | |
| FReply CopyFilesToClipboard(); | |
| #endif | |
| /** | |
| * Respond to the user pressing Submit | |
| * @return Whether the request was handled | |
| */ | |
| FReply Submit(); | |
| /** | |
| * Respond to the user pressing Submit and Restart | |
| * @return Whether the request was handled | |
| */ | |
| FReply SubmitAndRestart(); | |
| /** | |
| * Respond to the user requesting the callstack to be copied to the clipboard | |
| * @return Whether the request was handled | |
| */ | |
| FReply CopyCallstack(); | |
| /** | |
| * Pass on exception and callstack from the platform error report code | |
| * @return Localized text to display | |
| */ | |
| FText GetDiagnosticText() const; | |
| /** | |
| * @return the full path of the crash directory. | |
| */ | |
| FString GetCrashDirectory() const; | |
| /** | |
| * Handle the user updating the user comment text | |
| * @param Comment Text provided by the user | |
| * @param CommitType Event that caused this update | |
| */ | |
| void UserCommentChanged(const FText& Comment, ETextCommit::Type CommitType); | |
| /** | |
| * Handle user closing the main window | |
| * @param Window Main window | |
| */ | |
| void RequestCloseWindow(const TSharedRef<SWindow>& Window); | |
| /** Whether the main window should be hidden. */ | |
| bool ShouldWindowBeHidden() const | |
| { | |
| return bShouldWindowBeHidden; | |
| } | |
| /** Whether the app should enable widgets related to the displayed callstack. */ | |
| bool AreCallstackWidgetsEnabled() const; | |
| /** Whether the throbber should be visible while processing the callstack. */ | |
| EVisibility IsThrobberVisible() const; | |
| /** Returns true if user has elected to restart crashing process, and if it was a success */ | |
| bool GetIsSuccesfullRestart() const { return bIsSuccesfullRestart; } | |
| /** Retruns true if user has elected to close without sending the report. */ | |
| bool WasClosedWithoutSending() const { return !bSendData; } | |
| void AllowToBeContacted_OnCheckStateChanged( ECheckBoxState NewRadioState ); | |
| void SendLogFile_OnCheckStateChanged( ECheckBoxState NewRadioState ); | |
| bool IsUploadComplete() const { return bIsUploadComplete; } | |
| + FCrashSupportURLAvailableDelegate& GetCrashSupportURLAvailableDelegate() { return CrashSupportURLAvailableDelegate; } // BUGSPLAT BG support response | |
| private: | |
| /** | |
| * Write the user's comment to the report and begin uploading the entire report | |
| */ | |
| void StoreCommentAndUpload(); | |
| /** | |
| * Update received every second | |
| * @param DeltaTime Time since last update, unused | |
| * @return Whether the updates should continue | |
| */ | |
| bool Tick(float DeltaTime); | |
| /** | |
| * Begin calling Tick once a second | |
| */ | |
| void StartTicker(); | |
| /** Enqueued from the diagnose report worker thread to be executed on the game thread. */ | |
| void FinalizeDiagnoseReportWorker(); | |
| /** | |
| * @return true if we are still processing a callstack | |
| */ | |
| bool IsProcessingCallstack() const; | |
| /** Comment provided by the user */ | |
| FText UserComment; | |
| /** Exception and call-stack to show, valid once diagnosis task is complete */ | |
| FText DiagnosticText; | |
| /** Formatted diagnostics crash reporter data. */ | |
| FText FormattedDiagnosticText; | |
| /** Background worker to get a callstack from the report */ | |
| FAsyncTask<FDiagnoseReportWorker>* DiagnoseReportTask; | |
| /** Platform code for accessing the report */ | |
| FPlatformErrorReport ErrorReport; | |
| /** Object that uploads report files to the server */ | |
| FCrashUploadToReceiver ReceiverUploader; | |
| /** Object that uploads report files to the server */ | |
| FCrashUploadToDataRouter DataRouterUploader; | |
| /** Whether the main window should be hidden. */ | |
| bool bShouldWindowBeHidden; | |
| /** Whether we send the data. */ | |
| bool bSendData; | |
| /** Store if user has elected to restart crashing process, and if it was a success.*/ | |
| bool bIsSuccesfullRestart; | |
| /** Is the uploading complete. */ | |
| bool bIsUploadComplete; | |
| /** To know if the ticker was started.*/ | |
| FTSTicker::FDelegateHandle TickHandle; | |
| - | |
| + FCrashSupportURLAvailableDelegate CrashSupportURLAvailableDelegate; // BUGSPLAT BG support response | |
| }; | |
| #endif // !CRASH_REPORT_UNATTENDED_ONLY |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright Epic Games, Inc. All Rights Reserved. | |
| #include "CrashReportClientApp.h" | |
| #include "CrashReportClientDefines.h" | |
| #include "Misc/Parse.h" | |
| #include "Misc/CommandLine.h" | |
| #include "Misc/QueuedThreadPool.h" | |
| #include "Misc/ScopeExit.h" | |
| #include "Internationalization/Internationalization.h" | |
| #include "Math/Vector2D.h" | |
| #include "Misc/ConfigCacheIni.h" | |
| #include "GenericPlatform/GenericApplication.h" | |
| #include "Misc/App.h" | |
| #include "Misc/CString.h" | |
| #include "Misc/Paths.h" | |
| #include "Misc/FileHelper.h" | |
| #include "CrashReportCoreConfig.h" | |
| #include "GenericPlatform/GenericPlatformCrashContext.h" | |
| #include "CrashDescription.h" | |
| #include "CrashReportAnalytics.h" | |
| #include "Modules/ModuleManager.h" | |
| #include "HAL/PlatformApplicationMisc.h" | |
| #include "HAL/PlatformCrashContext.h" | |
| #include "HAL/PlatformProcess.h" | |
| #include "HAL/FileManager.h" | |
| #include "IAnalyticsProviderET.h" | |
| #include "XmlParser.h" | |
| #include "Containers/Map.h" | |
| #include "CrashReportAnalyticsSessionSummary.h" | |
| +#include "WebBrowserModule.h" // BUGSPLAT BG support response | |
| #if !CRASH_REPORT_UNATTENDED_ONLY | |
| #include "SCrashReportClient.h" | |
| #include "CrashReportClient.h" | |
| #include "CrashReportClientStyle.h" | |
| #if !UE_BUILD_SHIPPING | |
| #include "ISlateReflectorModule.h" | |
| #endif | |
| #include "Framework/Application/SlateApplication.h" | |
| #endif // !CRASH_REPORT_UNATTENDED_ONLY | |
| #include "CrashReportCoreUnattended.h" | |
| #include "Async/TaskGraphInterfaces.h" | |
| #include "RequiredProgramMainCPPInclude.h" | |
| #include "MainLoopTiming.h" | |
| #include "PlatformErrorReport.h" | |
| #include "XmlFile.h" | |
| #include "RecoveryService.h" | |
| class FRecoveryService; | |
| /** Default main window size */ | |
| const FVector2D InitialWindowDimensions(740, 560); | |
| /** Simple dialog window size */ | |
| const FVector2D InitialSimpleWindowDimensions(740, 300); | |
| /** Average tick rate the app aims for */ | |
| const float IdealTickRate = 30.f; | |
| /** Set this to true in the code to open the widget reflector to debug the UI */ | |
| const bool RunWidgetReflector = false; | |
| //WORKAROUND CL33938220. The module name is ok but the CrashReportClientEditor target is causing a warning that can be safely ignored. | |
| #ifdef __clang__ | |
| #pragma clang diagnostic push | |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" | |
| #elif defined(__GNUC__) | |
| #pragma GCC diagnostic push | |
| #pragma GCC diagnostic ignored "-Wdeprecated-declarations" | |
| #else | |
| #pragma warning(push) | |
| #pragma warning(disable : 4996) // 'function' was declared deprecated | |
| #endif | |
| IMPLEMENT_APPLICATION(CrashReportClient, "CrashReportClient"); | |
| #ifdef __clang__ | |
| #pragma clang diagnostic pop | |
| #elif defined(__GNUC__) | |
| #pragma GCC diagnostic pop | |
| #else | |
| #pragma warning(pop) | |
| #endif | |
| DEFINE_LOG_CATEGORY(CrashReportClientLog); | |
| /** Directory containing the report */ | |
| static TArray<FString> FoundReportDirectoryAbsolutePaths; | |
| /** Name of the game passed via the command line. */ | |
| static FString GameNameFromCmd; | |
| /** GUID of the crash passed via the command line. */ | |
| static FString CrashGUIDFromCmd; | |
| /** When the application invoking CRC cannot be restarted, force hiding the submit and restart button. */ | |
| static bool bForceHideSubmitAndRestartButtonFromCmd = false; | |
| /** If we are implicitly sending its assumed we are also unattended for now */ | |
| static bool bImplicitSendFromCmd = false; | |
| /** If we want to enable analytics */ | |
| static bool AnalyticsEnabledFromCmd = true; | |
| /** If in monitor mode, watch this pid. */ | |
| static uint64 MonitorPid = 0; | |
| /** If in monitor mode, pipe to read data from game. */ | |
| static void* MonitorReadPipe = nullptr; | |
| /** If in monitor mode, pipe to write data to game. */ | |
| static void* MonitorWritePipe = nullptr; | |
| /** If in monitor mode, set to true when the monitored app crashes. */ | |
| static bool bMonitoredAppCrashed = false; | |
| /** In in monitor mode, this is a strong (much stronger than PID) to uniquely tie the CRC process to the monitored process. */ | |
| FString MonitorProcessGroupId; | |
| /** Result of submission of report */ | |
| enum SubmitCrashReportResult { | |
| Failed, // Failed to send report | |
| SuccessClosed, // Succeeded sending report, user has not elected to relaunch | |
| SuccessRestarted, // Succeeded sending report, user has elected to restart process | |
| SuccessContinue, // Succeeded sending report, continue running (if monitor mode). | |
| SuccessDiscarded, // User declined sending the report. | |
| }; | |
| /** | |
| * Look for the report to upload, either in the command line or in the platform's report queue | |
| */ | |
| void ParseCommandLine(const TCHAR* CommandLine) | |
| { | |
| const TCHAR* CommandLineAfterExe = FCommandLine::RemoveExeName(CommandLine); | |
| FoundReportDirectoryAbsolutePaths.Empty(); | |
| // Use the first argument if present and it's not a flag | |
| if (*CommandLineAfterExe) | |
| { | |
| TArray<FString> Switches; | |
| TArray<FString> Tokens; | |
| TMap<FString, FString> Params; | |
| { | |
| FString NextToken; | |
| while (FParse::Token(CommandLineAfterExe, NextToken, false)) | |
| { | |
| if (**NextToken == TCHAR('-')) | |
| { | |
| new(Switches)FString(NextToken.Mid(1)); | |
| } | |
| else | |
| { | |
| new(Tokens)FString(NextToken); | |
| } | |
| } | |
| for (int32 SwitchIdx = Switches.Num() - 1; SwitchIdx >= 0; --SwitchIdx) | |
| { | |
| FString& Switch = Switches[SwitchIdx]; | |
| TArray<FString> SplitSwitch; | |
| if (2 == Switch.ParseIntoArray(SplitSwitch, TEXT("="), true)) | |
| { | |
| Params.Add(SplitSwitch[0], SplitSwitch[1].TrimQuotes()); | |
| Switches.RemoveAt(SwitchIdx); | |
| } | |
| } | |
| } | |
| if (Tokens.Num() > 0) | |
| { | |
| FoundReportDirectoryAbsolutePaths.Add(Tokens[0]); | |
| } | |
| GameNameFromCmd = Params.FindRef(TEXT("AppName")); | |
| CrashGUIDFromCmd = FString(); | |
| if (Params.Contains(TEXT("CrashGUID"))) | |
| { | |
| CrashGUIDFromCmd = Params.FindRef(TEXT("CrashGUID")); | |
| } | |
| if (Switches.Contains(TEXT("ImplicitSend"))) | |
| { | |
| bImplicitSendFromCmd = true; | |
| } | |
| if (Switches.Contains(TEXT("NoAnalytics"))) | |
| { | |
| AnalyticsEnabledFromCmd = false; | |
| } | |
| if (Switches.Contains(TEXT("HideSubmitAndRestart"))) | |
| { | |
| bForceHideSubmitAndRestartButtonFromCmd = true; | |
| } | |
| CrashGUIDFromCmd = Params.FindRef(TEXT("CrashGUID")); | |
| MonitorPid = FPlatformString::Atoi64(*Params.FindRef(TEXT("MONITOR"))); | |
| MonitorReadPipe = (void*) FPlatformString::Atoi64(*Params.FindRef(TEXT("READ"))); | |
| MonitorWritePipe = (void*) FPlatformString::Atoi64(*Params.FindRef(TEXT("WRITE"))); | |
| MonitorProcessGroupId = Params.FindRef(TEXT("ProcessGroupId")); | |
| } | |
| if (FoundReportDirectoryAbsolutePaths.Num() == 0) | |
| { | |
| FPlatformErrorReport::FindMostRecentErrorReports(FoundReportDirectoryAbsolutePaths, FTimespan::FromDays(30)); //FTimespan::FromMinutes(30)); | |
| } | |
| } | |
| /** | |
| * Find the error report folder and check it matches the app name if provided | |
| */ | |
| FPlatformErrorReport LoadErrorReport() | |
| { | |
| if (FoundReportDirectoryAbsolutePaths.Num() == 0) | |
| { | |
| UE_LOG(CrashReportClientLog, Warning, TEXT("No error report found")); | |
| return FPlatformErrorReport(); | |
| } | |
| for (const FString& ReportDirectoryAbsolutePath : FoundReportDirectoryAbsolutePaths) | |
| { | |
| FPlatformErrorReport ErrorReport(ReportDirectoryAbsolutePath); | |
| FString Filename; | |
| // CrashContext.runtime-xml has the precedence over the WER | |
| if (ErrorReport.FindFirstReportFileWithExtension(Filename, FGenericCrashContext::CrashContextExtension)) | |
| { | |
| FPrimaryCrashProperties::Set(new FCrashContext(ReportDirectoryAbsolutePath / Filename)); | |
| } | |
| else if (ErrorReport.FindFirstReportFileWithExtension(Filename, TEXT(".xml"))) | |
| { | |
| FPrimaryCrashProperties::Set(new FCrashWERContext(ReportDirectoryAbsolutePath / Filename)); | |
| } | |
| else | |
| { | |
| continue; | |
| } | |
| #if CRASH_REPORT_UNATTENDED_ONLY | |
| return ErrorReport; | |
| #else | |
| bool NameMatch = false; | |
| if (GameNameFromCmd.IsEmpty() || GameNameFromCmd == FPrimaryCrashProperties::Get()->GameName) | |
| { | |
| NameMatch = true; | |
| } | |
| bool GUIDMatch = false; | |
| if (CrashGUIDFromCmd.IsEmpty() || CrashGUIDFromCmd == FPrimaryCrashProperties::Get()->CrashGUID) | |
| { | |
| GUIDMatch = true; | |
| } | |
| if (NameMatch && GUIDMatch) | |
| { | |
| FString ConfigFilename; | |
| if (ErrorReport.FindFirstReportFileWithExtension(ConfigFilename, FGenericCrashContext::CrashConfigExtension)) | |
| { | |
| FConfigFile CrashConfigFile; | |
| CrashConfigFile.Read(ReportDirectoryAbsolutePath / ConfigFilename); | |
| FCrashReportCoreConfig::Get().SetProjectConfigOverrides(CrashConfigFile); | |
| } | |
| return ErrorReport; | |
| } | |
| #endif | |
| } | |
| // Don't display or upload anything if we can't find the report we expected | |
| return FPlatformErrorReport(); | |
| } | |
| static void OnRequestExit() | |
| { | |
| RequestEngineExit(TEXT("OnRequestExit")); | |
| } | |
| #if !CRASH_REPORT_UNATTENDED_ONLY | |
| SubmitCrashReportResult RunWithUI(FPlatformErrorReport ErrorReport, bool bImplicitSend) | |
| { | |
| // create the platform slate application (what FSlateApplication::Get() returns) | |
| TSharedRef<FSlateApplication> Slate = FSlateApplication::Create(MakeShareable(FPlatformApplicationMisc::CreateApplication())); | |
| // initialize renderer | |
| TSharedRef<FSlateRenderer> SlateRenderer = GetStandardStandaloneRenderer(); | |
| // Grab renderer initialization retry settings from ini | |
| int32 SlateRendererInitRetryCount = 10; | |
| GConfig->GetInt(TEXT("CrashReportClient"), TEXT("UIInitRetryCount"), SlateRendererInitRetryCount, GEngineIni); | |
| double SlateRendererInitRetryInterval = 2.0; | |
| GConfig->GetDouble(TEXT("CrashReportClient"), TEXT("UIInitRetryInterval"), SlateRendererInitRetryInterval, GEngineIni); | |
| // Try to initialize the renderer. It's possible that we launched when the driver crashed so try a few times before giving up. | |
| bool bRendererInitialized = false; | |
| bool bRendererFailedToInitializeAtLeastOnce = false; | |
| do | |
| { | |
| SlateRendererInitRetryCount--; | |
| bRendererInitialized = FSlateApplication::Get().InitializeRenderer(SlateRenderer, true); | |
| if (!bRendererInitialized && SlateRendererInitRetryCount > 0) | |
| { | |
| bRendererFailedToInitializeAtLeastOnce = true; | |
| FPlatformProcess::Sleep(SlateRendererInitRetryInterval); | |
| } | |
| } while (!bRendererInitialized && SlateRendererInitRetryCount > 0); | |
| if (!bRendererInitialized) | |
| { | |
| // Close down the Slate application | |
| FSlateApplication::Shutdown(); | |
| return Failed; | |
| } | |
| else if (bRendererFailedToInitializeAtLeastOnce) | |
| { | |
| // Wait until the driver is fully restored | |
| FPlatformProcess::Sleep(2.0f); | |
| // Update the display metrics | |
| FDisplayMetrics DisplayMetrics; | |
| FDisplayMetrics::RebuildDisplayMetrics(DisplayMetrics); | |
| FSlateApplication::Get().GetPlatformApplication()->OnDisplayMetricsChanged().Broadcast(DisplayMetrics); | |
| } | |
| // Set up the main ticker | |
| FMainLoopTiming MainLoop(IdealTickRate, EMainLoopOptions::UsingSlate); | |
| // set the normal IsEngineExitRequested() when outer frame is closed | |
| FSlateApplication::Get().SetExitRequestedHandler(FSimpleDelegate::CreateStatic(&OnRequestExit)); | |
| // Prepare the custom Slate styles | |
| FCrashReportClientStyle::Initialize(); | |
| // Create the main implementation object | |
| TSharedRef<FCrashReportClient> CrashReportClient = MakeShared<FCrashReportClient>(ErrorReport, bImplicitSend); | |
| // Open up the app window | |
| // bImplicitSend now implies bSimpleDialog i.e. immediately send the report and notify the user without requesting input | |
| TSharedRef<SCrashReportClient> ClientControl = SNew(SCrashReportClient, CrashReportClient, bImplicitSend) | |
| .bHideSubmitAndRestart(bForceHideSubmitAndRestartButtonFromCmd); | |
| FString CrashedAppName = FPrimaryCrashProperties::Get()->IsValid() ? FPrimaryCrashProperties::Get()->GameName : TEXT(""); | |
| // GameNames have taken on a number of prefixes over the years. Try to strip them all off. | |
| if (!CrashedAppName.RemoveFromStart(TEXT("UE4-"))) | |
| { | |
| if (!CrashedAppName.RemoveFromStart(TEXT("UE5-"))) | |
| { | |
| CrashedAppName.RemoveFromStart(TEXT("UE-")); | |
| } | |
| } | |
| CrashedAppName.RemoveFromEnd(TEXT("Game")); | |
| const FString CrashedAppString = NSLOCTEXT("CrashReportClient", "CrashReporterTitle", "Crash Reporter").ToString(); | |
| const FText CrashedAppText = FText::FromString(FString::Printf(TEXT("%s %s"), *CrashedAppName, *CrashedAppString)); | |
| // Get the engine major version to display in title. | |
| FBuildVersion BuildVersion; | |
| uint16 MajorEngineVersion = FBuildVersion::TryRead(FBuildVersion::GetDefaultFileName(), BuildVersion) ? BuildVersion.GetEngineVersion().GetMajor() : 5; | |
| FText WindowTitle = CrashedAppName.IsEmpty() ? | |
| FText::Format(NSLOCTEXT("CrashReportClient", "CrashReportClientAppName", "Unreal Engine {0} Crash Reporter"), MajorEngineVersion) : | |
| CrashedAppText; | |
| TSharedRef<SWindow> Window = FSlateApplication::Get().AddWindow( | |
| SNew(SWindow) | |
| .Title(WindowTitle) | |
| .HasCloseButton(FCrashReportCoreConfig::Get().IsAllowedToCloseWithoutSending()) | |
| .ClientSize(bImplicitSend ? InitialSimpleWindowDimensions : InitialWindowDimensions) | |
| [ | |
| ClientControl | |
| ]); | |
| Window->SetRequestDestroyWindowOverride(FRequestDestroyWindowOverride::CreateSP(CrashReportClient, &FCrashReportClient::RequestCloseWindow)); | |
| // Setting focus seems to have to happen after the Window has been added | |
| FSlateApplication::Get().ClearKeyboardFocus(EFocusCause::Cleared); | |
| #if !UE_BUILD_SHIPPING | |
| // Debugging code | |
| if (RunWidgetReflector) | |
| { | |
| FModuleManager::LoadModuleChecked<ISlateReflectorModule>("SlateReflector").DisplayWidgetReflector(); | |
| } | |
| #endif | |
| // | |
| // The Mac implementation of the window class did not implement HACK_ForceToFront(). | |
| // In order to patch a CRC visiblity issue without breaking binary compatibility on | |
| // the Mac, as well as not changing the behavior on other platforms, we explicity | |
| // pass in the force flag on that platform only. | |
| // | |
| // TODO: Implement HACK_ForceToFront() for macOS and remove bForceBringToFront from here. | |
| // | |
| const bool bForceBringToFront = (false || (PLATFORM_MAC)); | |
| // Bring the window to the foreground as it may be behind the crashed process | |
| Window->HACK_ForceToFront(); | |
| Window->BringToFront(bForceBringToFront); | |
| // loop until the app is ready to quit | |
| - while (!(IsEngineExitRequested() || ClientControl->IsFinished())) | |
| + while (!(IsEngineExitRequested() || (ClientControl->IsFinished() && CrashReportClient->ShouldWindowBeHidden()))) // BUGSPLAT BG support response | |
| { | |
| MainLoop.Tick(); | |
| if (CrashReportClient->ShouldWindowBeHidden()) | |
| { | |
| Window->HideWindow(); | |
| } | |
| } | |
| // Make sure the window is hidden, because it might take a while for the background thread to finish. | |
| Window->HideWindow(); | |
| // Stop the background thread | |
| CrashReportClient->StopBackgroundThread(); | |
| // Clean up the custom styles | |
| FCrashReportClientStyle::Shutdown(); | |
| // Close down the Slate application | |
| FSlateApplication::Shutdown(); | |
| // Detect if ensure, if user has selected to restart or close. | |
| if (CrashReportClient->WasClosedWithoutSending()) | |
| { | |
| return SuccessDiscarded; | |
| } | |
| else if (CrashReportClient->IsUploadComplete()) | |
| { | |
| return CrashReportClient->GetIsSuccesfullRestart() ? SuccessRestarted : (FPrimaryCrashProperties::Get()->bIsEnsure ? SuccessContinue : SuccessClosed); | |
| } | |
| return Failed; | |
| } | |
| #endif // !CRASH_REPORT_UNATTENDED_ONLY | |
| // When we want to implicitly send and use unattended we still want to show a message box of a crash if possible | |
| class FMessageBoxThread : public FRunnable | |
| { | |
| virtual uint32 Run() override | |
| { | |
| // We will not have any GUI for the crash reporter if we are sending implicitly, so pop a message box up at least | |
| if (FApp::CanEverRender() && !FApp::IsUnattended()) | |
| { | |
| FString Body = *NSLOCTEXT("MessageDialog", "ReportCrash_Body", "The application has crashed and will now close. We apologize for the inconvenience.").ToString(); | |
| if (FPrimaryCrashProperties::Get()->IsValid()) | |
| { | |
| Body = FPrimaryCrashProperties::Get()->CrashReporterMessage.AsString(); | |
| } | |
| FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, | |
| *Body, | |
| *NSLOCTEXT("MessageDialog", "ReportCrash_Title", "Application Crash Detected").ToString()); | |
| } | |
| return 0; | |
| } | |
| }; | |
| SubmitCrashReportResult RunUnattended(FPlatformErrorReport ErrorReport, bool bImplicitSend) | |
| { | |
| // Set up the main ticker | |
| FMainLoopTiming MainLoop(IdealTickRate, EMainLoopOptions::CoreTickerOnly); | |
| // In the unattended mode we don't send any PII. | |
| FCrashReportCoreUnattended CrashReportClient(ErrorReport); | |
| ErrorReport.SetUserComment(NSLOCTEXT("CrashReportClient", "UnattendedMode", "Sent in the unattended mode")); | |
| FMessageBoxThread MessageBox; | |
| FRunnableThread* MessageBoxThread = nullptr; | |
| if (bImplicitSend) | |
| { | |
| MessageBoxThread = FRunnableThread::Create(&MessageBox, TEXT("CrashReporter_MessageBox")); | |
| } | |
| // loop until the app is ready to quit | |
| while (!(IsEngineExitRequested() || CrashReportClient.IsUploadComplete())) | |
| { | |
| MainLoop.Tick(); | |
| } | |
| if (bImplicitSend && MessageBoxThread) | |
| { | |
| MessageBoxThread->WaitForCompletion(); | |
| } | |
| // Continue running in case of ensures, otherwise close | |
| return FPrimaryCrashProperties::Get()->bIsEnsure ? SuccessContinue : SuccessClosed; | |
| } | |
| FPlatformErrorReport CollectErrorReport(FRecoveryService* RecoveryService, uint32 Pid, const FSharedCrashContext& SharedCrashContext, void* WritePipe, bool& bOutCrashPortableCallstackAvailable) | |
| { | |
| bOutCrashPortableCallstackAvailable = false; | |
| // @note: This API is only partially implemented on Mac OS and Linux. | |
| FProcHandle ProcessHandle = FPlatformProcess::OpenProcess(Pid); | |
| if (!ProcessHandle.IsValid()) | |
| { | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Report/OpenProcessFail")); | |
| } | |
| else if (SharedCrashContext.CrashingThreadId == 0) | |
| { | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Report/BadCrashThreadId")); | |
| } | |
| else if (SharedCrashContext.NumThreads == CR_MAX_THREADS) | |
| { | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Report/BumpThreadLimits")); | |
| } | |
| // First init the static crash context state | |
| FPlatformCrashContext::InitializeFromContext( | |
| SharedCrashContext.SessionContext, | |
| SharedCrashContext.EnabledPluginsNum > 0 ? &SharedCrashContext.DynamicData[SharedCrashContext.EnabledPluginsOffset] : nullptr, | |
| SharedCrashContext.EngineDataNum > 0 ? &SharedCrashContext.DynamicData[SharedCrashContext.EngineDataOffset] : nullptr, | |
| SharedCrashContext.GameDataNum > 0 ? &SharedCrashContext.DynamicData[SharedCrashContext.GameDataOffset] : nullptr, | |
| &SharedCrashContext.GPUBreadcrumbs | |
| ); | |
| // Next create a crash context for the crashed process. | |
| FPlatformCrashContext CrashContext(SharedCrashContext.CrashType, SharedCrashContext.ErrorMessage); | |
| CrashContext.SetCrashedProcess(ProcessHandle); | |
| CrashContext.SetCrashedThreadId(SharedCrashContext.CrashingThreadId); | |
| CrashContext.SetNumMinidumpFramesToIgnore(0); | |
| FCrashReportAnalyticsSessionSummary::Get().OnCrashReportRemoteStackWalking(); | |
| // Initialize the stack walking for the monitored process (effectively overriding this process stack walking functionality) | |
| FPlatformStackWalk::InitStackWalkingForProcess(ProcessHandle); | |
| TArray<TArray<uint64>> ThreadCallStacks; | |
| ThreadCallStacks.Reserve(SharedCrashContext.NumThreads); | |
| for (uint32 ThreadIdx = 0; ThreadIdx < SharedCrashContext.NumThreads; ThreadIdx++) | |
| { | |
| const uint32 ThreadId = SharedCrashContext.ThreadIds[ThreadIdx]; | |
| TSharedPtr<void> PlatformContext; | |
| #if PLATFORM_WINDOWS | |
| // This code let us acquire the complete portable callstack of the remote process after it crashed on a null pointer function invokation. To successfully walk the | |
| // stack where a null pointer function is called, we need to provide the thread context reported in the crash (the pointer passed to minidump function), otherwise, | |
| // the portable callstack is incomplete. | |
| if (ThreadId == SharedCrashContext.CrashingThreadId) | |
| { | |
| SIZE_T ReadCount = 0; | |
| // On Windows, 'PlatformCrashContext' is a pointer to the EXCEPTION_POINTERS struct. Try to read it from the monitored process memory. | |
| EXCEPTION_POINTERS ExceptPtrs{nullptr, nullptr}; | |
| if (::ReadProcessMemory(ProcessHandle.Get(), SharedCrashContext.PlatformCrashContext, &ExceptPtrs, sizeof(EXCEPTION_POINTERS), &ReadCount) && ReadCount == sizeof(EXCEPTION_POINTERS)) | |
| { | |
| // Try to read memory of the CONTEXT member from the monitored process. | |
| CONTEXT WindowsContext; | |
| FMemory::Memzero(WindowsContext); | |
| if (::ReadProcessMemory(ProcessHandle.Get(), ExceptPtrs.ContextRecord, &WindowsContext, sizeof(CONTEXT), &ReadCount) && ReadCount == sizeof(CONTEXT)) | |
| { | |
| // NOTE: CaptureThreadStackBackTrace() will open and supply the thread handle specified as null here. | |
| PlatformContext = TSharedPtr<void>(FWindowsPlatformStackWalk::MakeThreadContextWrapper(&WindowsContext, nullptr), [](void* Ptr) | |
| { | |
| FWindowsPlatformStackWalk::ReleaseThreadContextWrapper(Ptr); | |
| }); | |
| } | |
| } | |
| } | |
| #endif | |
| uint64 StackFrames[CR_MAX_STACK_FRAMES] = {0}; | |
| uint32 StackFrameCount = FPlatformStackWalk::CaptureThreadStackBackTrace( | |
| ThreadId, | |
| StackFrames, | |
| CR_MAX_STACK_FRAMES, | |
| PlatformContext.Get() | |
| ); | |
| ThreadCallStacks.Emplace(TArray<uint64>(StackFrames, StackFrameCount)); | |
| // CrashContext.AddPortableThreadCallStack( | |
| // SharedCrashContext.ThreadIds[ThreadIdx], | |
| // &SharedCrashContext.ThreadNames[ThreadIdx*CR_MAX_THREAD_NAME_CHARS], | |
| // StackFrames, | |
| // StackFrameCount | |
| // ); | |
| // Add the crashing stack specifically. Is this really needed? | |
| if (ThreadId == SharedCrashContext.CrashingThreadId) | |
| { | |
| const uint64* StackFrameCursor = StackFrames; | |
| // If the address of where the error occurred has been provided then | |
| // we can remove the boilerplate noise from the callstack. | |
| if (uint64 ErrorPc = uint64(SharedCrashContext.ErrorProgramCounter)) | |
| { | |
| uint64 ExceptionPc = uint64(SharedCrashContext.ExceptionProgramCounter); | |
| int32 ExceptionDepth = -1; | |
| for (uint32 i = 0; i < StackFrameCount; ++i) | |
| { | |
| if (StackFrames[i] == ExceptionPc) | |
| { | |
| ExceptionDepth = i; | |
| } | |
| if (StackFrames[i] != ErrorPc) | |
| { | |
| continue; | |
| } | |
| if (ExceptionDepth >= 0) | |
| { | |
| CrashContext.SetNumMinidumpFramesToIgnore(i - ExceptionDepth); | |
| } | |
| StackFrameCursor = StackFrames + i; | |
| StackFrameCount -= i; | |
| break; | |
| } | |
| } | |
| CrashContext.SetPortableCallStack(StackFrameCursor, StackFrameCount); | |
| // A completely missing portable callstack usually means that the crashing process died before CRC could walk the stack. | |
| bOutCrashPortableCallstackAvailable = StackFrameCount > 0; | |
| } | |
| } | |
| { | |
| TArray<FThreadCallStack> Threads; | |
| Threads.Reserve(SharedCrashContext.NumThreads); | |
| for (uint32 ThreadIdx = 0; ThreadIdx < SharedCrashContext.NumThreads; ++ThreadIdx) | |
| { | |
| Threads.Add({ | |
| MakeArrayView(ThreadCallStacks[ThreadIdx]), | |
| &SharedCrashContext.ThreadNames[ThreadIdx*CR_MAX_THREAD_NAME_CHARS], | |
| SharedCrashContext.ThreadIds[ThreadIdx], | |
| }); | |
| } | |
| CrashContext.AddPortableThreadCallStacks(Threads); | |
| } | |
| FCrashReportAnalyticsSessionSummary::Get().OnCrashReportGatheringFiles(); | |
| // If the path is not set it is most likely that we have crashed during static init, in which case we need to construct a directory ourself. | |
| FString ReportDirectoryAbsolutePath(SharedCrashContext.CrashFilesDirectory); | |
| bool DirectoryExists = true; | |
| if (ReportDirectoryAbsolutePath.IsEmpty()) | |
| { | |
| DirectoryExists = FGenericCrashContext::CreateCrashReportDirectory( | |
| SharedCrashContext.SessionContext.CrashGUIDRoot, | |
| 0, | |
| ReportDirectoryAbsolutePath); | |
| } | |
| // Copy platform specific files (e.g. minidump) to output directory if it exists | |
| if (DirectoryExists) | |
| { | |
| CrashContext.CopyPlatformSpecificFiles(*ReportDirectoryAbsolutePath, SharedCrashContext.PlatformCrashContext); | |
| } | |
| FCrashReportAnalyticsSessionSummary::Get().OnCrashReportSignalingAppToResume(); | |
| // At this point the game can continue execution. It is important this happens | |
| // as soon as thread state and minidump has been created, so that ensures cause | |
| // as little hitch as possible. | |
| uint8 ResponseCode[] = { 0xd, 0xe, 0xa, 0xd }; | |
| FPlatformProcess::WritePipe(WritePipe, ResponseCode, sizeof(ResponseCode)); | |
| // Write out the XML file. | |
| const FString CrashContextXMLPath = FPaths::Combine(*ReportDirectoryAbsolutePath, FPlatformCrashContext::CrashContextRuntimeXMLNameW); | |
| CrashContext.SerializeAsXML(*CrashContextXMLPath); | |
| #if CRASH_REPORT_WITH_RECOVERY | |
| if (RecoveryService && | |
| DirectoryExists && | |
| SharedCrashContext.UserSettings.bSendUsageData && | |
| !FPlatformCrashContext::IsTypeContinuable(SharedCrashContext.CrashType) | |
| { | |
| RecoveryService->CollectFiles(ReportDirectoryAbsolutePath); | |
| } | |
| #endif | |
| // If the crash context wasn't implicitely serialized by SerializeAsXML() above, serialize it now. | |
| if (CrashContext.GetBuffer().IsEmpty()) | |
| { | |
| CrashContext.SerializeContentToBuffer(); | |
| } | |
| // Setup the FPrimaryCrashProperties singleton. | |
| const TCHAR* CrashContextBuffer = *CrashContext.GetBuffer(); | |
| FPrimaryCrashProperties::Set(new FCrashContext(ReportDirectoryAbsolutePath / TEXT("CrashContext.runtime-xml"), CrashContextBuffer)); | |
| FPlatformErrorReport ErrorReport(ReportDirectoryAbsolutePath); | |
| // Link the crash to the Editor summary event to help diagnose the abnormal termination quickly. | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(*FPrimaryCrashProperties::Get()->CrashGUID); | |
| // Reset stack walking to allow CRC to implicitly walk its own process and close the monitored process handle. | |
| FPlatformStackWalk::InitStackWalkingForProcess(FProcHandle()); | |
| FPlatformProcess::CloseProc(ProcessHandle); | |
| #if CRASH_REPORT_UNATTENDED_ONLY | |
| return ErrorReport; | |
| #else | |
| FString ConfigFilename; | |
| if (ErrorReport.FindFirstReportFileWithExtension(ConfigFilename, FGenericCrashContext::CrashConfigExtension)) | |
| { | |
| FConfigFile CrashConfigFile; | |
| CrashConfigFile.Read(ReportDirectoryAbsolutePath / ConfigFilename); | |
| FCrashReportCoreConfig::Get().SetProjectConfigOverrides(CrashConfigFile); | |
| } | |
| return ErrorReport; | |
| #endif | |
| } | |
| SubmitCrashReportResult SendErrorReport(FPlatformErrorReport& ErrorReport, | |
| TOptional<bool> bNoDialogOpt = TOptional<bool>(), | |
| TOptional<bool> bImplicitSendOpt = TOptional<bool>()) | |
| { | |
| if (!IsEngineExitRequested() && ErrorReport.HasFilesToUpload() && FPrimaryCrashProperties::Get() != nullptr) | |
| { | |
| const bool bImplicitSend = bImplicitSendOpt.Get(false); | |
| const bool bUnattended = CRASH_REPORT_UNATTENDED_ONLY ? true : bNoDialogOpt.Get(FApp::IsUnattended()); | |
| ErrorReport.SetCrashReportClientVersion(FCrashReportCoreConfig::Get().GetVersion()); | |
| if (bUnattended) | |
| { | |
| return RunUnattended(ErrorReport, bImplicitSend); | |
| } | |
| #if !CRASH_REPORT_UNATTENDED_ONLY | |
| else | |
| { | |
| const SubmitCrashReportResult Result = RunWithUI(ErrorReport, bImplicitSend); | |
| if (Result == Failed) | |
| { | |
| // UI failed to initialize, probably due to driver crash. Send in unattended mode if allowed. | |
| bool bCanSendWhenUIFailedToInitialize = true; | |
| GConfig->GetBool(TEXT("CrashReportClient"), TEXT("CanSendWhenUIFailedToInitialize"), bCanSendWhenUIFailedToInitialize, GEngineIni); | |
| if (bCanSendWhenUIFailedToInitialize && !FCrashReportCoreConfig::Get().IsAllowedToCloseWithoutSending()) | |
| { | |
| return RunUnattended(ErrorReport, bImplicitSend); | |
| } | |
| } | |
| return Result; | |
| } | |
| #endif // !CRASH_REPORT_UNATTENDED_ONLY | |
| } | |
| return Failed; | |
| } | |
| bool IsCrashReportAvailable(uint32 WatchedProcess, FSharedCrashContext& CrashContext, void* ReadPipe) | |
| { | |
| TArray<uint8> Buffer; | |
| // Is data available on the pipe. | |
| if (FPlatformProcess::ReadPipeToArray(ReadPipe, Buffer)) | |
| { | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Pipe/Read")); | |
| // This is to ensure the FSharedCrashContext compiled in the monitored process and this process has the same size. | |
| int32 TotalRead = Buffer.Num(); | |
| // Utility function to copy bytes from a source to a destination buffer. | |
| auto CopyFn = [](const TArray<uint8>& SrcData, uint8* DstIt, uint8* DstEndIt) | |
| { | |
| int32 CopyCount = FMath::Min(SrcData.Num(), static_cast<int32>(DstEndIt - DstIt)); // Limit the number of byte to copy to avoid writing passed the end of the destination. | |
| FPlatformMemory::Memcpy(DstIt, SrcData.GetData(), CopyCount); | |
| return DstIt + CopyCount; // Returns the updated position. | |
| }; | |
| // Iterators to defines the boundaries of the destination buffer in memory. | |
| uint8* SharedCtxIt = reinterpret_cast<uint8*>(&CrashContext); | |
| uint8* SharedCtxEndIt = SharedCtxIt + sizeof(FSharedCrashContext); | |
| // Copy the data already read and update the destination iterator. | |
| SharedCtxIt = CopyFn(Buffer, SharedCtxIt, SharedCtxEndIt); | |
| // Try to consume all the expected data within a defined period of time. | |
| double WaitEndTime = FPlatformTime::Seconds() + 5; | |
| while (SharedCtxIt != SharedCtxEndIt && FPlatformTime::Seconds() <= WaitEndTime) | |
| { | |
| if (FPlatformProcess::ReadPipeToArray(ReadPipe, Buffer)) // This is false if no data is available, but the writer may be still be writing. | |
| { | |
| TotalRead += Buffer.Num(); | |
| SharedCtxIt = CopyFn(Buffer, SharedCtxIt, SharedCtxEndIt); // Copy the data read. | |
| } | |
| else | |
| { | |
| FPlatformProcess::Sleep(0.1); // Give the writer some time. | |
| } | |
| } | |
| if (TotalRead < sizeof(FSharedCrashContext)) | |
| { | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Pipe/NotEnoughData")); | |
| } | |
| else if (TotalRead > sizeof(FSharedCrashContext)) | |
| { | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Pipe/TooMuchData")); | |
| } | |
| else | |
| { | |
| // Record the history of events sent by the Editor to help diagnose abnormal terminations. | |
| switch (CrashContext.CrashType) | |
| { | |
| case ECrashContextType::Assert: | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Pipe/Assert")); | |
| bMonitoredAppCrashed = true; | |
| break; | |
| case ECrashContextType::Ensure: | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Pipe/Ensure")); | |
| break; | |
| case ECrashContextType::Stall: | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Pipe/Stall")); | |
| break; | |
| case ECrashContextType::Crash: | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Pipe/Crash")); | |
| bMonitoredAppCrashed = true; | |
| break; | |
| case ECrashContextType::GPUCrash: | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Pipe/GPUCrash")); | |
| bMonitoredAppCrashed = true; | |
| break; | |
| case ECrashContextType::Hang: | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Pipe/Hang")); | |
| break; | |
| case ECrashContextType::OutOfMemory: | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Pipe/OOM")); | |
| bMonitoredAppCrashed = true; | |
| break; | |
| case ECrashContextType::AbnormalShutdown: | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Pipe/AbnormalShutdown")); | |
| bMonitoredAppCrashed = true; | |
| break; | |
| default: | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Pipe/Unknown")); | |
| break; | |
| } | |
| } | |
| return SharedCtxIt == SharedCtxEndIt; | |
| } | |
| return false; | |
| } | |
| static void DeleteTempCrashContextFile(uint64 ProcessID) | |
| { | |
| const FString SessionContextFile = FGenericCrashContext::GetTempSessionContextFilePath(ProcessID); | |
| FPlatformFileManager::Get().GetPlatformFile().DeleteFile(*SessionContextFile); | |
| } | |
| static void DeleteExpiredTempCrashContext(const FTimespan& ExpirationAge) | |
| { | |
| // Clean up old temp context that were likely left over by crashed/killed CRC (unless the process that wrote it is still alive). | |
| // If by any chances a CRC instance deletes the context that was produced for another CRC instance that is still running after the | |
| // expiration delay, then we may lose the analytics session and not spoof an abnormal shutdown. In general, the consequences are none | |
| // to meaningless, so we are better keeping the user disk clean. | |
| FGenericCrashContext::CleanupTempSessionContextFiles(ExpirationAge); | |
| } | |
| #if CRASH_REPORT_WITH_MTBF | |
| template <typename Type> | |
| bool FindAndParseValue(const TMap<FString, FString>& Map, const FString& Key, Type& OutValue) | |
| { | |
| const FString* ValueString = Map.Find(Key); | |
| if (ValueString != nullptr) | |
| { | |
| TTypeFromString<Type>::FromString(OutValue, **ValueString); | |
| return true; | |
| } | |
| return false; | |
| } | |
| template <size_t Size> | |
| bool FindAndCopyValue(const TMap<FString, FString>& Map, const FString& Key, TCHAR (&OutValue)[Size]) | |
| { | |
| const FString* ValueString = Map.Find(Key); | |
| if (ValueString != nullptr) | |
| { | |
| FCString::Strncpy(OutValue, **ValueString, Size); | |
| return true; | |
| } | |
| return false; | |
| } | |
| static bool LoadTempCrashContextFromFile(FSharedCrashContext& CrashContext, uint64 ProcessID) | |
| { | |
| const FString TempContextFilePath = FGenericCrashContext::GetTempSessionContextFilePath(ProcessID); | |
| FXmlFile File; | |
| if (!File.LoadFile(TempContextFilePath)) | |
| { | |
| return false; | |
| } | |
| TMap<FString, FString> ContextProperties; | |
| for (FXmlNode* Node : File.GetRootNode()->GetChildrenNodes()) | |
| { | |
| ContextProperties.Add(Node->GetTag(), Node->GetContent()); | |
| } | |
| FSessionContext& SessionContext = CrashContext.SessionContext; | |
| FindAndParseValue(ContextProperties, TEXT("SecondsSinceStart"), SessionContext.SecondsSinceStart); | |
| FindAndParseValue(ContextProperties, TEXT("IsInternalBuild"), SessionContext.bIsInternalBuild); | |
| FindAndParseValue(ContextProperties, TEXT("IsPerforceBuild"), SessionContext.bIsPerforceBuild); | |
| FindAndParseValue(ContextProperties, TEXT("IsSourceDistribution"), SessionContext.bIsSourceDistribution); | |
| FindAndCopyValue(ContextProperties, TEXT("GameName"), SessionContext.GameName); | |
| FindAndCopyValue(ContextProperties, TEXT("ExecutableName"), SessionContext.ExecutableName); | |
| FindAndCopyValue(ContextProperties, TEXT("BuildConfiguration"), SessionContext.BuildConfigurationName); | |
| FindAndCopyValue(ContextProperties, TEXT("GameSessionID"), SessionContext.GameSessionID); | |
| FindAndCopyValue(ContextProperties, TEXT("PlatformName"), SessionContext.PlatformName); | |
| FindAndCopyValue(ContextProperties, TEXT("PlatformNameIni"), SessionContext.PlatformNameIni); | |
| FindAndCopyValue(ContextProperties, TEXT("EngineMode"), SessionContext.EngineMode); | |
| FindAndCopyValue(ContextProperties, TEXT("EngineModeEx"), SessionContext.EngineModeEx); | |
| FindAndCopyValue(ContextProperties, TEXT("DeploymentName"), SessionContext.DeploymentName); | |
| FindAndCopyValue(ContextProperties, TEXT("EngineVersion"), SessionContext.EngineVersion); | |
| FindAndCopyValue(ContextProperties, TEXT("EngineCompatibleVersion"), SessionContext.EngineCompatibleVersion); | |
| FindAndCopyValue(ContextProperties, TEXT("CommandLine"), SessionContext.CommandLine); | |
| FindAndParseValue(ContextProperties, TEXT("LanguageLCID"), SessionContext.LanguageLCID); | |
| FindAndCopyValue(ContextProperties, TEXT("AppDefaultLocale"), SessionContext.DefaultLocale); | |
| FindAndCopyValue(ContextProperties, TEXT("BuildVersion"), SessionContext.BuildVersion); | |
| FindAndParseValue(ContextProperties, TEXT("IsUERelease"), SessionContext.bIsUERelease); | |
| FindAndCopyValue(ContextProperties, TEXT("UserName"), SessionContext.UserName); | |
| FindAndCopyValue(ContextProperties, TEXT("EpicAccountId"), SessionContext.EpicAccountId); | |
| FindAndCopyValue(ContextProperties, TEXT("BaseDir"), SessionContext.BaseDir); | |
| FindAndCopyValue(ContextProperties, TEXT("RootDir"), SessionContext.RootDir); | |
| FindAndCopyValue(ContextProperties, TEXT("LoginId"), SessionContext.LoginIdStr); | |
| FindAndCopyValue(ContextProperties, TEXT("EpicAccountId"), SessionContext.EpicAccountId); | |
| FindAndCopyValue(ContextProperties, TEXT("UserActivityHint"), SessionContext.UserActivityHint); | |
| FindAndParseValue(ContextProperties, TEXT("CrashDumpMode"), SessionContext.CrashDumpMode); | |
| FindAndCopyValue(ContextProperties, TEXT("GameStateName"), SessionContext.GameStateName); | |
| FindAndParseValue(ContextProperties, TEXT("Misc.NumberOfCores"), SessionContext.NumberOfCores); | |
| FindAndParseValue(ContextProperties, TEXT("Misc.NumberOfCoresIncludingHyperthreads"), SessionContext.NumberOfCoresIncludingHyperthreads); | |
| FindAndCopyValue(ContextProperties, TEXT("Misc.CPUVendor"), SessionContext.CPUVendor); | |
| FindAndCopyValue(ContextProperties, TEXT("Misc.CPUBrand"), SessionContext.CPUBrand); | |
| FindAndCopyValue(ContextProperties, TEXT("Misc.PrimaryGPUBrand"), SessionContext.PrimaryGPUBrand); | |
| FindAndCopyValue(ContextProperties, TEXT("Misc.OSVersionMajor"), SessionContext.OsVersion); | |
| FindAndCopyValue(ContextProperties, TEXT("Misc.OSVersionMinor"), SessionContext.OsSubVersion); | |
| FindAndCopyValue(ContextProperties, TEXT("Misc.AnticheatProvider"), SessionContext.AnticheatProvider); | |
| FindAndParseValue(ContextProperties, TEXT("Misc.IsStuck"), SessionContext.bIsStuck); | |
| FindAndParseValue(ContextProperties, TEXT("MemoryStats.AvailablePhysical"), SessionContext.MemoryStats.AvailablePhysical); | |
| FindAndParseValue(ContextProperties, TEXT("MemoryStats.AvailableVirtual"), SessionContext.MemoryStats.AvailableVirtual); | |
| FindAndParseValue(ContextProperties, TEXT("MemoryStats.UsedPhysical"), SessionContext.MemoryStats.UsedPhysical); | |
| FindAndParseValue(ContextProperties, TEXT("MemoryStats.PeakUsedPhysical"), SessionContext.MemoryStats.PeakUsedPhysical); | |
| FindAndParseValue(ContextProperties, TEXT("MemoryStats.UsedVirtual"), SessionContext.MemoryStats.UsedVirtual); | |
| FindAndParseValue(ContextProperties, TEXT("MemoryStats.PeakUsedVirtual"), SessionContext.MemoryStats.PeakUsedVirtual); | |
| FindAndParseValue(ContextProperties, TEXT("MemoryStats.bIsOOM"), SessionContext.bIsOOM); | |
| FindAndParseValue(ContextProperties, TEXT("MemoryStats.OOMAllocationSize"), SessionContext.OOMAllocationSize); | |
| FindAndParseValue(ContextProperties, TEXT("MemoryStats.OOMAllocationAlignment"), SessionContext.OOMAllocationAlignment); | |
| // user settings | |
| FUserSettingsContext& UserSettings = CrashContext.UserSettings; | |
| FindAndParseValue(ContextProperties, TEXT("NoDialog"), UserSettings.bNoDialog); | |
| FindAndParseValue(ContextProperties, TEXT("SendUnattendedBugReports"), UserSettings.bSendUnattendedBugReports); | |
| FindAndParseValue(ContextProperties, TEXT("SendUsageData"), UserSettings.bSendUsageData); | |
| FindAndCopyValue(ContextProperties, TEXT("LogFilePath"), UserSettings.LogFilePath); | |
| return true; | |
| } | |
| FString FormatExitCode(int32 ExitCode) | |
| { | |
| // Translate common exit codes. | |
| auto GetExitCodeName = [](int32 Code) -> const TCHAR* | |
| { | |
| #if PLATFORM_WINDOWS | |
| switch (Code) | |
| { | |
| case 1073807364: return TEXT("DBG_TERMINATE_PROCESS"); // Typically when the user logs out or the system is shutting down. | |
| case -1073740286: return TEXT("STATUS_FAIL_FAST_EXCEPTION"); | |
| case -1073740771: return TEXT("STATUS_FATAL_USER_CALLBACK_EXCEPTION"); | |
| case -1073740791: return TEXT("STATUS_STACK_BUFFER_OVERRUN"); | |
| case -1073740940: return TEXT("STATUS_HEAP_CORRUPTION"); | |
| case -1073741395: return TEXT("STATUS_FATAL_MEMORY_EXHAUSTION"); | |
| case -1073741510: return TEXT("STATUS_CONTROL_C_EXIT"); | |
| case -1073741571: return TEXT("STATUS_STACK_OVERFLOW"); | |
| case -1073741676: return TEXT("STATUS_INTEGER_DIVIDE_BY_ZERO"); | |
| case -1073741795: return TEXT("STATUS_ILLEGAL_INSTRUCTION"); | |
| case -1073741811: return TEXT("STATUS_INVALID_PARAMETER"); | |
| case -1073741818: return TEXT("STATUS_IN_PAGE_ERROR"); | |
| case -1073741819: return TEXT("STATUS_ACCESS_VIOLATION"); | |
| default: return nullptr; | |
| } | |
| #else | |
| return nullptr; | |
| #endif | |
| }; | |
| const TCHAR* ExitCodeName = GetExitCodeName(ExitCode); | |
| if (ExitCodeName) | |
| { | |
| return FString::Printf(TEXT("%d (%s)"), ExitCode, ExitCodeName); | |
| } | |
| return LexToString(ExitCode); | |
| } | |
| static void HandleAbnormalShutdown(FSharedCrashContext& CrashContext, uint64 ProcessID, void* WritePipe, const TSharedPtr<FRecoveryService>& RecoveryService, const TOptional<int32>& ExitCode) | |
| { | |
| CrashContext.CrashType = ECrashContextType::AbnormalShutdown; | |
| if (ExitCode.IsSet()) | |
| { | |
| // Set the error message like: AbnormalShutdown - ExitCode: -1073741571 (STATUS_STACK_OVERFLOW) | |
| FCString::Sprintf(CrashContext.ErrorMessage, TEXT("AbnormalShutdown - ExitCode: %s"), *FormatExitCode(*ExitCode)); | |
| } | |
| else | |
| { | |
| FCString::Strcpy(CrashContext.ErrorMessage, TEXT("AbnormalShutdown")); | |
| } | |
| // Normally, the CrashGUIDRoot is generated by the Editor/Engine and a counter is appended to it. Starting at zero, the counter is incremented after each ensure/crash by the Editor/Engine. | |
| // In this cases, the crash doesn't originate from the Editor/Engine, but CRC. The Editor/Engine CrashGUIDRoot isn't serialized in the temp context file so we need to generate a new one. | |
| // This also ensure we don't collide with the one emitted by the Editor as the counter part in this process would also start at zero. | |
| FGuid CrashGUID = FGuid::NewGuid(); | |
| FString IniPlatformName(FPlatformProperties::IniPlatformName()); // To convert from char* to TCHAR* | |
| FCString::Strcpy(CrashContext.SessionContext.CrashGUIDRoot, *FString::Printf(TEXT("%s%s-%s"), FGenericCrashContext::CrashGUIDRootPrefix, *IniPlatformName, *CrashGUID.ToString(EGuidFormats::Digits))); | |
| IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); | |
| // create a temporary crash directory | |
| const FString TempCrashDirectory = FPlatformProcess::UserTempDir() / FString::Printf(TEXT("UECrashContext-%d"), ProcessID); | |
| FCString::Strcpy(CrashContext.CrashFilesDirectory, *TempCrashDirectory); | |
| if (PlatformFile.CreateDirectory(CrashContext.CrashFilesDirectory)) | |
| { | |
| // copy the log file to the temporary directory | |
| const FString LogDestination = TempCrashDirectory / FPaths::GetCleanFilename(CrashContext.UserSettings.LogFilePath); | |
| PlatformFile.CopyFile(*LogDestination, CrashContext.UserSettings.LogFilePath); | |
| // This crash is not a real one, but one to capture the Editor logs in case of abnormal termination. | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("SyntheticCrash")); | |
| bool bPortableCallstackAvailable = false; // Should always be false here. The process is dead. | |
| FPlatformErrorReport ErrorReport = CollectErrorReport(RecoveryService.Get(), ProcessID, CrashContext, WritePipe, bPortableCallstackAvailable); | |
| SendErrorReport(ErrorReport, /*bNoDialog*/ true); | |
| // delete the temporary directory | |
| PlatformFile.DeleteDirectoryRecursively(*TempCrashDirectory); | |
| if (CrashContext.UserSettings.bSendUsageData) | |
| { | |
| // If analytics is enabled make sure they are submitted now. | |
| FCrashReportAnalytics::GetProvider().BlockUntilFlushed(5.0f); | |
| } | |
| } | |
| } | |
| #endif | |
| void RunCrashReportClient(const TCHAR* CommandLine) | |
| { | |
| #if !PLATFORM_MAC | |
| FTaskTagScope ThreadScope(ETaskTag::EGameThread); // Main thread is the game thread. | |
| #endif | |
| #if !(UE_BUILD_SHIPPING) | |
| // If "-waitforattach" or "-WaitForDebugger" was specified, halt startup and wait for a debugger to attach before continuing | |
| if (FParse::Param(CommandLine, TEXT("waitforattach")) || FParse::Param(CommandLine, TEXT("WaitForDebugger"))) | |
| { | |
| while (!FPlatformMisc::IsDebuggerPresent()); | |
| UE_DEBUG_BREAK(); | |
| } | |
| #endif | |
| // Override the stack size for the thread pool. | |
| FQueuedThreadPool::OverrideStackSize = 256 * 1024; | |
| // Initialize the engine. | |
| FString FinalCommandLine(CommandLine); | |
| #if CRASH_REPORT_WITH_RECOVERY | |
| // -Messaging enables MessageBus transports required by Concert (Recovery Service). | |
| FinalCommandLine += TEXT(" -Messaging -EnablePlugins=\"UdpMessaging,ConcertSyncServer\""); | |
| #endif | |
| GEngineLoop.PreInit(*FinalCommandLine); | |
| check(GConfig && GConfig->IsReadyForUse()); | |
| // Increase the HttpSendTimeout to 5 minutes | |
| GConfig->SetFloat(TEXT("HTTP"), TEXT("HttpSendTimeout"), 5 * 60.0f, GEngineIni); | |
| // Make sure all UObject classes are registered and default properties have been initialized | |
| ProcessNewlyLoadedUObjects(); | |
| // Tell the module manager is may now process newly-loaded UObjects when new C++ modules are loaded | |
| FModuleManager::Get().StartProcessingNewlyLoadedObjects(); | |
| // Load internal Concert plugins in the pre-default phase | |
| IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::PreDefault); | |
| // Load Concert Sync plugins in default phase | |
| IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::Default); | |
| + // Load the WebBrowser module | |
| + IWebBrowserModule::Get().StartupModule(); // BUGSPLAT BG support response | |
| + | |
| // Initialize config. | |
| FCrashReportCoreConfig::Get(); | |
| // Find the report to upload in the command line arguments | |
| ParseCommandLine(CommandLine); | |
| FPlatformErrorReport::Init(); | |
| if (MonitorPid == 0) // Does not monitor any process. | |
| { | |
| if (AnalyticsEnabledFromCmd) | |
| { | |
| FCrashReportAnalytics::Initialize(); | |
| } | |
| // Load error report generated by the process from disk | |
| FPlatformErrorReport ErrorReport = LoadErrorReport(); | |
| // Apply project and crash overrides | |
| FString CrashSettingsIni; | |
| ErrorReport.FindFirstReportFileWithExtension(CrashSettingsIni, FGenericCrashContext::CrashConfigExtension); | |
| FCrashReportCoreConfig::Get().ApplyProjectOverrides(FPaths::Combine(ErrorReport.GetReportDirectory(), CrashSettingsIni)); | |
| // At this point all overrides has been applied | |
| FCrashReportCoreConfig::Get().PrintSettingsToLog(); | |
| SendErrorReport(ErrorReport, FApp::IsUnattended(), bImplicitSendFromCmd); | |
| if (AnalyticsEnabledFromCmd) | |
| { | |
| FCrashReportAnalytics::Shutdown(); | |
| } | |
| } | |
| else // Launched in 'service mode - watches/serves a process' | |
| { | |
| FCrashReportAnalyticsSessionSummary::Get().Initialize(MonitorProcessGroupId, MonitorPid); | |
| if (!MonitorWritePipe) | |
| { | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("CRC/NoWritePipe")); | |
| } | |
| if (!MonitorReadPipe) | |
| { | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("CRC/NoReadPipe")); | |
| } | |
| const int32 IdealFramerate = 10; | |
| double PrevLoopStartTime = FPlatformTime::Seconds(); | |
| const float IdealFrameTime = 1.0f / IdealFramerate; | |
| TSharedPtr<FRecoveryService> RecoveryServicePtr; // Note: Shared rather than Unique due to FRecoveryService only being a forward declaration in some builds | |
| #if CRASH_REPORT_WITH_RECOVERY | |
| // Starts the disaster recovery service. This records transactions and allows users to recover from previous crashes. | |
| RecoveryServicePtr = MakeShared<FRecoveryService>(MonitorPid); | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Recovery/Started")); | |
| #endif | |
| // Open the process with a restricted set of permissions (for security reasons). | |
| FProcHandle MonitoredProcess = OpenProcessForMonitoring(MonitorPid); | |
| // Loop until the monitored process dies. | |
| while (MonitoredProcess.IsValid() && FPlatformProcess::IsProcRunning(MonitoredProcess)) | |
| { | |
| const double CurrLoopStartTime = FPlatformTime::Seconds(); | |
| if (MonitorWritePipe && MonitorReadPipe) | |
| { | |
| // Check if the monitored process signaled a crash or an ensure, read the pipe data to avoid blocking the writer, but process the data only if CRC wasn't requested to exit. | |
| // This purposedly ignores any ensure that could be piped out just after a crash. (The way concurrent crash/ensures are handled/reported make this unlikely, but possible). | |
| FSharedCrashContext CrashContext; | |
| if (IsCrashReportAvailable(MonitorPid, CrashContext, MonitorReadPipe) && !IsEngineExitRequested()) | |
| { | |
| FCrashReportAnalyticsSessionSummary::Get().OnCrashReportStarted(CrashContext.CrashType, CrashContext.ErrorMessage); | |
| const bool bReportCrashAnalyticInfo = CrashContext.UserSettings.bSendUsageData; | |
| if (bReportCrashAnalyticInfo) | |
| { | |
| FCrashReportAnalytics::Initialize(CrashContext.SessionContext.EpicAccountId); | |
| } | |
| FCrashReportAnalyticsSessionSummary::Get().OnCrashReportCollecting(); | |
| // Build error report in memory. | |
| bool bCrashedThreadCallstackAvailable = false; | |
| FPlatformErrorReport ErrorReport = CollectErrorReport(RecoveryServicePtr.Get(), MonitorPid, CrashContext, MonitorWritePipe, bCrashedThreadCallstackAvailable); | |
| // Apply project and crash overrides | |
| FString CrashSettingsIni; | |
| ErrorReport.FindFirstReportFileWithExtension(CrashSettingsIni, FGenericCrashContext::CrashConfigExtension); | |
| FCrashReportCoreConfig::Get().ApplyProjectOverrides(FPaths::Combine(ErrorReport.GetReportDirectory(), CrashSettingsIni)); | |
| // At this point all overrides has been applied | |
| FCrashReportCoreConfig::Get().PrintSettingsToLog(); | |
| // Log cases where the PCallstack is missing. For analytics, this event hints that the remote app exited before CRC could walk the process stack and possibly means | |
| // that the timeout in the crashing process waiting for CRC to reply 'continue' is too short. | |
| if (!bCrashedThreadCallstackAvailable) | |
| { | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Report/NoPCallstack")); | |
| } | |
| #if CRASH_REPORT_WITH_RECOVERY | |
| if (RecoveryServicePtr && !FPrimaryCrashProperties::Get()->bIsEnsure) | |
| { | |
| // Shutdown the recovery service. This will releases the recovery database file lock (not sharable) and let a new instance take it and offer the user to recover. | |
| FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Recovery/Shutdown")); | |
| RecoveryServicePtr.Reset(); | |
| } | |
| #endif | |
| const bool bNoDialog = (CrashContext.UserSettings.bNoDialog || CrashContext.UserSettings.bImplicitSend) && CrashContext.UserSettings.bSendUnattendedBugReports; | |
| FCrashReportAnalyticsSessionSummary::Get().OnCrashReportProcessing(/*bIsUserInteractive*/!bNoDialog); | |
| const SubmitCrashReportResult Result = SendErrorReport(ErrorReport, bNoDialog, CrashContext.UserSettings.bImplicitSend); | |
| if (bReportCrashAnalyticInfo) | |
| { | |
| if (FCrashReportAnalytics::IsAvailable()) | |
| { | |
| // If analytics is enabled make sure they are submitted now. | |
| FCrashReportAnalytics::GetProvider().BlockUntilFlushed(5.0f); | |
| } | |
| FCrashReportAnalytics::Shutdown(); | |
| } | |
| FCrashReportAnalyticsSessionSummary::Get().OnCrashReportCompleted(Result != SubmitCrashReportResult::SuccessDiscarded && Result != SubmitCrashReportResult::Failed); | |
| } | |
| } | |
| #if CRASH_REPORT_WITH_RECOVERY | |
| FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread); | |
| // Pump & Tick objects | |
| const double DeltaTime = CurrLoopStartTime - PrevLoopStartTime; | |
| FTSTicker::GetCoreTicker().Tick(DeltaTime); | |
| GFrameCounter++; | |
| FStats::AdvanceFrame(false); | |
| // Run garbage collection for the UObjects for the rest of the frame or at least to 2 ms, but never more than 1 second. | |
| const float PurgeSeconds = IdealFrameTime - static_cast<float>(FPlatformTime::Seconds() - CurrLoopStartTime); | |
| IncrementalPurgeGarbage(true, FMath::Clamp(PurgeSeconds, 0.002f, 1.0f))); | |
| #endif | |
| GLog->FlushThreadedLogs(); | |
| // Throttle main thread fps by sleeping if we still have time. | |
| const float SleepSeconds = IdealFrameTime - static_cast<float>(FPlatformTime::Seconds() - CurrLoopStartTime); | |
| FPlatformProcess::Sleep(FMath::Clamp(SleepSeconds, 0.0f, 1.0f)); | |
| PrevLoopStartTime = CurrLoopStartTime; | |
| } | |
| FCrashReportAnalyticsSessionSummary::Get().OnMonitoredAppDeath(MonitoredProcess); | |
| #if CRASH_REPORT_WITH_MTBF | |
| { | |
| // Load the temporary crash context file. | |
| FSharedCrashContext TempCrashContext; | |
| FMemory::Memzero(TempCrashContext); | |
| if (LoadTempCrashContextFromFile(TempCrashContext, MonitorPid) && TempCrashContext.UserSettings.bSendUsageData) | |
| { | |
| FCrashReportAnalytics::Initialize(TempCrashContext.SessionContext.EpicAccountId); | |
| if (FCrashReportAnalytics::IsAvailable()) | |
| { | |
| TOptional<int32> ExitCodeOpt; | |
| int32 ExitCode; | |
| if (FPlatformProcess::GetProcReturnCode(MonitoredProcess, &ExitCode)) | |
| { | |
| ExitCodeOpt.Emplace(ExitCode); | |
| } | |
| auto HandleAbnormalShutdownFunc = [&TempCrashContext, &RecoveryServicePtr, &ExitCodeOpt]() | |
| { | |
| if (TempCrashContext.UserSettings.bSendUnattendedBugReports) | |
| { | |
| // Send a spoofed crash report in the case that we detect an abnormal shutdown has occurred | |
| HandleAbnormalShutdown(TempCrashContext, MonitorPid, MonitorWritePipe, RecoveryServicePtr, ExitCodeOpt); | |
| } | |
| }; | |
| // Shutdown the session, sends the summary and if the session ended up abnormally (analyzing the summary), invoke the functor to spoof a crash report. | |
| FCrashReportAnalyticsSessionSummary::Get().Shutdown(&FCrashReportAnalytics::GetProvider(), HandleAbnormalShutdownFunc); | |
| } | |
| FCrashReportAnalytics::Shutdown(); | |
| } | |
| } | |
| #endif | |
| // Ensure to shutdown the summary analytics. If it was already shutdown above, this do nothing, otherwise, it destroys analytics data gathered by CRC. | |
| FCrashReportAnalyticsSessionSummary::Get().Shutdown(); | |
| // Clean up the context file | |
| DeleteTempCrashContextFile(MonitorPid); | |
| // Clean up left-over context files that weren't cleaned up properly by previous instance(s) (because it was killed, it crashed or user logged out). | |
| DeleteExpiredTempCrashContext(FTimespan::FromDays(30)); | |
| FPlatformProcess::CloseProc(MonitoredProcess); | |
| } | |
| FPrimaryCrashProperties::Shutdown(); | |
| FPlatformErrorReport::ShutDown(); | |
| RequestEngineExit(TEXT("CrashReportClientApp RequestExit")); | |
| // Allow the game thread to finish processing any latent tasks. | |
| FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread); | |
| FEngineLoop::AppPreExit(); | |
| FModuleManager::Get().UnloadModulesAtShutdown(); | |
| //FTaskGraphInterface::Shutdown(); | |
| FEngineLoop::AppExit(); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright Epic Games, Inc. All Rights Reserved. | |
| #include "CrashUpload.h" | |
| #include "AnalyticsEventAttribute.h" | |
| #include "CrashReportCoreModule.h" | |
| #include "HAL/FileManager.h" | |
| #include "Misc/Compression.h" | |
| #include "Misc/FileHelper.h" | |
| #include "Internationalization/Internationalization.h" | |
| #include "Misc/Guid.h" | |
| #include "Serialization/MemoryWriter.h" | |
| +#include "Serialization/JsonSerializer.h" // BUGSPLAT BG support response | |
| #include "Containers/Ticker.h" | |
| #include "CrashReportCoreConfig.h" | |
| #include "Interfaces/IHttpResponse.h" | |
| #include "HttpModule.h" | |
| #include "GenericPlatform/GenericPlatformHttp.h" | |
| #include "PendingReports.h" | |
| #include "CrashDescription.h" | |
| #include "Misc/EngineBuildSettings.h" | |
| #include "Misc/CommandLine.h" | |
| #include "Stats/Stats.h" | |
| // Switched off CRR upload - Jun 2016 | |
| #define PRIMARY_UPLOAD_RECEIVER 0 | |
| #define PRIMARY_UPLOAD_DATAROUTER 1 | |
| #define LOCTEXT_NAMESPACE "CrashReportClient" | |
| namespace CrashUploadDefs | |
| { | |
| const float PingTimeoutSeconds = 5.f; | |
| + const float CrashUploadRequestTimeoutSeconds = 30.0f; // BUGSPLAT BG support response | |
| const FString APIKey(TEXT("CrashReporter")); | |
| const FString AppEnvironmentInternal(TEXT("Dev")); | |
| const FString AppEnvironmentExternal(TEXT("Release")); | |
| const FString UploadType(TEXT("crashreports")); | |
| } | |
| enum class ECompressedCrashFileHeader | |
| { | |
| MAGIC = 0x7E1B83C1, | |
| }; | |
| struct FCompressedCrashFile : FNoncopyable | |
| { | |
| int32 CurrentFileIndex; | |
| FString Filename; | |
| TArray<uint8> Filedata; | |
| FCompressedCrashFile(int32 InCurrentFileIndex, const FString& InFilename, const TArray<uint8>& InFiledata) | |
| : CurrentFileIndex(InCurrentFileIndex) | |
| , Filename(InFilename) | |
| , Filedata(InFiledata) | |
| { | |
| } | |
| /** Serialization operator. */ | |
| friend FArchive& operator << (FArchive& Ar, FCompressedCrashFile& Data) | |
| { | |
| Ar << Data.CurrentFileIndex; | |
| Data.Filename.SerializeAsANSICharArray(Ar, 260); | |
| Ar << Data.Filedata; | |
| return Ar; | |
| } | |
| }; | |
| struct FCompressedHeader | |
| { | |
| FString DirectoryName; | |
| FString FileName; | |
| int32 UncompressedSize; | |
| int32 FileCount; | |
| /** Serialization operator. */ | |
| friend FArchive& operator << (FArchive& Ar, FCompressedHeader& Data) | |
| { | |
| // The 'CR1' marker prevents the data router backend to fallback to a backward compatibility version where | |
| // a buggy/incomplete header was written at the beginning of the stream and the correct/valid one at the end. | |
| uint8 Version[] = {'C', 'R', '1'}; | |
| Ar.Serialize(Version, sizeof(Version)); | |
| Data.DirectoryName.SerializeAsANSICharArray(Ar, 260); | |
| Data.FileName.SerializeAsANSICharArray(Ar, 260); | |
| Ar << Data.UncompressedSize; | |
| Ar << Data.FileCount; | |
| return Ar; | |
| } | |
| }; | |
| struct FCompressedData | |
| { | |
| TArray<uint8> Data; | |
| int32 CompressedSize; | |
| int32 UncompressedSize; | |
| int32 FileCount; | |
| }; | |
| bool FCrashUploadBase::bInitialized = false; | |
| TArray<FString> FCrashUploadBase::PendingReportDirectories; | |
| TArray<FString> FCrashUploadBase::FailedReportDirectories; | |
| FCrashUploadBase::FCrashUploadBase() | |
| : bUploadCalled(false) | |
| , State(EUploadState::NotSet) | |
| , PauseState(EUploadState::Ready) | |
| , PendingReportDirectoryIndex(0) | |
| { | |
| } | |
| FCrashUploadBase::~FCrashUploadBase() | |
| { | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("Final state (Receiver) = %s"), ToString(State)); | |
| } | |
| void FCrashUploadBase::StaticInitialize(const FPlatformErrorReport& PlatformErrorReport) | |
| { | |
| FPendingReports PendingReports; | |
| //PendingReports.Add(*PlatformErrorReport.GetReportDirectoryLeafName()); | |
| PendingReportDirectories = PendingReports.GetReportDirectories(); | |
| PendingReports.Clear(); | |
| PendingReports.Save(); | |
| } | |
| void FCrashUploadBase::StaticShutdown() | |
| { | |
| // Write failed uploads back to disk | |
| FPendingReports ReportsForNextTime; | |
| for (const FString& FailedReport : FailedReportDirectories) | |
| { | |
| ReportsForNextTime.Add(FailedReport); | |
| } | |
| ReportsForNextTime.Save(); | |
| } | |
| bool FCrashUploadBase::CompressData(const TArray<FString>& InPendingFiles, FCompressedData& OutCompressedData, TArray<uint8>& OutPostData, FCompressedHeader* OptionalHeader) | |
| { | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("CompressAndSendData have %d pending files"), InPendingFiles.Num()); | |
| // Compress all files into one archive. | |
| const int32 BufferSize = 32 * 1024 * 1024; | |
| TArray<uint8> UncompressedData; | |
| UncompressedData.Reserve(BufferSize); | |
| FMemoryWriter MemoryWriter(UncompressedData, false, true); | |
| if (OptionalHeader != nullptr) | |
| { | |
| // Write dummy to fill correct size | |
| MemoryWriter << *OptionalHeader; | |
| } | |
| int32 CurrentFileIndex = 0; | |
| const FString FullCrashDumpLocation = FPrimaryCrashProperties::Get()->FullCrashDumpLocation.AsString(); | |
| bool bIsFullDumpCrash = (FPrimaryCrashProperties::Get()->CrashDumpMode == ECrashDumpMode::FullDump || | |
| FPrimaryCrashProperties::Get()->CrashDumpMode == ECrashDumpMode::FullDumpAlways) && | |
| FPrimaryCrashProperties::Get()->CrashVersion >= ECrashDescVersions::VER_3_CrashContext; | |
| // Loop to keep trying files until a send succeeds or we run out of files | |
| for (const FString& PathOfFileToUpload : InPendingFiles) | |
| { | |
| const FString Filename = FPaths::GetCleanFilename(PathOfFileToUpload); | |
| const bool bIsFullDumpFile = Filename == FGenericCrashContext::UEMinidumpName && bIsFullDumpCrash; | |
| const bool bValidFullDumpForCopy = bIsFullDumpFile && !FullCrashDumpLocation.IsEmpty(); | |
| if (bIsFullDumpFile) | |
| { | |
| if (bValidFullDumpForCopy) | |
| { | |
| const FString DestinationPath = FullCrashDumpLocation / FGenericCrashContext::UEMinidumpName; | |
| const bool bCreated = IFileManager::Get().MakeDirectory(*FullCrashDumpLocation, true); | |
| if (!bCreated) | |
| { | |
| UE_LOG(CrashReportCoreLog, Error, TEXT("Couldn't create directory for full crash dump %s"), *DestinationPath); | |
| } | |
| else | |
| { | |
| UE_LOG(CrashReportCoreLog, Warning, TEXT("Copying full crash minidump to %s"), *DestinationPath); | |
| IFileManager::Get().Copy(*DestinationPath, *PathOfFileToUpload, false); | |
| } | |
| } | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("Skipping upload of full crash dump")); | |
| continue; | |
| } | |
| if (!FFileHelper::LoadFileToArray(OutPostData, *PathOfFileToUpload)) | |
| { | |
| UE_LOG(CrashReportCoreLog, Warning, TEXT("Failed to load crash report file")); | |
| continue; | |
| } | |
| const bool bSkipLogFile = !FCrashReportCoreConfig::Get().GetSendLogFile() && PathOfFileToUpload.EndsWith(TEXT(".log")); | |
| if (bSkipLogFile) | |
| { | |
| UE_LOG(CrashReportCoreLog, Warning, TEXT("Skipping the %s"), *Filename); | |
| continue; | |
| } | |
| // Disabled due to issues with Mac not using the crash context. | |
| /* | |
| // Skip old WERInternalMetadata. | |
| const bool bSkipXMLFile = PathOfFileToUpload.EndsWith( TEXT( ".xml" ) ); | |
| if (bSkipXMLFile) | |
| { | |
| UE_LOG( CrashReportCoreLog, Warning, TEXT( "Skipping the %s" ), *Filename ); | |
| continue; | |
| }*/ | |
| // Skip old Report.wer file. | |
| const bool bSkipWERFile = PathOfFileToUpload.Contains(TEXT("Report.wer")); | |
| if (bSkipWERFile) | |
| { | |
| UE_LOG(CrashReportCoreLog, Warning, TEXT("Skipping the %s"), *Filename); | |
| continue; | |
| } | |
| // Skip old diagnostics.txt file, all data is stored in the CrashContext.runtime-xml | |
| // Disabled due to issues with Mac not using the crash context. | |
| /* | |
| const bool bSkipDiagnostics = Filename == FCrashReportClientConfig::Get().GetDiagnosticsFilename(); | |
| if (bSkipDiagnostics) | |
| { | |
| UE_LOG( CrashReportCoreLog, Warning, TEXT( "Skipping the %s" ), *Filename ); | |
| continue; | |
| }*/ | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("CompressAndSendData compressing %d bytes ('%s')"), OutPostData.Num(), *PathOfFileToUpload); | |
| FCompressedCrashFile FileToCompress(CurrentFileIndex, Filename, OutPostData); | |
| CurrentFileIndex++; | |
| MemoryWriter << FileToCompress; | |
| } | |
| if (OptionalHeader != nullptr) | |
| { | |
| FMemoryWriter MemoryHeaderWriter(UncompressedData); | |
| OptionalHeader->UncompressedSize = UncompressedData.Num(); | |
| OptionalHeader->FileCount = CurrentFileIndex; | |
| MemoryHeaderWriter << *OptionalHeader; | |
| } | |
| int UncompressedSize = UncompressedData.Num(); | |
| TArray<uint8> CompressedDataRaw; | |
| CompressedDataRaw.AddUninitialized(UncompressedSize); | |
| OutCompressedData.FileCount = CurrentFileIndex; | |
| OutCompressedData.CompressedSize = UncompressedSize; | |
| OutCompressedData.UncompressedSize = UncompressedSize; | |
| const bool bResult = FCompression::CompressMemory(NAME_Zlib, CompressedDataRaw.GetData(), OutCompressedData.CompressedSize, UncompressedData.GetData(), OutCompressedData.UncompressedSize); | |
| if (bResult) | |
| { | |
| // Copy compressed data into the array. | |
| OutCompressedData.Data.Append(CompressedDataRaw.GetData(), OutCompressedData.CompressedSize); | |
| } | |
| return bResult; | |
| } | |
| const TCHAR* FCrashUploadBase::ToString(EUploadState::Type State) | |
| { | |
| switch (State) | |
| { | |
| case EUploadState::PingingServer: | |
| return TEXT("PingingServer"); | |
| case EUploadState::Ready: | |
| return TEXT("Ready"); | |
| case EUploadState::CheckingReport: | |
| return TEXT("CheckingReport"); | |
| case EUploadState::CheckingReportDetail: | |
| return TEXT("CheckingReportDetail"); | |
| case EUploadState::CompressAndSendData: | |
| return TEXT("SendingFiles"); | |
| case EUploadState::WaitingToPostReportComplete: | |
| return TEXT("WaitingToPostReportComplete"); | |
| case EUploadState::PostingReportComplete: | |
| return TEXT("PostingReportComplete"); | |
| case EUploadState::Finished: | |
| return TEXT("Finished"); | |
| case EUploadState::ServerNotAvailable: | |
| return TEXT("ServerNotAvailable"); | |
| case EUploadState::UploadError: | |
| return TEXT("UploadError"); | |
| case EUploadState::Cancelled: | |
| return TEXT("Cancelled"); | |
| default: | |
| break; | |
| } | |
| return TEXT("Unknown UploadState value"); | |
| } | |
| void FCrashUploadBase::SetCurrentState(EUploadState::Type InState) | |
| { | |
| if (State == EUploadState::NotSet) | |
| { | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("Initial state = %s"), ToString(State)); | |
| } | |
| else | |
| { | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("State change from %s to %s"), ToString(State), ToString(InState)); | |
| } | |
| State = InState; | |
| switch (State) | |
| { | |
| default: | |
| break; | |
| case EUploadState::PingingServer: | |
| UploadStateText = LOCTEXT("PingingServer", "Pinging server"); | |
| break; | |
| case EUploadState::Ready: | |
| UploadStateText = LOCTEXT("UploaderReady", "Ready to send to server"); | |
| break; | |
| case EUploadState::ServerNotAvailable: | |
| UploadStateText = LOCTEXT("ServerNotAvailable", "Server not available - report will be stored for later upload"); | |
| break; | |
| } | |
| } | |
| void FCrashUploadBase::AddReportToFailedList() const | |
| { | |
| if (PendingFiles.Num() > 0) | |
| { | |
| FailedReportDirectories.AddUnique(ErrorReport.GetReportDirectory()); | |
| } | |
| } | |
| void FCrashUploadBase::CleanCrashReportDirectory(const FString& CrashReportDirectory) | |
| { | |
| // Clean crash report folder if requested | |
| if (FParse::Param(FCommandLine::Get(), TEXT("CleanCrashReports"))) | |
| { | |
| // Check that the crash directory is valid and resides in /Saved/Crashes | |
| FString NormalizedReportDirectory = CrashReportDirectory; | |
| FPaths::NormalizeDirectoryName(NormalizedReportDirectory); | |
| if (NormalizedReportDirectory.Contains(TEXT("/Saved/Crashes/"))) | |
| { | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("Removing crash report directory %s"), *CrashReportDirectory); | |
| IFileManager& FileManager = IFileManager::Get(); | |
| FileManager.DeleteDirectory(*CrashReportDirectory, false, true); | |
| } | |
| } | |
| } | |
| // FCrashUploadToReceiver ////////////////////////////////////////////////////// | |
| FCrashUploadToReceiver::FCrashUploadToReceiver(const FString& InReceiverAddress) | |
| : UrlPrefix(InReceiverAddress.IsEmpty() ? TEXT("") : InReceiverAddress / TEXT("CrashReporter")) | |
| { | |
| if (!UrlPrefix.IsEmpty()) | |
| { | |
| // Sending to receiver | |
| SendPingRequest(); | |
| } | |
| else | |
| { | |
| SetCurrentState(EUploadState::Disabled); | |
| } | |
| } | |
| FCrashUploadToReceiver::~FCrashUploadToReceiver() | |
| { | |
| } | |
| bool FCrashUploadToReceiver::PingTimeout(float DeltaTime) | |
| { | |
| QUICK_SCOPE_CYCLE_COUNTER(STAT_FCrashUploadToReceiver_PingTimeout); | |
| if (EUploadState::PingingServer == State) | |
| { | |
| SetCurrentState(EUploadState::ServerNotAvailable); | |
| // PauseState will be Ready if user has not yet decided to send the report | |
| if (PauseState > EUploadState::Ready) | |
| { | |
| AddReportToFailedList(); | |
| } | |
| } | |
| // One-shot | |
| return false; | |
| } | |
| void FCrashUploadToReceiver::BeginUpload(const FPlatformErrorReport& PlatformErrorReport) | |
| { | |
| bUploadCalled = true; | |
| ErrorReport = PlatformErrorReport; | |
| PendingFiles = FPlatformErrorReport( ErrorReport.GetReportDirectory() ).GetFilesToUpload(); | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("Got %d pending files to upload from '%s'"), PendingFiles.Num(), *ErrorReport.GetReportDirectoryLeafName()); | |
| PauseState = EUploadState::Finished; | |
| if (State == EUploadState::Ready) | |
| { | |
| BeginUploadImpl(); | |
| } | |
| else if (State == EUploadState::ServerNotAvailable) | |
| { | |
| AddReportToFailedList(); | |
| } | |
| } | |
| bool FCrashUploadToReceiver::SendCheckReportRequest() | |
| { | |
| FString XMLString; | |
| auto Request = CreateHttpRequest(); | |
| if (State == EUploadState::CheckingReport) | |
| { | |
| #if PRIMARY_UPLOAD_RECEIVER | |
| // first stage of any upload to CRR so send analytics | |
| FPrimaryCrashProperties::Get()->SendPreUploadAnalytics(); | |
| #endif | |
| AssignReportIdToPostDataBuffer(); | |
| Request->SetURL(UrlPrefix / TEXT("CheckReport")); | |
| Request->SetHeader(TEXT("Content-Type"), TEXT("text/plain; charset=us-ascii")); | |
| UE_LOG( CrashReportCoreLog, Log, TEXT( "Sending HTTP request: %s" ), *Request->GetURL() ); | |
| } | |
| else | |
| { | |
| // This part is Windows-specific on the server | |
| ErrorReport.LoadWindowsReportXmlFile( XMLString ); | |
| // Convert the XMLString into the UTF-8. | |
| FTCHARToUTF8 Converter( (const TCHAR*)*XMLString, XMLString.Len() ); | |
| const int32 Length = Converter.Length(); | |
| PostData.Reset( Length ); | |
| PostData.AddUninitialized( Length ); | |
| CopyAssignItems( (ANSICHAR*)PostData.GetData(), Converter.Get(), Length ); | |
| Request->SetURL(UrlPrefix / TEXT("CheckReportDetail")); | |
| Request->SetHeader(TEXT("Content-Type"), TEXT("text/plain; charset=utf-8")); | |
| UE_LOG( CrashReportCoreLog, Log, TEXT( "Sending HTTP request: %s" ), *Request->GetURL() ); | |
| } | |
| UE_LOG( CrashReportCoreLog, Log, TEXT( "PostData Num: %i" ), PostData.Num() ); | |
| Request->SetVerb(TEXT("POST")); | |
| Request->SetContent(PostData); | |
| return Request->ProcessRequest(); | |
| } | |
| void FCrashUploadToReceiver::CompressAndSendData() | |
| { | |
| FCompressedData CompressedData; | |
| if (!CompressData(PendingFiles, CompressedData, PostData)) | |
| { | |
| UE_LOG(CrashReportCoreLog, Warning, TEXT("Couldn't compress the crash report files")); | |
| SetCurrentState(EUploadState::Cancelled); | |
| return; | |
| } | |
| PendingFiles.Empty(); | |
| const FString Filename = ErrorReport.GetReportDirectoryLeafName() + TEXT(".uecrash"); | |
| // Set up request for upload | |
| auto Request = CreateHttpRequest(); | |
| Request->SetVerb(TEXT("POST")); | |
| Request->SetHeader(TEXT("Content-Type"), TEXT("application/octet-stream")); | |
| Request->SetURL(UrlPrefix / TEXT("UploadReportFile")); | |
| Request->SetContent(CompressedData.Data); | |
| Request->SetHeader(TEXT("DirectoryName"), *ErrorReport.GetReportDirectoryLeafName()); | |
| Request->SetHeader(TEXT("FileName"), Filename); | |
| Request->SetHeader(TEXT("FileLength"), TTypeToString<int32>::ToString(CompressedData.Data.Num()) ); | |
| Request->SetHeader(TEXT("CompressedSize"), TTypeToString<int32>::ToString(CompressedData.CompressedSize) ); | |
| Request->SetHeader(TEXT("UncompressedSize"), TTypeToString<int32>::ToString(CompressedData.UncompressedSize) ); | |
| Request->SetHeader(TEXT("NumberOfFiles"), TTypeToString<int32>::ToString(CompressedData.FileCount) ); | |
| UE_LOG( CrashReportCoreLog, Log, TEXT( "Sending HTTP request: %s, Payload size: %d" ), *Request->GetURL(), CompressedData.Data.Num()); | |
| if (Request->ProcessRequest()) | |
| { | |
| return; | |
| } | |
| else | |
| { | |
| UE_LOG(CrashReportCoreLog, Warning, TEXT("Failed to send file upload request")); | |
| SetCurrentState(EUploadState::Cancelled); | |
| } | |
| } | |
| void FCrashUploadToReceiver::AssignReportIdToPostDataBuffer() | |
| { | |
| FString ReportDirectoryName = *ErrorReport.GetReportDirectoryLeafName(); | |
| const int32 DirectoryNameLength = ReportDirectoryName.Len(); | |
| PostData.SetNum(DirectoryNameLength); | |
| for (int32 Index = 0; Index != DirectoryNameLength; ++Index) | |
| { | |
| PostData[Index] = ReportDirectoryName[Index]; | |
| } | |
| } | |
| void FCrashUploadToReceiver::PostReportComplete() | |
| { | |
| if (PauseState == EUploadState::PostingReportComplete) | |
| { | |
| // Wait for confirmation | |
| SetCurrentState(EUploadState::WaitingToPostReportComplete); | |
| return; | |
| } | |
| AssignReportIdToPostDataBuffer(); | |
| auto Request = CreateHttpRequest(); | |
| Request->SetVerb( TEXT( "POST" ) ); | |
| Request->SetURL(UrlPrefix / TEXT("UploadComplete")); | |
| Request->SetHeader( TEXT( "Content-Type" ), TEXT( "text/plain; charset=us-ascii" ) ); | |
| Request->SetContent(PostData); | |
| UE_LOG( CrashReportCoreLog, Log, TEXT( "Sending HTTP request: %s, Payload size: %d" ), *Request->GetURL(), PostData.Num()); | |
| if (Request->ProcessRequest()) | |
| { | |
| #if PRIMARY_UPLOAD_RECEIVER | |
| // completed upload to CRR so send analytics | |
| FPrimaryCrashProperties::Get()->SendPostUploadAnalytics(0.0, false, 0, 0); | |
| #endif | |
| SetCurrentState(EUploadState::PostingReportComplete); | |
| } | |
| else | |
| { | |
| CheckPendingReportsForFilesToUpload(); | |
| } | |
| } | |
| void FCrashUploadToReceiver::OnProcessRequestComplete(FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) | |
| { | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("OnProcessRequestComplete(), State=%s bSucceeded=%i"), ToString(State), (int32)bSucceeded ); | |
| switch (State) | |
| { | |
| default: | |
| // May get here if response is received after time-out has passed | |
| break; | |
| case EUploadState::PingingServer: | |
| if (bSucceeded) | |
| { | |
| OnPingSuccess(); | |
| } | |
| else | |
| { | |
| PingTimeout(0); | |
| } | |
| break; | |
| case EUploadState::CheckingReport: | |
| case EUploadState::CheckingReportDetail: | |
| { | |
| bool bCheckedOkay = false; | |
| if (!bSucceeded || !ParseServerResponse(HttpResponse, bCheckedOkay)) | |
| { | |
| if (!bSucceeded) | |
| { | |
| UE_LOG(CrashReportCoreLog, Warning, TEXT("Request to server failed")); | |
| } | |
| else | |
| { | |
| UE_LOG(CrashReportCoreLog, Warning, TEXT("Did not get a valid server response.")); | |
| } | |
| // Failed to check with the server - skip this report for now | |
| AddReportToFailedList(); | |
| CheckPendingReportsForFilesToUpload(); | |
| } | |
| else if (!bCheckedOkay) | |
| { | |
| // Server rejected the report | |
| UE_LOG(CrashReportCoreLog, Warning, TEXT("Did not get a valid server response.")); | |
| CheckPendingReportsForFilesToUpload(); | |
| } | |
| else | |
| { | |
| SetCurrentState(EUploadState::CompressAndSendData); | |
| CompressAndSendData(); | |
| } | |
| } | |
| break; | |
| case EUploadState::CompressAndSendData: | |
| if (!bSucceeded) | |
| { | |
| UE_LOG(CrashReportCoreLog, Warning, TEXT("File upload failed to receiver")); | |
| AddReportToFailedList(); | |
| SetCurrentState(EUploadState::Cancelled); | |
| } | |
| else | |
| { | |
| PostReportComplete(); | |
| } | |
| break; | |
| case EUploadState::PostingReportComplete: | |
| CheckPendingReportsForFilesToUpload(); | |
| break; | |
| } | |
| } | |
| void FCrashUploadToReceiver::OnPingSuccess() | |
| { | |
| if (PauseState > EUploadState::Ready) | |
| { | |
| BeginUploadImpl(); | |
| } | |
| else | |
| { | |
| // Await instructions | |
| SetCurrentState(EUploadState::Ready); | |
| } | |
| } | |
| void FCrashUploadToReceiver::CheckPendingReportsForFilesToUpload() | |
| { | |
| SetCurrentState(EUploadState::CheckingReport); | |
| for (; PendingReportDirectoryIndex < PendingReportDirectories.Num(); PendingReportDirectoryIndex++) | |
| { | |
| ErrorReport = FPlatformErrorReport(PendingReportDirectories[PendingReportDirectoryIndex]); | |
| PendingFiles = ErrorReport.GetFilesToUpload(); | |
| if (PendingFiles.Num() > 0 && SendCheckReportRequest()) | |
| { | |
| return; | |
| } | |
| } | |
| // Nothing left to upload | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("All uploads done")); | |
| SetCurrentState(EUploadState::Finished); | |
| } | |
| void FCrashUploadToReceiver::BeginUploadImpl() | |
| { | |
| SetCurrentState(EUploadState::CheckingReport); | |
| if (!SendCheckReportRequest()) | |
| { | |
| CheckPendingReportsForFilesToUpload(); | |
| } | |
| } | |
| TSharedRef<IHttpRequest, ESPMode::ThreadSafe> FCrashUploadToReceiver::CreateHttpRequest() | |
| { | |
| auto Request = FHttpModule::Get().CreateRequest(); | |
| Request->OnProcessRequestComplete().BindRaw(this, &FCrashUploadToReceiver::OnProcessRequestComplete); | |
| return Request; | |
| } | |
| void FCrashUploadToReceiver::SendPingRequest() | |
| { | |
| SetCurrentState(EUploadState::PingingServer); | |
| auto Request = CreateHttpRequest(); | |
| Request->SetVerb(TEXT("GET")); | |
| Request->SetURL(UrlPrefix / TEXT("Ping")); | |
| UE_LOG( CrashReportCoreLog, Log, TEXT( "Sending HTTP request: %s" ), *Request->GetURL() ); | |
| if (Request->ProcessRequest()) | |
| { | |
| FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this, &FCrashUploadToReceiver::PingTimeout), CrashUploadDefs::PingTimeoutSeconds); | |
| } | |
| else | |
| { | |
| PingTimeout(0); | |
| } | |
| } | |
| bool FCrashUploadToReceiver::ParseServerResponse(FHttpResponsePtr Response, bool& OutValidReport) | |
| { | |
| if (!Response.IsValid()) | |
| { | |
| return false; | |
| } | |
| // Turn the snippet into a complete XML document, to keep the XML parser happy | |
| FXmlFile ParsedResponse(FString(TEXT("<Root>")) + Response->GetContentAsString() + TEXT("</Root>"), EConstructMethod::ConstructFromBuffer); | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("Response->GetContentAsString(): '%s'"), *Response->GetContentAsString()); | |
| if (!ParsedResponse.IsValid()) | |
| { | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("Invalid response!")); | |
| OutValidReport = false; | |
| return false; | |
| } | |
| if (auto ResultNode = ParsedResponse.GetRootNode()->FindChildNode(TEXT("CrashReporterResult"))) | |
| { | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("ResultNode->GetAttribute(TEXT(\"bSuccess\")) = %s"), *ResultNode->GetAttribute(TEXT("bSuccess"))); | |
| OutValidReport = ResultNode->GetAttribute(TEXT("bSuccess")) == TEXT("true"); | |
| return true; | |
| } | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("Could not find CrashReporterResult")); | |
| OutValidReport = false; | |
| return false; | |
| } | |
| // FCrashUploadToDataRouter ////////////////////////////////////////////////////// | |
| FCrashUploadToDataRouter::FCrashUploadToDataRouter(const FString& InDataRouterUrl) | |
| : DataRouterUrl(InDataRouterUrl) | |
| , Timer(Duration) | |
| { | |
| if (!DataRouterUrl.IsEmpty()) | |
| { | |
| SetCurrentState(EUploadState::Ready); | |
| } | |
| else | |
| { | |
| SetCurrentState(EUploadState::Disabled); | |
| } | |
| #if PRIMARY_UPLOAD_DATAROUTER | |
| // first stage of any upload to DR so send analytics | |
| FPrimaryCrashProperties::Get()->SendPreUploadAnalytics(); | |
| #endif | |
| } | |
| FCrashUploadToDataRouter::~FCrashUploadToDataRouter() | |
| { | |
| Timer.Stop(); | |
| #if PRIMARY_UPLOAD_DATAROUTER | |
| // completed upload to DR so send analytics | |
| FPrimaryCrashProperties::Get()->SendPostUploadAnalytics(Duration, bResult, ResponseCode, PayloadSize, ReportCount); | |
| #endif | |
| } | |
| void FCrashUploadToDataRouter::BeginUpload(const FPlatformErrorReport& PlatformErrorReport) | |
| { | |
| bUploadCalled = true; | |
| Timer.Start(); | |
| ErrorReport = PlatformErrorReport; | |
| PendingFiles = FPlatformErrorReport(ErrorReport.GetReportDirectory()).GetFilesToUpload(); | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("Got %d pending files to upload from '%s'"), PendingFiles.Num(), *ErrorReport.GetReportDirectoryLeafName()); | |
| PauseState = EUploadState::Finished; | |
| if (State == EUploadState::Ready) | |
| { | |
| SetCurrentState(EUploadState::CompressAndSendData); | |
| CompressAndSendData(); | |
| } | |
| } | |
| void FCrashUploadToDataRouter::CompressAndSendData() | |
| { | |
| FCompressedHeader CompressedHeader; | |
| CompressedHeader.DirectoryName = ErrorReport.GetReportDirectoryLeafName(); | |
| CompressedHeader.FileName = ErrorReport.GetReportDirectoryLeafName() + TEXT(".uecrash"); | |
| FCompressedData CompressedData; | |
| if (!CompressData(PendingFiles, CompressedData, PostData, &CompressedHeader)) | |
| { | |
| UE_LOG(CrashReportCoreLog, Warning, TEXT("Couldn't compress the crash report files")); | |
| SetCurrentState(EUploadState::Cancelled); | |
| return; | |
| } | |
| PayloadSize += CompressedData.Data.Num(); | |
| ++ReportCount; | |
| PendingFiles.Empty(); | |
| FString UserId = FString::Printf(TEXT("%s|%s|%s"), *FPlatformMisc::GetLoginId(), *FPlatformMisc::GetEpicAccountId(), *FPlatformMisc::GetOperatingSystemId()); | |
| - FString UrlParams = FString::Printf(TEXT("?AppID=%s&AppVersion=%s&AppEnvironment=%s&UploadType=%s&UserID=%s"), | |
| + FString UrlParams = FString::Printf(TEXT("?AppID=%s&AppVersion=%s&AppEnvironment=%s&UploadType=%s&UserID=%s&PCallStackHash=%s"), // BUGSPLAT BG support response | |
| *FGenericPlatformHttp::UrlEncode(CrashUploadDefs::APIKey), | |
| *FGenericPlatformHttp::UrlEncode(FEngineVersion::Current().ToString()), | |
| *FGenericPlatformHttp::UrlEncode(FEngineBuildSettings::IsInternalBuild() ? CrashUploadDefs::AppEnvironmentInternal : CrashUploadDefs::AppEnvironmentExternal), | |
| *FGenericPlatformHttp::UrlEncode(CrashUploadDefs::UploadType), | |
| - *FGenericPlatformHttp::UrlEncode(UserId)); | |
| + *FGenericPlatformHttp::UrlEncode(UserId), // BUGSPLAT BG support response | |
| + *FGenericPlatformHttp::UrlEncode(FPrimaryCrashProperties::Get()->PCallStackHash)); // BUGSPLAT BG support response | |
| + | |
| // Set up request for upload | |
| auto Request = CreateHttpRequest(); | |
| Request->SetVerb(TEXT("POST")); | |
| Request->SetHeader(TEXT("Content-Type"), TEXT("application/octet-stream")); | |
| Request->SetURL(DataRouterUrl + UrlParams); | |
| Request->SetContent(CompressedData.Data); | |
| + Request->SetTimeout(CrashUploadDefs::CrashUploadRequestTimeoutSeconds); // BUGSPLAT BG support response | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("Sending HTTP request: %s, Payload size: %d"), *Request->GetURL(), CompressedData.Data.Num()); | |
| if (!Request->ProcessRequest()) | |
| { | |
| UE_LOG(CrashReportCoreLog, Warning, TEXT("Failed to send file upload request")); | |
| SetCurrentState(EUploadState::Cancelled); | |
| } | |
| } | |
| TSharedRef<IHttpRequest, ESPMode::ThreadSafe> FCrashUploadToDataRouter::CreateHttpRequest() | |
| { | |
| auto Request = FHttpModule::Get().CreateRequest(); | |
| + ++PendingHttpRequests; // BUGSPLAT BG support response | |
| Request->OnProcessRequestComplete().BindRaw(this, &FCrashUploadToDataRouter::OnProcessRequestComplete); | |
| return Request; | |
| } | |
| +// BUGSPLAT BG support response | |
| +void FCrashUploadToDataRouter::ParseServerResponse(const FHttpResponsePtr& HttpResponse) | |
| +{ | |
| + if (!BugsplatSupportResponseURL.IsEmpty()) | |
| + { | |
| + return; // We already received the URL. | |
| + } | |
| + UE_LOG(CrashReportCoreLog, Log, TEXT("Response->GetContentAsString(): '%s'"), *HttpResponse->GetContentAsString()); | |
| + const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(HttpResponse->GetContentAsString()); | |
| + TSharedPtr<FJsonObject> JSONObject; | |
| + if (!FJsonSerializer::Deserialize(Reader, JSONObject) || !JSONObject.IsValid()) | |
| + { | |
| + UE_LOG(CrashReportCoreLog, Log, TEXT(" Failed to parse JSON object from response.")); | |
| + return; | |
| + } | |
| + static const FString kBugsplatURLAttributeName = TEXT("infoUrl"); | |
| + TSharedPtr<FJsonValue> JSONInfoURLValue = JSONObject->TryGetField(kBugsplatURLAttributeName); | |
| + if (JSONInfoURLValue.IsValid()) | |
| + { | |
| + BugsplatSupportResponseURL = JSONInfoURLValue->AsString(); | |
| + return; | |
| + } | |
| + UE_LOG(CrashReportCoreLog, Log, TEXT(" Failed to parse JSON object and retrieve '%s' from response."), *kBugsplatURLAttributeName); | |
| +} | |
| +// BUGSPLAT BG | |
| + | |
| void FCrashUploadToDataRouter::OnProcessRequestComplete(FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) | |
| { | |
| bResult = bSucceeded; | |
| ResponseCode = HttpResponse.IsValid() ? HttpResponse->GetResponseCode() : -1; | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("OnProcessRequestComplete(), State=%s Response=%i ConnectedSuccesfully=%i"), ToString(State), ResponseCode, (int32)bSucceeded); | |
| if (!EHttpResponseCodes::IsOk(ResponseCode)) | |
| { | |
| const FText Description = GetDescription((EHttpResponseCodes::Type)ResponseCode); | |
| UE_LOG(CrashReportCoreLog, Error, TEXT("Failed to send crash report. Server returned error code %u (%s)."), ResponseCode, *Description.ToString()); | |
| SetCurrentState(EUploadState::Cancelled); | |
| } | |
| + // BUGSPLAT BG support response | |
| + --PendingHttpRequests; | |
| + check(PendingHttpRequests >= 0); | |
| + // BUGSPLAT BG | |
| + | |
| switch (State) | |
| { | |
| default: | |
| // May get here if response is received after time-out has passed | |
| break; | |
| case EUploadState::CompressAndSendData: | |
| if (!bSucceeded) | |
| { | |
| UE_LOG(CrashReportCoreLog, Warning, TEXT("File upload failed to data router")); | |
| AddReportToFailedList(); | |
| SetCurrentState(EUploadState::Cancelled); | |
| } | |
| else | |
| { | |
| // Successfully submitted crash report | |
| CleanCrashReportDirectory(ErrorReport.GetReportDirectory()); | |
| + ParseServerResponse(HttpResponse); // BUGSPLAT BG support response | |
| CheckPendingReportsForFilesToUpload(); | |
| } | |
| break; | |
| } | |
| } | |
| void FCrashUploadToDataRouter::CheckPendingReportsForFilesToUpload() | |
| { | |
| if (!PendingReportDirectories.IsEmpty()) | |
| { | |
| UE_LOG(CrashReportCoreLog, Log, TEXT("Found additional pending reports")); | |
| SetCurrentState(EUploadState::CompressAndSendData); | |
| for (; PendingReportDirectoryIndex < PendingReportDirectories.Num(); PendingReportDirectoryIndex++) | |
| { | |
| ErrorReport = FPlatformErrorReport(PendingReportDirectories[PendingReportDirectoryIndex]); | |
| PendingFiles = ErrorReport.GetFilesToUpload(); | |
| if (PendingFiles.Num() > 0) | |
| { | |
| CompressAndSendData(); | |
| return; | |
| } | |
| } | |
| } | |
| // Nothing left to upload | |
| - UE_LOG(CrashReportCoreLog, Log, TEXT("All uploads done")); | |
| - SetCurrentState(EUploadState::Finished); | |
| + // BUGSPLAT BG support response | |
| + if (PendingHttpRequests == 0) | |
| + { | |
| + // BUGSPLAT BG | |
| + UE_LOG(CrashReportCoreLog, Log, TEXT("All uploads done")); | |
| + SetCurrentState(EUploadState::Finished); | |
| + } // BUGSPLAT BG support response | |
| } | |
| #undef LOCTEXT_NAMESPACE |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright Epic Games, Inc. All Rights Reserved. | |
| #pragma once | |
| #include "Containers/Array.h" | |
| #include "Containers/UnrealString.h" | |
| #include "CoreMinimal.h" | |
| #include "HAL/Platform.h" | |
| #include "Interfaces/IHttpRequest.h" | |
| #include "Internationalization/Text.h" | |
| #include "PlatformErrorReport.h" | |
| #include "ProfilingDebugging/ScopedTimers.h" | |
| #include "Templates/SharedPointer.h" | |
| struct FCompressedData; | |
| struct FCompressedHeader; | |
| class FCrashUploadBase | |
| { | |
| public: | |
| FCrashUploadBase(); | |
| virtual ~FCrashUploadBase(); | |
| /** | |
| * Is this uploader enabled or disabled? | |
| */ | |
| bool IsEnabled() const { return State != EUploadState::Disabled; } | |
| /** | |
| * Has BeginUpload been called? | |
| */ | |
| bool IsUploadCalled() const { return bUploadCalled; } | |
| /** | |
| * Provide progress or error information for the UI | |
| */ | |
| const FText& GetStatusText() const { return UploadStateText; } | |
| /** | |
| * Determine whether the upload has finished (successfully or otherwise) | |
| * @return Whether the upload has finished | |
| */ | |
| bool IsFinished() const | |
| { | |
| return State >= EUploadState::FirstCompletedState; | |
| } | |
| void Cancel() | |
| { | |
| SetCurrentState(EUploadState::Cancelled); | |
| } | |
| static bool IsInitialized() { return bInitialized; } | |
| static void StaticInitialize(const FPlatformErrorReport& PlatformErrorReport); | |
| static void StaticShutdown(); | |
| protected: | |
| /** State enum to keep track of what the uploader is doing */ | |
| struct EUploadState | |
| { | |
| enum Type | |
| { | |
| NotSet, | |
| PingingServer, | |
| Ready, | |
| CheckingReport, | |
| CheckingReportDetail, | |
| CompressAndSendData, | |
| WaitingToPostReportComplete, | |
| PostingReportComplete, | |
| Finished, | |
| ServerNotAvailable, | |
| UploadError, | |
| Cancelled, | |
| Disabled, | |
| FirstCompletedState = Finished, | |
| }; | |
| }; | |
| static bool CompressData(const TArray<FString>& InPendingFiles, struct FCompressedData& OutCompressedData, TArray<uint8>& OutPostData, struct FCompressedHeader* OptionalHeader = nullptr); | |
| /** | |
| * Get a string representation of the state, for logging purposes | |
| * @param State Value to stringize | |
| * @return Literal string value | |
| */ | |
| static const TCHAR* ToString(EUploadState::Type InState); | |
| /** | |
| * Set the current state, also updating the status text where necessary | |
| * @param State State the uploader is now in | |
| */ | |
| void SetCurrentState(EUploadState::Type InState); | |
| /** | |
| * When failed, add the report to a file list containing reports to upload next time | |
| */ | |
| void AddReportToFailedList() const; | |
| /** | |
| * Removes crash report folder once submitted to the crash reporting backend, as to not fill up server | |
| * -CleanCrashReports must be specified on the crash report client command line | |
| */ | |
| void CleanCrashReportDirectory(const FString& CrashReportDirectory); | |
| protected: | |
| bool bUploadCalled; | |
| /** What this class is currently doing */ | |
| EUploadState::Type State; | |
| /** Status of upload to display */ | |
| FText UploadStateText; | |
| /** State to pause at until confirmation has been received to continue */ | |
| EUploadState::Type PauseState; | |
| /** Full paths of files still to be uploaded */ | |
| TArray<FString> PendingFiles; | |
| /** Error report being processed */ | |
| FPlatformErrorReport ErrorReport; | |
| /** Buffer to keep reusing for file content and other messages */ | |
| TArray<uint8> PostData; | |
| int32 PendingReportDirectoryIndex; | |
| protected: | |
| static bool bInitialized; | |
| /** Full paths of reports from previous runs still to be uploaded */ | |
| static TArray<FString> PendingReportDirectories; | |
| /** Full paths of reports from this run that did not upload */ | |
| static TArray<FString> FailedReportDirectories; | |
| }; | |
| /** | |
| * Handles uploading files to the crash report server | |
| */ | |
| class FCrashUploadToReceiver : public FCrashUploadBase | |
| { | |
| public: | |
| /** | |
| * Constructor: pings server | |
| * @param ServerAddress Host IP of the crash report server | |
| */ | |
| explicit FCrashUploadToReceiver(const FString& InReceiverAddress); | |
| /** | |
| * Destructor for logging | |
| */ | |
| virtual ~FCrashUploadToReceiver(); | |
| /** | |
| * Commence upload when ready | |
| * @param PlatformErrorReport Error report to upload files from | |
| */ | |
| void BeginUpload(const FPlatformErrorReport& PlatformErrorReport); | |
| private: | |
| /** | |
| * Send a request to see if the server will accept this report | |
| * @return Whether request was successfully sent | |
| */ | |
| bool SendCheckReportRequest(); | |
| /** | |
| * Compresses all crash report files and sends one compressed file. | |
| */ | |
| void CompressAndSendData(); | |
| /** | |
| * Convert the report name to single byte non-zero-terminated HTTP post data | |
| */ | |
| void AssignReportIdToPostDataBuffer(); | |
| /** | |
| * Send a POST request to the server indicating that all the files for the current report have been sent | |
| */ | |
| void PostReportComplete(); | |
| /** | |
| * Callback from HTTP library when a request has completed | |
| * @param HttpRequest The request object | |
| * @param HttpResponse The response from the server | |
| * @param bSucceeded Whether a response was successfully received | |
| */ | |
| void OnProcessRequestComplete(FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded); | |
| /** | |
| * Start uploading if BeginUpload has been called | |
| */ | |
| void OnPingSuccess(); | |
| /** | |
| * Callback a set amount of time after ping request was sent | |
| * @note Gets fired no matter whether ping response was received | |
| * @param Unused time since last call | |
| * @return Always returns false, meaning one-shot | |
| */ | |
| bool PingTimeout(float DeltaTime); | |
| /** | |
| * If there a no pending files, look through pending reports for files to upload | |
| */ | |
| void CheckPendingReportsForFilesToUpload(); | |
| /** | |
| * Start uploading files, either when user presses Submit or Ping request succeeds, whichever is later | |
| */ | |
| void BeginUploadImpl(); | |
| /** | |
| * Create a request object and bind this class's response handler to it | |
| */ | |
| TSharedRef<IHttpRequest, ESPMode::ThreadSafe> CreateHttpRequest(); | |
| /** | |
| * Send a ping request to the server | |
| */ | |
| void SendPingRequest(); | |
| /** | |
| * Parse an XML response from the server for the success field | |
| * @param Response Response to get message to parse from | |
| * @param OutValidReport Answer from the server on whether to continue with this report upload | |
| * @return Whether a valid response was received from the server | |
| */ | |
| static bool ParseServerResponse(FHttpResponsePtr Response, bool& OutValidReport); | |
| /** Host, port and common prefix of all requests to the server */ | |
| FString UrlPrefix; | |
| }; | |
| /** | |
| * Handles uploading files to the data router | |
| */ | |
| class FCrashUploadToDataRouter : public FCrashUploadBase | |
| { | |
| public: | |
| /** | |
| * Constructor: pings server | |
| * @param ServerAddress Host IP of the crash report server | |
| */ | |
| explicit FCrashUploadToDataRouter(const FString& InDataRouterUrl); | |
| /** | |
| * Destructor for logging | |
| */ | |
| virtual ~FCrashUploadToDataRouter(); | |
| void BeginUpload(const FPlatformErrorReport& PlatformErrorReport); | |
| /** | |
| * Compresses all crash report files and sends one compressed file. | |
| */ | |
| void CompressAndSendData(); | |
| /** | |
| * Create a request object and bind this class's response handler to it | |
| */ | |
| TSharedRef<IHttpRequest, ESPMode::ThreadSafe> CreateHttpRequest(); | |
| /** | |
| * Callback from HTTP library when a request has completed | |
| * @param HttpRequest The request object | |
| * @param HttpResponse The response from the server | |
| * @param bSucceeded Whether a response was successfully received | |
| */ | |
| void OnProcessRequestComplete(FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded); | |
| + // BS use our URL directly | |
| + const FString& GetBugsplatSupportResponseURL() const { return BugsplatSupportResponseURL; } // BUGSPLAT BG support response | |
| private: | |
| + void ParseServerResponse(const FHttpResponsePtr& HttpResponse); // BUGSPLAT BG support response | |
| + | |
| /** | |
| * If there a no pending files, look through pending reports for files to upload | |
| */ | |
| void CheckPendingReportsForFilesToUpload(); | |
| private: | |
| /** Url for data router requests */ | |
| FString DataRouterUrl; | |
| + // BUGSPLAT BG support response | |
| + // BS use our URL directly | |
| + FString BugsplatSupportResponseURL; | |
| + int32 PendingHttpRequests = 0; | |
| + // BUGSPLAT BG | |
| /** HTTP request result */ | |
| bool bResult = false; | |
| /** HTTP response code */ | |
| int32 ResponseCode = 0; | |
| /** Payload size */ | |
| uint32 PayloadSize = 0; | |
| /** Reports uploaded */ | |
| uint32 ReportCount; | |
| /** Duration of compression + upload */ | |
| double Duration = 0.0; | |
| /** Timer */ | |
| FDurationTimer Timer; | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright Epic Games, Inc. All Rights Reserved. | |
| #include "SCrashReportClient.h" | |
| #if !CRASH_REPORT_UNATTENDED_ONLY | |
| #include "CrashReportClientStyle.h" | |
| #include "Styling/CoreStyle.h" | |
| #include "Widgets/Images/SThrobber.h" | |
| #include "CrashDescription.h" | |
| #include "Framework/Text/SlateHyperlinkRun.h" | |
| #include "Widgets/SBoxPanel.h" | |
| #include "Widgets/SOverlay.h" | |
| #include "Widgets/Text/STextBlock.h" | |
| #include "Widgets/Text/SRichTextBlock.h" | |
| #include "Widgets/Layout/SSplitter.h" | |
| #include "Widgets/Colors/SColorBlock.h" | |
| #include "Widgets/Input/SCheckBox.h" | |
| #include "Widgets/Input/SButton.h" | |
| #include "Widgets/Layout/SSpacer.h" | |
| #include "Framework/Application/SlateApplication.h" | |
| #include "Misc/EngineBuildSettings.h" | |
| #define LOCTEXT_NAMESPACE "CrashReportClient" | |
| static void OnBrowserLinkClicked(const FSlateHyperlinkRun::FMetadata& Metadata) | |
| { | |
| const FString* UrlPtr = Metadata.Find(TEXT("href")); | |
| if(UrlPtr) | |
| { | |
| FPlatformProcess::LaunchURL(**UrlPtr, nullptr, nullptr); | |
| } | |
| } | |
| static void OnViewCrashDirectory( const FSlateHyperlinkRun::FMetadata& Metadata) | |
| { | |
| const FString* UrlPtr = Metadata.Find( TEXT( "href" ) ); | |
| if (UrlPtr) | |
| { | |
| FPlatformProcess::ExploreFolder( **UrlPtr ); | |
| } | |
| } | |
| void SCrashReportClient::Construct(const FArguments& InArgs, const TSharedRef<FCrashReportClient>& Client, bool bSimpleDialog) | |
| { | |
| CrashReportClient = Client; | |
| bHasUserCommentErrors = false; | |
| bHideSubmitAndRestart = InArgs._bHideSubmitAndRestart; | |
| + // BUGSPLAT BG support response | |
| + CrashReportClient->GetCrashSupportURLAvailableDelegate().AddLambda([this](const FString& BugsplatSupportURL) | |
| + { | |
| + if (!BrowserWidget.IsValid()) | |
| + { | |
| + return; | |
| + } | |
| + | |
| + BrowserWidget->LoadURL(BugsplatSupportURL); | |
| + }); | |
| + // BUGSPLAT BG | |
| + | |
| FText CrashDetailedMessage = LOCTEXT("CrashDetailed", "We are very sorry that this crash occurred. Our goal is to prevent crashes like this from occurring in the future. Please help us track down and fix this crash by providing detailed information about what you were doing so that we may reproduce the crash and fix it quickly. You can also log a Bug Report with us using the <a id=\"browser\" href=\"https://epicsupport.force.com/unrealengine/s/\" style=\"Hyperlink\">Bug Submission Form</> and work directly with support staff to report this issue.\n\nThanks for your help in improving the Unreal Engine."); | |
| if (FPrimaryCrashProperties::Get()->IsValid()) | |
| { | |
| FString CrashDetailedMessageString = FPrimaryCrashProperties::Get()->CrashReporterMessage.AsString(); | |
| if (!CrashDetailedMessageString.IsEmpty()) | |
| { | |
| CrashDetailedMessage = FText::FromString(CrashDetailedMessageString); | |
| } | |
| } | |
| if (bSimpleDialog) | |
| { | |
| ConstructSimpleDialog(Client, CrashDetailedMessage); | |
| } | |
| else | |
| { | |
| ConstructDetailedDialog(Client, CrashDetailedMessage); | |
| } | |
| FSlateApplication::Get().SetUnhandledKeyDownEventHandler(FOnKeyEvent::CreateSP(this, &SCrashReportClient::OnUnhandledKeyDown)); | |
| } | |
| void SCrashReportClient::ConstructDetailedDialog(const TSharedRef<FCrashReportClient>& Client, const FText& CrashDetailedMessage) | |
| { | |
| auto CrashedAppName = FPrimaryCrashProperties::Get()->IsValid() ? FPrimaryCrashProperties::Get()->GameName : TEXT(""); | |
| // Set the text displaying the name of the crashed app, if available | |
| const FText CrashedAppText = CrashedAppName.IsEmpty() ? | |
| LOCTEXT( "CrashedAppNotFound", "An unknown process has crashed" ) : | |
| LOCTEXT( "CrashedAppUnreal", "An Unreal process has crashed: " ); | |
| const FText CrashReportDataText = FText::Format( | |
| LOCTEXT( "CrashReportData", "Crash reports comprise diagnostics files (<a id=\"browser\" href=\"{0}\" style=\"Richtext.Hyperlink\">click here to view directory</>) and the following summary information: " ), | |
| FText::FromString( CrashReportClient->GetCrashDirectory()) ); | |
| ChildSlot | |
| [ | |
| SNew(SBorder) | |
| .BorderImage(FCrashReportClientStyle::Get().GetBrush("ToolPanel.GroupBorder")) | |
| [ | |
| SNew(SVerticalBox) | |
| // Stuff anchored to the top | |
| +SVerticalBox::Slot() | |
| .AutoHeight() | |
| .Padding(4) | |
| [ | |
| SNew(SHorizontalBox) | |
| + .Visibility(this, &SCrashReportClient::CallstackWindowVisibility) // BUGSPLAT BG support response | |
| +SHorizontalBox::Slot() | |
| .AutoWidth() | |
| [ | |
| SNew(STextBlock) | |
| .TextStyle(FCrashReportClientStyle::Get(), "Title") | |
| .Text(CrashedAppText) | |
| ] | |
| +SHorizontalBox::Slot() | |
| .AutoWidth() | |
| [ | |
| SNew(STextBlock) | |
| .TextStyle(FCrashReportClientStyle::Get(), "Title") | |
| .Text(FText::FromString(CrashedAppName)) | |
| ] | |
| ] | |
| +SVerticalBox::Slot() | |
| .AutoHeight() | |
| .Padding( FMargin( 4, 10 ) ) | |
| [ | |
| SNew( SRichTextBlock ) | |
| + .Visibility(this, &SCrashReportClient::CallstackWindowVisibility) // BUGSPLAT BG support response | |
| .Text(CrashDetailedMessage) | |
| .AutoWrapText(true) | |
| + SRichTextBlock::HyperlinkDecorator( TEXT("browser"), FSlateHyperlinkRun::FOnClick::CreateStatic( &OnBrowserLinkClicked ) ) | |
| ] | |
| +SVerticalBox::Slot() | |
| .Padding(FMargin(4, 10, 4, 4)) | |
| [ | |
| SNew(SSplitter) | |
| .Orientation(Orient_Vertical) | |
| +SSplitter::Slot() | |
| .Value(0.3f) | |
| [ | |
| SNew( SOverlay ) | |
| + .Visibility(this, &SCrashReportClient::CallstackWindowVisibility) // BUGSPLAT BG support response | |
| + SOverlay::Slot() | |
| [ | |
| SAssignNew( CrashDetailsInformation, SMultiLineEditableTextBox ) | |
| .Style( &FCrashReportClientStyle::Get().GetWidgetStyle<FEditableTextBoxStyle>( "NormalEditableTextBox" ) ) | |
| .OnTextCommitted( CrashReportClient.ToSharedRef(), &FCrashReportClient::UserCommentChanged ) | |
| .OnTextChanged( this, &SCrashReportClient::OnUserCommentTextChanged) | |
| .Font( FCoreStyle::GetDefaultFontStyle("Regular", 9) ) | |
| .AutoWrapText( true ) | |
| .BackgroundColor( FSlateColor( FLinearColor::Black ) ) | |
| .ForegroundColor( FSlateColor( FLinearColor::White * 0.8f ) ) | |
| ] | |
| // HintText is not implemented in SMultiLineEditableTextBox, so this is a workaround. | |
| + SOverlay::Slot() | |
| [ | |
| SNew(STextBlock) | |
| .Margin( FMargin(4,2,0,0) ) | |
| .Font( FCoreStyle::GetDefaultFontStyle("Italic", 9) ) | |
| .ColorAndOpacity( FSlateColor( FLinearColor::White * 0.5f ) ) | |
| .Text( LOCTEXT( "CrashProvide", "Please provide detailed information about what you were doing when the crash occurred." ) ) | |
| .Visibility( this, &SCrashReportClient::IsHintTextVisible ) | |
| ] | |
| ] | |
| +SSplitter::Slot() | |
| .Value(0.7f) | |
| [ | |
| SNew(SVerticalBox) | |
| + .Visibility(this, &SCrashReportClient::CallstackWindowVisibility) // BUGSPLAT BG support response | |
| + SVerticalBox::Slot() | |
| .AutoHeight() | |
| [ | |
| SNew(SOverlay) | |
| + SOverlay::Slot() | |
| [ | |
| SNew(SColorBlock) | |
| .Color(FLinearColor::Black) | |
| ] | |
| + SOverlay::Slot() | |
| [ | |
| SNew( SRichTextBlock ) | |
| .Margin( FMargin( 4, 2, 0, 8 ) ) | |
| .TextStyle( &FCrashReportClientStyle::Get().GetWidgetStyle<FTextBlockStyle>( "CrashReportDataStyle" ) ) | |
| .Text( CrashReportDataText ) | |
| .AutoWrapText( true ) | |
| .DecoratorStyleSet( &FCrashReportClientStyle::Get() ) | |
| + SRichTextBlock::HyperlinkDecorator( TEXT( "browser" ), FSlateHyperlinkRun::FOnClick::CreateStatic( &OnViewCrashDirectory ) ) | |
| ] | |
| ] | |
| + SVerticalBox::Slot() | |
| .FillHeight(0.7f) | |
| [ | |
| SNew(SOverlay) | |
| + SOverlay::Slot() | |
| [ | |
| SNew( SMultiLineEditableTextBox ) | |
| .Style( &FCrashReportClientStyle::Get().GetWidgetStyle<FEditableTextBoxStyle>( "NormalEditableTextBox" ) ) | |
| .Font( FCoreStyle::GetDefaultFontStyle("Regular", 8) ) | |
| .AutoWrapText( false ) | |
| .IsReadOnly( true ) | |
| .ReadOnlyForegroundColor( FSlateColor( FLinearColor::White * 0.8f) ) | |
| .BackgroundColor( FSlateColor( FLinearColor::Black ) ) | |
| .ForegroundColor( FSlateColor( FLinearColor::White * 0.8f ) ) | |
| .Text( Client, &FCrashReportClient::GetDiagnosticText ) | |
| ] | |
| - | |
| + SOverlay::Slot() | |
| .HAlign(HAlign_Center) | |
| .VAlign(VAlign_Center) | |
| [ | |
| SNew(SThrobber) | |
| .Visibility(CrashReportClient.ToSharedRef(), &FCrashReportClient::IsThrobberVisible) | |
| .NumPieces(5) | |
| ] | |
| ] | |
| ] | |
| + | |
| + // BUGSPLAT BG support response | |
| + + SSplitter::Slot() | |
| + .Value(0.7f) | |
| + [ | |
| + SNew(SVerticalBox) | |
| + .Visibility(this, &SCrashReportClient::WebBrowserVisibility) | |
| + + SVerticalBox::Slot() | |
| + .FillHeight(0.7f) | |
| + [ | |
| + SNew(SOverlay) | |
| + + SOverlay::Slot() | |
| + [ | |
| + SAssignNew(BrowserWidget, SWebBrowser) | |
| + .ShowControls(false) | |
| + .ShowAddressBar(false) | |
| + .InitialURL(TEXT("https://octomore.bugsplat.com/browse/support/?wait=true")) | |
| + ] | |
| + ] | |
| + ] | |
| + // BUGSPLAT BG | |
| ] | |
| +SVerticalBox::Slot() | |
| .AutoHeight() | |
| .Padding( FMargin( 4, 12, 4, 4 ) ) | |
| [ | |
| SNew( SHorizontalBox ) | |
| - .Visibility( FCrashReportCoreConfig::Get().GetHideLogFilesOption() ? EVisibility::Collapsed : EVisibility::Visible ) | |
| + .Visibility(this, &SCrashReportClient::SendLogFilesVisibility) // BUGSPLAT BG support response | |
| + SHorizontalBox::Slot() | |
| .AutoWidth() | |
| .VAlign( VAlign_Center ) | |
| [ | |
| SNew( SCheckBox ) | |
| .IsChecked( FCrashReportCoreConfig::Get().GetSendLogFile() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked ) | |
| .OnCheckStateChanged( CrashReportClient.ToSharedRef(), &FCrashReportClient::SendLogFile_OnCheckStateChanged ) | |
| ] | |
| + SHorizontalBox::Slot() | |
| .FillWidth( 1.0f ) | |
| .VAlign( VAlign_Center ) | |
| [ | |
| SNew( STextBlock ) | |
| .AutoWrapText( true ) | |
| .Text( LOCTEXT( "IncludeLogs", "Include log files with submission. I understand that logs contain some personal information such as my system and user name." ) ) | |
| ] | |
| ] | |
| +SVerticalBox::Slot() | |
| .AutoHeight() | |
| .Padding( FMargin( 4, 4 ) ) | |
| [ | |
| SNew(SHorizontalBox) | |
| + .Visibility(this, &SCrashReportClient::CallstackWindowVisibility) // BUGSPLAT BG support response | |
| +SHorizontalBox::Slot() | |
| .AutoWidth() | |
| .VAlign(VAlign_Center) | |
| [ | |
| SNew(SCheckBox) | |
| .IsChecked( FCrashReportCoreConfig::Get().GetAllowToBeContacted() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked ) | |
| .IsEnabled( !FEngineBuildSettings::IsInternalBuild() ) | |
| .OnCheckStateChanged(CrashReportClient.ToSharedRef(), &FCrashReportClient::AllowToBeContacted_OnCheckStateChanged) | |
| ] | |
| +SHorizontalBox::Slot() | |
| .FillWidth(1.0f) | |
| .VAlign(VAlign_Center) | |
| [ | |
| SNew(STextBlock) | |
| .AutoWrapText(true) | |
| .IsEnabled( !FEngineBuildSettings::IsInternalBuild() ) | |
| .Text_Static(&SCrashReportClient::GetContactText) | |
| ] | |
| ] | |
| // Stuff anchored to the bottom | |
| +SVerticalBox::Slot() | |
| .AutoHeight() | |
| .Padding( FMargin(4, 4+16, 4, 4) ) | |
| [ | |
| SNew(SHorizontalBox) | |
| + SHorizontalBox::Slot() | |
| .HAlign( HAlign_Center ) | |
| .VAlign( VAlign_Center ) | |
| .AutoWidth() | |
| .Padding( FMargin( 0 ) ) | |
| [ | |
| SNew( SButton ) | |
| .ContentPadding( FMargin( 8, 2 ) ) | |
| .Text( LOCTEXT( "CloseWithoutSending", "Close Without Sending" ) ) | |
| .OnClicked( Client, &FCrashReportClient::CloseWithoutSending ) | |
| .Visibility(FCrashReportCoreConfig::Get().IsAllowedToCloseWithoutSending() ? EVisibility::Visible : EVisibility::Hidden) | |
| + .IsEnabled(this, &SCrashReportClient::IsSendEnabled) // BUGSPLAT BG support response | |
| ] | |
| +SHorizontalBox::Slot() | |
| .FillWidth(1.0f) | |
| .HAlign(HAlign_Left) | |
| .VAlign(VAlign_Center) | |
| .Padding(0) | |
| [ | |
| SNew(SSpacer) | |
| ] | |
| #if PLATFORM_WINDOWS | |
| +SHorizontalBox::Slot() | |
| .HAlign(HAlign_Center) | |
| .VAlign(VAlign_Center) | |
| .AutoWidth() | |
| .Padding( FMargin(6) ) | |
| [ | |
| SNew(SButton) | |
| .ContentPadding( FMargin(8,2) ) | |
| .Text(LOCTEXT("CopyFiles", "Copy Files To Clipboard")) | |
| .OnClicked(Client, &FCrashReportClient::CopyFilesToClipboard) | |
| .Visibility(FCrashReportCoreConfig::Get().IsAllowedToCopyFilesToClipboard() ? EVisibility::Visible : EVisibility::Hidden) | |
| ] | |
| #endif | |
| - +SHorizontalBox::Slot() | |
| - .HAlign(HAlign_Center) | |
| - .VAlign(VAlign_Center) | |
| - .AutoWidth() | |
| - .Padding( FMargin(6) ) | |
| - [ | |
| - SNew(SButton) | |
| - .ContentPadding( FMargin(8,2) ) | |
| - .Text(LOCTEXT("Send", "Send and Close")) | |
| - .OnClicked(Client, &FCrashReportClient::Submit) | |
| - .IsEnabled(this, &SCrashReportClient::IsSendEnabled) | |
| - .ToolTipText_Static(&SCrashReportClient::GetSendTooltip) | |
| - ] | |
| - +SHorizontalBox::Slot() | |
| - .HAlign(HAlign_Center) | |
| - .VAlign(VAlign_Center) | |
| - .AutoWidth() | |
| - .Padding( FMargin(0) ) | |
| - [ | |
| - SNew(SButton) | |
| - .ContentPadding( FMargin(8,2) ) | |
| - .Text(LOCTEXT("SendAndRestartEditor", "Send and Restart")) | |
| - .OnClicked(Client, &FCrashReportClient::SubmitAndRestart) | |
| - .IsEnabled(this, &SCrashReportClient::IsSendEnabled) | |
| - .Visibility( bHideSubmitAndRestart || FCrashReportCoreConfig::Get().GetHideRestartOption() ? EVisibility::Collapsed : EVisibility::Visible ) | |
| - .ToolTipText_Static(&SCrashReportClient::GetSendTooltip) | |
| - ] | |
| + // BUGSPLAT BG support response | |
| + +SHorizontalBox::Slot() | |
| + .HAlign(HAlign_Right) | |
| + .VAlign(VAlign_Center) | |
| + .AutoWidth() | |
| + .Padding(FMargin(6)) | |
| + [ | |
| + SNew(SButton) | |
| + .ContentPadding(FMargin(8, 2)) | |
| + .Text(LOCTEXT("SendKeepVisible", "Send")) | |
| + .OnClicked(Client, &FCrashReportClient::SubmitKeepVisible) | |
| + .IsEnabled(this, &SCrashReportClient::IsSendEnabled) | |
| + ] | |
| + // BUGSPLAT BG | |
| ] | |
| ] | |
| ]; | |
| } | |
| void SCrashReportClient::ConstructSimpleDialog(const TSharedRef<FCrashReportClient>& Client, const FText& CrashDetailedMessage) | |
| { | |
| FString CrashedAppName = FPrimaryCrashProperties::Get()->IsValid() ? FPrimaryCrashProperties::Get()->GameName : TEXT(""); | |
| // GameNames have taken on a number of prefixes over the years. Try to strip them all off. | |
| if (!CrashedAppName.RemoveFromStart(TEXT("UE4-"))) | |
| { | |
| if (!CrashedAppName.RemoveFromStart(TEXT("UE5-"))) | |
| { | |
| CrashedAppName.RemoveFromStart(TEXT("UE-")); | |
| } | |
| } | |
| CrashedAppName.RemoveFromEnd(TEXT("Game")); | |
| ChildSlot | |
| [ | |
| SNew(SBorder) | |
| .BorderImage(FCrashReportClientStyle::Get().GetBrush("ToolPanel.GroupBorder")) | |
| [ | |
| SNew(SVerticalBox) | |
| // Stuff anchored to the top | |
| +SVerticalBox::Slot() | |
| .AutoHeight() | |
| .Padding(4) | |
| [ | |
| SNew(SHorizontalBox) | |
| +SHorizontalBox::Slot() | |
| .AutoWidth() | |
| [ | |
| SNew(STextBlock) | |
| .TextStyle(FCrashReportClientStyle::Get(), "Title") | |
| .Text(FText::FromString(CrashedAppName)) | |
| ] | |
| ] | |
| +SVerticalBox::Slot() | |
| .AutoHeight() | |
| .Padding( FMargin( 4, 10 ) ) | |
| [ | |
| SNew( SRichTextBlock ) | |
| .Text(CrashDetailedMessage) | |
| .AutoWrapText(true) | |
| + SRichTextBlock::HyperlinkDecorator( TEXT("browser"), FSlateHyperlinkRun::FOnClick::CreateStatic( &OnBrowserLinkClicked ) ) | |
| ] | |
| // Stuff anchored to the bottom | |
| +SVerticalBox::Slot() | |
| .Padding( FMargin(4, 4+16, 4, 4) ) | |
| [ | |
| SNew(SHorizontalBox) | |
| +SHorizontalBox::Slot() | |
| .HAlign(HAlign_Right) | |
| .VAlign(VAlign_Bottom) | |
| .Padding( FMargin(6) ) | |
| [ | |
| SNew(SButton) | |
| .ContentPadding( FMargin(8,2) ) | |
| .Text(LOCTEXT("Close", "Close")) | |
| .OnClicked(Client, &FCrashReportClient::Close) | |
| ] | |
| ] | |
| ] | |
| ]; | |
| } | |
| FReply SCrashReportClient::OnUnhandledKeyDown(const FKeyEvent& InKeyEvent) | |
| { | |
| const FKey Key = InKeyEvent.GetKey(); | |
| if (Key == EKeys::Enter) | |
| { | |
| CrashReportClient->Submit(); | |
| return FReply::Handled(); | |
| } | |
| return FReply::Unhandled(); | |
| } | |
| void SCrashReportClient::OnUserCommentTextChanged(const FText& NewText) | |
| { | |
| FText ErrorMessage = FText::GetEmpty(); | |
| bHasUserCommentErrors = false; | |
| int SizeLimit = FCrashReportCoreConfig::Get().GetUserCommentSizeLimit(); | |
| int Size = NewText.ToString().Len(); | |
| if (Size > SizeLimit) | |
| { | |
| bHasUserCommentErrors = true; | |
| ErrorMessage = FText::Format(LOCTEXT("UserCommentTooLongError", "Description may only be a maximum of {0} characters (currently {1})"), SizeLimit, Size); | |
| } | |
| CrashDetailsInformation->SetError(ErrorMessage); | |
| } | |
| EVisibility SCrashReportClient::IsHintTextVisible() const | |
| { | |
| + // BUGSPLAT BG support response | |
| + const EVisibility CallstackVisibility = CallstackWindowVisibility(); | |
| + if (CallstackVisibility != EVisibility::Visible) | |
| + { | |
| + return CallstackVisibility; | |
| + } | |
| + // BUGSPLAT BG | |
| return CrashDetailsInformation->GetText().IsEmpty() ? EVisibility::HitTestInvisible : EVisibility::Hidden; | |
| } | |
| +// BUGSPLAT BG support response | |
| +EVisibility SCrashReportClient::SendLogFilesVisibility() const | |
| +{ | |
| + if (FCrashReportCoreConfig::Get().GetHideLogFilesOption() || | |
| + !IsSendEnabled()) | |
| + { | |
| + return EVisibility::Collapsed; | |
| + } | |
| + return EVisibility::Visible; | |
| +} | |
| +EVisibility SCrashReportClient::CallstackWindowVisibility() const | |
| +{ | |
| + return IsSendEnabled() ? EVisibility::Visible : EVisibility::Collapsed; | |
| +} | |
| +EVisibility SCrashReportClient::WebBrowserVisibility() const | |
| +{ | |
| + return IsSendEnabled() ? EVisibility::Collapsed : EVisibility::Visible; | |
| +} | |
| +// BUGSPLAT BG | |
| + | |
| bool SCrashReportClient::IsSendEnabled() const | |
| { | |
| const bool bValidAppName = FPrimaryCrashProperties::Get()->IsValid() && !FPrimaryCrashProperties::Get()->GameName.IsEmpty(); | |
| const bool bValidEndPoint = !FCrashReportCoreConfig::Get().GetReceiverAddress().IsEmpty() || !FCrashReportCoreConfig::Get().GetDataRouterURL().IsEmpty(); | |
| - return bValidAppName && bValidEndPoint && !bHasUserCommentErrors; | |
| + return bValidAppName && !bHasUserCommentErrors && CrashReportClient->IsSendEnabled(); // BUGSPLAT BG support response | |
| } | |
| FText SCrashReportClient::GetSendTooltip() | |
| { | |
| // Optionally show a tooltip with the endpoint domain to the user. If the old receiver address is used it is | |
| // just a IP number, so there is no point is showing that. | |
| static FText CachedDomain = []() { | |
| FStringView Endpoint = FCrashReportCoreConfig::Get().GetReceiverAddress(); | |
| bool bIsUrl = false; | |
| if (Endpoint.IsEmpty()) | |
| { | |
| Endpoint = FCrashReportCoreConfig::Get().GetDataRouterURL(); | |
| bIsUrl = true; | |
| } | |
| if (Endpoint.IsEmpty()) | |
| { | |
| return LOCTEXT("SendTooltipEmpty", "No server specified."); | |
| } | |
| if (bIsUrl && FCrashReportCoreConfig::Get().GetShowEndpointInTooltip()) | |
| { | |
| // Show only domain, not full url | |
| const int32 Start = Endpoint.StartsWith(TEXT("https://")) ? 8 : 0; | |
| Endpoint.RightChopInline(Start); | |
| int32 End(INDEX_NONE); | |
| Endpoint.FindChar('/', End); | |
| Endpoint.LeftInline(End); | |
| return FText::Format(LOCTEXT("SendTooltipUrl", "Send to {0}"), FText::FromStringView(Endpoint)); | |
| } | |
| return LOCTEXT("SendTooltip", "Send to server"); | |
| }(); | |
| return CachedDomain; | |
| } | |
| FText SCrashReportClient::GetContactText() | |
| { | |
| static FText CachedContactText = []() | |
| { | |
| const FStringView Company = FCrashReportCoreConfig::Get().GetCompanyName(); | |
| if (Company.IsEmpty()) | |
| { | |
| return LOCTEXT("IAgreeNoCompany", "I agree to be contacted via email if additional information about this crash would help fix it."); | |
| } | |
| return FText::Format( | |
| LOCTEXT("IAgreeCompany", "I agree to be contacted by {0} via email if additional information about this crash would help fix it."), | |
| FText::FromStringView(Company) | |
| ); | |
| }(); | |
| return CachedContactText; | |
| } | |
| #undef LOCTEXT_NAMESPACE | |
| #endif // !CRASH_REPORT_UNATTENDED_ONLY |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright Epic Games, Inc. All Rights Reserved. | |
| #pragma once | |
| #include "CrashReportClient.h" | |
| #if !CRASH_REPORT_UNATTENDED_ONLY | |
| +#include "SWebBrowser.h" // BUGSPLAT BG support response | |
| #include "Widgets/SCompoundWidget.h" | |
| #include "Widgets/DeclarativeSyntaxSupport.h" | |
| #include "Widgets/Input/SMultiLineEditableTextBox.h" | |
| /** | |
| * UI for the crash report client app | |
| */ | |
| class SCrashReportClient : public SCompoundWidget | |
| { | |
| public: | |
| /** | |
| * Slate arguments | |
| */ | |
| SLATE_BEGIN_ARGS(SCrashReportClient) | |
| : _bHideSubmitAndRestart(false) | |
| { | |
| } | |
| /** Should the Submit and Send button be hitten. This can be overriden by a platform settings in the crash report config ini file. */ | |
| SLATE_ARGUMENT(bool, bHideSubmitAndRestart) | |
| SLATE_END_ARGS() | |
| /** | |
| * Construct this Slate ui | |
| * @param InArgs Slate arguments, not used | |
| * @param Client Crash report client implementation object | |
| * @param bSimpleDialog Whether to use the simple dialog UI that implicitly sends the report | |
| */ | |
| void Construct(const FArguments& InArgs, const TSharedRef<FCrashReportClient>& Client, bool bSimpleDialog); | |
| bool IsFinished() { return CrashReportClient->IsUploadComplete() && CrashReportClient->ShouldWindowBeHidden(); } | |
| private: | |
| /** | |
| * Construct the detailed Slate ui with controls for sending the report and comments | |
| * @param Client Crash report client implementation object | |
| */ | |
| void ConstructDetailedDialog(const TSharedRef<FCrashReportClient>& Client, const FText& CrashDetailedMessage); | |
| /** | |
| * Construct the minimal Slate ui with just a button to close | |
| * @param Client Crash report client implementation object | |
| */ | |
| void ConstructSimpleDialog(const TSharedRef<FCrashReportClient>& Client, const FText& CrashDetailedMessage); | |
| /** | |
| * Keyboard short-cut handler | |
| * @param InKeyEvent Which key was released, and which auxiliary keys were pressed | |
| * @return Whether the event was handled | |
| */ | |
| FReply OnUnhandledKeyDown(const FKeyEvent& InKeyEvent); | |
| /** Called if the multi line widget text changes */ | |
| void OnUserCommentTextChanged(const FText& NewText); | |
| /** Whether the hint text should be visible. */ | |
| EVisibility IsHintTextVisible() const; | |
| + // BUGSPLAT BG support response | |
| + EVisibility SendLogFilesVisibility() const; | |
| + EVisibility CallstackWindowVisibility() const; | |
| + EVisibility WebBrowserVisibility() const; | |
| + | |
| /** Whether the send buttons are enabled. */ | |
| bool IsSendEnabled() const; | |
| /** Returns the tooltop text for send button */ | |
| static FText GetSendTooltip(); | |
| /** Returns the 'allow contact' text */ | |
| static FText GetContactText(); | |
| #if PLATFORM_WINDOWS | |
| /** Whether the copy to clipboard button is available. */ | |
| bool IsCopyToClipboardEnabled() const; | |
| #endif | |
| /** Crash report client implementation object */ | |
| TSharedPtr<FCrashReportClient> CrashReportClient; | |
| + TSharedPtr<SWebBrowser> BrowserWidget; // BUGSPLAT BG support response | |
| TSharedPtr<SMultiLineEditableTextBox> CrashDetailsInformation; | |
| bool bHasUserCommentErrors; | |
| bool bHideSubmitAndRestart; | |
| }; | |
| #endif // !CRASH_REPORT_UNATTENDED_ONLY |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment