2019-12-26 23:01:54 -05:00
// Copyright Epic Games, Inc. All Rights Reserved.
2017-09-01 12:46:25 -04:00
# include "CrashReportClient.h"
# include "Misc/CommandLine.h"
# include "Internationalization/Internationalization.h"
# include "Containers/Ticker.h"
2019-04-09 21:27:26 -04:00
# include "CrashReportCoreConfig.h"
2017-09-01 12:46:25 -04:00
# 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 )
{
2024-01-25 03:54:34 -05:00
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 ) ;
2017-09-01 12:46:25 -04:00
}
} ;
# if !CRASH_REPORT_UNATTENDED_ONLY
# include "PlatformHttp.h"
# include "Framework/Application/SlateApplication.h"
2021-06-23 17:51:32 -04:00
FCrashReportClient : : FCrashReportClient ( const FPlatformErrorReport & InErrorReport , bool bImplicitSend )
2017-09-01 12:46:25 -04:00
: DiagnosticText ( LOCTEXT ( " ProcessingReport " , " Processing crash report ... " ) )
, DiagnoseReportTask ( nullptr )
, ErrorReport ( InErrorReport )
2019-04-09 21:27:26 -04:00
, ReceiverUploader ( FCrashReportCoreConfig : : Get ( ) . GetReceiverAddress ( ) )
, DataRouterUploader ( FCrashReportCoreConfig : : Get ( ) . GetDataRouterURL ( ) )
2017-09-01 12:46:25 -04:00
, bShouldWindowBeHidden ( false )
2021-06-23 17:51:32 -04:00
, bSendData ( bImplicitSend )
2019-10-03 16:26:48 -04:00
, bIsSuccesfullRestart ( false )
, bIsUploadComplete ( false )
2017-09-01 12:46:25 -04:00
{
if ( FPrimaryCrashProperties : : Get ( ) - > IsValid ( ) )
{
2019-01-14 12:11:24 -05:00
bool bUsePrimaryData = false ;
if ( FPrimaryCrashProperties : : Get ( ) - > HasProcessedData ( ) )
2017-09-01 12:46:25 -04:00
{
bUsePrimaryData = true ;
}
2019-01-14 12:11:24 -05:00
else
{
if ( ! ErrorReport . TryReadDiagnosticsFile ( ) & & ! FParse : : Param ( FCommandLine : : Get ( ) , TEXT ( " no-local-diagnosis " ) ) )
{
DiagnoseReportTask = new FAsyncTask < FDiagnoseReportWorker > ( this ) ;
DiagnoseReportTask - > StartBackgroundTask ( ) ;
2020-02-21 19:31:35 -05:00
StartTicker ( ) ;
2019-01-14 12:11:24 -05:00
}
else
{
bUsePrimaryData = true ;
}
}
2017-09-01 12:46:25 -04:00
2019-01-14 12:11:24 -05:00
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 ) ;
2017-09-01 12:46:25 -04:00
2019-01-14 12:11:24 -05:00
FormattedDiagnosticText = FCrashReportUtil : : FormatDiagnosticText ( FText : : FromString ( ReportString ) ) ;
}
2017-09-01 12:46:25 -04:00
}
2021-06-23 17:51:32 -04:00
if ( bSendData )
{
StartTicker ( ) ;
}
2017-09-01 12:46:25 -04:00
}
FCrashReportClient : : ~ FCrashReportClient ( )
{
2020-02-21 19:31:35 -05:00
if ( TickHandle . IsValid ( ) )
{
2021-08-16 11:05:18 -04:00
FTSTicker : : GetCoreTicker ( ) . RemoveTicker ( TickHandle ) ;
2020-02-21 19:31:35 -05:00
TickHandle . Reset ( ) ;
}
2019-01-14 12:11:24 -05:00
StopBackgroundThread ( ) ;
}
void FCrashReportClient : : StopBackgroundThread ( )
{
if ( DiagnoseReportTask )
2017-09-01 12:46:25 -04:00
{
DiagnoseReportTask - > EnsureCompletion ( ) ;
delete DiagnoseReportTask ;
2019-01-14 12:11:24 -05:00
DiagnoseReportTask = nullptr ;
2017-09-01 12:46:25 -04:00
}
}
FReply FCrashReportClient : : CloseWithoutSending ( )
{
2020-02-17 14:41:35 -05:00
bSendData = false ;
bShouldWindowBeHidden = true ;
StartTicker ( ) ;
2017-09-01 12:46:25 -04:00
return FReply : : Handled ( ) ;
}
2021-06-23 17:51:32 -04:00
FReply FCrashReportClient : : Close ( )
{
bShouldWindowBeHidden = true ;
return FReply : : Handled ( ) ;
}
2023-08-22 15:17:30 -04:00
# 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
2017-09-01 12:46:25 -04:00
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()
2020-09-10 15:39:00 -04:00
if ( ! FPaths : : GetCleanFilename ( CrashedAppPath ) . StartsWith ( TEXT ( " UnrealEditor " ) ) )
2017-09-01 12:46:25 -04:00
{
// 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
2018-11-14 19:05:13 -05:00
FOpenLauncherOptions OpenOptions ( FString : : Printf ( TEXT ( " apps/%s?action=launch " ) , * CrashedAppPathUri ) ) ;
2017-09-01 12:46:25 -04:00
OpenOptions . bSilent = true ;
if ( LauncherPlatform - > OpenLauncher ( OpenOptions ) )
{
bLauncherRestarted = true ;
2019-10-03 16:26:48 -04:00
bIsSuccesfullRestart = true ;
2017-09-01 12:46:25 -04:00
}
}
}
}
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 ) ;
2019-10-03 16:26:48 -04:00
bIsSuccesfullRestart = true ;
2017-09-01 12:46:25 -04:00
}
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 )
{
2019-04-09 21:27:26 -04:00
FCrashReportCoreConfig : : Get ( ) . SetAllowToBeContacted ( NewRadioState = = ECheckBoxState : : Checked ) ;
2017-09-01 12:46:25 -04:00
// 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 )
{
2019-04-09 21:27:26 -04:00
FCrashReportCoreConfig : : Get ( ) . SetSendLogFile ( NewRadioState = = ECheckBoxState : : Checked ) ;
2017-09-01 12:46:25 -04:00
}
void FCrashReportClient : : StartTicker ( )
{
2020-02-21 19:31:35 -05:00
if ( ! TickHandle . IsValid ( ) )
{
2021-08-16 11:05:18 -04:00
TickHandle = FTSTicker : : GetCoreTicker ( ) . AddTicker ( FTickerDelegate : : CreateRaw ( this , & FCrashReportClient : : Tick ) , 1.f ) ;
2020-02-21 19:31:35 -05:00
}
2017-09-01 12:46:25 -04:00
}
void FCrashReportClient : : StoreCommentAndUpload ( )
{
// Write user's comment
ErrorReport . SetUserComment ( UserComment ) ;
StartTicker ( ) ;
}
bool FCrashReportClient : : Tick ( float UnusedDeltaTime )
{
2018-05-23 21:04:31 -04:00
QUICK_SCOPE_CYCLE_COUNTER ( STAT_FCrashReportClient_Tick ) ;
2017-09-01 12:46:25 -04:00
// We are waiting for diagnose report task to complete.
if ( IsProcessingCallstack ( ) )
{
return true ;
}
2020-02-17 14:41:35 -05:00
if ( DiagnoseReportTask )
{
check ( DiagnoseReportTask - > IsWorkDone ( ) ) ; // Expected when IsProcessingCallstack() returns false.
2020-02-21 19:31:35 -05:00
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)
2020-02-17 14:41:35 -05:00
check ( DiagnoseReportTask = = nullptr ) ; // Expected after StopBackgroundThread() call.
}
2021-06-23 17:51:32 -04:00
// Implicit send will begin uploading immediately and continue after the window is hidden
2017-09-01 12:46:25 -04:00
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 ;
}
}
}
2021-06-23 17:51:32 -04:00
if ( ! bShouldWindowBeHidden )
{
return true ;
}
2017-09-01 12:46:25 -04:00
if ( FCrashUploadBase : : IsInitialized ( ) )
{
FCrashUploadBase : : StaticShutdown ( ) ;
}
2019-10-03 16:26:48 -04:00
bIsUploadComplete = true ;
2017-09-01 12:46:25 -04:00
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 ( ) )
{
Fixed out-of-process crash reporting used for the Editor on Windows to prevent deadlocking on allocation.
- The function reporting the crash on the pipe doesn't need to suspend all the threads. The original purpose for suspending the threads was likely to preserve the state of the process as best as possible, but that is not required and prone to deadlocks.
Not suspending all the threads may would fix the hyphotetical case where CRC main thread is waiting for a prior ensure call stack to get resolved - I observed degenerated cases on my machine where this could take more than 15 minutes - preventing it to respond promptly to an incoming crash from the Editor. The flow was as following:
- Editor fires an ensure, suspends all the thread, pipe a message to CRC to process the ensure.
- CRC collects the ensure artefacts quickly, replies to the Editor, the Editor resumes, then CRC starts to resolve the call stack (blocking the main thread) from the minidump - degenerated cases can take several minutes.
- Editor gets CRC messages and resumes its threads.
- Editor fires a crash, suspends all the threads, pipes a message to CRC to process the crash.
- CRC main thread is busy, waiting for the previous ensure call stack to be resolved... and doesn't respond promptly to the crash message.
- Editor threads behing suspended, the code responsible to timeout if CRC takes too long never executes and Editor stalls until CRC dies or responds -> The user likely kills the Editor (and possibly CRC).
As a side effect from this change, if CRC doesn't respond promptly to a crash, the thread calling ReportCrash( )/ReportGPUCrash( ) will timeout and likely terminate the Editor before CRC could collect the crash artifacts or walk the thread to collect the call stacks.
- Added a hint to the diagnostic logs reported with the Editor 'SummaryEvent' analytic event to indicate if the crash report was produced after the Editor died, so that the portable call stack wasn't captured.
- Added a message displayed to the user by CRC saying that the the system failed to capture the callstack.
#jira UE-108701 - Editor deadlocks when reporting an ensure, a stall or a crash.
#rb Johan.Berg
[CL 15452515 by Patrick Laflamme in ue5-main branch]
2021-02-18 10:40:40 -04:00
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. " ) ;
}
2017-09-01 12:46:25 -04:00
}
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