Files
UnrealEngineUWP/Engine/Source/Programs/CrashReporter/CrashReportWebSite/Controllers/CrashesController.cs
Ben Marsh 3de35115ca Copying //UE4/Dev-Core to //UE4/Dev-Main (Source: //UE4/Dev-Core @ 3283640)
#lockdown Nick.Penwarden
#rb none

==========================
MAJOR FEATURES + CHANGES
==========================

Change 3229011 on 2016/12/09 by Steve.Robb

	Licensee version updated in FWorldTileInfo::Read().

	https://udn.unrealengine.com/questions/325874/fworldtileinfo-not-passing-fileversionlicenseeue4.html

Change 3230493 on 2016/12/12 by Robert.Manuszewski

	Adding a check against assembling the reference token stream while streaming without locking GC.

Change 3230515 on 2016/12/12 by Steve.Robb

	GetStaticEnum and GetStaticStruct removed.
	Various generated code tidy-ups.

Change 3230522 on 2016/12/12 by Steve.Robb

	UHT no longer complains about bases with different prefixes.
	References to obsolete DependsOn removed.

Change 3230528 on 2016/12/12 by Steve.Robb

	ReferenceChainSearch tidyups.

Change 3234235 on 2016/12/14 by Robert.Manuszewski

	PR #2695: fix comments (Contributed by wyhily2010)

Change 3234237 on 2016/12/14 by Robert.Manuszewski

	PR #2614: [GenericPlatformFile] New Function, GetTimeStampLocal, returns file time stamp in local time instead of UTC   Rama (Contributed by EverNewJoy)

Change 3236214 on 2016/12/15 by Robert.Manuszewski

	PR# 1988 : Allow absolute path in -UserDir=<Path> argument (contributed by bozaro)

Change 3236582 on 2016/12/15 by Robert.Manuszewski

	Allow commandline use in shipping builds

	#jira UE-24613

Change 3236591 on 2016/12/15 by Robert.Manuszewski

	Removed unnecessary console variable logspam

	#jira UE-24614

Change 3236737 on 2016/12/15 by Steve.Robb

	Fixes to non-contiguous enums in OSS.

Change 3239686 on 2016/12/19 by Chris.Wood

	Fixed CompressionHelper method UE4CompressFileGZIP() that leaked a file handle when a compression error occurred (CRP v1.2.12)
	[UE-39910] - CrashReportProcess leaks file handles and doesn't cleanup folders after compression fails during output to S3

Change 3240687 on 2016/12/20 by Chris.Wood

	Improved CrashReportProcess retry logic to avoid stuck threads when CRW fails to add crashes (CRP 1.2.13)
	[UE-39941] - Improve CrashReportProcess retry logic when CR website returns failed response to AddCrash Request

Change 3246347 on 2017/01/04 by Steve.Robb

	Readability, debuggability and standards improvements.

Change 3249122 on 2017/01/06 by Steve.Robb

	Generic FPaths::Combine, allowing a mix of string argument types and unlimited arity.

Change 3249580 on 2017/01/06 by Steve.Robb

	Fix for TArray::HeapSort when array contains pointers.

	See: https://answers.unrealengine.com/questions/545533/bug-heapsort-with-tarray-of-pointers-fails-to-comp.html

Change 3250593 on 2017/01/09 by Robert.Manuszewski

	PR #3046: UE-39578: Added none to invalid filenames (Contributed by projectgheist)

Change 3250596 on 2017/01/09 by Robert.Manuszewski

	PR #3094: Fixing typo in comments for LODColoration in BaseEngine.ini - UE-40196 (Contributed by sanjay-nambiar)

Change 3250599 on 2017/01/09 by Robert.Manuszewski

	PR #3096: Fixed Log message in ExclusiveLoadPackageTimeTracker : UE-37583 (Contributed by sanjay-nambiar)

Change 3250863 on 2017/01/09 by Steve.Robb

	Build configuration option to force the use of the Debug version of UnrealHeaderTool.

Change 3250994 on 2017/01/09 by Ben.Zeigler

	Remove bad or redundant ini redirects. These did not work with the old system but were silently ignored, my new system throws warnings about them

