// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "GenericErrorReport.h" #include "HAL/PlatformProcess.h" #include "HAL/PlatformFilemanager.h" #include "UObject/NameTypes.h" #include "Logging/LogMacros.h" #include "Misc/Parse.h" #include "Misc/FileHelper.h" #include "Internationalization/Text.h" #include "CrashReportClientConfig.h" #include "Modules/ModuleManager.h" #include "CrashDebugHelper.h" #include "CrashDebugHelperModule.h" #include "CrashReportUtil.h" // ---------------------------------------------------------------- // Helpers namespace { /** Enum specifying a particular part of a crash report text file */ namespace EReportSection { enum Type { CallStack, SourceContext, Other }; } } // ---------------------------------------------------------------- // FGenericErrorReport FGenericErrorReport::FGenericErrorReport(const FString& Directory) : ReportDirectory(Directory) , bValidCallstack(true) { auto FilenamesVisitor = MakeDirectoryVisitor([this](const TCHAR* FilenameOrDirectory, bool bIsDirectory) { if (!bIsDirectory) { ReportFilenames.Push(FPaths::GetCleanFilename(FilenameOrDirectory)); } return true; }); FPlatformFileManager::Get().GetPlatformFile().IterateDirectory(*ReportDirectory, FilenamesVisitor); } bool FGenericErrorReport::SetUserComment(const FText& UserComment) { const bool bAllowToBeContacted = FCrashReportClientConfig::Get().GetAllowToBeContacted(); const FString UserName1 = FPlatformProcess::UserName( false ); const FString UserName2 = FPlatformProcess::UserName( true ); const TCHAR* Anonymous = TEXT( "Anonymous" ); FPrimaryCrashProperties::Get()->UserDescription = UserComment.ToString(); // Load the file and remove all PII if bAllowToBeContacted is set to false. const bool bRemovePersonalData = !bAllowToBeContacted; if( bRemovePersonalData ) { FPrimaryCrashProperties::Get()->UserName = TEXT( "" ); FPrimaryCrashProperties::Get()->EpicAccountId = TEXT( "" ); // For now remove the command line completely, to hide the potential personal data. Need to revisit it later. FPrimaryCrashProperties::Get()->CommandLine = TEXT( "CommandLineRemoved" ); } // Save updated properties, including removed all PII if bAllowToBeContacted is set to false. FPrimaryCrashProperties::Get()->Save(); // Remove it later, in the next iteration. // Find .xml file FString XmlFilename; if (!FindFirstReportFileWithExtension(XmlFilename, TEXT(".xml"))) { return false; } FString XmlFilePath = GetReportDirectory() / XmlFilename; // FXmlFile's constructor loads the file to memory, closes the file and parses the data FXmlFile XmlFile(XmlFilePath); FXmlNode* DynamicSignaturesNode = XmlFile.IsValid() ? XmlFile.GetRootNode()->FindChildNode(TEXT("DynamicSignatures")) : nullptr; if (!DynamicSignaturesNode) { return false; } if (bRemovePersonalData) { FXmlNode* ProblemNode = XmlFile.GetRootNode()->FindChildNode( TEXT( "ProblemSignatures" ) ); if (ProblemNode) { FXmlNode* Parameter8Node = ProblemNode->FindChildNode( TEXT( "Parameter8" ) ); if (Parameter8Node) { // Replace user name in assert message, command line etc. FString Content = Parameter8Node->GetContent(); Content = Content.Replace( *UserName1, Anonymous ); Content = Content.Replace( *UserName2, Anonymous ); // Remove the command line. Command line is between first and second ! TArray ParsedParameters8; Content.ParseIntoArray( ParsedParameters8, TEXT( "!" ), false ); if (ParsedParameters8.Num() > 1) { ParsedParameters8[1] = TEXT( "CommandLineRemoved" ); } Content = FString::Join( ParsedParameters8, TEXT( "!" ) ); Parameter8Node->SetContent( Content ); } FXmlNode* Parameter9Node = ProblemNode->FindChildNode( TEXT( "Parameter9" ) ); if (Parameter9Node) { // Replace user name in assert message, command line etc. FString Content = Parameter9Node->GetContent(); Content = Content.Replace( *UserName1, Anonymous ); Content = Content.Replace( *UserName2, Anonymous ); Parameter9Node->SetContent( Content ); } } } // Add or update the user comment. FXmlNode* Parameter3Node = DynamicSignaturesNode->FindChildNode(TEXT("Parameter3")); if( Parameter3Node ) { Parameter3Node->SetContent(UserComment.ToString()); } else { DynamicSignaturesNode->AppendChildNode(TEXT("Parameter3"), UserComment.ToString()); } // @see FCrashDescription::UpdateIDs const FString EpicLoginAndUserNameIDs = FString::Printf( TEXT( "!LoginId:%s!EpicAccountId:%s!Name:%s" ), *FPrimaryCrashProperties::Get()->LoginId.AsString(), *FPrimaryCrashProperties::Get()->EpicAccountId.AsString(), *FPrimaryCrashProperties::Get()->UserName.AsString() ); // Add or update a user ID. FXmlNode* Parameter4Node = DynamicSignaturesNode->FindChildNode(TEXT("Parameter4")); if( Parameter4Node ) { Parameter4Node->SetContent(EpicLoginAndUserNameIDs); } else { DynamicSignaturesNode->AppendChildNode(TEXT("Parameter4"), EpicLoginAndUserNameIDs); } // Add or update bAllowToBeContacted const FString AllowToBeContacted = bAllowToBeContacted ? TEXT("true") : TEXT("false"); FXmlNode* AllowToBeContactedNode = DynamicSignaturesNode->FindChildNode(TEXT("bAllowToBeContacted")); if( AllowToBeContactedNode ) { AllowToBeContactedNode->SetContent(AllowToBeContacted); } else { DynamicSignaturesNode->AppendChildNode(TEXT("bAllowToBeContacted"), AllowToBeContacted); } // Re-save over the top return XmlFile.Save(XmlFilePath); } void FGenericErrorReport::SetPrimaryCrashProperties( FPrimaryCrashProperties& out_PrimaryCrashProperties ) { FCrashDebugHelperModule& CrashHelperModule = FModuleManager::LoadModuleChecked( FName( "CrashDebugHelper" ) ); ICrashDebugHelper* Helper = CrashHelperModule.Get(); if (Helper && bValidCallstack) { TArray CallStack = Helper->CrashInfo.Exception.CallStackString; // Get the callstack and remove any frames that we don't care about int64 NumMinidumpFramesToIgnore = out_PrimaryCrashProperties.NumMinidumpFramesToIgnore; if (NumMinidumpFramesToIgnore > 0) { CallStack.RemoveAt(0, FMath::Min(CallStack.Num(), (int32)NumMinidumpFramesToIgnore)); } out_PrimaryCrashProperties.CallStack = CallStack; out_PrimaryCrashProperties.Modules = Helper->CrashInfo.ModuleNames; out_PrimaryCrashProperties.SourceContext = Helper->CrashInfo.SourceContext; // If error message is empty, it means general crash like accessing invalid memory ptr. if (out_PrimaryCrashProperties.ErrorMessage.AsString().Len() == 0) { out_PrimaryCrashProperties.ErrorMessage = Helper->CrashInfo.Exception.ExceptionString; } out_PrimaryCrashProperties.Save(); } } void FGenericErrorReport::SetCrashReportClientVersion(const FString& InVersion) { FPrimaryCrashProperties::Get()->CrashReportClientVersion = InVersion; FPrimaryCrashProperties::Get()->Save(); } TArray FGenericErrorReport::GetFilesToUpload() const { TArray FilesToUpload; for (const auto& Filename: ReportFilenames) { FilesToUpload.Push(ReportDirectory / Filename); } return FilesToUpload; } bool FGenericErrorReport::LoadWindowsReportXmlFile( FString& OutString ) const { // Find .xml file FString XmlFilename; if (!FindFirstReportFileWithExtension(XmlFilename, TEXT(".xml"))) { return false; } return FFileHelper::LoadFileToString( OutString, *(ReportDirectory / XmlFilename) ); } bool FGenericErrorReport::TryReadDiagnosticsFile() { FString FileContent; if (!FFileHelper::LoadFileToString(FileContent, *(ReportDirectory / FCrashReportClientConfig::Get().GetDiagnosticsFilename()))) { // No diagnostics file return false; } FString Exception; TArray Callstack; static const TCHAR CallStackStartKey[] = TEXT(""); static const TCHAR CallStackEndKey[] = TEXT(""); static const TCHAR SourceContextStartKey[] = TEXT(""); static const TCHAR SourceContextEndKey[] = TEXT(""); static const TCHAR ExceptionLineStart[] = TEXT("Exception was "); auto ReportSection = EReportSection::Other; const TCHAR* Stream = *FileContent; FString Line; while (FParse::Line(&Stream, Line, true /* don't treat |s as new-lines! */)) { switch (ReportSection) { case EReportSection::CallStack: if (Line.StartsWith(CallStackEndKey)) { ReportSection = EReportSection::Other; } else { Callstack.Push(Line); } break; case EReportSection::SourceContext: if (Line.StartsWith(SourceContextEndKey)) { ReportSection = EReportSection::Other; } else { // Not doing anything with this at the moment } break; case EReportSection::Other: if (Line.StartsWith(CallStackStartKey)) { ReportSection = EReportSection::CallStack; } else if (Line.StartsWith(SourceContextStartKey)) { ReportSection = EReportSection::SourceContext; } else if (Line.StartsWith(ExceptionLineStart)) { // Not subtracting 1 from the array count so it gobbles the initial quote Exception = Line.RightChop(ARRAY_COUNT(ExceptionLineStart)).LeftChop(1); } break; } } // Update properties for the crash. FPrimaryCrashProperties::Get()->CallStack = Callstack; // If error message is empty, it means general crash like accessing invalid memory ptr. if (FPrimaryCrashProperties::Get()->ErrorMessage.AsString().Len() == 0) { FPrimaryCrashProperties::Get()->ErrorMessage = Exception; } return true; } bool FGenericErrorReport::FindFirstReportFileWithExtension( FString& OutFilename, const TCHAR* Extension ) const { for (const auto& Filename: ReportFilenames) { if (Filename.EndsWith(Extension)) { OutFilename = Filename; return true; } } return false; } FString FGenericErrorReport::FindCrashedAppName() const { FString AppPath = FindCrashedAppPath(); if (!AppPath.IsEmpty()) { return FPaths::GetCleanFilename(AppPath); } return AppPath; } FString FGenericErrorReport::FindCrashedAppPath() const { UE_LOG( LogTemp, Warning, TEXT( "FGenericErrorReport::FindCrashedAppPath not implemented on this platform" ) ); return FString( TEXT( "GenericAppPath" ) ); }