// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Validation; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Web; using System.Web.Mvc; using Tools.CrashReporter.CrashReportWebSite.Classes; using Tools.CrashReporter.CrashReportWebSite.DataModels; using Tools.CrashReporter.CrashReportWebSite.DataModels.Repositories; using Tools.CrashReporter.CrashReportWebSite.Properties; using Tools.CrashReporter.CrashReportWebSite.ViewModels; using Tools.DotNETCommon.XmlHandler; using Tools.CrashReporter.CrashReportCommon; using System.Data.SqlClient; using System.Runtime.Caching; namespace Tools.CrashReporter.CrashReportWebSite.Controllers { /// /// The controller to handle the Crash data. /// [HandleError] public class CrashesController : Controller { private static MemoryCache crashCache = MemoryCache.Default; //Ugly instantiation of Crash repository will replace with dependency injection BEFORE this gets anywhere near live. private readonly SlackWriter _slackWriter; private static readonly PerformanceTracker PerfTracker = new PerformanceTracker(100); /// Special user name, currently used to mark Crashes from UE4 releases. const string UserNameAnonymous = "Anonymous"; /// /// An empty constructor. /// public CrashesController() { _slackWriter = new SlackWriter() { WebhookUrl = Settings.Default.SlackWebhookUrl, Channel = Settings.Default.SlackChannel, Username = Settings.Default.SlackUsername, IconEmoji = Settings.Default.SlackEmoji }; } class CrashCacheItem { public List Crashes { get; set; } public Dictionary GroupCount { get; set; } public int ResultCount { get; set; } } private string getCacheKeyFromFormData(FormHelper formData) { return string.Format("{0}-{1}-{2}-{3}-{4}-{5}-{6}-{7}-{8}-{9}-{10}-{11}-{12}-{13}-{14}-{15}-{16}-{17}-{18}-{19}-{20}-{21}-{22}", formData.BranchName, formData.BuggId, formData.BuiltFromCL, formData.CrashType, formData.DateFrom, formData.DateTo, formData.EngineMode, formData.EngineVersion, formData.EpicIdOrMachineQuery, formData.GameName, formData.JiraId, formData.JiraQuery, formData.MessageQuery, formData.Page, formData.PageSize, formData.PlatformName, formData.PreviousOrder, formData.SearchQuery, formData.SortOrder, formData.SortTerm, formData.UserGroup, formData.UsernameQuery, formData.VersionName); } /// /// Display a summary list of Crashes based on the search criteria. /// /// A form of user data passed up from the client. /// A view to display a list of Crash reports. public ActionResult Index(FormCollection CrashesForm ) { using( var logTimer = new FAutoScopedLogTimer( this.GetType().ToString()) ) { // Handle any edits made in the Set form fields //foreach( var Entry in CrashesForm ) //{ // int Id = 0; // if( int.TryParse( Entry.ToString(), out Id ) ) // { // Crash currentCrash = _unitOfWork.CrashRepository.GetById(Id); // if( currentCrash != null ) // { // if( !string.IsNullOrEmpty( CrashesForm["SetStatus"] ) ) // { // currentCrash.Status = CrashesForm["SetStatus"]; // } // if( !string.IsNullOrEmpty( CrashesForm["SetFixedIn"] ) ) // { // currentCrash.FixedChangeList = CrashesForm["SetFixedIn"]; // } // if( !string.IsNullOrEmpty( CrashesForm["SetTTP"] ) ) // { // currentCrash.Jira = CrashesForm["SetTTP"]; // } // } // } // _unitOfWork.Save(); //} // // Parse the contents of the query string, and populate the form var formData = new FormHelper( Request, CrashesForm, "TimeOfCrash" ); var result = GetResults( formData ); using (var unitOfWork = new UnitOfWork(new CrashReportEntities())) { result.BranchNames = unitOfWork.CrashRepository.GetBranchesAsListItems(); result.VersionNames = unitOfWork.CrashRepository.GetVersionsAsListItems(); result.PlatformNames = unitOfWork.CrashRepository.GetPlatformsAsListItems(); result.EngineModes = unitOfWork.CrashRepository.GetEngineModesAsListItems(); result.EngineVersions = unitOfWork.CrashRepository.GetEngineVersionsAsListItems(); } // Add the FromCollection to the CrashesViewModel since we don't need it for the get results function but we do want to post it back to the page. result.FormCollection = CrashesForm; result.GenerationTime = logTimer.GetElapsedSeconds().ToString( "F2" ); return View( "Index", result ); } } /// /// Show detailed information about a Crash. /// /// A form of user data passed up from the client. /// The unique id of the Crash we wish to show the details of. /// A view to show Crash details. public ActionResult Show(FormCollection CrashesForm, int id) { using( var logTimer = new FAutoScopedLogTimer( this.GetType().ToString() + "(CrashId=" + id + ")", bCreateNewLog: true ) ) { CallStackContainer currentCallStack = null; Crash currentCrash = null; User CrashUser = null; Bugg currentBugg = null; UserGroup currentUserGroup = null; // Update the selected Crash based on the form contents using (var unitOfWork = new UnitOfWork(new CrashReportEntities())) { currentCrash = unitOfWork.CrashRepository.GetById(id); CrashUser = currentCrash.User; currentBugg = currentCrash.Bugg; currentUserGroup = CrashUser.UserGroup; } if( currentCrash == null ) { return RedirectToAction( "Index" ); } string FormValue; FormValue = CrashesForm["SetStatus"]; if( !string.IsNullOrEmpty( FormValue ) ) { currentCrash.Status = FormValue; } FormValue = CrashesForm["SetFixedIn"]; if( !string.IsNullOrEmpty( FormValue ) ) { currentCrash.FixedChangeList = FormValue; } FormValue = CrashesForm["SetTTP"]; if( !string.IsNullOrEmpty( FormValue ) ) { currentCrash.Jira = FormValue; if(currentCrash.Bugg != null) currentCrash.Bugg.TTPID = FormValue; } // Valid to set description to an empty string FormValue = CrashesForm["Description"]; if( FormValue != null ) { currentCrash.Description = FormValue; } using (var unitOfWork = new UnitOfWork(new CrashReportEntities())) { unitOfWork.CrashRepository.Update(currentCrash); unitOfWork.Save(); } currentCallStack = new CallStackContainer( currentCrash.CrashType, currentCrash.RawCallStack, currentCrash.PlatformName ); currentCrash.Module = currentCallStack.GetModuleName(); //Set call stack properties currentCallStack.bDisplayModuleNames = true; currentCallStack.bDisplayFunctionNames = true; currentCallStack.bDisplayFileNames = true; currentCallStack.bDisplayFilePathNames = true; currentCallStack.bDisplayUnformattedCallStack = false; currentCrash.CallStackContainer = currentCallStack; var Model = new CrashViewModel { Crash = currentCrash, User = CrashUser, Bugg = currentBugg, UserGroup = currentUserGroup, CallStack = currentCallStack }; Model.GenerationTime = logTimer.GetElapsedSeconds().ToString( "F2" ); return View( "Show", Model ); } } /// /// Add a Crash passed in the payload as Xml to the database. /// /// Unused. /// The row id of the newly added Crash. public ActionResult AddCrash(int id) { var newCrashResult = new CrashReporterResult(); CrashDescription newCrash; newCrashResult.ID = -1; string payloadString; using (var logTimer = new FAutoScopedLogTimer(this.GetType().ToString() + "(CrashId=" + id + ")")) { //Read the request payload try { using (var reader = new StreamReader(Request.InputStream, Request.ContentEncoding)) { payloadString = reader.ReadToEnd(); if (string.IsNullOrEmpty(payloadString)) { FLogger.Global.WriteEvent(string.Format("Add Crash Failed : Payload string empty")); } } } catch (Exception ex) { var messageBuilder = new StringBuilder(); messageBuilder.AppendLine("Error Reading Crash Payload"); messageBuilder.AppendLine("Exception was:"); messageBuilder.AppendLine(ex.ToString()); FLogger.Global.WriteException(messageBuilder.ToString()); newCrashResult.Message = messageBuilder.ToString(); newCrashResult.bSuccess = false; return Content(XmlHandler.ToXmlString(newCrashResult), "text/xml"); } // De-serialize the payload string try { newCrash = XmlHandler.FromXmlString(payloadString); } catch (Exception ex) { var messageBuilder = new StringBuilder(); messageBuilder.AppendLine("Error Reading CrashDescription XML"); messageBuilder.AppendLine("Exception was: "); messageBuilder.AppendLine(ex.ToString()); FLogger.Global.WriteException(messageBuilder.ToString()); newCrashResult.Message = messageBuilder.ToString(); newCrashResult.bSuccess = false; return Content(XmlHandler.ToXmlString(newCrashResult), "text/xml"); } //Add Crash to database try { var crash = CreateCrash(newCrash); newCrashResult.ID = crash.Id; newCrashResult.bSuccess = true; } catch (DbEntityValidationException dbentEx) { var messageBuilder = new StringBuilder(); messageBuilder.AppendLine("Exception was:"); messageBuilder.AppendLine(dbentEx.ToString()); var innerEx = dbentEx.InnerException; while (innerEx != null) { messageBuilder.AppendLine("Inner Exception : " + innerEx.Message); innerEx = innerEx.InnerException; } if (dbentEx.EntityValidationErrors != null) { messageBuilder.AppendLine("Validation Errors : "); foreach (var valErr in dbentEx.EntityValidationErrors) { messageBuilder.AppendLine( valErr.ValidationErrors.Select(data => data.ErrorMessage) .Aggregate((current, next) => current + "; /n" + next)); } } messageBuilder.AppendLine("Received payload was:"); messageBuilder.AppendLine(payloadString); FLogger.Global.WriteException(messageBuilder.ToString()); _slackWriter.Write(messageBuilder.ToString()); newCrashResult.Message = messageBuilder.ToString(); newCrashResult.bSuccess = false; } catch (SqlException sqlExc) { if (sqlExc.Number == -2) //If this is an sql timeout log the timeout and try again. { FLogger.Global.WriteEvent(string.Format("AddCrash: Timeout")); newCrashResult.bTimeout = true; } else { var messageBuilder = new StringBuilder(); messageBuilder.AppendLine("Exception was:"); messageBuilder.AppendLine(sqlExc.ToString()); messageBuilder.AppendLine("Received payload was:"); messageBuilder.AppendLine(payloadString); FLogger.Global.WriteException(messageBuilder.ToString()); newCrashResult.Message = messageBuilder.ToString(); newCrashResult.bSuccess = false; } } catch (Exception ex) { var messageBuilder = new StringBuilder(); messageBuilder.AppendLine("Exception was:"); messageBuilder.AppendLine(ex.ToString()); messageBuilder.AppendLine("Received payload was:"); messageBuilder.AppendLine(payloadString); FLogger.Global.WriteException(messageBuilder.ToString()); newCrashResult.Message = messageBuilder.ToString(); newCrashResult.bSuccess = false; } var returnResult = XmlHandler.ToXmlString(newCrashResult); PerfTracker.AddStat("Add Crash Complete", logTimer.GetElapsedSeconds()); PerfTracker.IncrementCount(); return Content(returnResult, "text/xml"); } } /// /// Gets a list of Crashes filtered based on our form data. /// /// /// public CrashesViewModel GetResults(FormHelper formData) { List results = null; IQueryable resultsQuery = null; var cacheKey = getCacheKeyFromFormData(formData); var cachedResults = crashCache.Get(cacheKey) as CrashCacheItem; if (cachedResults == null) { var skip = (formData.Page - 1) * formData.PageSize; var take = formData.PageSize; Dictionary groupCounts; var resultCount = 0; using (var unitOfWork = new UnitOfWork(new CrashReportEntities())) { resultsQuery = ConstructQueryForFiltering(unitOfWork, formData); // Filter by data and get as enumerable. resultsQuery = FilterByDate(resultsQuery, formData.DateFrom, formData.DateTo); // Filter by BuggId if (!string.IsNullOrEmpty(formData.BuggId)) { var buggId = 0; var bValid = int.TryParse(formData.BuggId, out buggId); if (bValid) { var newBugg = unitOfWork.BuggRepository.GetById(buggId); if (newBugg != null) { resultsQuery = resultsQuery.Where(data => data.PatternId == newBugg.PatternId); } } } var countsQuery = resultsQuery.GroupBy(data => data.User.UserGroup) .Select(data => new { Key = data.Key.Name, Count = data.Count() }); groupCounts = countsQuery.OrderBy(data => data.Key) .ToDictionary(data => data.Key, data => data.Count); // Filter by user group if present var userGroupId = !string.IsNullOrEmpty(formData.UserGroup) ? unitOfWork.UserGroupRepository.First(data => data.Name == formData.UserGroup).Id : 1; resultsQuery = resultsQuery.Where(data => data.User.UserGroupId == userGroupId); var orderedQuery = GetSortedQuery(resultsQuery, formData.SortTerm ?? "TimeOfCrash", formData.SortOrder == "Descending"); // Grab just the results we want to display on this page results = orderedQuery.Skip(skip).Take(take).ToList(); // Get the Count for pagination resultCount = orderedQuery.Count(); // Process call stack for display foreach (var CrashInstance in results) { // Put call stacks into an list so we can access them line by line in the view CrashInstance.CallStackContainer = new CallStackContainer(CrashInstance.CrashType, CrashInstance.RawCallStack, CrashInstance.PlatformName); } cachedResults = new CrashCacheItem() { Crashes = results, GroupCount = groupCounts, ResultCount = resultCount }; var itemCachePolicy = new CacheItemPolicy() { AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(Settings.Default.CrashesCacheInMinutes) }; crashCache.Add(cacheKey, cachedResults, itemCachePolicy); } } return new CrashesViewModel { Results = cachedResults.Crashes, PagingInfo = new PagingInfo { CurrentPage = formData.Page, PageSize = formData.PageSize, TotalResults = cachedResults.ResultCount }, SortOrder = formData.SortOrder, SortTerm = formData.SortTerm, UserGroup = formData.UserGroup, CrashType = formData.CrashType, SearchQuery = formData.SearchQuery, UsernameQuery = formData.UsernameQuery, EpicIdOrMachineQuery = formData.EpicIdOrMachineQuery, MessageQuery = formData.MessageQuery, BuiltFromCL = formData.BuiltFromCL, BuggId = formData.BuggId, JiraQuery = formData.JiraQuery, DateFrom = (long)(formData.DateFrom.ToUniversalTime() - CrashesViewModel.Epoch).TotalMilliseconds, DateTo = (long)(formData.DateTo.ToUniversalTime() - CrashesViewModel.Epoch).TotalMilliseconds, BranchName = formData.BranchName, VersionName = formData.VersionName, PlatformName = formData.PlatformName, GameName = formData.GameName, GroupCounts = cachedResults.GroupCount }; } /// /// Returns a lambda expression used to sort our Crash data. /// /// A query filter on the Crashes entity. /// Sort term identifying the field on which to sort. /// bool indicating sort order /// public IOrderedQueryable GetSortedQuery(IQueryable resultsQuery, string sortTerm, bool sortDescending) { switch (sortTerm) { case "Id": return sortDescending ? resultsQuery.OrderByDescending(data => data.Id) : resultsQuery.OrderBy(data => data.Id); break; case "TimeOfCrash": return sortDescending ? resultsQuery.OrderByDescending(data => data.TimeOfCrash) : resultsQuery.OrderBy(CrashInstance => CrashInstance.TimeOfCrash); break; case "UserName": return sortDescending ? resultsQuery.OrderByDescending(data => data.User.UserName) : resultsQuery.OrderBy(CrashInstance => CrashInstance.User.UserName); break; case "RawCallStack": return sortDescending ? resultsQuery.OrderByDescending(data => data.RawCallStack) : resultsQuery.OrderBy(CrashInstance => CrashInstance.RawCallStack); break; case "GameName": return sortDescending ? resultsQuery.OrderByDescending(data => data.GameName) : resultsQuery.OrderBy(CrashInstance => CrashInstance.GameName); break; case "EngineMode": return sortDescending ? resultsQuery.OrderByDescending(data => data.EngineMode) : resultsQuery.OrderBy(CrashInstance => CrashInstance.EngineMode); break; case "FixedChangeList": return sortDescending ? resultsQuery.OrderByDescending(data => data.FixedChangeList) : resultsQuery.OrderBy(CrashInstance => CrashInstance.FixedChangeList); break; case "TTPID": return sortDescending ? resultsQuery.OrderByDescending(data => data.Jira) : resultsQuery.OrderBy(CrashInstance => CrashInstance.Jira); break; case "Branch": return sortDescending ? resultsQuery.OrderByDescending(data => data.Branch) : resultsQuery.OrderBy(CrashInstance => CrashInstance.Branch); break; case "ChangeListVersion": return sortDescending ? resultsQuery.OrderByDescending(data => data.ChangelistVersion) : resultsQuery.OrderBy(CrashInstance => CrashInstance.ChangelistVersion); break; case "ComputerName": return sortDescending ? resultsQuery.OrderByDescending(data => data.ComputerName) : resultsQuery.OrderBy(CrashInstance => CrashInstance.ComputerName); break; case "PlatformName": return sortDescending ? resultsQuery.OrderByDescending(data => data.PlatformName) : resultsQuery.OrderBy(CrashInstance => CrashInstance.PlatformName); break; case "Status": return sortDescending ? resultsQuery.OrderByDescending(data => data.Status) : resultsQuery.OrderBy(CrashInstance => CrashInstance.Status); break; case "Module": return sortDescending ? resultsQuery.OrderByDescending(data => data.Module) : resultsQuery.OrderBy(CrashInstance => CrashInstance.Module); break; case "Summary": return sortDescending ? resultsQuery.OrderByDescending(data => data.Summary) : resultsQuery.OrderBy(data => data.Summary); } return null; } /// Constructs query for filtering. private IQueryable ConstructQueryForFiltering(IUnitOfWork unitOfWork, FormHelper formData) { //Instead of returning a queryable and filtering it here we should construct a filtering expression and pass that to the repository. //I don't like that data handling is taking place in the controller. var results = unitOfWork.CrashRepository.ListAll(); // Grab Results var queryString = HttpUtility.HtmlDecode(formData.SearchQuery); if (!string.IsNullOrEmpty(queryString)) { if (!string.IsNullOrEmpty(queryString)) { //We only use SearchQuery now for CallStack searching - if there's a SearchQuery value and a Username value, we need to get rid of the //Username so that we can create a broader search range formData.UsernameQuery = ""; } results = results.Where(data => data.RawCallStack.Contains(formData.SearchQuery)); } if (formData.IsVanilla.HasValue) { results = results.Where(data => data.IsVanilla == formData.IsVanilla.Value); } // Filter by Crash Type if (formData.CrashType != "All") { switch (formData.CrashType) { case "Crashes": results = results.Where(CrashInstance => CrashInstance.CrashType == 1); break; case "Assert": results = results.Where(CrashInstance => CrashInstance.CrashType == 2); break; case "Ensure": results = results.Where(CrashInstance => CrashInstance.CrashType == 3); break; case "CrashesAsserts": results = results.Where(CrashInstance => CrashInstance.CrashType == 1 || CrashInstance.CrashType == 2); break; } } // JRX Restore EpicID/UserName searching if (!string.IsNullOrEmpty(formData.UsernameQuery)) { var decodedUsername = HttpUtility.HtmlDecode(formData.UsernameQuery).ToLower(); var user = unitOfWork.UserRepository.First(data => data.UserName.Contains(decodedUsername)); if (user != null) { var userId = user.Id; results = ( from CrashDetail in results where CrashDetail.UserId == userId select CrashDetail); } } // Start Filtering the results if (!string.IsNullOrEmpty(formData.EpicIdOrMachineQuery)) { var decodedEpicOrMachineId = HttpUtility.HtmlDecode(formData.EpicIdOrMachineQuery).ToLower(); results = ( from CrashDetail in results where CrashDetail.EpicAccountId.Equals(decodedEpicOrMachineId) || CrashDetail.ComputerName.Equals(decodedEpicOrMachineId) select CrashDetail ); } if (!string.IsNullOrEmpty(formData.JiraQuery)) { var decodedJira = HttpUtility.HtmlDecode(formData.JiraQuery).ToLower(); results = ( from CrashDetail in results where CrashDetail.Jira.Contains(decodedJira) select CrashDetail ); } // Filter by BranchName if (!string.IsNullOrEmpty(formData.BranchName)) { results = ( from CrashDetail in results where CrashDetail.Branch.Equals(formData.BranchName) select CrashDetail ); } // Filter by VersionName if (!string.IsNullOrEmpty(formData.VersionName)) { results = ( from CrashDetail in results where CrashDetail.BuildVersion.Equals(formData.VersionName) select CrashDetail ); } //Filter by Engine Version if (!string.IsNullOrEmpty(formData.EngineVersion)) { results = ( from CrashDetail in results where CrashDetail.EngineVersion.Equals(formData.EngineVersion) select CrashDetail ); } // Filter by VersionName if (!string.IsNullOrEmpty(formData.EngineMode)) { results = ( from CrashDetail in results where CrashDetail.EngineMode.Equals(formData.EngineMode) select CrashDetail ); } // Filter by PlatformName if (!string.IsNullOrEmpty(formData.PlatformName)) { results = ( from CrashDetail in results where CrashDetail.PlatformName.Contains(formData.PlatformName) select CrashDetail ); } // Filter by GameName if (!string.IsNullOrEmpty(formData.GameName)) { var DecodedGameName = HttpUtility.HtmlDecode(formData.GameName).ToLower(); if (DecodedGameName.StartsWith("-")) { results = ( from CrashDetail in results where !CrashDetail.GameName.Contains(DecodedGameName.Substring(1)) select CrashDetail ); } else { results = ( from CrashDetail in results where CrashDetail.GameName.Contains(DecodedGameName) select CrashDetail ); } } // Filter by MessageQuery if (!string.IsNullOrEmpty(formData.MessageQuery)) { results = ( from CrashDetail in results where CrashDetail.Summary.Contains(formData.MessageQuery) || CrashDetail.Description.Contains(formData.MessageQuery) select CrashDetail ); } // Filter by BuiltFromCL if (!string.IsNullOrEmpty(formData.BuiltFromCL)) { var builtFromCl = 0; var bValid = int.TryParse(formData.BuiltFromCL, out builtFromCl); if (bValid) { results = ( from CrashDetail in results where CrashDetail.ChangelistVersion.Equals(formData.BuiltFromCL) select CrashDetail ); } } return results.Include(data => data.User); } /// /// Filter a set of Crashes to a date range. /// /// The unfiltered set of Crashes. /// The earliest date to filter by. /// The latest date to filter by. /// The set of Crashes between the earliest and latest date. public IQueryable FilterByDate(IQueryable Results, DateTime DateFrom, DateTime DateTo) { using (FAutoScopedLogTimer LogTimer = new FAutoScopedLogTimer(this.GetType().ToString() + " SQL")) { DateTo = DateTo.AddDays(1); IQueryable CrashesInTimeFrame = Results .Where(MyCrash => MyCrash.TimeOfCrash >= DateFrom && MyCrash.TimeOfCrash <= DateTo); return CrashesInTimeFrame; } } /// /// Create a new Crash data model object and insert it into the database /// /// /// private Crash CreateCrash(CrashDescription description) { var newCrash = new Crash { Branch = description.BranchName, BaseDir = description.BaseDir, BuildVersion = description.EngineVersion, EngineVersion = description.BuildVersion, ChangelistVersion = description.BuiltFromCL.ToString(), CommandLine = description.CommandLine, EngineMode = description.EngineMode, ComputerName = description.MachineGuid, IsVanilla = description.EngineModeEx.ToLower() == "vanilla" }; //If there's a valid EpicAccountId assign that. if (!string.IsNullOrEmpty(description.EpicAccountId)) { newCrash.EpicAccountId = description.EpicAccountId; } newCrash.Description = ""; if (description.UserDescription != null) { newCrash.Description = string.Join(Environment.NewLine, description.UserDescription); } if (newCrash.Description.Length > 4095) newCrash.Description = newCrash.Description.Substring(0, 4095); newCrash.EngineMode = description.EngineMode; newCrash.GameName = description.GameName; newCrash.LanguageExt = description.Language; //Converted by the Crash process. newCrash.PlatformName = description.Platform; newCrash.HasLogFile = description.bHasLog; newCrash.HasVideoFile = description.bHasVideo; newCrash.HasMiniDumpFile = description.bHasMiniDump; if (description.ErrorMessage != null) { newCrash.Summary = string.Join("\n", description.ErrorMessage); } if (description.CallStack != null) { newCrash.RawCallStack = string.Join("\n", description.CallStack); } if (description.SourceContext != null) { newCrash.SourceContext = string.Join("\n", description.SourceContext); } newCrash.TimeOfCrash = description.TimeofCrash; newCrash.Jira = ""; newCrash.FixedChangeList = ""; newCrash.ProcessFailed = description.bProcessorFailed; // Set the Crash type newCrash.CrashType = 1; //if we have a Crash type set the Crash type if (!string.IsNullOrEmpty(description.CrashType)) { switch (description.CrashType.ToLower()) { case "Crash": newCrash.CrashType = 1; break; case "assert": newCrash.CrashType = 2; break; case "ensure": newCrash.CrashType = 3; break; case "": case null: default: newCrash.CrashType = 1; break; } } else //else fall back to the old behavior and try to determine type from RawCallStack { if (newCrash.RawCallStack != null) { if (newCrash.RawCallStack.Contains("FDebug::AssertFailed")) { newCrash.CrashType = 2; } else if (newCrash.RawCallStack.Contains("FDebug::Ensure")) { newCrash.CrashType = 3; } else if (newCrash.RawCallStack.Contains("FDebug::OptionallyLogFormattedEnsureMessageReturningFalse")) { newCrash.CrashType = 3; } else if (newCrash.RawCallStack.Contains("NewReportEnsure")) { newCrash.CrashType = 3; } } } // As we're adding it, the status is always new newCrash.Status = "New"; newCrash.UserActivityHint = description.UserActivityHint; newCrash.UserName = (!string.IsNullOrEmpty(description.UserName)) ? description.UserName : UserNameAnonymous; //Match this crash to an existing user or create a new user if one does not exist. using (var unitOfWork = new UnitOfWork(new CrashReportEntities())) { //if there's a valid username assign the associated UserNameId else use "anonymous". var userName = (!string.IsNullOrEmpty(description.UserName)) ? description.UserName : UserNameAnonymous; if (unitOfWork.UserRepository.Any(data => data.UserName.Equals(userName))) { newCrash.UserId = unitOfWork.UserRepository.ListAll() .Where(data => data.UserName.Equals(userName)) .Select(data => data.Id) .First(); } else { newCrash.User = new User() { UserName = description.UserName, UserGroupId = 5 }; } } try { //Build the callstack pattern for this crash BuildPattern(newCrash); } catch (Exception ex) { FLogger.Global.WriteException("Error in Create Crash Build Pattern Method"); _slackWriter.Write("Error in Create Crash Build Pattern Method"); _slackWriter.Write(ex.Message); if (ex.InnerException != null) { _slackWriter.Write(ex.InnerException.Message); } throw; } if(newCrash.CommandLine == null) newCrash.CommandLine = ""; using (var unitOfWork = new UnitOfWork(new CrashReportEntities())) { var callStackRepository = unitOfWork.CallstackRepository; try { var crashRepo = unitOfWork.CrashRepository; //if we don't have any callstack data then insert the Crash and return if (string.IsNullOrEmpty(newCrash.Pattern)) { crashRepo.Save(newCrash); unitOfWork.Save(); return newCrash; } //If this isn't a new pattern then link it to our Crash data model if (callStackRepository.Any(data => data.Pattern == newCrash.Pattern)) { var callstackPattern = callStackRepository.First(data => data.Pattern == newCrash.Pattern); newCrash.PatternId = callstackPattern.id; } else { //if this is a new callstack pattern then insert into data model and create a new bugg. var callstackPattern = new CallStackPattern {Pattern = newCrash.Pattern}; callStackRepository.Save(callstackPattern); unitOfWork.Save(); newCrash.PatternId = callstackPattern.id; } //Mask out the line number and File path from our error message. var errorMessageString = description.ErrorMessage != null ? String.Join("", description.ErrorMessage) : ""; //Create our masking regular expressions var fileRegex = new Regex(@"(\[File:).*?(])"); //Match the filename out the file name var lineRegex = new Regex(@"(\[Line:).*?(])"); //Match the line no. /** * * Regex to match ints of two characters or longer * * First term ((?<=\s)|(-)) : Positive look behind, match if preceeded by whitespace or if first character is '-' * Second term (\d{3,}) match three or more decimal chracters in a row. * Third term (?=(\s|$)) positive look ahead match if followed by whitespace or end of line/file. */ var intRegex = new Regex(@"-?\d{3,}"); /** * Regular expression for masking out floats */ var floatRegex = new Regex(@"-?\d+\.\d+"); /** * Regular expression for masking out hexadecimal numbers */ var hexRegex = new Regex(@"0x[\da-fA-F]+"); //mask out terms matches by our regex's var trimmedError = fileRegex.Replace(errorMessageString, ""); trimmedError = lineRegex.Replace(trimmedError, ""); trimmedError = floatRegex.Replace(trimmedError, ""); trimmedError = hexRegex.Replace(trimmedError, ""); trimmedError = intRegex.Replace(trimmedError, ""); if (trimmedError.Length > 450) trimmedError = trimmedError.Substring(0, 450); //error message is used as an index - trim to max index length //Check to see if the masked error message is unique ErrorMessage errorMessage = null; if ( unitOfWork.ErrorMessageRepository.Any( data => data.ErrorMessageText.Equals(trimmedError, StringComparison.InvariantCultureIgnoreCase))) { errorMessage = unitOfWork.ErrorMessageRepository.First( data => data.ErrorMessageText.Equals(trimmedError, StringComparison.InvariantCultureIgnoreCase)); } else { //if it's a new message then add it to the database. errorMessage = new ErrorMessage() { ErrorMessageText = trimmedError.ToLower(CultureInfo.InvariantCulture) }; unitOfWork.ErrorMessageRepository.Save(errorMessage); unitOfWork.Save(); } //Check for an existing bugg with this pattern and error message / no error message if (unitOfWork.BuggRepository.Any(data => (data.PatternId == newCrash.PatternId) && (data.ErrorMessageId == errorMessage.Id || data.ErrorMessageId == null))) { //if a bugg exists for this pattern update the bugg data var bugg = unitOfWork.BuggRepository.First(data => data.PatternId == newCrash.PatternId); bugg.PatternId = newCrash.PatternId; bugg.CrashType = newCrash.CrashType; bugg.ErrorMessageId = errorMessage.Id; //also update the bugg data while we're here bugg.TimeOfLastCrash = newCrash.TimeOfCrash; bugg.NumberOfCrashes = bugg.NumberOfCrashes + 1; if (String.Compare(newCrash.BuildVersion, bugg.BuildVersion, StringComparison.Ordinal) != 1) bugg.BuildVersion = newCrash.BuildVersion; unitOfWork.Save(); //if a bugg exists update this Crash from the bugg //buggs are authoritative in this case newCrash.BuggId = bugg.Id; newCrash.Jira = bugg.TTPID; newCrash.FixedChangeList = bugg.FixedChangeList; newCrash.Status = bugg.Status; unitOfWork.CrashRepository.Save(newCrash); unitOfWork.Save(); } else { //if there's no bugg for this pattern create a new bugg and insert into the data store. var bugg = new Bugg(); bugg.TimeOfFirstCrash = newCrash.TimeOfCrash; bugg.TimeOfLastCrash = newCrash.TimeOfCrash; bugg.TTPID = newCrash.Jira; bugg.Pattern = newCrash.Pattern; bugg.PatternId = newCrash.PatternId; bugg.NumberOfCrashes = 1; bugg.NumberOfUsers = 1; bugg.NumberOfUniqueMachines = 1; bugg.BuildVersion = newCrash.BuildVersion; bugg.CrashType = newCrash.CrashType; bugg.Status = newCrash.Status; bugg.FixedChangeList = newCrash.FixedChangeList; bugg.ErrorMessageId = errorMessage.Id; bugg.NumberOfCrashes = 1; newCrash.Bugg = bugg; unitOfWork.BuggRepository.Save(bugg); unitOfWork.CrashRepository.Save(newCrash); unitOfWork.Save(); } } catch (DbEntityValidationException dbentEx) { var messageBuilder = new StringBuilder(); messageBuilder.AppendLine("Db Entity Validation Exception Exception was:"); messageBuilder.AppendLine(dbentEx.ToString()); var innerEx = dbentEx.InnerException; while (innerEx != null) { messageBuilder.AppendLine("Inner Exception : " + innerEx.Message); innerEx = innerEx.InnerException; } if (dbentEx.EntityValidationErrors != null) { messageBuilder.AppendLine("Validation Errors : "); foreach (var valErr in dbentEx.EntityValidationErrors) { messageBuilder.AppendLine( valErr.ValidationErrors.Select(data => data.ErrorMessage) .Aggregate((current, next) => current + "; /n" + next)); } } FLogger.Global.WriteException(messageBuilder.ToString()); _slackWriter.Write("Create Crash Exception : " + messageBuilder.ToString()); throw; } catch (Exception ex) { var messageBuilder = new StringBuilder(); messageBuilder.AppendLine("Create Crash Exception : "); messageBuilder.AppendLine(ex.Message.ToString()); var innerEx = ex.InnerException; while (innerEx != null) { messageBuilder.AppendLine("Inner Exception : " + innerEx.Message); innerEx = innerEx.InnerException; } FLogger.Global.WriteException("Create Crash Exception : " + messageBuilder.ToString()); _slackWriter.Write("Create Crash Exception : " + messageBuilder.ToString()); throw; } } return newCrash; } /// /// Create call stack pattern and either insert into database or match to existing. /// Update Associate Buggs. /// /// private void BuildPattern(Crash newCrash) { var callstack = new CallStackContainer(newCrash.CrashType, newCrash.RawCallStack, newCrash.PlatformName); newCrash.Module = callstack.GetModuleName(); newCrash.Module = newCrash.Module.Length >= 127 ? newCrash.Module.Substring(0, 127) : newCrash.Module; if (newCrash.PatternId == null) { var patternList = new List(); try { using (var unitOfWork = new UnitOfWork(new CrashReportEntities())) { foreach (var entry in callstack.CallStackEntries.Take(CallStackContainer.MaxLinesToParse)) { var functionCallId = 0; //if this function is already in the database get id if (unitOfWork.FunctionRepository.Any(f => f.Call == entry.FunctionName)) { functionCallId = unitOfWork.FunctionRepository.FirstId(f => f.Call == entry.FunctionName); } else { //else add this function to the db. var call = entry.FunctionName; if (call.Length > 899) call = call.Substring(0, 899); var currentFunctionCall = new FunctionCall {Call = call}; unitOfWork.FunctionRepository.Save(currentFunctionCall); unitOfWork.Save(); functionCallId = currentFunctionCall.Id; } patternList.Add(functionCallId.ToString()); } } newCrash.Pattern = string.Join("+", patternList); } catch (Exception ex) { var messageBuilder = new StringBuilder(); FLogger.Global.WriteException("Build Pattern exception: " + ex.Message.ToString(CultureInfo.InvariantCulture)); messageBuilder.AppendLine("Exception was:"); messageBuilder.AppendLine(ex.ToString()); while (ex.InnerException != null) { ex = ex.InnerException; messageBuilder.AppendLine(ex.ToString()); } _slackWriter.Write("Build Pattern Exception : " + ex.Message.ToString(CultureInfo.InvariantCulture)); throw; } } } /// /// protected override void Dispose(bool disposing) { base.Dispose(disposing); } /// /// /// public void TestAddCrash() { using ( var logTimer = new FAutoScopedLogTimer(this.GetType().ToString() + "()")) { string CrashContext; using ( var stream = new FileStream( "D:/CR/Engine/Source/Programs/CrashReporter/CrashReportWebSite/bin/Content/CrashContext.txt", FileMode.Open)) { CrashContext = new StreamReader(stream).ReadToEnd(); } var Crash = XmlHandler.FromXmlString(CrashContext); var crash = CreateCrash(Crash); _slackWriter.Write("Test Add Crash completed. Total Time : " + logTimer.GetElapsedSeconds().ToString("F2")); } } } }