Change 3251000 on 2017/01/09 by Ben.Zeigler

	#jira UE-39599 Add FCoreRedirects which replaces and unifies the redirect systems in LinkerLoad, K2Node, Enum, and TaggedProperty. This fixes various bugs and makes things uniform.
	It will parse the previous ini files, or load out of a [CoreRedirects] section in any loaded ini file
	The old redirect system can be re-enabled by setting USE_CORE_REDIRECTS to 0 in CoreRedirects.h. This will be removed eventually
	Some refactors to pass in information needed by the new system that the old system didn't need
	Add LoadTimeVerbose stats for processing redirects and enable that group during -LoadTimeFile

Change 3253580 on 2017/01/11 by Graeme.Thornton

	Added some validation of the class index in exportmap entries

	#jira UE-37873

Change 3253777 on 2017/01/11 by Graeme.Thornton

	Increase SerialSize and SerialOffset in FObjectExport to 64bits, to handle super large files

	#jira UE-39946

Change 3257750 on 2017/01/13 by Ben.Zeigler

	Fix issue where incorrectly set up animation node redirects (were ActiveClassRedirects, should have been ActiveStructRedirects) didn't work in the new redirect system because it validated more.
	Added backward compatibilty code and fixed some conflicts in the ini.

Change 3261176 on 2017/01/17 by Ben.Zeigler

	#jira UE-40746 Fix redundant ini redirect
	#jira UE-40725 Fix section of Match3 defaultengine.ini that appears to have been accidentally duplicated from baseengine.ini several years ago

Change 3261915 on 2017/01/18 by Steve.Robb

	Fixes to localized printf formats.

Change 3262142 on 2017/01/18 by Ben.Zeigler

	Remove runtime code for old ActiveClassRedirects and related systems.
	It was already disabled and the old ini format is still parsed and converted to FCoreRedirects at runtime so there should be no functionality change.
	Merged the deprecated tagged property and enum redirect ini parsing into LinkerLoad, and remove the RemapImports step entirely as it's part of FixupImportMap.

Change 3263596 on 2017/01/19 by Gil.Gribb

	UE4 - Fixed many bugs with the event driven loader and allowed it to work at boot time.

Change 3263597 on 2017/01/19 by Gil.Gribb

	UE4 - Allowed UnrealPak to do a better job with EDL pak files when the order provided is old or from the cooker. Several minor tweaks to low level async IO stuff in support of switch experiments.

Change 3263922 on 2017/01/19 by Gil.Gribb

	UE4 - Fixed a bug with nativized blueprints that was introduced with the boot time EDL changes.

Change 3264131 on 2017/01/19 by Robert.Manuszewski

	Simple app to test hard to repro bugs

Change 3264849 on 2017/01/19 by Ben.Zeigler

	Change FParse::Value to treat ) like , for parsing to handle config parsing struct format. This fixes cases where lines end with bool or FName variables that aren't written out quoted:
	+ClassRedirects=(OldName="LandscapeProxy",NewName="LandscapeStreamingProxy",InstanceOnly=True)

Change 3265232 on 2017/01/19 by Ben.Zeigler

	#jira UE-39599 Finish class redirect refactor by cleaning up BaseEngine.ini
	Move plugin-specific redirects to new plugin ini files
	Move all redirects from BaseEngine.ini prior to 4.11 to native registration in FCoreRedirects. Needed to split up functions to avoid long compile times
	Move all redirects after 4.11 to new ini format
	Some related blueprint fixup code changes, these weren't cooperating well with some ini redirects

Change 3265490 on 2017/01/20 by Steve.Robb

	Prevent engine reinstancing on hot reload.

	#jira UE-40765

Change 3265593 on 2017/01/20 by Gil.Gribb

	UE4 - Stored a copy of the callback in async read request so that we don't need to worry about lifetime so we can capture variables as needed. Also fixed race in audio streaming.

Change 3266003 on 2017/01/20 by Gil.Gribb

	UE4 - Fixed bug which would cause a fatal error when cooking subobjects that were pending kill.

Change 3267433 on 2017/01/22 by Gil.Gribb

	UE4 - Fixed a bug with EDL at boot time which caused a fatal error with unfired imports.

Change 3267677 on 2017/01/23 by Steve.Robb

	Fix for whitespace before UCLASS() causing compile errors.

	#jira UE-24110

Change 3267685 on 2017/01/23 by Steve.Robb

	First pass of fixes to printf-style calls to only use TCHAR[] specifiers.

