// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. using System; using System.IO; using System.Xml.Serialization; using System.Collections; using System.Collections.Generic; using Tools.DotNETCommon.XmlHandler; using System.Xml; using System.Linq; namespace Tools.CrashReporter.CrashReportCommon { /* Unused Crashes' fields. Title nchar(20) Selected bit Version int AutoReporterID int Processed bit HasDiagnosticsFile bit HasNewLogFile bit HasMetaData bit Fields of Crashes used only by the crash report website. Status varchar(64) - Status of the crash. Pattern varchar(800) - Pattern used to group cashes into buggs. FixedChangeList varchar(64) - Changelist that fixed this crash. Module varchar(128) - Name of the module where the crash occurred. UserNameId int - The ID of the user name. Jira varchar(64) - The Jira. CrashType smallint NOT NULL - The type of the crash, crash(1), assert(2), ensure(3) */ /* PrimaryCrashProperties. Extracted from: FGenericCrashContext::SerializeContentToBuffer */ /* "CrashVersion" "CrashGUID" "ProcessId" "IsInternalBuild" "IsPerforceBuild" "IsSourceDistribution" "IsEnsure" "IsAssert" "CrashType" "SecondsSinceStart" "GameName" "ExecutableName" "BuildConfiguration" "GameSessionID" "PlatformName" "PlatformNameIni" "PlatformFullName" "EngineMode" "EngineModeEx" "DeploymentName" "EngineVersion" "BuildVersion" "CommandLine" "LanguageLCID" "AppDefaultLocale" "IsUE4Release" "UserName" "BaseDir" "RootDir" "MachineId" "LoginId" "EpicAccountId" "CallStack" "SourceContext" "UserDescription" "UserActivityHint" "ErrorMessage" "CrashDumpMode" "FullCrashDumpLocation" "Misc.NumberOfCores" "Misc.NumberOfCoresIncludingHyperthreads" "Misc.Is64bitOperatingSystem" "Misc.CPUVendor" "Misc.CPUBrand" "Misc.PrimaryGPUBrand" "Misc.OSVersionMajor" "Misc.OSVersionMinor" "Misc.AppDiskTotalNumberOfBytes" "Misc.AppDiskNumberOfFreeBytes" "MemoryStats.TotalPhysical" "MemoryStats.TotalVirtual" "MemoryStats.PageSize" "MemoryStats.TotalPhysicalGB" "MemoryStats.AvailablePhysical" "MemoryStats.AvailableVirtual" "MemoryStats.UsedPhysical" "MemoryStats.PeakUsedPhysical" "MemoryStats.UsedVirtual" "MemoryStats.PeakUsedVirtual" "MemoryStats.bIsOOM" "MemoryStats.OOMAllocationSize" "MemoryStats.OOMAllocationAlignment" "TimeofCrash" "bAllowToBeContacted" "PlatformCallbackResult" */ /// Enumerates crash description versions. public enum ECrashDescVersions { /// Introduces a new crash description format. VER_1_NewCrashFormat, /// Added misc properties (CPU,GPU,OS,etc), memory related stats and platform specific properties as generic payload. VER_2_AddedNewProperties, /// Using crash context when available. VER_3_CrashContext = 3, } /// /// GenericCrashContext for managed code. Based on XML file. /// Holds data from the CrashContext.runtime-xml file /// Must match FGenericCrashContext::SerializeContentToBuffer /// /// 1. Generated by FGenericCrashContext::SerializeContentToBuffer via the native code /// 2. Updated with callstack, source context etc via the crash report client / crash report process /// 3. Read by the crash report process, verified and forward in the same form to the crash report frontend /// 4. Basic properties displayed directly in the frontend (already existing in the database), remaining properties are displayed per crash read from the CrashContext.runtime-xml file. /// /// /// /// /// /// /// public class FGenericCrashContext { /// Name of the crash context file. public const string CrashContextRuntimeXMLName = "CrashContext.runtime-xml"; /// Required properties [XmlElement( ElementName = "RuntimeProperties" )] public FPrimaryCrashProperties PrimaryCrashProperties = new FPrimaryCrashProperties(); /// Optional, platform specific properties [XmlElement( ElementName = "PlatformProperties" )] public object UntypedXMLNodes; /// Directory of the crash. public string CrashDirectory; //// Platform specific properties. //[XmlElement] //public List> PlatformProperties = new List>(); /// A simple default constructor to allow Xml serialization. public FGenericCrashContext() { } /// /// Generates a crash context rom a Url /// /// /// public static FGenericCrashContext FromUrl(string Url) { using (var wc = new System.Net.WebClient()) { var CopyText = wc.DownloadString(Url); return FromString(CopyText); } } /// Creates a new instance of the crash report context from the specified file. public static FGenericCrashContext FromFile(string Filepath) { string[] XMLText = File.ReadAllLines(Filepath); string CopyText = string.Join(Environment.NewLine, XMLText); return FromString(CopyText); } /// Creates a new instance of the crash report context from the specified file. public static FGenericCrashContext FromString(string Text) { // The crash report context contains invalid characters, we need to fix them all in order to parse the XML. HashSet TagsToIgnore = new HashSet() { "", "", "", "", "", "" }; string[] XMLText = Text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); string NodeOpen = ""; string NodeEnd = ""; List Multiline = new List(); List MultiIndex = new List(); int OpenEnd = 0; int EndStart = 0; for (int LineIndex = 1; LineIndex < XMLText.Length; LineIndex++) { string XMLLine = XMLText[LineIndex].Replace( "\t", "" ); bool bIgnore = TagsToIgnore.Contains( XMLLine ); if (bIgnore) { continue; } // XML node without data, skip. if (XMLLine.Contains( "/>" )) { continue; } if (NodeOpen == "") { // Parse the tag. //"1" // int OpenStart = XMLLine.IndexOf( '<' ); OpenEnd = XMLLine.IndexOf( '>', OpenStart ); NodeOpen = XMLLine.Substring( OpenStart + 1, OpenEnd - OpenStart - 1 ); } EndStart = XMLLine.IndexOf( "', EndStart ); NodeEnd = XMLLine.Substring( EndStart + 2, EndEnd - EndStart - 2 ); } else { Multiline.Add( XMLLine.Substring( OpenEnd != 0 ? OpenEnd + 1 : 0 ) ); MultiIndex.Add( LineIndex ); OpenEnd = 0; } // Valid tag, fix invalid characters. if (NodeOpen == NodeEnd) { if (Multiline.Count == 0) { string NodeValue = XMLLine.Substring( OpenEnd + 1, EndStart - OpenEnd - 1 ); string EscapedXMLText = EscapeXMLString( UnescapeXMLString( NodeValue ) ); // Required for support old and new type format. XMLText[LineIndex] = string.Format( "<{0}>{1}", NodeOpen, EscapedXMLText ); } else { Multiline.Add( XMLLine.Substring( 0, EndStart ) ); MultiIndex.Add( LineIndex ); for (int Inner = 0; Inner < Multiline.Count; Inner++) { string EscapedXMLText = EscapeXMLString( UnescapeXMLString( Multiline[Inner] ) ); if (Inner == 0) { XMLText[MultiIndex[Inner]] = string.Format( "<{0}>{1}", NodeOpen, EscapedXMLText ); } else if (Inner == Multiline.Count - 1) { XMLText[MultiIndex[Inner]] = string.Format( "{1}", NodeOpen, EscapedXMLText ); } else { XMLText[MultiIndex[Inner]] = EscapedXMLText; } } Multiline.Clear(); MultiIndex.Clear(); } NodeOpen = NodeEnd = ""; } } string XMLTextJoined = string.Join( Environment.NewLine, XMLText ); FGenericCrashContext Context = XmlHandler.FromXmlString( XMLTextJoined ); if (Context.UntypedXMLNodes != null) { var Typed = ( (IEnumerable)Context.UntypedXMLNodes ) .Cast() .Select( X => new KeyValuePair( X.Name, X.InnerText ) ) .ToList(); } return Context; } const string NewLineTag = "&nl;"; /// Escape XML string. static public string EscapeXMLString( string Text ) { return Text .Replace( "&", "&" ) .Replace( "\"", """ ) .Replace( "'", "'" ) .Replace( "<", "<" ) .Replace( ">", ">" ) // Ignore return carriage. .Replace( "\r", "" ); //.Replace( "\n", NewLineTag ); } /// Unescape XML string. static public string UnescapeXMLString( string Text ) { return Text .Replace( "&", "&" ) .Replace( """, "\"" ) .Replace( "'", "'" ) .Replace( "<", "<" ) .Replace( ">", ">" ) .Replace( NewLineTag, "\n" ); } /// Whether this crash contains callstack, error message and source context thus it means that crash is complete. public bool HasProcessedData() { return !string.IsNullOrEmpty(PrimaryCrashProperties.CallStack) && !string.IsNullOrEmpty(PrimaryCrashProperties.ErrorMessage); } /// Writes this instance of the crash report context to the specified file. public void ToFile() { XmlHandler.WriteXml( this, Path.Combine( CrashDirectory, FGenericCrashContext.CrashContextRuntimeXMLName ) ); } /// Unique name of this report, can be used as name of file. public string GetAsFilename() { return Path.GetFileName(CrashDirectory); } } /* "EngineVersion" */ /// Required properties for the generic crash context. public class FPrimaryCrashProperties { /// [XmlElement] public int CrashVersion; /// [XmlElement] public string CrashGUID; /// [XmlElement] public int ProcessId; /// [XmlElement] public bool IsInternalBuild; /// [XmlElement] public bool IsPerforceBuild; /// [XmlElement] public bool IsSourceDistribution; /// [XmlElement] public bool IsEnsure; /// [XmlElement] public bool IsAssert; /// [XmlElement] public string CrashType; /// /// Get the crash type. Can be empty but we don't want null. /// public string GetCrashType() { return CrashType ?? string.Empty; } /// [XmlElement] public int SecondsSinceStart; /// [XmlElement] public string GameName; /// [XmlElement] public string ExecutableName; /// [XmlElement] public string BuildConfiguration; /// [XmlElement] public string GameSessionID; /// [XmlElement] public string PlatformName; /// [XmlElement] public string PlatformNameIni; /// [XmlElement] public string PlatformFullName; /// /// Creates a full platform name, if missing. PlatformName [OS.Major Os.Minor Arch] /// Must match /// ...\Engine\Source\Programs\CrashReporter\CrashReportClient\Private\CrashDescription.cpp FCrashContext.FCrashContext /// public void SetPlatformFullName() { if (string.IsNullOrEmpty( PlatformFullName )) { // Create a new one. string LocalPlatformName = ""; if (BaseDir != "") { string[] FolderComponents = BaseDir.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); if (FolderComponents.Length != 0) { LocalPlatformName = FolderComponents[FolderComponents.Length - 1]; } } else { LocalPlatformName = PlatformNameIni; } if (!string.IsNullOrEmpty( Misc_OSVersionMajor )) { PlatformFullName = string.Format( "{0} [{1} {2} {3}]", LocalPlatformName, Misc_OSVersionMajor, string.IsNullOrEmpty( Misc_OSVersionMinor ) ? "" : Misc_OSVersionMinor, Misc_Is64bitOperatingSystem ? "64b" : "32b" ); } } } /// [XmlElement] public string EngineMode; /// [XmlElement] public string EngineModeEx; /// /// Get the engine mode ex. Can be empty but we don't want null. /// public string GetEngineModeEx() { return EngineModeEx ?? string.Empty; } /// [XmlElement] public string DeploymentName; /// [XmlElement] public string EngineVersion; /// [XmlElement] public string BuildVersion; /// [XmlElement] public string CommandLine; /// [XmlElement] public int LanguageLCID; /// [XmlElement] public string AppDefaultLocale; /// [XmlElement] public bool IsUE4Release; /// [XmlElement] public string UserName; /// [XmlElement] public string BaseDir; /// [XmlElement] public string RootDir; /// [XmlElement] public string MachineId; /// [XmlElement] public string LoginId; /// [XmlElement] public string EpicAccountId; /// [XmlElement] public string CallStack; /// Callstack as string[] unescaped. public string[] GetCallstack() { if (string.IsNullOrWhiteSpace(CallStack)) return new string[0]; string UnescapedValue = FGenericCrashContext.UnescapeXMLString( CallStack ); return UnescapedValue.Split( new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries ); } /// [XmlElement] public string SourceContext; /// SourceContext as string[] unescaped. public string[] GetSourceContext() { if (string.IsNullOrWhiteSpace(SourceContext)) return new string[0]; string UnescapedValue = FGenericCrashContext.UnescapeXMLString( SourceContext ); return UnescapedValue.Split( new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries ); } /// [XmlElement] public string UserDescription; /// UserDescription as string[] unescaped. public string[] GetUserDescription() { if (string.IsNullOrWhiteSpace(UserDescription)) return new string[0]; string UnescapedValue = FGenericCrashContext.UnescapeXMLString( UserDescription ); return UnescapedValue.Split( new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries ); } /// [XmlElement] public string UserActivityHint; /// [XmlElement] public string ErrorMessage; /// ErrorMessage as string[] unescaped. public string[] GetErrorMessage() { if (string.IsNullOrWhiteSpace(ErrorMessage)) return new string[0]; string UnescapedValue = FGenericCrashContext.UnescapeXMLString( ErrorMessage ); UnescapedValue = UnescapedValue.Substring( 0, Math.Min( 511, UnescapedValue.Length ) ); // Database limitation. return UnescapedValue.Split( new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries ); } /// [XmlElement] public int CrashDumpMode; /// Location of the full crash dump. Displayed in the crash report frontend, without the filename. [XmlElement] public string FullCrashDumpLocation; /// Fullname of the full crash dump. public string GetFullCrashDumpLocation() { if (string.IsNullOrWhiteSpace(FullCrashDumpLocation)) return string.Empty; string Fullname = Path.Combine( FullCrashDumpLocation, CrashReporterConstants.UE4MinidumpName ); return Fullname; } /// [XmlElement( ElementName = "Misc.NumberOfCores" )] public int Misc_NumberOfCores; /// [XmlElement( ElementName = "Misc.NumberOfCoresIncludingHyperthreads" )] public int Misc_NumberOfCoresIncludingHyperthreads; /// [XmlElement( ElementName = "Misc.Is64bitOperatingSystem" )] public bool Misc_Is64bitOperatingSystem; /// [XmlElement( ElementName = "Misc.CPUVendor" )] public string Misc_CPUVendor; /// [XmlElement( ElementName = "Misc.CPUBrand" )] public string Misc_CPUBrand; /// [XmlElement( ElementName = "Misc.PrimaryGPUBrand" )] public string Misc_PrimaryGPUBrand; /// [XmlElement( ElementName = "Misc.OSVersionMajor" )] public string Misc_OSVersionMajor; /// [XmlElement( ElementName = "Misc.OSVersionMinor" )] public string Misc_OSVersionMinor; /// [XmlElement( ElementName = "Misc.AppDiskTotalNumberOfBytes")] public string Misc_AppDiskTotalNumberOfBytes; /// [XmlElement( ElementName = "Misc.AppDiskNumberOfFreeBytes")] public string Misc_AppDiskNumberOfFreeBytes; /// [XmlElement( ElementName = "MemoryStats.TotalPhysical")] public string MemoryStats_TotalPhysical; /// [XmlElement( ElementName = "MemoryStats.TotalVirtual")] public string MemoryStats_TotalVirtual; /// [XmlElement( ElementName = "MemoryStats.PageSize")] public string MemoryStats_PageSize; /// [XmlElement( ElementName = "MemoryStats.TotalPhysicalGB")] public string MemoryStats_TotalPhysicalGB; /// [XmlElement( ElementName = "MemoryStats.AvailablePhysical")] public string MemoryStats_AvailablePhysical; /// [XmlElement( ElementName = "MemoryStats.AvailableVirtual")] public string MemoryStats_AvailableVirtual; /// [XmlElement( ElementName = "MemoryStats.UsedPhysical")] public string MemoryStats_UsedPhysical; /// [XmlElement( ElementName = "MemoryStats.PeakUsedPhysical")] public string MemoryStats_PeakUsedPhysical; /// [XmlElement( ElementName = "MemoryStats.UsedVirtual")] public string MemoryStats_UsedVirtual; /// [XmlElement( ElementName = "MemoryStats.PeakUsedVirtual")] public string MemoryStats_PeakUsedVirtual; /// [XmlElement( ElementName = "MemoryStats.bIsOOM")] public string MemoryStats_bIsOOM; /// [XmlElement( ElementName = "MemoryStats.OOMAllocationSize")] public string MemoryStats_OOMAllocationSize; /// [XmlElement( ElementName = "MemoryStats.OOMAllocationAlignment")] public string MemoryStats_OOMAllocationAlignment; /// The UTC time the crash occurred. [XmlElement] public DateTime TimeofCrash; /// Whether the user allowed us to be contacted. [XmlElement] public bool bAllowToBeContacted = false; /// Platform status. [XmlElement] public int PlatformCallbackResult; /// CRC version that uploaded the crash. Blank for older builds. [XmlElement] public string CrashReportClientVersion; /// An array of module names used by the game that crashed. [XmlElement] public string Modules; /// Warning info for missing data or failed processing in the Crash Report Processor. [XmlElement] public string ProcessorFailedMessage; /// A simple default constructor to allow Xml serialization. public FPrimaryCrashProperties() { } } }