Change 3267746 on 2017/01/23 by Steven.Hutton

	Resolve offline work

	Changes to repositories to support better handling of db connections.

Change 3267865 on 2017/01/23 by Steve.Robb

	Clarification of TArray::FindLastByPredicate() and FString::FindLastCharByPredicate().

	#fyi nick.darnell

Change 3268075 on 2017/01/23 by Gil.Gribb

	UE4 - Fixed another bug with RF_PendingKill subobjects and the new loader.

Change 3268447 on 2017/01/23 by Gil.Gribb

	Fortnite - Removed calls to ::StaticClass() before main starts; this is not allowed.

Change 3269491 on 2017/01/24 by Gil.Gribb

	UE4 - Cancelling async loading with the EDL loader now prints a warning and does a flush instead.

Change 3269492 on 2017/01/24 by Gil.Gribb

	UE4 - Suppressed a few EDL cook wanrings.

Change 3270085 on 2017/01/24 by Gil.Gribb

	UE4 - Remove pak highwater spam.

Change 3270089 on 2017/01/24 by Gil.Gribb

	UE4 - fix random bug with memory counting and some vertex buffer

Change 3271246 on 2017/01/25 by Chris.Wood

	Fixed CrashReportProcess pipeline for Mac and Linux crashes lacking machine Ids (CRP v1.2.14)
	[UE-40605] - Machine ID is not being shown on the crashreporter website

Change 3271827 on 2017/01/25 by Steve.Robb

	C4946 warning disabled in third party headers (triggers in Clang/LLVM).

Change 3271874 on 2017/01/25 by Steve.Robb

	Fix for missing error check after header preparsing.

Change 3271911 on 2017/01/25 by Steve.Robb

	ObjectMacros.h now automatically included by generated headers.

	#fyi jamie.dale

Change 3273125 on 2017/01/26 by Steve.Robb

	Check to ensure that a .generated.h header is included by headers which have exported types, to avoid crazy compiler errors.

	#fyi james.golding

Change 3273209 on 2017/01/26 by Steve.Robb

	UnrealCodeAnalyzer compilation fixes.

Change 3274917 on 2017/01/27 by Steve.Robb

	GC disabled when recompiling child BPs, as is already the case for the parent (CL# 2731120).
	Now-unused field removed.

Change 3279091 on 2017/01/31 by Ben.Marsh

	UBT: Remove code paths which assume relative paths based on a particular CWD. Use the absolute paths stored in UnrealBuildTool.RootDirectory/UnrealBuildTool.EngineDirectory instead.

Change 3279195 on 2017/01/31 by Gil.Gribb

	Turned EDL on for orion

Change 3279493 on 2017/01/31 by Ben.Zeigler

	#jira UE-41341 Redo redirector fixups that got undone in merge from Main

Change 3280284 on 2017/01/31 by Ben.Zeigler

	#jira UE-41357 Fix typo in vehicle redirect. Also fix base crash when converting old content with nodes that don't exist.
	Fix issues with loading plugin ini files. They weren't properly "diffing" against the base/default source file so my redirect typo fix didn't propagate.
	Some general config system refactors on Josh's advice, and make base.ini optional if reading out of a non-standard engine directory
	Engine plugin ini are now BasePlugin.ini, game plugins are still DefaultPlugin.ini.
	Fix crash when loading old content pointing to nonexistent node type. It will still error/ensure but won't crash.

Change 3280299 on 2017/01/31 by Gil.Gribb

	possibly fix edl at boot with orion server....though was no-repro

Change 3280386 on 2017/01/31 by Ben.Zeigler

	Header include fixes for -nopch, fixes incremental build

Change 3280557 on 2017/01/31 by Ben.Zeigler

	Fix Config crash. FConfigFile's copy constructor is apparently not safe and resulted in garbage memory in some cases

Change 3280817 on 2017/02/01 by Steve.Robb

	Unused SmartCastProperty removed.

Change 3280897 on 2017/02/01 by Chris.Wood

	Improved CRP shutdown code to abort AddCrash requests when cancel is requested (CRP v1.2.15)
	[UE-41338] - Fix CRP shutdown when website isn't accepting new crashes

	Also, improved shutdown code to try to avoid occassional exception when writing out the report index. Looks like it isn't shutting down worker threads cleanly sometimes. Added more logging to this too.

Change 3280989 on 2017/02/01 by Gil.Gribb

	New unrealpak binaries

Change 3281416 on 2017/02/01 by Michael.Trepka

	Updated UnrealPak binaries for Mac

Change 3282457 on 2017/02/01 by Ben.Zeigler

	#jira UE-41425 Protect against issues with streamable manager requests recursively completing by caching the array locally.
	This code is safer in general in my local version so just doing a quick fix for now

Change 3282619 on 2017/02/01 by Arciel.Rekman

	Linux: update UnrealPak.

[CL 3283649 by Ben Marsh in Main branch]
2017-02-02 14:41:50 -05:00

1122 lines
46 KiB
C#

// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
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.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;
namespace Tools.CrashReporter.CrashReportWebSite.Controllers
{
/// <summary>
/// The controller to handle the crash data.
/// </summary>
[HandleError]
public class CrashesController : Controller
{
//Ugly instantiation of crash repository will replace with dependency injection BEFORE this gets anywhere near live.
private readonly SlackWriter _slackWriter;
/// <summary> Special user name, currently used to mark crashes from UE4 releases. </summary>
const string UserNameAnonymous = "Anonymous";
/// <summary>
/// An empty constructor.
/// </summary>
public CrashesController()
{
_slackWriter = new SlackWriter()
{
WebhookUrl = Settings.Default.SlackWebhookUrl,
Channel = Settings.Default.SlackChannel,
Username = Settings.Default.SlackUsername,
IconEmoji = Settings.Default.SlackEmoji
};
}
/// <summary>
/// Display a summary list of crashes based on the search criteria.
/// </summary>
/// <param name="crashesForm">A form of user data passed up from the client.</param>
/// <returns>A view to display a list of crash reports.</returns>
public ActionResult Index(FormCollection crashesForm )
{
using( var logTimer = new FAutoScopedLogTimer( this.GetType().ToString(), bCreateNewLog: true ) )
{
// 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();
//}
// <STATUS>
// 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 );
}
}
/// <summary>
/// Show detailed information about a crash.
/// </summary>
/// <param name="crashesForm">A form of user data passed up from the client.</param>
/// <param name="id">The unique id of the crash we wish to show the details of.</param>
/// <returns>A view to show crash details.</returns>
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 );
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 = new CallStackContainer(currentCrash);
var Model = new CrashViewModel { Crash = currentCrash, User = crashUser, Bugg = currentBugg, UserGroup = currentUserGroup, CallStack = currentCallStack };
Model.GenerationTime = logTimer.GetElapsedSeconds().ToString( "F2" );
return View( "Show", Model );
}
}
/// <summary>
/// Add a crash passed in the payload as Xml to the database.
/// </summary>
/// <param name="id">Unused.</param>
/// <returns>The row id of the newly added crash.</returns>
public ActionResult AddCrash(int id)
{
var newCrashResult = new CrashReporterResult();
CrashDescription newCrash;
newCrashResult.ID = -1;
string payloadString;
//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<CrashReporterResult>(newCrashResult), "text/xml");
}
// De-serialize the payload string
try
{
newCrash = XmlHandler.FromXmlString<CrashDescription>(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<CrashReporterResult>(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" ));
}
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<CrashReporterResult>( newCrashResult );
return Content( returnResult, "text/xml" );
}
/// <summary>
/// Gets a list of crashes filtered based on our form data.
/// </summary>
/// <param name="formData"></param>
/// <returns></returns>
public CrashesViewModel GetResults(FormHelper formData)
{
List<Crash> results = null;
IQueryable<Crash> resultsQuery = null;
var skip = (formData.Page - 1) * formData.PageSize;
var take = formData.PageSize;
Dictionary<string, int> 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);
}
}
return new CrashesViewModel
{
Results = results,
PagingInfo = new PagingInfo { CurrentPage = formData.Page, PageSize = formData.PageSize, TotalResults = 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 - CrashesViewModel.Epoch).TotalMilliseconds,
DateTo = (long)(formData.DateTo - CrashesViewModel.Epoch).TotalMilliseconds,
BranchName = formData.BranchName,
VersionName = formData.VersionName,
PlatformName = formData.PlatformName,
GameName = formData.GameName,
GroupCounts = groupCounts
};
}
/// <summary>
/// Returns a lambda expression used to sort our crash data.
/// </summary>
/// <param name="resultsQuery">A query filter on the crashes entity.</param>
/// <param name="sortTerm">Sort term identifying the field on which to sort.</param>
/// <param name="sortDescending">bool indicating sort order</param>
/// <returns></returns>
public IOrderedQueryable<Crash> GetSortedQuery(IQueryable<Crash> 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.UserName) : resultsQuery.OrderBy(crashInstance => crashInstance.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;
}
/// <summary>Constructs query for filtering.</summary>
private IQueryable<Crash> 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 == !data.IsVanilla.HasValue || 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.UserNameId == 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;
}
/// <summary>
/// Filter a set of crashes to a date range.
/// </summary>
/// <param name="Results">The unfiltered set of crashes.</param>
/// <param name="DateFrom">The earliest date to filter by.</param>
/// <param name="DateTo">The latest date to filter by.</param>
/// <returns>The set of crashes between the earliest and latest date.</returns>
public IQueryable<Crash> FilterByDate(IQueryable<Crash> Results, DateTime DateFrom, DateTime DateTo)
{
using (FAutoScopedLogTimer LogTimer = new FAutoScopedLogTimer(this.GetType().ToString() + " SQL"))
{
DateTo = DateTo.AddDays(1);
IQueryable<Crash> CrashesInTimeFrame = Results
.Where(MyCrash => MyCrash.TimeOfCrash >= DateFrom && MyCrash.TimeOfCrash <= DateTo);
return CrashesInTimeFrame;
}
}
/// <summary>
/// Create a new crash data model object and insert it into the database
/// </summary>
/// <param name="description"></param>
/// <returns></returns>
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"
};
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;
var user = unitOfWork.UserRepository.GetByUserName(userName);
if (user != null)
{
newCrash.UserNameId = user.Id;
newCrash.UserName = user.UserName;
}
else
{
newCrash.User = new User() {UserName = description.UserName, UserGroupId = 5};
}
}
//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;
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.Processed = description.bAllowToBeContacted;
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;
try
{
BuildPattern(newCrash);
}
catch (Exception ex)
{
FLogger.Global.WriteException("Error in Create Crash Build Pattern Method");
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.Message.Equals(trimmedError, StringComparison.InvariantCultureIgnoreCase)))
{
errorMessage =
unitOfWork.ErrorMessageRepository.First(
data => data.Message.Equals(trimmedError, StringComparison.InvariantCultureIgnoreCase));
}
else
{
//if it's a new message then add it to the database.
errorMessage = new ErrorMessage() {Message = 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;
newCrash.Buggs.Add(bugg);
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;
newCrash.Buggs.Add(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;
}
/// <summary>
/// Create call stack pattern and either insert into database or match to existing.
/// Update Associate Buggs.
/// </summary>
/// <param name="newCrash"></param>
private void BuildPattern(Crash newCrash)
{
var callstack = new CallStackContainer(newCrash);
newCrash.Module = callstack.GetModuleName();
if (newCrash.PatternId == null)
{
var patternList = new List<string>();
try
{
using (var unitOfWork = new UnitOfWork(new CrashReportEntities()))
{
foreach (var entry in callstack.CallStackEntries.Take(CallStackContainer.MaxLinesToParse))
{
FunctionCall currentFunctionCall;
var csEntry = entry;
var functionCall = unitOfWork.FunctionRepository.First(f => f.Call == csEntry.FunctionName);
if (functionCall != null)
{
currentFunctionCall = functionCall;
}
else
{
var call = csEntry.FunctionName;
if (call.Length > 899)
call = call.Substring(0, 899);
currentFunctionCall = new FunctionCall { Call = call };
unitOfWork.FunctionRepository.Save(currentFunctionCall);
unitOfWork.Save();
}
patternList.Add(currentFunctionCall.Id.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;
}
}
}
/// <summary></summary>
/// <param name="disposing"></param>
protected override void Dispose(bool disposing)
{
//if (disposing)
//{
// if (_unitOfWork != null)
// {
// _unitOfWork.Dispose();
// _unitOfWork = null;
// }
//}
base.Dispose(disposing);
}
/// <summary>
///
/// </summary>
public void TestAddCrash()
{
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<CrashDescription>(crashContext);
CreateCrash(crash);
}
}
}