From bc47fae8b19c1af0d0cbcae400b4705da7b5eaf5 Mon Sep 17 00:00:00 2001 From: Vitalii Mikhailov Date: Sun, 24 Mar 2024 19:29:32 +0200 Subject: [PATCH] Abstracted the library more * Harmony is exposed from an interface * The CrashReportCreator logic moved to the main project * Added LoaderPlugin support (BepInEx, BLSE) * Decompiler is now ILMerged into the main library --- .../CrashReportParser.cs | 96 ++- .../Extensions/StringExtensions.cs | 4 +- .../BUTR.CrashReport.Bannerlord.Source.csproj | 3 +- .../BUTR.CrashReport.Bannerlord.Source.props | 0 .../CrashReportArchiveRenderer.cs | 1 - .../CrashReportCreator.cs | 437 ------------- .../CrashReportCreatorHelper.cs | 343 ++++++++++ .../CrashReportHtmlRenderer.Html.cs | 367 +++++++++++ .../CrashReportHtmlRenderer.cs | 608 +++++------------- .../CrashReportShared.cs | 54 +- .../HarmonyProvider.cs | 125 ++++ ...uleCapabilities.cs => LoaderPluginInfo.cs} | 38 +- .../ModuleInfo.cs | 1 + .../ModuleSubModuleInfo.cs | 3 +- .../HtmlOptions.cs | 2 +- .../Program.cs | 2 +- .../BUTR.CrashReport.BepInEx5.Source.csproj | 55 ++ .../BUTR.CrashReport.BepInEx5.Source.props | 13 + .../BepInExIntegration.cs | 104 +++ .../BUTR.CrashReport.BepInEx6.Source.csproj | 55 ++ .../BUTR.CrashReport.BepInEx6.Source.props | 13 + .../BepInExIntegration.cs | 96 +++ .../BUTR.CrashReport.Decompilers.csproj | 53 +- .../ILSpy/CSharpILMixedLanguage.cs | 10 +- .../ILSpy/CSharpLanguage.cs | 106 +-- .../ILSpy/ILLanguage.cs | 22 + .../ILSpy/Language.cs | 460 ++++++------- .../ILSpy/PlainTextOutput2.cs | 4 +- .../ILSpy/StringBuilderExtensions.cs | 2 +- .../ILSpy/TextWriterExtensions.cs | 2 +- .../Utils/AssemblyNameFormatter.cs | 73 +-- .../Utils/FileSystemName.cs | 4 +- .../Utils/MethodCopier.cs | 17 +- .../Utils/MethodDecompiler.AsmResolver.cs | 38 +- .../Utils/MethodDecompiler.ILSpy.cs | 47 +- .../Utils/MethodDecompiler.Iced.cs | 16 +- .../Utils/MethodDecompiler.System.cs | 2 +- ...thodDecompiler.SystemReflectionMetadata.cs | 13 +- .../Utils/MethodDecompiler.cs | 2 +- .../Utils/MonoModUtils.cs | 87 ++- .../Utils/ReferenceImporter.cs | 45 +- .../AssemblyIdModel.cs | 57 ++ src/BUTR.CrashReport.Models/AssemblyModel.cs | 25 +- .../AssemblyModelType.cs | 15 + .../BUTR.CrashReport.Models.csproj | 18 +- .../CapabilityModuleOrPluginModel.cs | 22 + .../CrashReportMetadataModel.cs | 22 +- .../CrashReportModel.cs | 35 +- ...ataModel.cs => DependencyMetadataModel.cs} | 12 +- ...Type.cs => DependencyMetadataModelType.cs} | 2 +- .../EnhancedStacktraceFrameModel.cs | 6 +- src/BUTR.CrashReport.Models/ExceptionModel.cs | 13 +- .../HarmonyPatchModel.cs | 24 +- ...yPatchModelType.cs => HarmonyPatchType.cs} | 10 +- .../HarmonyPatchesModel.cs | 4 +- ...odel.cs => InvolvedModuleOrPluginModel.cs} | 6 +- .../LoaderPluginModel.cs | 45 ++ src/BUTR.CrashReport.Models/LogEntry.cs | 2 +- src/BUTR.CrashReport.Models/LogLevel.cs | 47 ++ src/BUTR.CrashReport.Models/LogSource.cs | 4 +- .../MethodExecuting.cs | 2 +- .../MethodHarmonyPatch.cs | 39 ++ src/BUTR.CrashReport.Models/MethodSimple.cs | 25 +- src/BUTR.CrashReport.Models/ModuleModel.cs | 19 +- .../ModuleSubModuleModel.cs | 4 +- .../MonoModDetourModel.cs | 16 +- .../MonoModDetourModelType.cs | 4 +- .../MonoModDetoursModel.cs | 8 +- .../UpdateInfoModuleOrLoaderPlugin.cs | 20 + .../Utils/AssemblyUtils.cs | 18 + src/BUTR.CrashReport.sln | 15 +- src/BUTR.CrashReport/BUTR.CrashReport.csproj | 23 +- src/BUTR.CrashReport/CrashReportInfo.cs | 346 ++-------- ...ssemblyImportedReferenceModelExtensions.cs | 2 +- .../Extensions/AssemblyModelExtensions.cs | 4 +- .../Extensions/ModuleModelExtensions.cs | 24 +- src/BUTR.CrashReport/ICrashReportHelper.cs | 37 -- .../Interfaces/IAssemblyUtilities.cs | 32 + .../ICrashReportMetadataProvider.cs | 14 + .../Interfaces/IHarmonyProvider.cs | 35 + .../Interfaces/ILoaderPluginProvider.cs | 22 + .../Interfaces/IModelConverter.cs | 21 + .../Interfaces/IModuleProvider.cs | 23 + .../Interfaces/IPathAnonymizer.cs | 12 + .../Interfaces/IStacktraceFilter.cs | 16 + .../Models/AssemblyTypeReference.cs | 25 + src/BUTR.CrashReport/Models/HarmonyPatch.cs | 45 ++ src/BUTR.CrashReport/Models/HarmonyPatches.cs | 29 + .../Models/ILoaderPluginInfo.cs | 25 + .../{ => Models}/IModuleInfo.cs | 2 +- .../{ => Models}/IModuleSubModuleInfo.cs | 8 +- src/BUTR.CrashReport/Models/MethodEntry.cs | 44 ++ .../Models/MethodEntryHarmony.cs | 12 + .../Models/MethodEntrySimple.cs | 6 + .../Models/StacktraceEntry.cs | 85 +++ src/BUTR.CrashReport/Utils/Anonymizer.cs | 5 +- .../Utils/CrashReportModelUtils.cs | 335 ++++++++++ .../Utils/CrashReportUtils.cs | 275 ++++++++ src/BUTR.CrashReport/Utils/HarmonyUtils.cs | 64 ++ src/nuget.config | 1 + 100 files changed, 3707 insertions(+), 1827 deletions(-) rename src/{ => BUTR.CrashReport.Bannerlord.Source}/BUTR.CrashReport.Bannerlord.Source.props (100%) delete mode 100644 src/BUTR.CrashReport.Bannerlord.Source/CrashReportCreator.cs create mode 100644 src/BUTR.CrashReport.Bannerlord.Source/CrashReportCreatorHelper.cs create mode 100644 src/BUTR.CrashReport.Bannerlord.Source/CrashReportHtmlRenderer.Html.cs create mode 100644 src/BUTR.CrashReport.Bannerlord.Source/HarmonyProvider.cs rename src/BUTR.CrashReport.Bannerlord.Source/{ModuleCapabilities.cs => LoaderPluginInfo.cs} (79%) create mode 100644 src/BUTR.CrashReport.BepInEx5.Source/BUTR.CrashReport.BepInEx5.Source.csproj create mode 100644 src/BUTR.CrashReport.BepInEx5.Source/BUTR.CrashReport.BepInEx5.Source.props create mode 100644 src/BUTR.CrashReport.BepInEx5.Source/BepInExIntegration.cs create mode 100644 src/BUTR.CrashReport.BepInEx6.Source/BUTR.CrashReport.BepInEx6.Source.csproj create mode 100644 src/BUTR.CrashReport.BepInEx6.Source/BUTR.CrashReport.BepInEx6.Source.props create mode 100644 src/BUTR.CrashReport.BepInEx6.Source/BepInExIntegration.cs create mode 100644 src/BUTR.CrashReport.Decompilers/ILSpy/ILLanguage.cs create mode 100644 src/BUTR.CrashReport.Models/AssemblyIdModel.cs create mode 100644 src/BUTR.CrashReport.Models/CapabilityModuleOrPluginModel.cs rename src/BUTR.CrashReport.Models/{ModuleDependencyMetadataModel.cs => DependencyMetadataModel.cs} (67%) rename src/BUTR.CrashReport.Models/{ModuleDependencyMetadataModelType.cs => DependencyMetadataModelType.cs} (90%) rename src/BUTR.CrashReport.Models/{HarmonyPatchModelType.cs => HarmonyPatchType.cs} (83%) rename src/BUTR.CrashReport.Models/{InvolvedModuleModel.cs => InvolvedModuleOrPluginModel.cs} (78%) create mode 100644 src/BUTR.CrashReport.Models/LoaderPluginModel.cs create mode 100644 src/BUTR.CrashReport.Models/LogLevel.cs create mode 100644 src/BUTR.CrashReport.Models/MethodHarmonyPatch.cs create mode 100644 src/BUTR.CrashReport.Models/UpdateInfoModuleOrLoaderPlugin.cs create mode 100644 src/BUTR.CrashReport.Models/Utils/AssemblyUtils.cs delete mode 100644 src/BUTR.CrashReport/ICrashReportHelper.cs create mode 100644 src/BUTR.CrashReport/Interfaces/IAssemblyUtilities.cs create mode 100644 src/BUTR.CrashReport/Interfaces/ICrashReportMetadataProvider.cs create mode 100644 src/BUTR.CrashReport/Interfaces/IHarmonyProvider.cs create mode 100644 src/BUTR.CrashReport/Interfaces/ILoaderPluginProvider.cs create mode 100644 src/BUTR.CrashReport/Interfaces/IModelConverter.cs create mode 100644 src/BUTR.CrashReport/Interfaces/IModuleProvider.cs create mode 100644 src/BUTR.CrashReport/Interfaces/IPathAnonymizer.cs create mode 100644 src/BUTR.CrashReport/Interfaces/IStacktraceFilter.cs create mode 100644 src/BUTR.CrashReport/Models/AssemblyTypeReference.cs create mode 100644 src/BUTR.CrashReport/Models/HarmonyPatch.cs create mode 100644 src/BUTR.CrashReport/Models/HarmonyPatches.cs create mode 100644 src/BUTR.CrashReport/Models/ILoaderPluginInfo.cs rename src/BUTR.CrashReport/{ => Models}/IModuleInfo.cs (96%) rename src/BUTR.CrashReport/{ => Models}/IModuleSubModuleInfo.cs (50%) create mode 100644 src/BUTR.CrashReport/Models/MethodEntry.cs create mode 100644 src/BUTR.CrashReport/Models/MethodEntryHarmony.cs create mode 100644 src/BUTR.CrashReport/Models/MethodEntrySimple.cs create mode 100644 src/BUTR.CrashReport/Models/StacktraceEntry.cs create mode 100644 src/BUTR.CrashReport/Utils/CrashReportModelUtils.cs create mode 100644 src/BUTR.CrashReport/Utils/CrashReportUtils.cs create mode 100644 src/BUTR.CrashReport/Utils/HarmonyUtils.cs diff --git a/src/BUTR.CrashReport.Bannerlord.Parser/CrashReportParser.cs b/src/BUTR.CrashReport.Bannerlord.Parser/CrashReportParser.cs index a4ab9bf..8b6bcfb 100644 --- a/src/BUTR.CrashReport.Bannerlord.Parser/CrashReportParser.cs +++ b/src/BUTR.CrashReport.Bannerlord.Parser/CrashReportParser.cs @@ -34,7 +34,7 @@ private static IReadOnlyList GetAllOpenTags(ReadOnlySpan content, return list; } - private static IReadOnlyList GetEnhancedStacktrace(ReadOnlySpan rawContent, int version, HtmlNode node) + private static IList GetEnhancedStacktrace(ReadOnlySpan rawContent, int version, HtmlNode node) { const string enhancedStacktraceStartDelimiter1 = "
"; const string enhancedStacktraceStartDelimiter2 = "
"; @@ -179,7 +179,16 @@ private static IEnumerable ParseLogsInternal(HtmlNode node) { Date = date, Type = line.Substring(idxTypeStart, idxTypeEnd - idxTypeStart), - Level = line.Substring(idxLevelStart, idxLevelEnd - idxLevelStart), + Level = line.Substring(idxLevelStart, idxLevelEnd - idxLevelStart) switch + { + "VRB" => LogLevel.Verbose, + "DBG" => LogLevel.Debug, + "INF" => LogLevel.Information, + "WRN" => LogLevel.Warning, + "ERR" => LogLevel.Error, + "FTL" => LogLevel.Fatal, + _ => LogLevel.Information, + }, Message = line.Substring(idxLevelEnd + 3), }; } @@ -213,7 +222,7 @@ private static CrashReportModel ParseLegacyHtmlInternal(byte version, HtmlDocume var gameVersion = node.SelectSingleNode("descendant::game")?.Attributes?["version"]?.Value ?? string.Empty; var installedModules = node.SelectSingleNode("descendant::div[@id=\"installed-modules\"]/ul")?.ChildNodes.Where(cn => cn.Name == "li").Select(x => ParseModule(version, x)).DistinctBy(x => x.Id).ToArray() ?? Array.Empty(); var exception = ParseExceptions(node.SelectSingleNode("descendant::div[@id=\"exception\"]"), installedModules); - var involvedModules = node.SelectSingleNode("descendant::div[@id=\"involved-modules\"]/ul")?.ChildNodes.Where(cn => cn.Name == "li").SelectMany(ParseInvolvedModule).ToArray() ?? Array.Empty(); + var involvedModules = node.SelectSingleNode("descendant::div[@id=\"involved-modules\"]/ul")?.ChildNodes.Where(cn => cn.Name == "li").SelectMany(ParseInvolvedModule).ToArray() ?? Array.Empty(); var enhancedStacktrace = GetEnhancedStacktrace(content.AsSpan(), version, node); var assemblies = node.SelectSingleNode("descendant::div[@id=\"assemblies\"]/ul")?.ChildNodes.Where(cn => cn.Name == "li").Select(x => ParseAssembly(x, installedModules)).ToArray() ?? Array.Empty(); @@ -229,17 +238,18 @@ private static CrashReportModel ParseLegacyHtmlInternal(byte version, HtmlDocume { Id = Guid.TryParse(id, out var val) ? val : Guid.Empty, Version = version, - GameVersion = gameVersion, Exception = exception, Metadata = new() { + GameName = "Bannerlord", + GameVersion = gameVersion, + LoaderPluginProviderName = !string.IsNullOrEmpty(butrloaderVersion) ? "BUTRLoader" : string.IsNullOrEmpty(blseVersion) ? "BLSE" : null, + LoaderPluginProviderVersion = !string.IsNullOrEmpty(butrloaderVersion) ? butrloaderVersion : string.IsNullOrEmpty(blseVersion) ? blseVersion : null, LauncherType = launcherType, LauncherVersion = launcherVersion, Runtime = runtime, AdditionalMetadata = new List { - new() { Key = "BUTRLoaderVersion", Value = butrloaderVersion }, - new() { Key = "BLSEVersion", Value = blseVersion }, new() { Key = "LauncherExVersion", Value = launcherexVersion }, }, }, @@ -248,7 +258,9 @@ private static CrashReportModel ParseLegacyHtmlInternal(byte version, HtmlDocume EnhancedStacktrace = enhancedStacktrace, Assemblies = assemblies, HarmonyPatches = harmonyPatches, - MonoModDetours = Array.Empty(), + //MonoModDetours = Array.Empty(), + LoaderPlugins = Array.Empty(), + InvolvedLoaderPlugins = Array.Empty(), AdditionalMetadata = Array.Empty(), }; } @@ -259,7 +271,7 @@ private static ExceptionModel ParseExceptions(HtmlNode node, ModuleModel[] modul foreach (var exception in node.InnerHtml.Split("Inner Exception information")) { - var exceptionLines = exception.Split(new[] { "
", "
" }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim().Trim('\n')).Where(x => x.Length != 0).ToList(); + var exceptionLines = exception.Split(["
", "
"], StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim().Trim('\n')).Where(x => x.Length != 0).ToList(); var type = exceptionLines.First(x => x.StartsWith("Type: ")).Substring(6); var message = exceptionLines.First(x => x.StartsWith("Message: ")).Substring(9); var source = exceptionLines.First(x => x.StartsWith("Source: ")).Substring(8); @@ -267,7 +279,9 @@ private static ExceptionModel ParseExceptions(HtmlNode node, ModuleModel[] modul var callstack = string.Join(Environment.NewLine, exceptionLines.Skip(callstackIdx + 1)).Replace("
    \n", "").Replace("
  1. ", "").Replace("
  2. \n", Environment.NewLine).Replace("
", ""); exceptions.Add(new ExceptionModel { + SourceAssemblyId = null, SourceModuleId = modules.Any(x => x.Id == source) ? source : null, + SourceLoaderPluginId = null, Type = type, Message = message, CallStack = callstack, @@ -289,34 +303,39 @@ private static ExceptionModel ParseExceptions(HtmlNode node, ModuleModel[] modul private static ModuleModel ParseModule(byte version, HtmlNode node) { static string GetField(IEnumerable lines, string field) => lines - .FirstOrDefault(l => l.StartsWith($"{field}:"))?.Split(new[] { $"{field}:" }, StringSplitOptions.None).Skip(1).FirstOrDefault()?.Trim() ?? string.Empty; + .FirstOrDefault(l => l.StartsWith($"{field}:"))?.Split([$"{field}:"], StringSplitOptions.None).Skip(1).FirstOrDefault()?.Trim() ?? string.Empty; static IReadOnlyList GetRange(IEnumerable lines, string bField, IEnumerable eFields) => lines .SkipWhile(l => !l.StartsWith($"{bField}:")).Skip(1) .TakeWhile(l => eFields.All(f => !l.StartsWith($"{f}:"))) .ToArray(); - static IReadOnlyList GetModuleDependencyMetadatas(IReadOnlyList lines) => lines.Select(sml => new ModuleDependencyMetadataModel + static IList GetModuleDependencyMetadatas(IReadOnlyList lines) => lines.Select(sml => new DependencyMetadataModel { - Type = sml.StartsWith("Load Before") ? ModuleDependencyMetadataModelType.LoadBefore - : sml.StartsWith("Load After") ? ModuleDependencyMetadataModelType.LoadAfter - : sml.StartsWith("Incompatible") ? ModuleDependencyMetadataModelType.Incompatible + Type = sml.StartsWith("Load Before") ? DependencyMetadataModelType.LoadBefore + : sml.StartsWith("Load After") ? DependencyMetadataModelType.LoadAfter + : sml.StartsWith("Incompatible") ? DependencyMetadataModelType.Incompatible : 0, - ModuleId = sml.Replace("Load Before", "").Replace("Load After", "").Replace("Incompatible", "").Replace("(optional)", "").Trim(), + ModuleOrPluginId = sml.Replace("Load Before", "").Replace("Load After", "").Replace("Incompatible", "").Replace("(optional)", "").Trim(), IsOptional = sml.Contains("(optional)"), Version = null, // Was not available pre 13 VersionRange = null, // Was not available pre 13 AdditionalMetadata = Array.Empty(), }).ToArray(); - static IReadOnlyList GetModuleSubModules(IReadOnlyList lines) => lines + static IList GetModuleSubModules(IReadOnlyList lines) => lines .Select((item, index) => new { Item = item, Index = index }) .Where(o => !o.Item.Contains(':') && !o.Item.Contains(".dll")) .Select(o => lines.Skip(o.Index + 1).TakeWhile(l => l.Contains(':') || l.Contains(".dll")).ToArray()) .Select(sml => new ModuleSubModuleModel { Name = sml.FirstOrDefault(l => l.StartsWith("Name:"))?.Split("Name:").Skip(1).FirstOrDefault()?.Trim() ?? string.Empty, - AssemblyName = sml.FirstOrDefault(l => l.StartsWith("DLLName:"))?.Split("DLLName:").Skip(1).FirstOrDefault()?.Trim() ?? string.Empty, + AssemblyId = new() + { + Name = sml.FirstOrDefault(l => l.StartsWith("DLLName:"))?.Split("DLLName:").Skip(1).FirstOrDefault()?.Trim() ?? string.Empty, + Version = null, + PublicKeyToken = null + }, Entrypoint = sml.FirstOrDefault(l => l.StartsWith("SubModuleClassType:"))?.Split("SubModuleClassType:").Skip(1).FirstOrDefault()?.Trim() ?? string.Empty, AdditionalMetadata = sml.SkipWhile(l => !l.StartsWith("Tags:")).Skip(1).TakeWhile(l => !l.StartsWith("Assemblies:")).Select(l => { @@ -329,7 +348,7 @@ static IReadOnlyList GetModuleSubModules(IReadOnlyList x.Trim()).ToArray(); + var lines = node.InnerText.Split(["\r\n", "\r", "\n"], StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToArray(); var isVortex = GetField(lines, "Vortex").Equals("true", StringComparison.OrdinalIgnoreCase); var moduleModel = new ModuleModel { @@ -344,6 +363,7 @@ static IReadOnlyList GetModuleSubModules(IReadOnlyList(), AdditionalMetadata = new List { new() { Key = "METADATA:MANAGED_BY_VORTEX", Value = isVortex.ToString() } }.Concat(lines.SkipWhile(l => !l.StartsWith("Additional Assemblies:")).Skip(1).Select(l => { return new MetadataModel { Key = "METADATA:AdditionalAssembly", Value = l, }; @@ -352,20 +372,20 @@ static IReadOnlyList GetModuleSubModules(IReadOnlyList ParseInvolvedModule(HtmlNode node) + private static IEnumerable ParseInvolvedModule(HtmlNode node) { var id = node.ChildNodes.FirstOrDefault(x => x.Name == "a")?.InnerText.Trim() ?? string.Empty; return node.ChildNodes.FirstOrDefault(x => x.Name == "ul")?.ChildNodes.Select(x => { var lines = x.InnerHtml.Split("
"); var frame = lines.FirstOrDefault(y => y.StartsWith("Frame: "))?.Replace("::", ".").Substring(7) ?? string.Empty; - return new InvolvedModuleModel + return new InvolvedModuleOrPluginModel { - ModuleId = id, + ModuleOrLoaderPluginId = id, EnhancedStacktraceFrameName = frame, AdditionalMetadata = Array.Empty(), }; - }) ?? Array.Empty(); + }) ?? Array.Empty(); } private static EnhancedStacktraceFrameModel ParseEnhancedStacktrace(HtmlNode node) @@ -391,12 +411,14 @@ private static EnhancedStacktraceFrameModel ParseEnhancedStacktrace(HtmlNode nod .Where((_, i) => i % 2 == 0) .Select(x => x.Trim(',')) .ToList() - : new(); + : []; var methodFullName = methodSplit[0].Replace("::", "."); var split = methodFullName.Split('.'); - methods.Add(new MethodSimple + methods.Add(new() { + AssemblyId = null, ModuleId = module, + LoaderPluginId = null, MethodDeclaredTypeName = split.Length == 1 ? null : string.Join(".", split.Take(split.Length - 1)), MethodName = split.Last(), MethodFullDescription = methodFullDescription, @@ -416,7 +438,9 @@ private static EnhancedStacktraceFrameModel ParseEnhancedStacktrace(HtmlNode nod FrameDescription = name, ExecutingMethod = new() { + AssemblyId = null, ModuleId = executingMethod.ModuleId, + LoaderPluginId = null, MethodDeclaredTypeName = executingMethod.MethodDeclaredTypeName, MethodName = executingMethod.MethodName, MethodFullDescription = executingMethod.MethodFullDescription, @@ -450,11 +474,15 @@ private static AssemblyModel ParseAssembly(HtmlNode node, ModuleModel[] modules) })); var assemblyModel = new AssemblyModel { + Id = new() + { + Name = splt[0], + Version = splt[1], + PublicKeyToken = null, + }, ModuleId = module?.Id, - Name = splt[0], - Version = splt[1], - Culture = null, - PublicKeyToken = null, + LoaderPluginId = null, + CultureName = null, Architecture = splt[2], Hash = isDynamic || isEmpty ? string.Empty : splt[3], AnonymizedPath = isDynamic ? "DYNAMIC" : isEmpty ? "EMPTY" : Anonymizer.AnonymizePath(splt[4]), @@ -476,13 +504,15 @@ private static AssemblyModel ParseAssembly(HtmlNode node, ModuleModel[] modules) private static HarmonyPatchesModel ParseHarmonyPatch(HtmlNode node) { - static HarmonyPatchModel ParsePatch(HtmlNode node, HarmonyPatchModelType type) + static HarmonyPatchModel ParsePatch(HtmlNode node, HarmonyPatchType type) { var split = node.InnerText.Split(';', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToArray(); return new HarmonyPatchModel { Type = type, - AssemblyName = null, + AssemblyId = null, + ModuleId = null, + LoaderPluginId = null, Owner = split.FirstOrDefault(x => x.StartsWith("Owner: "))?.Split(':')[1] ?? string.Empty, Namespace = split.FirstOrDefault(x => x.StartsWith("Namespace: "))?.Split(':')[1] ?? string.Empty, Index = split.FirstOrDefault(x => x.StartsWith("Index: "))?.Split(':')[1] is { } strIndex && int.TryParse(strIndex, out var index) ? index : 0, @@ -494,10 +524,10 @@ static HarmonyPatchModel ParsePatch(HtmlNode node, HarmonyPatchModelType type) } var originalMethodFullName = node.ChildNodes.Skip(0).First().InnerText.Trim('\n'); - var prefixes = node.ChildNodes.FirstOrDefault(x => x.InnerText?.Contains("Prefixes") == true)?.SelectSingleNode("descendant::ul/li")?.ChildNodes.Select(x => ParsePatch(x, HarmonyPatchModelType.Prefix)).ToArray() ?? Array.Empty(); - var postfixes = node.ChildNodes.FirstOrDefault(x => x.InnerText?.Contains("Postfixes") == true)?.SelectSingleNode("descendant::ul/li")?.ChildNodes.Select(x => ParsePatch(x, HarmonyPatchModelType.Postfix)).ToArray() ?? Array.Empty(); - var transpilers = node.ChildNodes.FirstOrDefault(x => x.InnerText?.Contains("Transpilers") == true)?.SelectSingleNode("descendant::ul/li")?.ChildNodes.Select(x => ParsePatch(x, HarmonyPatchModelType.Transpiler)).ToArray() ?? Array.Empty(); - var finalizers = node.ChildNodes.FirstOrDefault(x => x.InnerText?.Contains("Finalizers") == true)?.SelectSingleNode("descendant::ul/li")?.ChildNodes.Select(x => ParsePatch(x, HarmonyPatchModelType.Finalizer)).ToArray() ?? Array.Empty(); + var prefixes = node.ChildNodes.FirstOrDefault(x => x.InnerText?.Contains("Prefixes") == true)?.SelectSingleNode("descendant::ul/li")?.ChildNodes.Select(x => ParsePatch(x, HarmonyPatchType.Prefix)).ToArray() ?? Array.Empty(); + var postfixes = node.ChildNodes.FirstOrDefault(x => x.InnerText?.Contains("Postfixes") == true)?.SelectSingleNode("descendant::ul/li")?.ChildNodes.Select(x => ParsePatch(x, HarmonyPatchType.Postfix)).ToArray() ?? Array.Empty(); + var transpilers = node.ChildNodes.FirstOrDefault(x => x.InnerText?.Contains("Transpilers") == true)?.SelectSingleNode("descendant::ul/li")?.ChildNodes.Select(x => ParsePatch(x, HarmonyPatchType.Transpiler)).ToArray() ?? Array.Empty(); + var finalizers = node.ChildNodes.FirstOrDefault(x => x.InnerText?.Contains("Finalizers") == true)?.SelectSingleNode("descendant::ul/li")?.ChildNodes.Select(x => ParsePatch(x, HarmonyPatchType.Finalizer)).ToArray() ?? Array.Empty(); var harmonyPatchModel = new HarmonyPatchesModel { OriginalMethodName = originalMethodFullName.Split('.').Last(), diff --git a/src/BUTR.CrashReport.Bannerlord.Parser/Extensions/StringExtensions.cs b/src/BUTR.CrashReport.Bannerlord.Parser/Extensions/StringExtensions.cs index 30073ef..5b16938 100644 --- a/src/BUTR.CrashReport.Bannerlord.Parser/Extensions/StringExtensions.cs +++ b/src/BUTR.CrashReport.Bannerlord.Parser/Extensions/StringExtensions.cs @@ -4,7 +4,7 @@ namespace BUTR.CrashReport.Bannerlord.Parser.Extensions; internal static class StringExtensions { - public static string[] Split(this string str, string separator) => str.Split(new[] { separator }, StringSplitOptions.None); + public static string[] Split(this string str, string separator) => str.Split([separator], StringSplitOptions.None); - public static string[] Split(this string str, string separator, StringSplitOptions stringSplitOptions) => str.Split(new[] { separator }, stringSplitOptions); + public static string[] Split(this string str, string separator, StringSplitOptions stringSplitOptions) => str.Split([separator], stringSplitOptions); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Bannerlord.Source/BUTR.CrashReport.Bannerlord.Source.csproj b/src/BUTR.CrashReport.Bannerlord.Source/BUTR.CrashReport.Bannerlord.Source.csproj index de8ec16..15b6f46 100644 --- a/src/BUTR.CrashReport.Bannerlord.Source/BUTR.CrashReport.Bannerlord.Source.csproj +++ b/src/BUTR.CrashReport.Bannerlord.Source/BUTR.CrashReport.Bannerlord.Source.csproj @@ -36,13 +36,14 @@ - + + diff --git a/src/BUTR.CrashReport.Bannerlord.Source.props b/src/BUTR.CrashReport.Bannerlord.Source/BUTR.CrashReport.Bannerlord.Source.props similarity index 100% rename from src/BUTR.CrashReport.Bannerlord.Source.props rename to src/BUTR.CrashReport.Bannerlord.Source/BUTR.CrashReport.Bannerlord.Source.props diff --git a/src/BUTR.CrashReport.Bannerlord.Source/CrashReportArchiveRenderer.cs b/src/BUTR.CrashReport.Bannerlord.Source/CrashReportArchiveRenderer.cs index cf66f53..1226d34 100644 --- a/src/BUTR.CrashReport.Bannerlord.Source/CrashReportArchiveRenderer.cs +++ b/src/BUTR.CrashReport.Bannerlord.Source/CrashReportArchiveRenderer.cs @@ -36,7 +36,6 @@ // SOFTWARE. #endregion - #if !BUTRCRASHREPORT_DISABLE || BUTRCRASHREPORT_ENABLE_ARCHIVE_RENDERER #nullable enable #if !BUTRCRASHREPORT_ENABLEWARNINGS diff --git a/src/BUTR.CrashReport.Bannerlord.Source/CrashReportCreator.cs b/src/BUTR.CrashReport.Bannerlord.Source/CrashReportCreator.cs deleted file mode 100644 index 644233d..0000000 --- a/src/BUTR.CrashReport.Bannerlord.Source/CrashReportCreator.cs +++ /dev/null @@ -1,437 +0,0 @@ -// -// This code file has automatically been added by the "BUTR.CrashReport.Bannerlord.Source" NuGet package (https://www.nuget.org/packages/BUTR.CrashReport.Bannerlord.Source). -// Please see https://github.com/BUTR/BUTR.CrashReport for more information. -// -// IMPORTANT: -// DO NOT DELETE THIS FILE if you are using a "packages.config" file to manage your NuGet references. -// Consider migrating to PackageReferences instead: -// https://docs.microsoft.com/en-us/nuget/consume-packages/migrate-packages-config-to-package-reference -// Migrating brings the following benefits: -// * The "BUTR.CrashReport.Bannerlord.Source" folder and the "CrashReportCreator.cs" file don't appear in your project. -// * The added file is immutable and can therefore not be modified by coincidence. -// * Updating/Uninstalling the package will work flawlessly. -// - -#region License -// MIT License -// -// Copyright (c) Bannerlord's Unofficial Tools & Resources -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -#endregion - -#if !BUTRCRASHREPORT_DISABLE -#nullable enable -#if !BUTRCRASHREPORT_ENABLEWARNINGS -#pragma warning disable -#endif - -namespace BUTR.CrashReport.Bannerlord -{ - using global::Bannerlord.BUTR.Shared.Extensions; - using global::Bannerlord.BUTR.Shared.Helpers; - using global::Bannerlord.ModuleManager; - - using global::BUTR.CrashReport.Extensions; - using global::BUTR.CrashReport.Models; - using global::BUTR.CrashReport.Utils; - - using global::HarmonyLib; - - using global::System; - using global::System.Collections.Generic; - using global::System.Globalization; - using global::System.IO; - using global::System.Linq; - using global::System.Reflection; - using global::System.Security.Cryptography; - - internal class CrashReportCreator - { - public static CrashReportModel Create(CrashReportInfo crashReport) - { - var modules = GetModuleList(crashReport); - var assemblies = GetAssemblyList(crashReport); - return new CrashReportModel - { - Id = crashReport.Id, - GameVersion = ApplicationVersionHelper.GameVersionStr(), - Version = crashReport.Version, - Exception = GetRecursiveException(crashReport, modules, assemblies), - EnhancedStacktrace = GetEnhancedStacktrace(crashReport), - InvolvedModules = GetInvolvedModuleList(crashReport), - Modules = modules, - Assemblies = GetAssemblyList(crashReport), - HarmonyPatches = GetHarmonyPatchesListHtml(crashReport), - MonoModDetours = Array.Empty(), - Metadata = new() - { - LauncherType = GetLauncherType(crashReport), - LauncherVersion = GetLauncherVersion(crashReport), - - Runtime = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription, - - AdditionalMetadata = new MetadataModel[] - { - new MetadataModel { Key = "BUTRLoaderVersion", Value = GetBUTRLoaderVersion(crashReport) }, - new MetadataModel { Key = "BLSEVersion", Value = GetBLSEVersion(crashReport) }, - new MetadataModel { Key = "LauncherExVersion", Value = GetLauncherExVersion(crashReport) }, - }, - }, - AdditionalMetadata = Array.Empty(), - }; - } - - private static string GetBUTRLoaderVersion(CrashReportInfo crashReport) - { - if (crashReport.AvailableAssemblies.FirstOrDefault(x => x.Key.Name == "Bannerlord.BUTRLoader") is { Key: { } assemblyName } ) - return assemblyName.Version?.ToString() ?? string.Empty; - return string.Empty; - } - private static string GetBLSEVersion(CrashReportInfo crashReport) - { - var blseMetadata = crashReport.AvailableAssemblies.FirstOrDefault(x => x.Key.Name == "Bannerlord.BLSE").Value?.GetCustomAttributes(); - return blseMetadata?.FirstOrDefault(x => x.Key == "BLSEVersion")?.Value ?? string.Empty; - } - private static string GetLauncherExVersion(CrashReportInfo crashReport) - { - var launcherExMetadata = crashReport.AvailableAssemblies.FirstOrDefault(x => x.Key.Name == "Bannerlord.LauncherEx").Value?.GetCustomAttributes(); - return launcherExMetadata?.FirstOrDefault(x => x.Key == "LauncherExVersion")?.Value ?? string.Empty; - } - - private static string GetLauncherType(CrashReportInfo crashReport) - { - if (crashReport.AdditionalMetadata.TryGetValue("Parent_Process_Name", out var parentProcessName)) - { - return parentProcessName switch - { - "Vortex" => "vortex", - "BannerLordLauncher" => "bannerlordlauncher", - "steam" => "steam", - "GalaxyClient" => "gog", - "EpicGamesLauncher" => "epicgames", - "devenv" => "debuggervisualstudio", - "JetBrains.Debugger.Worker64c" => "debuggerjetbrains", - "explorer" => "explorer", - "NovusLauncher" => "novus", - "ModOrganizer" => "modorganizer", - _ => $"unknown launcher - {parentProcessName}" - }; - } - - if (!string.IsNullOrEmpty(GetBUTRLoaderVersion(crashReport))) - return "butrloader"; - - return "vanilla"; - } - - private static string GetLauncherVersion(CrashReportInfo crashReport) - { - if (crashReport.AdditionalMetadata.TryGetValue("Parent_Process_File_Version", out var parentProcessFileVersion)) - return parentProcessFileVersion; - - if (GetBUTRLoaderVersion(crashReport) is { } bVersion && !string.IsNullOrEmpty(bVersion)) - return bVersion; - - return "0"; - } - - private static ExceptionModel GetRecursiveException(CrashReportInfo crashReport, List modules, List assemblies) - { - static ExceptionModel GetRecursiveException(CrashReportInfo crashReport, List modules, List assemblies, Exception ex) => new() - { - SourceModuleId = modules.FirstOrDefault(x => assemblies.Where(y => y.ModuleId == x.Id).Any(x => x.Name == ex.Source))?.Id, - Type = ex.GetType().FullName ?? string.Empty, - Message = ex.Message, - CallStack = ex.StackTrace ?? string.Empty, - InnerException = ex.InnerException is not null ? GetRecursiveException(crashReport, modules, assemblies, ex.InnerException) : null, - AdditionalMetadata = Array.Empty(), - }; - - return GetRecursiveException(crashReport, modules, assemblies, crashReport.Exception); - } - - private static List GetEnhancedStacktrace(CrashReportInfo crashReport) - { - var enhancedStacktraceFrameModels = new List(); - foreach (var stacktrace in crashReport.Stacktrace.GroupBy(x => x.StackFrameDescription)) - { - foreach (var entry in stacktrace) - { - var methods = new List(entry.PatchMethods.Length); - foreach (var patchMethod in entry.PatchMethods) - { - methods.Add(new() - { - ModuleId = patchMethod.ModuleInfo?.Id, - MethodDeclaredTypeName = patchMethod.Method.DeclaringType?.FullName, - MethodName = patchMethod.Method.Name, - MethodFullDescription = patchMethod.Method.FullDescription(), - MethodParameters = patchMethod.Method.GetParameters().Select(x => x.ParameterType.FullName ?? string.Empty).ToArray(), - ILInstructions = patchMethod.ILInstructions, - CSharpILMixedInstructions = patchMethod.CSharpILMixedInstructions, - CSharpInstructions = patchMethod.CSharpInstructions, - AdditionalMetadata = Array.Empty(), - }); - } - - enhancedStacktraceFrameModels.Add(new() - { - FrameDescription = entry.StackFrameDescription, - ExecutingMethod = new() - { - ModuleId = entry.ModuleInfo?.Id, - MethodDeclaredTypeName = entry.Method.DeclaringType?.FullName, - MethodName = entry.Method.Name, - MethodFullDescription = entry.Method.FullDescription(), - MethodParameters = entry.Method.GetParameters().Select(x => x.ParameterType.FullName ?? string.Empty).ToArray(), - NativeInstructions = entry.NativeInstructions, - ILInstructions = entry.ILInstructions, - CSharpILMixedInstructions = entry.CSharpILMixedInstructions, - CSharpInstructions = entry.CSharpInstructions, - AdditionalMetadata = Array.Empty(), - }, - OriginalMethod = entry.OriginalMethod is not null ? new() - { - ModuleId = entry.OriginalMethod.ModuleInfo?.Id, - MethodDeclaredTypeName = entry.OriginalMethod.Method.DeclaringType?.FullName, - MethodName = entry.OriginalMethod.Method.Name, - MethodFullDescription = entry.OriginalMethod.Method.FullDescription(), - MethodParameters = entry.OriginalMethod.Method.GetParameters().Select(x => x.ParameterType.FullName ?? string.Empty).ToArray(), - ILInstructions = entry.OriginalMethod.ILInstructions, - CSharpILMixedInstructions = entry.OriginalMethod.CSharpILMixedInstructions, - CSharpInstructions = entry.OriginalMethod.CSharpInstructions, - AdditionalMetadata = Array.Empty() - } : null, - PatchMethods = methods, - ILOffset = entry.ILOffset, - NativeOffset = entry.NativeOffset, - MethodFromStackframeIssue = entry.MethodFromStackframeIssue, - AdditionalMetadata = Array.Empty(), - }); - } - } - return enhancedStacktraceFrameModels; - } - - private static List GetInvolvedModuleList(CrashReportInfo crashReport) - { - var involvedModuleModels = new List(); - foreach (var stacktrace in crashReport.FilteredStacktrace.GroupBy(m => m.ModuleInfo)) - { - var module = stacktrace.Key; - if (module is null) continue; - - involvedModuleModels.Add(new() - { - ModuleId = module.Id, - EnhancedStacktraceFrameName = stacktrace.Last().StackFrameDescription, - AdditionalMetadata = Array.Empty(), - }); - } - return involvedModuleModels; - } - - private static List GetModuleList(CrashReportInfo crashReport) - { - var moduleModels = new List(crashReport.LoadedModules.Count); - foreach (var module in crashReport.LoadedModules.OfType().Select(x => x.InternalModuleInfo)) - { - var isManagedByVortex = File.Exists(Path.Combine(module.Path, "__folder_managed_by_vortex")); - - moduleModels.Add(new() - { - Id = module.Id, - Name = module.Name, - Version = module.Version.ToString(), - IsExternal = module.IsExternal, - IsOfficial = module.IsOfficial, - IsSingleplayer = module.IsSingleplayerModule, - IsMultiplayer = module.IsMultiplayerModule, - Url = !string.IsNullOrEmpty(module.Url) ? module.Url : null, - UpdateInfo = !string.IsNullOrEmpty(module.UpdateInfo) ? module.UpdateInfo : null, - DependencyMetadatas = module.DependenciesAllDistinct().Select(x => new ModuleDependencyMetadataModel - { - ModuleId = x.Id, - Type = x.IsIncompatible ? ModuleDependencyMetadataModelType.Incompatible : (ModuleDependencyMetadataModelType) x.LoadType, - IsOptional = x.IsOptional, - Version = !x.Version.Equals(ApplicationVersion.Empty) ? x.Version.ToString() : null, - VersionRange = !x.VersionRange.Equals(ApplicationVersionRange.Empty) ? x.VersionRange.ToString() : null, - AdditionalMetadata = Array.Empty(), - }).ToArray(), - SubModules = module.SubModules.Where(ModuleInfoHelper.CheckIfSubModuleCanBeLoaded).Select(x => new ModuleSubModuleModel - { - Name = x.Name, - AssemblyName = x.DLLName, - Entrypoint = x.SubModuleClassType, - AdditionalMetadata = x.Assemblies.Select(y => new MetadataModel { Key = "METADATA:Assembly", Value = y }) - .Concat(x.Tags.SelectMany(y => y.Value.Select(z => new MetadataModel { Key = y.Key, Value = z }))) - .ToArray(), - }).ToArray(), - AdditionalMetadata = new MetadataModel[] - { - new MetadataModel { Key = "METADATA:MANAGED_BY_VORTEX", Value = isManagedByVortex.ToString() }, - }, - }); - } - return moduleModels; - } - - private static List GetAssemblyList(CrashReportInfo crashReport) - { - static bool IsGAC(Assembly assembly) - { - try - { -#pragma warning disable SYSLIB0005 - return assembly.GlobalAssemblyCache; -#pragma warning restore SYSLIB0005 - } - catch (Exception) { return false; } - } - static ProcessorArchitecture GetProcessorArchitecture(AssemblyName assemblyName) - { - try - { -#pragma warning disable SYSLIB0037 - return assemblyName.ProcessorArchitecture; -#pragma warning restore SYSLIB0037 - } - catch (Exception) { return ProcessorArchitecture.None; } - } - - static string CalculateMD5(string filename) - { - using var md5 = MD5.Create(); - using var stream = File.OpenRead(filename); - var hash = md5.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - - var assemblyModels = new List(crashReport.AvailableAssemblies.Count); - - foreach (var (assemblyName, assembly) in crashReport.AvailableAssemblies) - { - ModuleInfoExtendedWithMetadata? module = null; - foreach (var loadedModule in crashReport.LoadedModules.OfType().Select(x => x.InternalModuleInfo)) - { - if (ModuleInfoHelper.IsModuleAssembly(loadedModule, assembly)) - { - module = loadedModule; - break; - } - } - - var systemAssemblyDirectory = Path.GetDirectoryName(typeof(object).Assembly.Location); - var isGAC = IsGAC(assembly); - var isSystem = - (!assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location) && Path.GetDirectoryName(assembly.Location)?.Equals(systemAssemblyDirectory, StringComparison.Ordinal) == true) || - (assembly.GetCustomAttribute()?.Product == "Microsoft® .NET Framework") || - (assembly.GetCustomAttribute()?.Product == "Microsoft® .NET"); - var isTWCore = !assembly.IsDynamic && assembly.Location.IndexOf(@"Mount & Blade II Bannerlord\bin\", StringComparison.InvariantCultureIgnoreCase) >= 0; - - var type = AssemblyModelType.Unclassified; - if (assembly.IsDynamic) type |= AssemblyModelType.Dynamic; - if (isGAC) type |= AssemblyModelType.GAC; - if (isSystem) type |= AssemblyModelType.System; - if (isTWCore) type |= AssemblyModelType.GameCore; - if (module is not null) type |= AssemblyModelType.Module; - if (module is not null && module.IsOfficial) type |= AssemblyModelType.GameModule; - assemblyModels.Add(new() - { - ModuleId = module?.Id, - Name = assemblyName.Name ?? string.Empty, - Culture = assemblyName.CultureName, - PublicKeyToken = string.Join(string.Empty, Array.ConvertAll(assemblyName.GetPublicKeyToken() ?? Array.Empty(), x => x.ToString("x2", CultureInfo.InvariantCulture))), - Version = assemblyName.Version?.ToString() ?? string.Empty, - Architecture = GetProcessorArchitecture(assemblyName).ToString(), - Hash = assembly.IsDynamic || string.IsNullOrWhiteSpace(assembly.Location) || !File.Exists(assembly.Location) ? string.Empty : CalculateMD5(assembly.Location), - AnonymizedPath = assembly.IsDynamic ? "DYNAMIC" : string.IsNullOrWhiteSpace(assembly.Location) ? "EMPTY" : !File.Exists(assembly.Location) ? "MISSING" : Anonymizer.AnonymizePath(assembly.Location), - Type = type, - ImportedTypeReferences = !isSystem - ? crashReport.ImportedTypeReferences.TryGetValue(assemblyName, out var values) ? values.Select(x => new AssemblyImportedTypeReferenceModel() - { - Namespace = x.Namespace, - Name = x.Name, - FullName = x.FullName, - }).ToArray() : Array.Empty() - : Array.Empty(), - ImportedAssemblyReferences = !isSystem - ? assembly.GetReferencedAssemblies().Select(x => AssemblyImportedReferenceModelExtensions.Create(x)).ToArray() - : Array.Empty(), - AdditionalMetadata = Array.Empty(), - }); - } - - return assemblyModels; - } - - private static List GetHarmonyPatchesListHtml(CrashReportInfo crashReport) - { - var builder = new List(crashReport.LoadedHarmonyPatches.Count); - - static void AppendPatches(List builder, HarmonyPatchModelType type, IEnumerable patches) - { - foreach (var patch in patches) - { - var moduleAssembly = patch.PatchMethod.DeclaringType?.Assembly; - builder.Add(new() - { - Type = type, - AssemblyName = patch.PatchMethod.DeclaringType?.Assembly.GetName().Name, - Owner = patch.owner, - Namespace = $"{patch.PatchMethod.DeclaringType!.FullName}.{patch.PatchMethod.Name}", - Index = patch.index, - Priority = patch.priority, - Before = patch.before, - After = patch.after, - AdditionalMetadata = Array.Empty(), - }); - } - } - - foreach (var (originalMethod, patches) in crashReport.LoadedHarmonyPatches) - { - var patchBuilder = new List(patches.Prefixes.Count + patches.Postfixes.Count + patches.Finalizers.Count + patches.Transpilers.Count); - - AppendPatches(patchBuilder, HarmonyPatchModelType.Prefix, patches.Prefixes); - AppendPatches(patchBuilder, HarmonyPatchModelType.Postfix, patches.Postfixes); - AppendPatches(patchBuilder, HarmonyPatchModelType.Finalizer, patches.Finalizers); - AppendPatches(patchBuilder, HarmonyPatchModelType.Transpiler, patches.Transpilers); - - if (patchBuilder.Count > 0) - { - builder.Add(new() - { - OriginalMethodDeclaredTypeName = originalMethod.DeclaringType?.FullName, - OriginalMethodName = originalMethod.Name, - Patches = patchBuilder, - AdditionalMetadata = Array.Empty(), - }); - } - } - - return builder; - } - } -} - -#pragma warning restore -#nullable restore -#endif // BUTRCRASHREPORT_DISABLE \ No newline at end of file diff --git a/src/BUTR.CrashReport.Bannerlord.Source/CrashReportCreatorHelper.cs b/src/BUTR.CrashReport.Bannerlord.Source/CrashReportCreatorHelper.cs new file mode 100644 index 0000000..eb7c21a --- /dev/null +++ b/src/BUTR.CrashReport.Bannerlord.Source/CrashReportCreatorHelper.cs @@ -0,0 +1,343 @@ +// +// This code file has automatically been added by the "BUTR.CrashReport.Bannerlord.Source" NuGet package (https://www.nuget.org/packages/BUTR.CrashReport.Bannerlord.Source). +// Please see https://github.com/BUTR/BUTR.CrashReport for more information. +// +// IMPORTANT: +// DO NOT DELETE THIS FILE if you are using a "packages.config" file to manage your NuGet references. +// Consider migrating to PackageReferences instead: +// https://docs.microsoft.com/en-us/nuget/consume-packages/migrate-packages-config-to-package-reference +// Migrating brings the following benefits: +// * The "BUTR.CrashReport.Bannerlord.Source" folder and the "CrashReportCreatorHelper.cs" file don't appear in your project. +// * The added file is immutable and can therefore not be modified by coincidence. +// * Updating/Uninstalling the package will work flawlessly. +// + +#region License +// MIT License +// +// Copyright (c) Bannerlord's Unofficial Tools & Resources +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +#endregion + +#if !BUTRCRASHREPORT_DISABLE +#nullable enable +#if !BUTRCRASHREPORT_ENABLEWARNINGS +#pragma warning disable +#endif + +namespace BUTR.CrashReport.Bannerlord +{ + using global::Bannerlord.BUTR.Shared.Extensions; + using global::Bannerlord.BUTR.Shared.Helpers; + using global::Bannerlord.ModuleManager; + + using global::BUTR.CrashReport.Extensions; + using global::BUTR.CrashReport.Interfaces; + using global::BUTR.CrashReport.Models; + using global::BUTR.CrashReport.Utils; + + using global::HarmonyLib; + using global::HarmonyLib.BUTR.Extensions; + + using global::System; + using global::System.Collections.Generic; + using global::System.Diagnostics; + using global::System.Globalization; + using global::System.IO; + using global::System.Linq; + using global::System.Reflection; + using global::System.Security.Cryptography; + + internal class CrashReportInfoHelper : + IAssemblyUtilities, + ICrashReportMetadataProvider, + ILoaderPluginProvider, + IModelConverter, + IModuleProvider, + IPathAnonymizer + { + private static readonly AccessTools.FieldRef>? GetFeatures = + AccessTools2.StaticFieldRefAccess>("Bannerlord.BLSE.FeatureIds:Features"); + + private static List GetPlugins() + { + var patches = Harmony.GetAllPatchedMethods().Select(Harmony.GetPatchInfo); + var featurePatches = patches + .SelectMany(x => x.Prefixes.Concat(x.Postfixes).Concat(x.Finalizers).Concat(x.Transpilers)) + .Select(x => x.PatchMethod) + .Select(x => x.DeclaringType?.Namespace) + .OfType() + .Where(x => x.StartsWith("Bannerlord.BLSE.Features.")) + .Select(x => x.Substring("Bannerlord.BLSE.Features.".Length)) + .Select(x => x.Substring(0, x.IndexOf('.') is var idx and not -1 ? idx : x.Length)) + .Distinct() + .Select(x => $"BLSE.{x}"); + + return featurePatches.Concat(new[] { "BLSE.AssemblyResolver"}).Select(x => new LoaderPluginInfo + { + Id = x, + Version = null, + UpdateInfo = null, + }).OfType().ToList() ?? new(); + /* + return GetFeatures?.Invoke().Select(x => new LoaderPluginInfo() + { + Id = x, + Version = null, + UpdateInfo = null, + }).OfType().ToList() ?? new(); + */ + } + + public virtual CrashReportMetadataModel GetCrashReportMetadataModel(CrashReportInfo crashReport) + { + var butrLoaderVersion = GetBUTRLoaderVersion(crashReport); + var blseVersion = GetBLSEVersion(crashReport); + return new CrashReportMetadataModel + { + GameName = "Bannerlord", + GameVersion = ApplicationVersionHelper.GameVersionStr(), + + LoaderPluginProviderName = !string.IsNullOrEmpty(butrLoaderVersion) ? "BUTRLoader" : !string.IsNullOrEmpty(blseVersion) ? "BLSE" : null, + LoaderPluginProviderVersion = !string.IsNullOrEmpty(butrLoaderVersion) ? butrLoaderVersion : !string.IsNullOrEmpty(blseVersion) ? blseVersion : null, + + LauncherType = GetLauncherType(crashReport), + LauncherVersion = GetLauncherVersion(crashReport), + + Runtime = null, + + AdditionalMetadata = new MetadataModel[] + { + new MetadataModel { Key = "LauncherExVersion", Value = GetLauncherExVersion(crashReport) }, + }, + }; + } + + + public virtual IEnumerable Assemblies() => AccessTools2.AllAssemblies(); + + public virtual IModuleInfo? GetAssemblyModule(CrashReportInfo crashReport, Assembly assembly) + { + try + { + var module = !assembly.IsDynamic ? ModuleInfoHelper.GetModuleByType(AccessTools2.GetTypesFromAssembly(assembly).FirstOrDefault()) : null; + return module is not null ? crashReport.LoadedModules.FirstOrDefault(x => x.Id == module.Id) : null; + } + catch (Exception e) + { + Trace.TraceError(e.ToString()); + return null; + } + } + + public virtual ILoaderPluginInfo? GetAssemblyPlugin(CrashReportInfo crashReport, Assembly assembly) => null; + + public virtual AssemblyModelType GetAssemblyType(AssemblyModelType type, CrashReportInfo crashReport, Assembly assembly) + { + var isTWCore = !assembly.IsDynamic && assembly.Location.IndexOf(@"Mount & Blade II Bannerlord\bin\", StringComparison.InvariantCultureIgnoreCase) >= 0; + if (isTWCore) type |= AssemblyModelType.GameCore; + + var module = !assembly.IsDynamic ? ModuleInfoHelper.GetModuleByType(AccessTools2.GetTypesFromAssembly(assembly).FirstOrDefault()) : null; + if (module is not null && !module.IsOfficial) type |= AssemblyModelType.Module; + if (module is not null && module.IsOfficial) type |= AssemblyModelType.GameModule; + + return type; + } + + + public virtual ICollection GetLoadedModules() => ModuleInfoHelper.GetLoadedModules().Select(x => new ModuleInfo(x)).ToArray(); + + public virtual IModuleInfo? GetModuleByType(Type? type) => ModuleInfoHelper.GetModuleByType(type) is { } moduleInfo ? new ModuleInfo(moduleInfo) : null; + + + public virtual ICollection GetLoadedLoaderPlugins() => GetPlugins(); + + public virtual ILoaderPluginInfo? GetLoaderPluginByType(Type? type) + { + if (type is null) + return null; + + if (type.Namespace is null || !type.Namespace.StartsWith("Bannerlord.BLSE.Features.")) + return null; + + var id = type.Namespace.Substring("Bannerlord.BLSE.Features.".Length); + id = id.Substring(0, id.IndexOf('.') is var idx and not -1 ? idx : id.Length); + return new LoaderPluginInfo + { + Id = $"BLSE.{id}", + Version = null, + UpdateInfo = null, + }; + } + + + public virtual List ToModuleModels(ICollection loadedModules, ICollection assemblies) + { + var moduleModels = new List(loadedModules.Count); + foreach (var module in loadedModules.OfType().Select(x => x.InternalModuleInfo)) + { + var isManagedByVortex = File.Exists(Path.Combine(module.Path, "__folder_managed_by_vortex")); + + moduleModels.Add(Convert(module, isManagedByVortex, assemblies)); + } + return moduleModels; + } + + public virtual List ToLoaderPluginModels(ICollection loadedLoaderPlugins, ICollection assemblies) => loadedLoaderPlugins.OfType().Select(x => new LoaderPluginModel + { + Id = x.Id, + Name = x.Id, + Version = x.Version, + UpdateInfo = x.UpdateInfo is not null && x.UpdateInfo.Split(':') is { Length: 2 } split ? new UpdateInfoModuleOrLoaderPlugin + { + Provider = split[0], + Value = split[1], + } : null, + Dependencies = Array.Empty(), + Capabilities = Array.Empty(), + AdditionalMetadata = Array.Empty(), + }).ToList(); + + + public virtual bool TryHandlePath(string path, out string anonymizedPath) + { + anonymizedPath = string.Empty; + + if (path.IndexOf("Mount & Blade II Bannerlord", StringComparison.OrdinalIgnoreCase) is var idxRoot and not -1) + { + anonymizedPath = path.Substring(idxRoot); + return true; + } + + return false; + } + + protected static ModuleModel Convert(ModuleInfoExtendedWithMetadata module, bool isManagedByVortex, ICollection assemblies) + { + var capabilities = new List(); + var moduleModel = new ModuleModel + { + Id = module.Id, + Name = module.Name, + Version = module.Version.ToString(), + IsExternal = module.IsExternal, + IsOfficial = module.IsOfficial, + IsSingleplayer = module.IsSingleplayerModule, + IsMultiplayer = module.IsMultiplayerModule, + Url = !string.IsNullOrEmpty(module.Url) ? module.Url : null, + UpdateInfo = !string.IsNullOrEmpty(module.UpdateInfo) && module.UpdateInfo.Split(':') is {Length: 2} split + ? new UpdateInfoModuleOrLoaderPlugin() + { + Provider = split[0], + Value = split[1], + } + : null, + DependencyMetadatas = module.DependenciesAllDistinct().Select(x => new DependencyMetadataModel + { + ModuleOrPluginId = x.Id, + Type = x.IsIncompatible ? DependencyMetadataModelType.Incompatible : (DependencyMetadataModelType) x.LoadType, + IsOptional = x.IsOptional, + Version = !x.Version.Equals(ApplicationVersion.Empty) ? x.Version.ToString() : null, + VersionRange = !x.VersionRange.Equals(ApplicationVersionRange.Empty) ? x.VersionRange.ToString() : null, + AdditionalMetadata = Array.Empty(), + }).ToArray(), + SubModules = module.SubModules.Where(ModuleInfoHelper.CheckIfSubModuleCanBeLoaded).Select(x => new ModuleSubModuleModel + { + Name = x.Name, + AssemblyId = new() + { + Name = x.DLLName, + Version = null, + PublicKeyToken = null, + }, + Entrypoint = x.SubModuleClassType, + AdditionalMetadata = x.Assemblies.Select(y => new MetadataModel {Key = "METADATA:Assembly", Value = y}) + .Concat(x.Tags.SelectMany(y => y.Value.Select(z => new MetadataModel {Key = y.Key, Value = z}))) + .ToArray(), + }).ToArray(), + Capabilities = capabilities, + AdditionalMetadata = new MetadataModel[] + { + new MetadataModel {Key = "METADATA:MANAGED_BY_VORTEX", Value = isManagedByVortex.ToString()}, + }, + }; + capabilities.AddRange(CollectionsExtensions.DistinctBy(CrashReportShared.GetModuleCapabilities(assemblies, moduleModel), x => x.Name)); + return moduleModel; + } + + protected static string GetBUTRLoaderVersion(CrashReportInfo crashReport) + { + if (crashReport.AvailableAssemblies.FirstOrDefault(x => x.Key.Name == "Bannerlord.BUTRLoader") is { Key: { } assemblyName } ) + return assemblyName.Version?.ToString() ?? string.Empty; + return string.Empty; + } + protected static string GetBLSEVersion(CrashReportInfo crashReport) + { + var blseMetadata = crashReport.AvailableAssemblies.FirstOrDefault(x => x.Key.Name == "Bannerlord.BLSE.Shared").Value?.GetCustomAttributes(); + return blseMetadata?.FirstOrDefault(x => x.Key == "BLSEVersion")?.Value ?? string.Empty; + } + protected static string GetLauncherExVersion(CrashReportInfo crashReport) + { + var launcherExMetadata = crashReport.AvailableAssemblies.FirstOrDefault(x => x.Key.Name == "Bannerlord.LauncherEx").Value?.GetCustomAttributes(); + return launcherExMetadata?.FirstOrDefault(x => x.Key == "LauncherExVersion")?.Value ?? string.Empty; + } + + protected static string GetLauncherType(CrashReportInfo crashReport) + { + if (crashReport.AdditionalMetadata.TryGetValue("Parent_Process_Name", out var parentProcessName)) + { + return parentProcessName switch + { + "Vortex" => "vortex", + "BannerLordLauncher" => "bannerlordlauncher", + "steam" => "steam", + "GalaxyClient" => "gog", + "EpicGamesLauncher" => "epicgames", + "devenv" => "debuggervisualstudio", + "JetBrains.Debugger.Worker64c" => "debuggerjetbrains", + "explorer" => "explorer", + "NovusLauncher" => "novus", + "ModOrganizer" => "modorganizer", + _ => $"unknown launcher - {parentProcessName}" + }; + } + + if (!string.IsNullOrEmpty(GetBUTRLoaderVersion(crashReport))) + return "butrloader"; + + return "vanilla"; + } + + protected static string GetLauncherVersion(CrashReportInfo crashReport) + { + if (crashReport.AdditionalMetadata.TryGetValue("Parent_Process_File_Version", out var parentProcessFileVersion)) + return parentProcessFileVersion; + + if (GetBUTRLoaderVersion(crashReport) is { } bVersion && !string.IsNullOrEmpty(bVersion)) + return bVersion; + + return "0"; + } + } +} + +#pragma warning restore +#nullable restore +#endif // BUTRCRASHREPORT_DISABLE \ No newline at end of file diff --git a/src/BUTR.CrashReport.Bannerlord.Source/CrashReportHtmlRenderer.Html.cs b/src/BUTR.CrashReport.Bannerlord.Source/CrashReportHtmlRenderer.Html.cs new file mode 100644 index 0000000..f8948c8 --- /dev/null +++ b/src/BUTR.CrashReport.Bannerlord.Source/CrashReportHtmlRenderer.Html.cs @@ -0,0 +1,367 @@ +// +// This code file has automatically been added by the "BUTR.CrashReport.Bannerlord.Source" NuGet package (https://www.nuget.org/packages/BUTR.CrashReport.Bannerlord.Source). +// Please see https://github.com/BUTR/BUTR.CrashReport for more information. +// +// IMPORTANT: +// DO NOT DELETE THIS FILE if you are using a "packages.config" file to manage your NuGet references. +// Consider migrating to PackageReferences instead: +// https://docs.microsoft.com/en-us/nuget/consume-packages/migrate-packages-config-to-package-reference +// Migrating brings the following benefits: +// * The "BUTR.CrashReport.Bannerlord.Source" folder and the "CrashReportHtmlRenderer.Html.cs" file don't appear in your project. +// * The added file is immutable and can therefore not be modified by coincidence. +// * Updating/Uninstalling the package will work flawlessly. +// + +#region License +// MIT License +// +// Copyright (c) Bannerlord's Unofficial Tools & Resources +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +#endregion + +#if !BUTRCRASHREPORT_DISABLE || BUTRCRASHREPORT_ENABLE_HTML_RENDERER +#nullable enable +#if !BUTRCRASHREPORT_ENABLEWARNINGS +#pragma warning disable +#endif + +namespace BUTR.CrashReport.Bannerlord +{ + using global::BUTR.CrashReport.Models; + + using global::System.Collections.Generic; + using global::System.Linq; + + partial class CrashReportHtmlRenderer + { +#pragma warning disable format // @formatter:off + private static string GetBase(CrashReportModel crashReport,IEnumerable files) + { + var launcherExVersion = crashReport.Metadata.AdditionalMetadata.FirstOrDefault(x => x.Key == "LauncherExVersion")?.Value is { } launcherExVersionVal ? launcherExVersionVal : string.Empty; + + var pluginLoaderVersion = crashReport.Metadata.LoaderPluginProviderVersion; + + return $$""" + + + {{crashReport.Metadata.GameName}} Crash Report + + + + + + + + + + + +
+
+ {{crashReport.Metadata.GameName}} has encountered a problem and will close itself. +
+ This is a community Crash Report. Please save it and use it for reporting the error. Do not provide screenshots, provide the report! +
+ Most likely this error was caused by a custom installed module. +
+
+ If you were in the middle of something, the progress might be lost. +
+
+ Launcher: {{crashReport.Metadata.LauncherType}} ({{crashReport.Metadata.LauncherVersion}}) +
+ Runtime: {{crashReport.Metadata.Runtime}} + {{(!string.IsNullOrEmpty(pluginLoaderVersion) ? $"
{crashReport.Metadata.LoaderPluginProviderName} Version: {pluginLoaderVersion}" : string.Empty)}} + {{(!string.IsNullOrEmpty(launcherExVersion) ? $"
LauncherEx Version: {launcherExVersion}" : string.Empty)}} +
+
+
+
+ + +
+
+ + + {{JsonModelButtonTag}} {{MiniDumpButtonTag}} {{SaveFileButtonTag}} {{ScreenshotButtonTag}} +
+
+{{Container("exception", "Exception", GetRecursiveExceptionHtml(crashReport, crashReport.Exception))}} +{{Container("enhanced-stacktrace", "Enhanced Stacktrace", GetEnhancedStacktraceHtml(crashReport))}} +{{Container("involved", "Involved Modules and Plugins", GetInvolvedHtml(crashReport))}} +{{Container("installed-modules", "Installed Modules", GetInstalledModulesHtml(crashReport))}} +{{Container("installed-plugins", $"Loaded {crashReport.Metadata.LoaderPluginProviderName} Plugins", GetLoadedBLSEPluginsHtml(crashReport))}} +{{Container("assemblies", "Assemblies", $$$""" + + + + + + + + + + + {{{GetAssembliesHtml(crashReport)}}} +""")}} +{{Container("harmony-patches", "Harmony Patches", GetHarmonyPatchesHtml(crashReport))}} +{{Container("log-files", "Log Files", GetLogFilesHtml(files))}} + +{{Container("mini-dump", "Mini Dump", MiniDumpTag, true)}} +{{Container("save-file", "Save File", SaveFileTag, true)}} +{{Container("save-file", "Screenshot", "Screenshot", true)}} +{{Container("screenshot-data", "Screenshot Data", ScreenshotTag, true)}} +{{Container("json-model-data", "Json Model Data", JsonModelTag, true)}} + + + + + {{Scripts}} + + +"""; + } + + + private static readonly string Scripts = $$""" + +"""; + + private static string ContainerNew(string id, string name, string content, bool hide = false) => $$""" +
+ {{name}} +
+ {{content}} +
+
+"""; + + private static string ContainerCodeNew(string id, string name, string content, bool hide = false) => $$""" +
+ {{name}} +
+
+          {{content}}
+        
+
+
+"""; + + private static string Container(string id, string name, string content, bool hide = false) => $$""" +
+

+ {{name}}

+
+ {{content}} +
+
+"""; + + private static string ContainerCode(string id, string name, string content, bool hide = false) => $$""" +
+ + {{name}} + +
+"""; + #pragma warning disable format // @formatter:on + } +} + +#pragma warning restore +#nullable restore +#endif // BUTRCRASHREPORT_DISABLE \ No newline at end of file diff --git a/src/BUTR.CrashReport.Bannerlord.Source/CrashReportHtmlRenderer.cs b/src/BUTR.CrashReport.Bannerlord.Source/CrashReportHtmlRenderer.cs index ddbfaf0..eefb6f1 100644 --- a/src/BUTR.CrashReport.Bannerlord.Source/CrashReportHtmlRenderer.cs +++ b/src/BUTR.CrashReport.Bannerlord.Source/CrashReportHtmlRenderer.cs @@ -36,8 +36,6 @@ // SOFTWARE. #endregion -using System.IO.Compression; - #if !BUTRCRASHREPORT_DISABLE || BUTRCRASHREPORT_ENABLE_HTML_RENDERER #nullable enable #if !BUTRCRASHREPORT_ENABLEWARNINGS @@ -54,8 +52,8 @@ namespace BUTR.CrashReport.Bannerlord using global::System.IO; using global::System.Linq; using global::System.Text; - - internal static class CrashReportHtmlRenderer + + internal static partial class CrashReportHtmlRenderer { private static readonly string MiniDumpTag = ""; private static readonly string MiniDumpButtonTag = ""; @@ -66,124 +64,6 @@ internal static class CrashReportHtmlRenderer private static readonly string JsonModelTag = ""; private static readonly string JsonModelButtonTag = ""; -#pragma warning disable format // @formatter:off - private static readonly string Scripts = """ - -"""; -#pragma warning disable format // @formatter:on - public static string AddData(string htmlReport, string gzipBase64CrashReportJson, string? gZipBase64MiniDump = null, string? gZipBase64SaveFile = null, string? base64Screenshot = null) { var IncludeMiniDump = !string.IsNullOrEmpty(gZipBase64MiniDump); @@ -194,7 +74,7 @@ public static string AddData(string htmlReport, string gzipBase64CrashReportJson { htmlReport = htmlReport .Replace(CrashReportHtmlRenderer.MiniDumpTag, gZipBase64MiniDump) - .Replace(CrashReportHtmlRenderer.MiniDumpButtonTag, """ + .Replace(CrashReportHtmlRenderer.MiniDumpButtonTag, $$"""
@@ -208,7 +88,7 @@ public static string AddData(string htmlReport, string gzipBase64CrashReportJson { htmlReport = htmlReport .Replace(CrashReportHtmlRenderer.SaveFileTag, gZipBase64SaveFile) - .Replace(CrashReportHtmlRenderer.SaveFileButtonTag, """ + .Replace(CrashReportHtmlRenderer.SaveFileButtonTag, $$"""
@@ -222,7 +102,7 @@ public static string AddData(string htmlReport, string gzipBase64CrashReportJson { htmlReport = htmlReport .Replace(CrashReportHtmlRenderer.ScreenshotTag, base64Screenshot) - .Replace(CrashReportHtmlRenderer.ScreenshotButtonTag, """ + .Replace(CrashReportHtmlRenderer.ScreenshotButtonTag, $$"""
@@ -234,7 +114,7 @@ public static string AddData(string htmlReport, string gzipBase64CrashReportJson htmlReport = htmlReport .Replace(CrashReportHtmlRenderer.JsonModelTag, gzipBase64CrashReportJson) - .Replace(CrashReportHtmlRenderer.JsonModelButtonTag, """ + .Replace(CrashReportHtmlRenderer.JsonModelButtonTag, $$"""
@@ -245,220 +125,8 @@ public static string AddData(string htmlReport, string gzipBase64CrashReportJson return htmlReport; } - - public static string Build(CrashReportModel crashReportModel, IEnumerable files) - { - var runtime = crashReportModel.Metadata.Runtime; - - var launcherType = crashReportModel.Metadata.LauncherType; - var launcherVersion = crashReportModel.Metadata.LauncherVersion; - - var butrLoaderVersion = crashReportModel.Metadata.AdditionalMetadata.FirstOrDefault(x => x.Key == "BUTRLoaderVersion")?.Value is { } butrLoaderVersionVal ? butrLoaderVersionVal : string.Empty; - var blseVersion = crashReportModel.Metadata.AdditionalMetadata.FirstOrDefault(x => x.Key == "BLSEVersion")?.Value is { } blseVersionVal ? blseVersionVal : string.Empty; - var launcherExVersion = crashReportModel.Metadata.AdditionalMetadata.FirstOrDefault(x => x.Key == "LauncherExVersion")?.Value is { } launcherExVersionVal ? launcherExVersionVal : string.Empty; - -#pragma warning disable format // @formatter:off - return $$""" - - - Bannerlord Crash Report - - - - - {{(!string.IsNullOrEmpty(butrLoaderVersion) ? $"" : string.Empty)}} - {{(!string.IsNullOrEmpty(blseVersion) ? $"" : string.Empty)}} - {{(!string.IsNullOrEmpty(launcherExVersion) ? $"" : string.Empty)}} - - - - - - - - - - - -
-
- Bannerlord has encountered a problem and will close itself. -
- This is a community Crash Report. Please save it and use it for reporting the error. Do not provide screenshots, provide the report! -
- Most likely this error was caused by a custom installed module. -
-
- If you were in the middle of something, the progress might be lost. -
-
- Launcher: {{launcherType}} ({{launcherVersion}}) -
- Runtime: {{runtime}} - {{(!string.IsNullOrEmpty(blseVersion) ? $"
BLSE Version: {blseVersion}" : string.Empty)}} - {{(!string.IsNullOrEmpty(launcherExVersion) ? $"
LauncherEx Version: {launcherExVersion}" : string.Empty)}} -
-
-
-
- - -
-
- - - {{JsonModelButtonTag}} {{MiniDumpButtonTag}} {{SaveFileButtonTag}} {{ScreenshotButtonTag}} -
-
-
-

+ Exception

-
- {{GetRecursiveExceptionHtml(crashReportModel, crashReportModel.Exception)}} -
-
-
-

+ Enhanced Stacktrace

-
- {{GetEnhancedStacktraceHtml(crashReportModel)}} -
-
-
-

+ Involved Modules

-
- {{GetInvolvedModuleListHtml(crashReportModel)}} -
-
-
-

+ Installed Modules

-
- {{GetModuleListHtml(crashReportModel)}} -
-
-
-

+ Assemblies

-
- - - - - - - - - {{GetAssemblyListHtml(crashReportModel)}} -
-
-
-

+ Harmony Patches

-
- {{GetHarmonyPatchesListHtml(crashReportModel)}} -
-
-
-

+ Log Files

-
- {{GetLogFilesListHtml(files)}} -
-
- - - - - - - - - {{Scripts}} - - -"""; -#pragma warning disable format // @formatter:on - } + + public static string Build(CrashReportModel crashReportModel, IEnumerable files) => GetBase(crashReportModel, files); private static string GetRecursiveExceptionHtml(CrashReportModel crashReport, ExceptionModel? ex) { @@ -470,16 +138,19 @@ private static string GetRecursiveExceptionHtml(CrashReportModel crashReport, Ex var moduleId = stacktrace?.ExecutingMethod.ModuleId ?? "UNKNOWN"; var sourceModuleId = ex.SourceModuleId ?? "UNKNOWN"; + + var pluginId = stacktrace?.ExecutingMethod.LoaderPluginId ?? "UNKNOWN"; + var sourcePluginId = ex.SourceLoaderPluginId ?? "UNKNOWN"; var hasMessage = !string.IsNullOrWhiteSpace(ex.Message); var hasCallStack = !string.IsNullOrWhiteSpace(ex.CallStack); var hasInner = ex.InnerException is not null; return new StringBuilder() .Append("Exception Information:").Append("
") - .AppendIf(moduleId == "UNKNOWN", sb => sb.Append("Potential Module Id: ").Append(moduleId).Append("
")) .AppendIf(moduleId != "UNKNOWN", sb => sb.Append("Potential Module Id: ").Append("").Append(moduleId).Append("").Append("
")) - .AppendIf(sourceModuleId == "UNKNOWN", sb => sb.Append("Potential Source Module Id: ").Append(sourceModuleId).Append("
")) .AppendIf(sourceModuleId != "UNKNOWN", sb => sb.Append("Potential Source Module Id: ").Append("").Append(sourceModuleId).Append("").Append("
")) + .AppendIf(pluginId != "UNKNOWN", sb => sb.Append("Potential Plugin Id: ").Append("").Append(pluginId).Append("").Append("
")) + .AppendIf(sourcePluginId != "UNKNOWN", sb => sb.Append("Potential Source Plugin Id: ").Append("").Append(sourcePluginId).Append("").Append("
")) .Append("Type: ").Append(ex.Type.EscapeGenerics()).Append("
") .AppendIf(hasMessage, sb => sb.Append("Message: ").Append(ex.Message.EscapeGenerics()).Append("
")) .AppendIf(hasCallStack, sb => sb.Append("Stacktrace:").Append("
")) @@ -507,33 +178,26 @@ private static string GetEnhancedStacktraceHtml(CrashReportModel crashReport) var id3 = random.Next(); var id4 = random.Next(); var moduleId2 = stacktrace.ExecutingMethod.ModuleId ?? "UNKNOWN"; + var pluginId2 = stacktrace.ExecutingMethod.LoaderPluginId ?? "UNKNOWN"; sb.Append("
  • ") .Append("Frame: ").Append(stacktrace.FrameDescription.EscapeGenerics()).Append("
    ") .Append("Executing Method:") .Append("
      ") .Append("
    • ") - .AppendIf(moduleId2 == "UNKNOWN", sb => sb.Append("Module Id: ").Append(moduleId2).Append("
      ")) .AppendIf(moduleId2 != "UNKNOWN", sb => sb.Append("Module Id: ").Append("").Append(moduleId2).Append("").Append("
      ")) + .AppendIf(pluginId2 != "UNKNOWN", sb => sb.Append("Plugin Id: ").Append("").Append(pluginId2).Append("").Append("
      ")) .Append("Method: ").Append(stacktrace.ExecutingMethod.MethodFullDescription.EscapeGenerics()).Append("
      ") .Append("Method From Stackframe Issue: ").Append(stacktrace.MethodFromStackframeIssue).Append("
      ") .Append("Approximate IL Offset: ").Append(stacktrace.ILOffset is not null ? $"{stacktrace.ILOffset:X4}" : "UNKNOWN").Append("
      ") .Append("Native Offset: ").Append(stacktrace.NativeOffset is not null ? $"{stacktrace.NativeOffset:X4}" : "UNKNOWN").Append("
      ") .AppendIf(stacktrace.ExecutingMethod.ILInstructions.Count > 0, sp => sp - .Append($"
      + IL:
      ")
      -                        .AppendJoin(Environment.NewLine, stacktrace.ExecutingMethod.ILInstructions.Select(x => x.EscapeGenerics()))
      -                        .Append("
      ")) + .Append(ContainerCode($"{id1}", "IL:", string.Join(Environment.NewLine, stacktrace.ExecutingMethod.ILInstructions.Select(x => x.EscapeGenerics()))))) .AppendIf(stacktrace.ExecutingMethod.CSharpILMixedInstructions.Count > 0, sp => sp - .Append($"
      + IL with C#:
      ")
      -                        .AppendJoin(Environment.NewLine, stacktrace.ExecutingMethod.CSharpILMixedInstructions.Select(x => x.EscapeGenerics()))
      -                        .Append("
      ")) + .Append(ContainerCode($"{id2}", "IL with C#:", string.Join(Environment.NewLine, stacktrace.ExecutingMethod.CSharpILMixedInstructions.Select(x => x.EscapeGenerics()))))) .AppendIf(stacktrace.ExecutingMethod.CSharpInstructions.Count > 0, sp => sp - .Append($"
      + C#:
      ")
      -                        .AppendJoin(Environment.NewLine, stacktrace.ExecutingMethod.CSharpInstructions.Select(x => x.EscapeGenerics()))
      -                        .Append("
      ")) + .Append(ContainerCode($"{id3}", "C#:", string.Join(Environment.NewLine, stacktrace.ExecutingMethod.CSharpILMixedInstructions.Select(x => x.EscapeGenerics()))))) .AppendIf(stacktrace.ExecutingMethod.NativeInstructions.Count > 0, sp => sp - .Append($"
      + Native:
      ")
      -                        .AppendJoin(Environment.NewLine, stacktrace.ExecutingMethod.NativeInstructions.Select(x => x.EscapeGenerics()))
      -                        .Append("
      ")) + .Append(ContainerCode($"{id4}", "Native:", string.Join(Environment.NewLine, stacktrace.ExecutingMethod.NativeInstructions.Select(x => x.EscapeGenerics()))))) .Append("
    • ") .Append("
    "); @@ -547,22 +211,20 @@ private static string GetEnhancedStacktraceHtml(CrashReportModel crashReport) var id02 = random.Next(); var id03 = random.Next(); var moduleId = method.ModuleId ?? "UNKNOWN"; + var pluginId = method.LoaderPluginId ?? "UNKNOWN"; + var harmonyPatch = method as MethodHarmonyPatch; sb.Append("
  • ") - .AppendIf(moduleId == "UNKNOWN", sb => sb.Append("Module Id: ").Append(moduleId).Append("
    ")) - .AppendIf(moduleId != "UNKNOWN", sb => sb.Append("Module Id: ").Append("").Append(moduleId).Append("").Append("
    ")) + .Append("Type: ").Append(harmonyPatch is not null ? "Harmony" : "UNKNOWN").Append("
    ") + .AppendIf(harmonyPatch is not null, sb => sb.Append("Patch Type: ").Append(harmonyPatch!.PatchType.ToString()).Append("
    ")) + .AppendIf(moduleId != "UNKNOWN", sb => sb.Append("Module Id: ").Append("").Append(moduleId).Append("").Append("
    ")) + .AppendIf(pluginId != "UNKNOWN", sb => sb.Append("Plugin Id: ").Append("").Append(pluginId).Append("").Append("
    ")) .Append("Method: ").Append(method.MethodFullDescription.EscapeGenerics()).Append("
    ") .AppendIf(method.ILInstructions.Count > 0, sp => sp - .Append($"
    + IL:
    ")
    -                                .AppendJoin(Environment.NewLine, method.ILInstructions.Select(x => x.EscapeGenerics()))
    -                                .Append("
    ")) + .Append(ContainerCode($"{id01}", "IL:", string.Join(Environment.NewLine, method.ILInstructions.Select(x => x.EscapeGenerics()))))) .AppendIf(method.CSharpILMixedInstructions.Count > 0, sp => sp - .Append($"
    + IL with C#:
    ")
    -                                .AppendJoin(Environment.NewLine, method.CSharpILMixedInstructions.Select(x => x.EscapeGenerics()))
    -                                .Append("
    ")) + .Append(ContainerCode($"{id02}", "IL with C#:", string.Join(Environment.NewLine, method.CSharpILMixedInstructions.Select(x => x.EscapeGenerics()))))) .AppendIf(method.CSharpInstructions.Count > 0, sp => sp - .Append($"
    + C#:
    ")
    -                                .AppendJoin(Environment.NewLine, method.CSharpInstructions.Select(x => x.EscapeGenerics()))
    -                                .Append("
    ")) + .Append(ContainerCode($"{id03}", "C#:", string.Join(Environment.NewLine, method.CSharpILMixedInstructions.Select(x => x.EscapeGenerics()))))) .Append("
  • "); } sb.Append(""); @@ -570,27 +232,24 @@ private static string GetEnhancedStacktraceHtml(CrashReportModel crashReport) if (stacktrace.OriginalMethod is not null) { + var moduleId3 = stacktrace.OriginalMethod.ModuleId ?? "UNKNOWN"; + var pluginId3 = stacktrace.OriginalMethod.LoaderPluginId ?? "UNKNOWN"; + var id01 = random.Next(); var id02 = random.Next(); var id03 = random.Next(); sb.Append("Original Method:") .Append("
      ") .Append("
    • ") - .AppendIf(moduleId2 == "UNKNOWN", sb => sb.Append("Module Id: ").Append(moduleId2).Append("
      ")) - .AppendIf(moduleId2 != "UNKNOWN", sb => sb.Append("Module Id: ").Append("").Append(moduleId2).Append("").Append("
      ")) + .AppendIf(moduleId3 != "UNKNOWN", sb => sb.Append("Module Id: ").Append("").Append(moduleId3).Append("").Append("
      ")) + .AppendIf(pluginId3 != "UNKNOWN", sb => sb.Append("Plugin Id: ").Append("").Append(pluginId3).Append("").Append("
      ")) .Append("Method: ").Append(stacktrace.OriginalMethod.MethodFullDescription.EscapeGenerics()).Append("
      ") .AppendIf(stacktrace.OriginalMethod.ILInstructions.Count > 0, sb => sb - .Append($"
      + IL:
      ")
      -                            .AppendJoin(Environment.NewLine, stacktrace.OriginalMethod.ILInstructions.Select(x => x.EscapeGenerics()))
      -                            .Append("
      ")) + .Append(ContainerCode($"{id01}", "IL:", string.Join(Environment.NewLine, stacktrace.OriginalMethod.ILInstructions.Select(x => x.EscapeGenerics()))))) .AppendIf(stacktrace.OriginalMethod.CSharpILMixedInstructions.Count > 0, sb => sb - .Append($"
      + IL with C#:
      ")
      -                            .AppendJoin(Environment.NewLine, stacktrace.OriginalMethod.CSharpILMixedInstructions.Select(x => x.EscapeGenerics()))
      -                            .Append("
      ")) + .Append(ContainerCode($"{id02}", "IL with C#:", string.Join(Environment.NewLine, stacktrace.OriginalMethod.CSharpILMixedInstructions.Select(x => x.EscapeGenerics()))))) .AppendIf(stacktrace.OriginalMethod.CSharpInstructions.Count > 0, sb => sb - .Append($"
      + C#:
      ")
      -                            .AppendJoin(Environment.NewLine, stacktrace.OriginalMethod.CSharpInstructions.Select(x => x.EscapeGenerics()))
      -                            .Append("
      ")) + .Append(ContainerCode($"{id03}", "C#:", string.Join(Environment.NewLine, stacktrace.OriginalMethod.CSharpILMixedInstructions.Select(x => x.EscapeGenerics()))))) .Append("
    • ") .Append("
    "); } @@ -603,18 +262,15 @@ private static string GetEnhancedStacktraceHtml(CrashReportModel crashReport) return sb.ToString(); } - private static string GetInvolvedModuleListHtml(CrashReportModel crashReport) + private static void AddInvolvedModules(CrashReportModel crashReport, StringBuilder sb) { - var sb = new StringBuilder(); - sb.Append("Based on Stacktrace:") - .Append("
      "); foreach (var grouping in crashReport.EnhancedStacktrace.GroupBy(x => x.ExecutingMethod.ModuleId ?? "UNKNOWN")) { var moduleId = grouping.Key; if (moduleId == "UNKNOWN") continue; sb.Append("
    • ") - .Append("").Append(moduleId).Append("").Append("
      "); + .Append("Module Id: ").Append("").Append(moduleId).Append("").Append("
      "); foreach (var stacktrace in grouping) { @@ -627,6 +283,8 @@ private static string GetInvolvedModuleListHtml(CrashReportModel crashReport) .Append("
        "); foreach (var method in stacktrace.PatchMethods) { + var harmonyPatch = method as MethodHarmonyPatch; + // Ignore blank transpilers used to force the jitter to skip inlining if (method.MethodName == "BlankTranspiler") continue; var moduleId2 = method.ModuleId ?? "UNKNOWN"; @@ -634,6 +292,51 @@ private static string GetInvolvedModuleListHtml(CrashReportModel crashReport) .AppendIf(moduleId2 == "UNKNOWN", sb => sb.Append("Module Id: ").Append(moduleId2).Append("
        ")) .AppendIf(moduleId2 != "UNKNOWN", sb => sb.Append("Module Id: ").Append("").Append(moduleId2).Append("").Append("
        ")) .Append("Method: ").Append(method.MethodFullDescription.EscapeGenerics()).Append("
        ") + .AppendIf(harmonyPatch is not null, sb => sb.Append("Harmony Patch Type: ").Append(harmonyPatch!.PatchType).Append("
        ")) + .Append(""); + } + sb.Append("
      "); + } + + sb.Append("
      "); + + sb.Append("
    • "); + } + + sb.Append(""); + } + } + private static void AddInvolvedPlugins(CrashReportModel crashReport, StringBuilder sb) + { + foreach (var grouping in crashReport.EnhancedStacktrace.GroupBy(x => x.ExecutingMethod.LoaderPluginId ?? "UNKNOWN")) + { + var pluginId = grouping.Key; + if (pluginId == "UNKNOWN") continue; + + sb.Append("
    • ") + .Append("Plugin Id: ").Append("").Append(pluginId).Append("").Append("
      "); + + foreach (var stacktrace in grouping) + { + sb.Append("Method: ").Append(stacktrace.ExecutingMethod.MethodFullDescription.EscapeGenerics()).Append("
      ") + .Append("Frame: ").Append(stacktrace.FrameDescription.EscapeGenerics()).Append("
      "); + + if (stacktrace.PatchMethods.Count > 0) + { + sb.Append("Patches:").Append("
      ") + .Append("
        "); + foreach (var method in stacktrace.PatchMethods) + { + var harmonyPatch = method as MethodHarmonyPatch; + + // Ignore blank transpilers used to force the jitter to skip inlining + if (method.MethodName == "BlankTranspiler") continue; + var pluginId2 = method.LoaderPluginId ?? "UNKNOWN"; + sb.Append("
      • ") + .AppendIf(pluginId2 == "UNKNOWN", sb => sb.Append("Plugin Id: ").Append(pluginId2).Append("
        ")) + .AppendIf(pluginId2 != "UNKNOWN", sb => sb.Append("Plugin Id: ").Append("").Append(pluginId2).Append("").Append("
        ")) + .Append("Method: ").Append(method.MethodFullDescription.EscapeGenerics()).Append("
        ") + .AppendIf(harmonyPatch is not null, sb => sb.Append("Harmony Patch Type: ").Append(harmonyPatch!.PatchType).Append("
        ")) .Append("
      • "); } sb.Append("
      "); @@ -646,11 +349,19 @@ private static string GetInvolvedModuleListHtml(CrashReportModel crashReport) sb.Append("
    • "); } + } + private static string GetInvolvedHtml(CrashReportModel crashReport) + { + var sb = new StringBuilder(); + sb.Append("Based on Stacktrace:") + .Append("
        "); + AddInvolvedModules(crashReport, sb); + AddInvolvedPlugins(crashReport, sb); sb.Append("
      "); return sb.ToString(); } - private static string GetModuleListHtml(CrashReportModel crashReport) + private static string GetInstalledModulesHtml(CrashReportModel crashReport) { var moduleBuilder = new StringBuilder(); var subModulesBuilder = new StringBuilder(); @@ -667,36 +378,36 @@ void AppendDependencies(ModuleModel module) { var hasVersion = !string.IsNullOrEmpty(dependentModule.Version); var hasVersionRange = !string.IsNullOrEmpty(dependentModule.VersionRange); - if (dependentModule.Type == ModuleDependencyMetadataModelType.Incompatible) + if (dependentModule.Type == DependencyMetadataModelType.Incompatible) { - deps[dependentModule.ModuleId] = tmp.Clear() + deps[dependentModule.ModuleOrPluginId] = tmp.Clear() .Append("Incompatible ") - .Append("") - .Append(dependentModule.ModuleId) + .Append("") + .Append(dependentModule.ModuleOrPluginId) .Append("") .AppendIf(dependentModule.IsOptional, " (optional)") .AppendIf(hasVersion, sb => sb.Append(" >= ").Append(dependentModule.Version)) .AppendIf(hasVersionRange, dependentModule.VersionRange) .ToString(); } - else if (dependentModule.Type == ModuleDependencyMetadataModelType.LoadAfter) + else if (dependentModule.Type == DependencyMetadataModelType.LoadAfter) { - deps[dependentModule.ModuleId] = tmp.Clear() - .Append("Load ").Append("Before ") - .Append("") - .Append(dependentModule.ModuleId) + deps[dependentModule.ModuleOrPluginId] = tmp.Clear() + .Append("Load ").Append("After ") + .Append("") + .Append(dependentModule.ModuleOrPluginId) .Append("") .AppendIf(dependentModule.IsOptional, " (optional)") .AppendIf(hasVersion, sb => sb.Append(" >= ").Append(dependentModule.Version)) .AppendIf(hasVersionRange, dependentModule.VersionRange) .ToString(); } - else if (dependentModule.Type == ModuleDependencyMetadataModelType.LoadBefore) + else if (dependentModule.Type == DependencyMetadataModelType.LoadBefore) { - deps[dependentModule.ModuleId] = tmp.Clear() - .Append("Load ").Append("After ") - .Append("") - .Append(dependentModule.ModuleId) + deps[dependentModule.ModuleOrPluginId] = tmp.Clear() + .Append("Load ").Append("Before ") + .Append("") + .Append(dependentModule.ModuleOrPluginId) .Append("") .AppendIf(dependentModule.IsOptional, " (optional)") .AppendIf(hasVersion, sb => sb.Append(" >= ").Append(dependentModule.Version)) @@ -733,7 +444,7 @@ void AppendSubModules(ModuleModel module) .Append(module.IsOfficial ? "
      " : "
      ") .Append("").Append(subModule.Name).Append("").Append("
      ") .Append("Name: ").Append(subModule.Name).Append("
      ") - .Append("DLLName: ").Append(subModule.AssemblyName).Append("
      ") + .Append("DLLName: ").Append(subModule.AssemblyId?.Name).Append("
      ") .Append("SubModuleClassType: ").Append(subModule.Entrypoint).Append("
      ") .AppendIf(hasTags, sb => sb.Append("Tags:").Append("
      ")) .AppendIf(hasTags, "
        ") @@ -752,7 +463,7 @@ void AppendAdditionalAssemblies(ModuleModel module) { additionalAssembliesBuilder.Clear(); foreach (var assembly in crashReport.Assemblies.Where(y => y.ModuleId == module.Id)) - additionalAssembliesBuilder.Append("
      • ").Append(assembly.Name).Append(" (").Append(assembly.GetFullName()).Append(")").Append("
      • "); + additionalAssembliesBuilder.Append("
      • ").Append(assembly.Id.Name).Append(" (").Append(assembly.GetFullName()).Append(")").Append("
      • "); } moduleBuilder.Append("
          "); @@ -764,9 +475,6 @@ void AppendAdditionalAssemblies(ModuleModel module) var isVortexManaged = module.AdditionalMetadata.FirstOrDefault(x => x.Key == "METADATA:MANAGED_BY_VORTEX")?.Value is { } str && bool.TryParse(str, out var val) && val; - var capabilities = new HashSet(CrashReportShared.GetModuleCapabilities(crashReport, module)); - if (capabilities.Count == 0) capabilities.Add(ModuleCapabilities.None); - var container = module switch { { IsOfficial: true } => "modules-official-container", @@ -775,7 +483,7 @@ void AppendAdditionalAssemblies(ModuleModel module) }; var hasDependencies = dependenciesBuilder.Length != 0; var hasUrl = !string.IsNullOrWhiteSpace(module.Url); - var hasUpdateInfo = !string.IsNullOrWhiteSpace(module.UpdateInfo); + var hasUpdateInfo = module.UpdateInfo is not null; var hasSubModules = subModulesBuilder.Length != 0; var hasAssemblies = additionalAssembliesBuilder.Length != 0; moduleBuilder.Append("
        • ") @@ -796,21 +504,16 @@ void AppendAdditionalAssemblies(ModuleModel module) .AppendIf(hasDependencies, "
        ") .Append("Capabilities:").Append("
        ") .Append("
          ") - .AppendIf(capabilities.Contains(ModuleCapabilities.None), sb => sb.Append("
        • ").Append("None").Append("
        • ")) - .AppendIf(capabilities.Contains(ModuleCapabilities.OSFileSystem), sb => sb.Append("
        • ").Append("OS File System").Append("
        • ")) - .AppendIf(capabilities.Contains(ModuleCapabilities.GameFileSystem), sb => sb.Append("
        • ").Append("Game File System").Append("
        • ")) - .AppendIf(capabilities.Contains(ModuleCapabilities.Shell), sb => sb.Append("
        • ").Append("Shell").Append("
        • ")) - .AppendIf(capabilities.Contains(ModuleCapabilities.SaveSystem), sb => sb.Append("
        • ").Append("Save System").Append("
        • ")) - .AppendIf(capabilities.Contains(ModuleCapabilities.GameEntities), sb => sb.Append("
        • ").Append("Game Entities").Append("
        • ")) - .AppendIf(capabilities.Contains(ModuleCapabilities.InputSystem), sb => sb.Append("
        • ").Append("Input System").Append("
        • ")) - .AppendIf(capabilities.Contains(ModuleCapabilities.Localization), sb => sb.Append("
        • ").Append("Localization System").Append("
        • ")) - .AppendIf(capabilities.Contains(ModuleCapabilities.UserInterface), sb => sb.Append("
        • ").Append("User Interface").Append("
        • ")) - .AppendIf(capabilities.Contains(ModuleCapabilities.Http), sb => sb.Append("
        • ").Append("Http").Append("
        • ")) - .AppendIf(capabilities.Contains(ModuleCapabilities.Achievements), sb => sb.Append("
        • ").Append("Achievements").Append("
        • ")) - .AppendIf(capabilities.Contains(ModuleCapabilities.Campaign), sb => sb.Append("
        • ").Append("Campaign").Append("
        • ")) - .AppendIf(capabilities.Contains(ModuleCapabilities.Skills), sb => sb.Append("
        • ").Append("Skills").Append("
        • ")) - .AppendIf(capabilities.Contains(ModuleCapabilities.Items), sb => sb.Append("
        • ").Append("Items").Append("
        • ")) - .AppendIf(capabilities.Contains(ModuleCapabilities.Cultures), sb => sb.Append("
        • ").Append("Cultures").Append("
        • ")) + .Append((StringBuilder sb) => + { + if (module.Capabilities.Count == 0) + sb.Append("
        • ").Append("None").Append("
        • "); + + foreach (var capability in module.Capabilities) + sb.Append("
        • ").Append(capability).Append("
        • "); + + return sb; + }) .Append("
        ") .AppendIf(hasUrl, sb => sb.Append("Url: ").Append(module.Url).Append("").Append("
        ")) .AppendIf(hasUpdateInfo, sb => sb.Append("Update Info: ").Append(module.UpdateInfo).Append("
        ")) @@ -830,8 +533,34 @@ void AppendAdditionalAssemblies(ModuleModel module) return moduleBuilder.ToString(); } + + private static string GetLoadedBLSEPluginsHtml(CrashReportModel crashReport) + { + var moduleBuilder = new StringBuilder(); + + moduleBuilder.Append(""); + + return moduleBuilder.ToString(); + } - private static string GetAssemblyListHtml(CrashReportModel crashReport) + private static string GetAssembliesHtml(CrashReportModel crashReport) { var sb0 = new StringBuilder(); @@ -842,16 +571,18 @@ void AppendAssembly(AssemblyModel assembly) AssemblyModelType.Dynamic => "dynamic_assembly", AssemblyModelType.GAC => "gac_assembly", AssemblyModelType.System => "sys_assembly", - AssemblyModelType.GameCore => "tw_assembly", - AssemblyModelType.GameModule => "tw_module_assembly", + AssemblyModelType.GameCore => "game_assembly", + AssemblyModelType.GameModule => "game_module_assembly", AssemblyModelType.Module => "module_assembly", + AssemblyModelType.Loader => "loader_assembly", + AssemblyModelType.LoaderPlugin => "loader_plugin_assembly", _ => string.Empty, })); var isDynamic = assembly.Type.HasFlag(AssemblyModelType.Dynamic); var hasPath = assembly.AnonymizedPath != "EMPTY" && !string.IsNullOrWhiteSpace(assembly.AnonymizedPath); sb0.Append("
      • ") - .Append(assembly.Name).Append(", ") - .Append(assembly.Version).Append(", ") + .Append(assembly.Id.Name).Append(", ") + .Append(assembly.Id.Version).Append(", ") .Append(assembly.Architecture).Append(", ") .AppendIf(!isDynamic, sb => sb.Append(assembly.Hash).Append(", ")) .AppendIf(isDynamic && !hasPath, "DYNAMIC") @@ -868,7 +599,7 @@ void AppendAssembly(AssemblyModel assembly) return sb0.ToString(); } - private static string GetHarmonyPatchesListHtml(CrashReportModel crashReport) + private static string GetHarmonyPatchesHtml(CrashReportModel crashReport) { var harmonyPatchesListBuilder = new StringBuilder(); var patchesBuilder = new StringBuilder(); @@ -879,14 +610,15 @@ void AppendPatches(string name, IEnumerable patches) patchBuilder.Clear(); foreach (var patch in patches) { - var moduleId = crashReport.Modules.FirstOrDefault(x => crashReport.Assemblies.Where(y => y.ModuleId == x.Id).Any(y => y.Name == patch.AssemblyName))?.Id ?? "UNKNOWN"; + var moduleId = patch.ModuleId ?? "UNKNOWN"; + var pluginId = patch.LoaderPluginId ?? "UNKNOWN"; var hasIndex = patch.Index != 0; var hasPriority = patch.Priority != 400; var hasBefore = patch.Before.Count > 0; var hasAfter = patch.After.Count > 0; patchBuilder.Append("
      • ") - .AppendIf(moduleId == "UNKNOWN", sb => sb.Append("Module Id: ").Append(moduleId).Append("; ")) .AppendIf(moduleId != "UNKNOWN", sb => sb.Append("Module Id: ").Append("").Append(moduleId).Append("").Append("; ")) + .AppendIf(pluginId != "UNKNOWN", sb => sb.Append("Plugin Id: ").Append("").Append(pluginId).Append("").Append("; ")) .Append("Owner: ").Append(patch.Owner).Append("; ") .Append("Namespace: ").Append(patch.Namespace).Append("; ") .AppendIf(hasIndex, sb => sb.Append("Index: ").Append(patch.Index).Append("; ")) @@ -907,10 +639,10 @@ void AppendPatches(string name, IEnumerable patches) { patchesBuilder.Clear(); - AppendPatches("Prefixes", harmonyPatch.Patches.Where(x => x.Type == HarmonyPatchModelType.Prefix)); - AppendPatches("Postfixes", harmonyPatch.Patches.Where(x => x.Type == HarmonyPatchModelType.Postfix)); - AppendPatches("Finalizers", harmonyPatch.Patches.Where(x => x.Type == HarmonyPatchModelType.Finalizer)); - AppendPatches("Transpilers", harmonyPatch.Patches.Where(x => x.Type == HarmonyPatchModelType.Transpiler)); + AppendPatches("Prefixes", harmonyPatch.Patches.Where(x => x.Type == HarmonyPatchType.Prefix)); + AppendPatches("Postfixes", harmonyPatch.Patches.Where(x => x.Type == HarmonyPatchType.Postfix)); + AppendPatches("Finalizers", harmonyPatch.Patches.Where(x => x.Type == HarmonyPatchType.Finalizer)); + AppendPatches("Transpilers", harmonyPatch.Patches.Where(x => x.Type == HarmonyPatchType.Transpiler)); if (patchesBuilder.Length > 0) { @@ -928,7 +660,7 @@ void AppendPatches(string name, IEnumerable patches) return harmonyPatchesListBuilder.ToString(); } - private static string GetLogFilesListHtml(IEnumerable files) + private static string GetLogFilesHtml(IEnumerable files) { var sb = new StringBuilder(); @@ -943,8 +675,18 @@ private static string GetLogFilesListHtml(IEnumerable files) foreach (var logEntry in logSource.Logs) { var toAppend = (longestType - logEntry.Type.Length) + 1; - var style = logEntry.Level == "ERR" ? "color:red" : logEntry.Level == "WRN" ? "color:orange" : ""; - sb.Append(logEntry.Date.ToString("u")).Append(" [").Append(logEntry.Type).Append(']').Append(' ', toAppend).Append('[').Append("").Append(logEntry.Level).Append("").Append("]: ").Append(logEntry.Message).AppendLine(); + var style = logEntry.Level is LogLevel.Error or LogLevel.Fatal ? "color:red" : logEntry.Level is LogLevel.Warning ? "color:orange" : ""; + var level = logEntry.Level switch + { + LogLevel.Fatal => "FTL", + LogLevel.Error => "ERR", + LogLevel.Warning => "WRN", + LogLevel.Information => "INF", + LogLevel.Debug => "DBG", + LogLevel.Verbose => "VRB", + _ => " " + }; + sb.Append(logEntry.Date.ToString("u")).Append(" [").Append(logEntry.Type).Append(']').Append(' ', toAppend).Append('[').Append("").Append(level).Append("").Append("]: ").Append(logEntry.Message).AppendLine(); } sb.Append("").Append("
      "); } diff --git a/src/BUTR.CrashReport.Bannerlord.Source/CrashReportShared.cs b/src/BUTR.CrashReport.Bannerlord.Source/CrashReportShared.cs index 5a36563..81df0e2 100644 --- a/src/BUTR.CrashReport.Bannerlord.Source/CrashReportShared.cs +++ b/src/BUTR.CrashReport.Bannerlord.Source/CrashReportShared.cs @@ -136,62 +136,68 @@ internal static class CrashReportShared "TaleWorlds.*Culture*", }; - public static IEnumerable GetModuleCapabilities(CrashReportModel crashReport, ModuleModel module) + public static IEnumerable GetModuleCapabilities(ICollection assemblies, ModuleModel module) { - var assemblies = crashReport.Assemblies; if (module.ContainsTypeReferences(assemblies, CrashReportShared.OSFileSystemTypeReferences)) - yield return ModuleCapabilities.OSFileSystem; + yield return new CapabilityModuleOrPluginModel("OS File System"); if (module.ContainsTypeReferences(assemblies, CrashReportShared.GameFileSystemTypeReferences)) - yield return ModuleCapabilities.GameFileSystem; + yield return new CapabilityModuleOrPluginModel("Game File System"); if (module.ContainsTypeReferences(assemblies, CrashReportShared.ShellTypeReferences)) - yield return ModuleCapabilities.Shell; + yield return new CapabilityModuleOrPluginModel("Shell"); if (module.ContainsTypeReferences(assemblies, CrashReportShared.SaveSystemTypeReferences)) - yield return ModuleCapabilities.SaveSystem; + yield return new CapabilityModuleOrPluginModel("Save System"); if (module.ContainsAssemblyReferences(assemblies, CrashReportShared.SaveSystemAssemblyReferences)) - yield return ModuleCapabilities.SaveSystem; + yield return new CapabilityModuleOrPluginModel("Save System"); if (module.ContainsTypeReferences(assemblies, CrashReportShared.GameEntitiesTypeReferences)) - yield return ModuleCapabilities.GameEntities; + yield return new CapabilityModuleOrPluginModel("Game Entities"); if (module.ContainsAssemblyReferences(assemblies, CrashReportShared.GameEntitiesAssemblyReferences)) - yield return ModuleCapabilities.GameEntities; + yield return new CapabilityModuleOrPluginModel("Game Entities"); if (module.ContainsAssemblyReferences(assemblies, CrashReportShared.InputSystemAssemblyReferences)) - yield return ModuleCapabilities.InputSystem; + yield return new CapabilityModuleOrPluginModel("Input System"); if (module.ContainsAssemblyReferences(assemblies, CrashReportShared.LocalizationSystemAssemblyReferences)) - yield return ModuleCapabilities.Localization; + yield return new CapabilityModuleOrPluginModel("Localization"); if (module.ContainsTypeReferences(assemblies, CrashReportShared.UITypeReferences)) - yield return ModuleCapabilities.UserInterface; + yield return new CapabilityModuleOrPluginModel("User Interface"); if (module.ContainsAssemblyReferences(assemblies, CrashReportShared.UIAssemblyReferences)) - yield return ModuleCapabilities.UserInterface; + yield return new CapabilityModuleOrPluginModel("User Interface"); if (module.ContainsTypeReferences(assemblies, CrashReportShared.HttpTypeReferences)) - yield return ModuleCapabilities.Http; + yield return new CapabilityModuleOrPluginModel("Http"); if (module.ContainsTypeReferences(assemblies, CrashReportShared.AchievementSystemTypeReferences)) - yield return ModuleCapabilities.Achievements; + yield return new CapabilityModuleOrPluginModel("Achievements"); if (module.ContainsTypeReferences(assemblies, CrashReportShared.CampaignSystemTypeReferences)) - yield return ModuleCapabilities.Campaign; + yield return new CapabilityModuleOrPluginModel("Campaign"); if (module.ContainsTypeReferences(assemblies, CrashReportShared.SkillSystemTypeReferences)) - yield return ModuleCapabilities.Skills; + yield return new CapabilityModuleOrPluginModel("Skills"); if (module.ContainsTypeReferences(assemblies, CrashReportShared.ItemSystemTypeReferences)) - yield return ModuleCapabilities.Items; + yield return new CapabilityModuleOrPluginModel("Items"); if (module.ContainsTypeReferences(assemblies, CrashReportShared.CultureSystemTypeReferences)) - yield return ModuleCapabilities.Cultures; + yield return new CapabilityModuleOrPluginModel("Cultures"); } public static string GetBUTRLoaderVersion(CrashReportModel crashReport) { - if (crashReport.Assemblies.FirstOrDefault(x => x.Name == "Bannerlord.BUTRLoader") is { } bAssembly) - return bAssembly.Version; + if (crashReport.Assemblies.FirstOrDefault(x => x.Id.Name == "Bannerlord.BUTRLoader") is { } bAssembly) + return bAssembly.Id.Version ?? string.Empty; + return string.Empty; + } + + public static string GetBLSEVersion(CrashReportModel crashReport) + { + if (crashReport.Assemblies.FirstOrDefault(x => x.Id.Name == "Bannerlord.BLSE") is { } bAssembly) + return bAssembly.Id.Version ?? string.Empty; return string.Empty; } @@ -218,6 +224,9 @@ public static string GetLauncherType(CrashReportModel crashReport) if (!string.IsNullOrEmpty(GetBUTRLoaderVersion(crashReport))) return "butrloader"; + if (!string.IsNullOrEmpty(GetBLSEVersion(crashReport))) + return "blse"; + return "vanilla"; } @@ -229,6 +238,9 @@ public static string GetLauncherVersion(CrashReportModel crashReport) if (GetBUTRLoaderVersion(crashReport) is { } bVersion && !string.IsNullOrEmpty(bVersion)) return bVersion; + if (GetBLSEVersion(crashReport) is { } blseVersion && !string.IsNullOrEmpty(blseVersion)) + return blseVersion; + return "0"; } } diff --git a/src/BUTR.CrashReport.Bannerlord.Source/HarmonyProvider.cs b/src/BUTR.CrashReport.Bannerlord.Source/HarmonyProvider.cs new file mode 100644 index 0000000..4253cb3 --- /dev/null +++ b/src/BUTR.CrashReport.Bannerlord.Source/HarmonyProvider.cs @@ -0,0 +1,125 @@ +// +// This code file has automatically been added by the "BUTR.CrashReport.Bannerlord.Source" NuGet package (https://www.nuget.org/packages/BUTR.CrashReport.Bannerlord.Source). +// Please see https://github.com/BUTR/BUTR.CrashReport for more information. +// +// IMPORTANT: +// DO NOT DELETE THIS FILE if you are using a "packages.config" file to manage your NuGet references. +// Consider migrating to PackageReferences instead: +// https://docs.microsoft.com/en-us/nuget/consume-packages/migrate-packages-config-to-package-reference +// Migrating brings the following benefits: +// * The "BUTR.CrashReport.Bannerlord.Source" folder and the "CrashReportCreatorHelper.cs" file don't appear in your project. +// * The added file is immutable and can therefore not be modified by coincidence. +// * Updating/Uninstalling the package will work flawlessly. +// + +#region License +// MIT License +// +// Copyright (c) Bannerlord's Unofficial Tools & Resources +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +#endregion + +#if !BUTRCRASHREPORT_DISABLE +#nullable enable +#if !BUTRCRASHREPORT_ENABLEWARNINGS +#pragma warning disable +#endif + +namespace BUTR.CrashReport.Bannerlord +{ + using global::Bannerlord.BUTR.Shared.Extensions; + using global::Bannerlord.BUTR.Shared.Helpers; + using global::Bannerlord.ModuleManager; + + using global::BUTR.CrashReport.Extensions; + using global::BUTR.CrashReport.Interfaces; + using global::BUTR.CrashReport.Models; + using global::BUTR.CrashReport.Utils; + + using global::HarmonyLib; + using global::HarmonyLib.BUTR.Extensions; + + using global::System; + using global::System.Collections.Generic; + using global::System.Diagnostics; + using global::System.Globalization; + using global::System.IO; + using global::System.Linq; + using global::System.Reflection; + using global::System.Security.Cryptography; + + public class HarmonyProvider : IHarmonyProvider + { + public virtual IEnumerable GetAllPatchedMethods() => Harmony.GetAllPatchedMethods(); + + public virtual global::BUTR.CrashReport.Models.HarmonyPatches GetPatchInfo(MethodBase originalMethod) + { + static global::BUTR.CrashReport.Models.HarmonyPatch Convert(Patch patch, global::BUTR.CrashReport.Models.HarmonyPatchType type) => new() + { + Owner = patch.owner, + Index = patch.index, + Priority = patch.priority, + Before = patch.before, + After = patch.after, + PatchMethod = patch.PatchMethod, + Type = type, + }; + + var patches = Harmony.GetPatchInfo(originalMethod); + return new() + { + Prefixes = patches.Prefixes.Select(x => Convert(x, Models.HarmonyPatchType.Prefix)).ToArray(), + Postfixes = patches.Postfixes.Select(x => Convert(x, Models.HarmonyPatchType.Postfix)).ToArray(), + Finalizers = patches.Finalizers.Select(x => Convert(x, Models.HarmonyPatchType.Finalizer)).ToArray(), + Transpilers = patches.Transpilers.Select(x => Convert(x, Models.HarmonyPatchType.Transpiler)).ToArray(), + }; + } + + public virtual MethodBase? GetOriginalMethod(MethodInfo replacement) + { + try + { + return Harmony.GetOriginalMethod(replacement); + } + catch (Exception e) + { + Trace.TraceError(e.ToString()); + return null; + } + } + + public virtual MethodBase? GetMethodFromStackframe(StackFrame frame) + { + try + { + return Harmony.GetMethodFromStackframe(frame); + } + catch (Exception e) + { + Trace.TraceError(e.ToString()); + return null; + } + } + } +} + +#pragma warning restore +#nullable restore +#endif // BUTRCRASHREPORT_DISABLE \ No newline at end of file diff --git a/src/BUTR.CrashReport.Bannerlord.Source/ModuleCapabilities.cs b/src/BUTR.CrashReport.Bannerlord.Source/LoaderPluginInfo.cs similarity index 79% rename from src/BUTR.CrashReport.Bannerlord.Source/ModuleCapabilities.cs rename to src/BUTR.CrashReport.Bannerlord.Source/LoaderPluginInfo.cs index dd36263..5db9136 100644 --- a/src/BUTR.CrashReport.Bannerlord.Source/ModuleCapabilities.cs +++ b/src/BUTR.CrashReport.Bannerlord.Source/LoaderPluginInfo.cs @@ -1,4 +1,4 @@ -// +// // This code file has automatically been added by the "BUTR.CrashReport.Bannerlord.Source" NuGet package (https://www.nuget.org/packages/BUTR.CrashReport.Bannerlord.Source). // Please see https://github.com/BUTR/BUTR.CrashReport for more information. // @@ -7,7 +7,7 @@ // Consider migrating to PackageReferences instead: // https://docs.microsoft.com/en-us/nuget/consume-packages/migrate-packages-config-to-package-reference // Migrating brings the following benefits: -// * The "BUTR.CrashReport.Bannerlord.Source" folder and the "ModuleCapabilities.cs" file don't appear in your project. +// * The "BUTR.CrashReport.Bannerlord.Source" folder and the "LoaderPluginInfo.cs" file don't appear in your project. // * The added file is immutable and can therefore not be modified by coincidence. // * Updating/Uninstalling the package will work flawlessly. // @@ -36,7 +36,7 @@ // SOFTWARE. #endregion -#if !BUTRCRASHREPORT_DISABLE || BUTRCRASHREPORT_ENABLE_HTML_RENDERER +#if !BUTRCRASHREPORT_DISABLE #nullable enable #if !BUTRCRASHREPORT_ENABLEWARNINGS #pragma warning disable @@ -44,23 +44,23 @@ namespace BUTR.CrashReport.Bannerlord { - internal enum ModuleCapabilities + using global::Bannerlord.BUTR.Shared.Helpers; + + using global::BUTR.CrashReport.Models; + + using global::System.Collections.Generic; + using global::System.Linq; + + internal class LoaderPluginInfo : ILoaderPluginInfo { - None, - OSFileSystem, - GameFileSystem, - Shell, - SaveSystem, - GameEntities, - InputSystem, - Localization, - UserInterface, - Http, - Achievements, - Campaign, - Skills, - Items, - Cultures, + /// + public string Id { get; set; } = string.Empty; + + /// + public string? Version { get; set; } + + /// + public string? UpdateInfo { get; set; } } } diff --git a/src/BUTR.CrashReport.Bannerlord.Source/ModuleInfo.cs b/src/BUTR.CrashReport.Bannerlord.Source/ModuleInfo.cs index 046b82b..522dbf2 100644 --- a/src/BUTR.CrashReport.Bannerlord.Source/ModuleInfo.cs +++ b/src/BUTR.CrashReport.Bannerlord.Source/ModuleInfo.cs @@ -45,6 +45,7 @@ namespace BUTR.CrashReport.Bannerlord { using global::Bannerlord.BUTR.Shared.Helpers; + using global::BUTR.CrashReport.Models; using global::System.Collections.Generic; using global::System.Linq; diff --git a/src/BUTR.CrashReport.Bannerlord.Source/ModuleSubModuleInfo.cs b/src/BUTR.CrashReport.Bannerlord.Source/ModuleSubModuleInfo.cs index 9d12213..f698950 100644 --- a/src/BUTR.CrashReport.Bannerlord.Source/ModuleSubModuleInfo.cs +++ b/src/BUTR.CrashReport.Bannerlord.Source/ModuleSubModuleInfo.cs @@ -45,6 +45,7 @@ namespace BUTR.CrashReport.Bannerlord { using global::Bannerlord.ModuleManager; + using global::BUTR.CrashReport.Models; using global::System.Linq; @@ -52,7 +53,7 @@ internal class ModuleSubModuleInfo : IModuleSubModuleInfo { public SubModuleInfoExtended InternalSubModuleInfo { get; } - public string AssemblyName => InternalSubModuleInfo.DLLName; + public string AssemblyFile => InternalSubModuleInfo.DLLName; public string[] Dependencies => InternalSubModuleInfo.Assemblies.ToArray(); public ModuleSubModuleInfo(SubModuleInfoExtended internalSubModuleInfo) => InternalSubModuleInfo = internalSubModuleInfo; diff --git a/src/BUTR.CrashReport.Bannerlord.Tool/HtmlOptions.cs b/src/BUTR.CrashReport.Bannerlord.Tool/HtmlOptions.cs index efd350b..79b44d2 100644 --- a/src/BUTR.CrashReport.Bannerlord.Tool/HtmlOptions.cs +++ b/src/BUTR.CrashReport.Bannerlord.Tool/HtmlOptions.cs @@ -2,7 +2,7 @@ namespace BUTR.CrashReport.Bannerlord.Tool; -[Verb("html", HelpText = "Converts to the HTML report")] +[Verb("html", HelpText = "Converts the zip file to the HTML report")] public class HtmlOptions { [Option('i', "input", Required = true, HelpText = "The full path to the zip file to parse.")] diff --git a/src/BUTR.CrashReport.Bannerlord.Tool/Program.cs b/src/BUTR.CrashReport.Bannerlord.Tool/Program.cs index 47aba05..6a7ea26 100644 --- a/src/BUTR.CrashReport.Bannerlord.Tool/Program.cs +++ b/src/BUTR.CrashReport.Bannerlord.Tool/Program.cs @@ -78,7 +78,7 @@ public static async Task Main(string[] args) return 0; } - catch (FileNotFoundException fex) + catch (FileNotFoundException) { Console.WriteLine("The input file could not be found"); if (parsedOptions is not null) diff --git a/src/BUTR.CrashReport.BepInEx5.Source/BUTR.CrashReport.BepInEx5.Source.csproj b/src/BUTR.CrashReport.BepInEx5.Source/BUTR.CrashReport.BepInEx5.Source.csproj new file mode 100644 index 0000000..4acd785 --- /dev/null +++ b/src/BUTR.CrashReport.BepInEx5.Source/BUTR.CrashReport.BepInEx5.Source.csproj @@ -0,0 +1,55 @@ + + + + netstandard2.0 + enable + + + + BUTR.CrashReport.BepInEx5.Source + BUTR.CrashReport.BepInEx5.Source + Source code for creating the crash report model and render it as HTML + true + MIT + icon.png + https://raw.githubusercontent.com/BUTR/BUTR.CrashReport/master/assets/Icon128x128.png + butr crash report bannerlord + + + + + false + false + false + false + false + false + true + false + false + true + true + + obj + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/BUTR.CrashReport.BepInEx5.Source/BUTR.CrashReport.BepInEx5.Source.props b/src/BUTR.CrashReport.BepInEx5.Source/BUTR.CrashReport.BepInEx5.Source.props new file mode 100644 index 0000000..600e7d7 --- /dev/null +++ b/src/BUTR.CrashReport.BepInEx5.Source/BUTR.CrashReport.BepInEx5.Source.props @@ -0,0 +1,13 @@ + + + + + + false + + + + \ No newline at end of file diff --git a/src/BUTR.CrashReport.BepInEx5.Source/BepInExIntegration.cs b/src/BUTR.CrashReport.BepInEx5.Source/BepInExIntegration.cs new file mode 100644 index 0000000..88840b7 --- /dev/null +++ b/src/BUTR.CrashReport.BepInEx5.Source/BepInExIntegration.cs @@ -0,0 +1,104 @@ +// +// This code file has automatically been added by the "BUTR.CrashReport.Bannerlord.Source" NuGet package (https://www.nuget.org/packages/BUTR.CrashReport.Bannerlord.Source). +// Please see https://github.com/BUTR/BUTR.CrashReport for more information. +// +// IMPORTANT: +// DO NOT DELETE THIS FILE if you are using a "packages.config" file to manage your NuGet references. +// Consider migrating to PackageReferences instead: +// https://docs.microsoft.com/en-us/nuget/consume-packages/migrate-packages-config-to-package-reference +// Migrating brings the following benefits: +// * The "BUTR.CrashReport.Bannerlord.Source" folder and the "BepInExIntegration.cs" file don't appear in your project. +// * The added file is immutable and can therefore not be modified by coincidence. +// * Updating/Uninstalling the package will work flawlessly. +// + +#region License +// MIT License +// +// Copyright (c) Bannerlord's Unofficial Tools & Resources +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +#endregion + +#if !BUTRCRASHREPORT_DISABLE +#nullable enable +#if !BUTRCRASHREPORT_ENABLEWARNINGS +#pragma warning disable +#endif + +namespace BUTR.CrashReport.BepInEx5 +{ + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::BepInEx; + using global::BepInEx.Bootstrap; + using global::BUTR.CrashReport.Models; + + public static class BepInExIntegration + { + public static List GetPlugins() => Chainloader.PluginInfos.Select(kv => new LoaderPluginModel + { + Id = kv.Value.Metadata.GUID, + Name = kv.Value.Metadata.Name, + Version = kv.Value.Metadata.Version.ToString(4), + Dependencies = kv.Value.Dependencies.Select(x => new DependencyMetadataModel + { + ModuleOrPluginId = x.DependencyGUID, + Version = x.MinimumVersion.ToString(4), + VersionRange = null, + Type = DependencyMetadataModelType.LoadBefore, + IsOptional = x.Flags.HasFlag(BepInDependency.DependencyFlags.SoftDependency), + AdditionalMetadata = new List + { + new MetadataModel + { + Key = "IsHardDependency", + Value = x.Flags.HasFlag(BepInDependency.DependencyFlags.HardDependency).ToString() + }, + new MetadataModel + { + Key = "IsSoftDependency", + Value = x.Flags.HasFlag(BepInDependency.DependencyFlags.SoftDependency).ToString() + }, + }, + }).Concat(kv.Value.Incompatibilities.Select(x => new DependencyMetadataModel + { + ModuleOrPluginId = x.IncompatibilityGUID, + Version = null, + VersionRange = null, + Type = DependencyMetadataModelType.Incompatible, + IsOptional = false, + AdditionalMetadata = Array.Empty(), + })).ToList(), + AdditionalMetadata = new[] + { + //new MetadataModel { Key = "TargettedBepInExVersion", Value = kv.Value.TargettedBepInExVersion.ToString() }, + new MetadataModel { Key = "Location", Value = kv.Value.Location }, + //new MetadataModel { Key = "TypeName", Value = kv.Value.TypeName }, + new MetadataModel { Key = "Processes", Value = string.Join("; ", kv.Value.Processes.Select(x => x.ProcessName)) }, + }, + UpdateInfo = null, + }).ToList(); + } +} + +#pragma warning restore +#nullable restore +#endif // BUTRCRASHREPORT_DISABLE \ No newline at end of file diff --git a/src/BUTR.CrashReport.BepInEx6.Source/BUTR.CrashReport.BepInEx6.Source.csproj b/src/BUTR.CrashReport.BepInEx6.Source/BUTR.CrashReport.BepInEx6.Source.csproj new file mode 100644 index 0000000..57f3c5c --- /dev/null +++ b/src/BUTR.CrashReport.BepInEx6.Source/BUTR.CrashReport.BepInEx6.Source.csproj @@ -0,0 +1,55 @@ + + + + netstandard2.0 + enable + + + + BUTR.CrashReport.BepInEx6.Source + BUTR.CrashReport.BepInEx6.Source + Source code for creating the crash report model and render it as HTML + true + MIT + icon.png + https://raw.githubusercontent.com/BUTR/BUTR.CrashReport/master/assets/Icon128x128.png + butr crash report bannerlord + + + + + false + false + false + false + false + false + true + false + false + true + true + + obj + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/BUTR.CrashReport.BepInEx6.Source/BUTR.CrashReport.BepInEx6.Source.props b/src/BUTR.CrashReport.BepInEx6.Source/BUTR.CrashReport.BepInEx6.Source.props new file mode 100644 index 0000000..73ceb02 --- /dev/null +++ b/src/BUTR.CrashReport.BepInEx6.Source/BUTR.CrashReport.BepInEx6.Source.props @@ -0,0 +1,13 @@ + + + + + + false + + + + \ No newline at end of file diff --git a/src/BUTR.CrashReport.BepInEx6.Source/BepInExIntegration.cs b/src/BUTR.CrashReport.BepInEx6.Source/BepInExIntegration.cs new file mode 100644 index 0000000..e6f4335 --- /dev/null +++ b/src/BUTR.CrashReport.BepInEx6.Source/BepInExIntegration.cs @@ -0,0 +1,96 @@ +// +// This code file has automatically been added by the "BUTR.CrashReport.Bannerlord.Source" NuGet package (https://www.nuget.org/packages/BUTR.CrashReport.Bannerlord.Source). +// Please see https://github.com/BUTR/BUTR.CrashReport for more information. +// +// IMPORTANT: +// DO NOT DELETE THIS FILE if you are using a "packages.config" file to manage your NuGet references. +// Consider migrating to PackageReferences instead: +// https://docs.microsoft.com/en-us/nuget/consume-packages/migrate-packages-config-to-package-reference +// Migrating brings the following benefits: +// * The "BUTR.CrashReport.Bannerlord.Source" folder and the "BepInExIntegration.cs" file don't appear in your project. +// * The added file is immutable and can therefore not be modified by coincidence. +// * Updating/Uninstalling the package will work flawlessly. +// + +#region License +// MIT License +// +// Copyright (c) Bannerlord's Unofficial Tools & Resources +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +#endregion + +#if !BUTRCRASHREPORT_DISABLE +#nullable enable +#if !BUTRCRASHREPORT_ENABLEWARNINGS +#pragma warning disable +#endif + +namespace BUTR.CrashReport.BepInEx6 +{ + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::BepInEx; + using global::BepInEx.Bootstrap; + using global::BUTR.CrashReport.Models; + + public static class BepInExIntegration + { + private static List GetPlugins(BaseChainloader chainloader) => chainloader.Plugins.Select(kv => new LoaderPluginModel + { + Id = kv.Value.Metadata.GUID, + Name = kv.Value.Metadata.Name, + Version = kv.Value.Metadata.Version.ToString(), + UpdateInfo = null, + Dependencies = kv.Value.Dependencies.Select(x => new DependencyMetadataModel + { + ModuleOrPluginId = x.DependencyGUID, + Version = null, + VersionRange = x.VersionRange.ToString(), + Type = DependencyMetadataModelType.LoadBefore, + IsOptional = x.Flags.HasFlag(BepInDependency.DependencyFlags.SoftDependency), + AdditionalMetadata = new List + { + new MetadataModel { Key = "IsHardDependency", Value = x.Flags.HasFlag(BepInDependency.DependencyFlags.HardDependency).ToString() }, + new MetadataModel { Key = "IsSoftDependency", Value = x.Flags.HasFlag(BepInDependency.DependencyFlags.SoftDependency).ToString() }, + }, + }).Concat(kv.Value.Incompatibilities.Select(x => new DependencyMetadataModel + { + ModuleOrPluginId = x.IncompatibilityGUID, + Version = null, + VersionRange = null, + Type = DependencyMetadataModelType.Incompatible, + IsOptional = false, + AdditionalMetadata = Array.Empty(), + })).ToList(), + AdditionalMetadata = new[] + { + //new MetadataModel { Key = "TargettedBepInExVersion", Value = kv.Value.TargettedBepInExVersion.ToString() }, + new MetadataModel { Key = "Location", Value = kv.Value.Location }, + new MetadataModel { Key = "TypeName", Value = kv.Value.TypeName }, + new MetadataModel { Key = "Processes", Value = string.Join("; ", kv.Value.Processes.Select(x => x.ProcessName)) }, + }, + }).ToList(); + } +} + +#pragma warning restore +#nullable restore +#endif // BUTRCRASHREPORT_DISABLE \ No newline at end of file diff --git a/src/BUTR.CrashReport.Decompilers/BUTR.CrashReport.Decompilers.csproj b/src/BUTR.CrashReport.Decompilers/BUTR.CrashReport.Decompilers.csproj index 7d5f0f8..2300dc8 100644 --- a/src/BUTR.CrashReport.Decompilers/BUTR.CrashReport.Decompilers.csproj +++ b/src/BUTR.CrashReport.Decompilers/BUTR.CrashReport.Decompilers.csproj @@ -4,47 +4,29 @@ netstandard2.0 preview enable - true - true - BUTR.CrashReport - - Debug;Release - false - true - true - false - $(PkgBUTR_ILRepack)\tools\net461\ILRepack.exe - - - - + + + - - BUTR.CrashReport.Decompilers - BUTR.CrashReport.Decompilers - Contains the decompilers for creating the crash report - MIT - https://raw.githubusercontent.com/BUTR/BUTR.CrashReport/master/assets/Icon128x128.png - butr crash report bannerlord - - - - - + - - + + - + + + + + @@ -62,17 +44,4 @@ - - - - $(ILRepackExcludeAssemblies);$(ProjectDir)$(OutputPath)0Harmony.dll; - - - - - - false - - - diff --git a/src/BUTR.CrashReport.Decompilers/ILSpy/CSharpILMixedLanguage.cs b/src/BUTR.CrashReport.Decompilers/ILSpy/CSharpILMixedLanguage.cs index 23b0543..7d434b4 100644 --- a/src/BUTR.CrashReport.Decompilers/ILSpy/CSharpILMixedLanguage.cs +++ b/src/BUTR.CrashReport.Decompilers/ILSpy/CSharpILMixedLanguage.cs @@ -1,4 +1,4 @@ -using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.OutputVisitor; using ICSharpCode.Decompiler.CSharp.Syntax; @@ -13,7 +13,7 @@ using System.Text; using System.Threading; -namespace BUTR.CrashReport.ILSpy; +namespace BUTR.CrashReport.Decompilers.ILSpy; internal static class CSharpILMixedLanguage { @@ -42,13 +42,13 @@ public static ReflectionDisassembler CreateDisassembler(ITextOutput output, Canc private static CSharpDecompiler CreateDecompiler(PEFile module, DecompilerSettings settings, CancellationToken ct) { var resolver = new UniversalAssemblyResolver(null, false, module.DetectTargetFrameworkId(), module.DetectRuntimePack()); - return new CSharpDecompiler(module, resolver, settings) {CancellationToken = ct}; + return new CSharpDecompiler(module, resolver, settings) { CancellationToken = ct }; } private static void WriteCode(TextWriter output, DecompilerSettings settings, SyntaxTree syntaxTree) { - syntaxTree.AcceptVisitor(new InsertParenthesesVisitor {InsertParenthesesForReadability = true}); - TokenWriter tokenWriter = new TextWriterTokenWriter(output) {IndentationString = settings.CSharpFormattingOptions.IndentationString}; + syntaxTree.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true }); + TokenWriter tokenWriter = new TextWriterTokenWriter(output) { IndentationString = settings.CSharpFormattingOptions.IndentationString }; tokenWriter = TokenWriter.WrapInWriterThatSetsLocationsInAST(tokenWriter); syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions)); } diff --git a/src/BUTR.CrashReport.Decompilers/ILSpy/CSharpLanguage.cs b/src/BUTR.CrashReport.Decompilers/ILSpy/CSharpLanguage.cs index 92ecf1a..59b4d61 100644 --- a/src/BUTR.CrashReport.Decompilers/ILSpy/CSharpLanguage.cs +++ b/src/BUTR.CrashReport.Decompilers/ILSpy/CSharpLanguage.cs @@ -13,7 +13,7 @@ using System.Reflection.Metadata; using System.Threading; -namespace BUTR.CrashReport.ILSpy; +namespace BUTR.CrashReport.Decompilers.ILSpy; internal class CSharpLanguage : Language { @@ -23,76 +23,76 @@ public static CSharpDecompiler CreateDecompiler(PEFile module, DecompilerSetting { var resolver = new UniversalAssemblyResolver(null, false, module.DetectTargetFrameworkId(), module.DetectRuntimePack()); var decompiler = new CSharpDecompiler(module, resolver, settings) { CancellationToken = ct }; - while (decompiler.AstTransforms.Count >= _transformCount) - decompiler.AstTransforms.RemoveAt(decompiler.AstTransforms.Count - 1); + while (decompiler.AstTransforms.Count >= _transformCount) + decompiler.AstTransforms.RemoveAt(decompiler.AstTransforms.Count - 1); decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers()); - return decompiler; - } + return decompiler; + } private static void WriteCode(ITextOutput output, DecompilerSettings settings, SyntaxTree syntaxTree, IDecompilerTypeSystem typeSystem) - { - syntaxTree.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true }); - output.IndentationString = settings.CSharpFormattingOptions.IndentationString; - TokenWriter tokenWriter = new TextTokenWriter(output, settings, typeSystem); - syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions)); - } - - public override void DecompileMethod(IMethod method, ITextOutput output, DecompilerSettings settings) - { + { + syntaxTree.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true }); + output.IndentationString = settings.CSharpFormattingOptions.IndentationString; + TokenWriter tokenWriter = new TextTokenWriter(output, settings, typeSystem); + syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions)); + } + + public override void DecompileMethod(IMethod method, ITextOutput output, DecompilerSettings settings) + { if (method.ParentModule?.PEFile is null) return; - - var assembly = method.ParentModule.PEFile; - var decompiler = CreateDecompiler(assembly, settings, CancellationToken.None); - WriteCommentLine(output, assembly.FullName); - WriteCommentLine(output, TypeToString(method.DeclaringType, includeNamespace: true)); - + + var assembly = method.ParentModule.PEFile; + var decompiler = CreateDecompiler(assembly, settings, CancellationToken.None); + WriteCommentLine(output, assembly.FullName); + WriteCommentLine(output, TypeToString(method.DeclaringType, includeNamespace: true)); + if (decompiler.TypeSystem.MainModule.ResolveEntity(method.MetadataToken) is not IMethod methodDefinition) return; - - if (methodDefinition.DeclaringTypeDefinition is not null && methodDefinition.IsConstructor && methodDefinition.DeclaringType.IsReferenceType != false) - { - var members = CollectFieldsAndCtors(methodDefinition.DeclaringTypeDefinition, methodDefinition.IsStatic); - decompiler.AstTransforms.Add(new SelectCtorTransform(methodDefinition)); - WriteCode(output, settings, decompiler.Decompile(members), decompiler.TypeSystem); - } - else - { - WriteCode(output, settings, decompiler.Decompile(method.MetadataToken), decompiler.TypeSystem); - } - } + + if (methodDefinition.DeclaringTypeDefinition is not null && methodDefinition.IsConstructor && methodDefinition.DeclaringType.IsReferenceType != false) + { + var members = CollectFieldsAndCtors(methodDefinition.DeclaringTypeDefinition, methodDefinition.IsStatic); + decompiler.AstTransforms.Add(new SelectCtorTransform(methodDefinition)); + WriteCode(output, settings, decompiler.Decompile(members), decompiler.TypeSystem); + } + else + { + WriteCode(output, settings, decompiler.Decompile(method.MetadataToken), decompiler.TypeSystem); + } + } private static List CollectFieldsAndCtors(ITypeDefinition type, bool isStatic) - { - var members = new List(); + { + var members = new List(); members.AddRange(type.Fields.Where(field => !field.MetadataToken.IsNil && field.IsStatic == isStatic).Select(field => field.MetadataToken)); members.AddRange(type.Methods.Where(ctor => !ctor.MetadataToken.IsNil && ctor.IsConstructor && ctor.IsStatic == isStatic).Select(ctor => ctor.MetadataToken)); return members; - } + } private static CSharpAmbience CreateAmbience() => new() { ConversionFlags = ConversionFlags.ShowTypeParameterList | ConversionFlags.PlaceReturnTypeAfterParameterList }; public override string TypeToString(IType type, bool includeNamespace) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - - var ambience = CreateAmbience(); - if (includeNamespace) - { - ambience.ConversionFlags |= ConversionFlags.UseFullyQualifiedTypeNames; - ambience.ConversionFlags |= ConversionFlags.UseFullyQualifiedEntityNames; - } - - if (type is ITypeDefinition definition) - return ambience.ConvertSymbol(definition); - // HACK : UnknownType is not supported by CSharpAmbience. - - if (type.Kind == TypeKind.Unknown) - return (includeNamespace ? type.FullName : type.Name) + (type.TypeParameterCount > 0 ? "<" + string.Join(", ", type.TypeArguments.Select(t => t.Name)) + ">" : ""); + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + var ambience = CreateAmbience(); + if (includeNamespace) + { + ambience.ConversionFlags |= ConversionFlags.UseFullyQualifiedTypeNames; + ambience.ConversionFlags |= ConversionFlags.UseFullyQualifiedEntityNames; + } + + if (type is ITypeDefinition definition) + return ambience.ConvertSymbol(definition); + // HACK : UnknownType is not supported by CSharpAmbience. + + if (type.Kind == TypeKind.Unknown) + return (includeNamespace ? type.FullName : type.Name) + (type.TypeParameterCount > 0 ? "<" + string.Join(", ", type.TypeArguments.Select(t => t.Name)) + ">" : ""); return ambience.ConvertType(type); - } + } private class SelectCtorTransform : IAstTransform { diff --git a/src/BUTR.CrashReport.Decompilers/ILSpy/ILLanguage.cs b/src/BUTR.CrashReport.Decompilers/ILSpy/ILLanguage.cs new file mode 100644 index 0000000..03f81af --- /dev/null +++ b/src/BUTR.CrashReport.Decompilers/ILSpy/ILLanguage.cs @@ -0,0 +1,22 @@ +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Disassembler; + +using System.Threading; + +namespace BUTR.CrashReport.Decompilers.ILSpy; + +internal class ILLanguage +{ + public static ReflectionDisassembler CreateDisassembler(ITextOutput output, CancellationToken ct) + { + return new(output, ct) + { + ShowMetadataTokens = false, + ShowMetadataTokensInBase10 = false, + ShowRawRVAOffsetAndBytes = false, + ShowSequencePoints = false, + DetectControlStructure = true, + ExpandMemberDefinitions = false, + }; + } +} \ No newline at end of file diff --git a/src/BUTR.CrashReport.Decompilers/ILSpy/Language.cs b/src/BUTR.CrashReport.Decompilers/ILSpy/Language.cs index a2ff0ba..7685bb9 100644 --- a/src/BUTR.CrashReport.Decompilers/ILSpy/Language.cs +++ b/src/BUTR.CrashReport.Decompilers/ILSpy/Language.cs @@ -5,238 +5,238 @@ using System.Reflection.Metadata; using System.Text; -namespace BUTR.CrashReport.ILSpy; +namespace BUTR.CrashReport.Decompilers.ILSpy; internal abstract class Language { - public virtual void DecompileMethod(IMethod method, ITextOutput output, DecompilerSettings settings) - { + public virtual void DecompileMethod(IMethod method, ITextOutput output, DecompilerSettings settings) + { if (method.DeclaringTypeDefinition is not null) - WriteCommentLine(output, $"{TypeToString(method.DeclaringTypeDefinition, includeNamespace: true)}.{method.Name}"); - } - - public virtual void WriteCommentLine(ITextOutput output, string comment) - { - output.WriteLine("// " + comment); - } - - #region TypeToString - /// - /// Converts a type definition, reference or specification into a string. This method is used by tree nodes and search results. - /// - public virtual string TypeToString(IType type, bool includeNamespace) - { - var visitor = new TypeToStringVisitor(includeNamespace); - type.AcceptVisitor(visitor); - return visitor.ToString(); - } - - class TypeToStringVisitor : TypeVisitor - { - readonly bool includeNamespace; - readonly StringBuilder builder; - - public override string ToString() - { - return builder.ToString(); - } - - public TypeToStringVisitor(bool includeNamespace) - { - this.includeNamespace = includeNamespace; - builder = new StringBuilder(); - } - - public override IType VisitArrayType(ArrayType type) - { - base.VisitArrayType(type); - builder.Append('['); - builder.Append(',', type.Dimensions - 1); - builder.Append(']'); - return type; - } - - public override IType VisitByReferenceType(ByReferenceType type) - { - base.VisitByReferenceType(type); - builder.Append('&'); - return type; - } - - public override IType VisitModOpt(ModifiedType type) - { - type.ElementType.AcceptVisitor(this); - builder.Append(" modopt("); - type.Modifier.AcceptVisitor(this); - builder.Append(")"); - return type; - } - - public override IType VisitModReq(ModifiedType type) - { - type.ElementType.AcceptVisitor(this); - builder.Append(" modreq("); - type.Modifier.AcceptVisitor(this); - builder.Append(")"); - return type; - } - - public override IType VisitPointerType(PointerType type) - { - base.VisitPointerType(type); - builder.Append('*'); - return type; - } - - public override IType VisitTypeParameter(ITypeParameter type) - { - base.VisitTypeParameter(type); - EscapeName(builder, type.Name); - return type; - } - - public override IType VisitParameterizedType(ParameterizedType type) - { - type.GenericType.AcceptVisitor(this); - builder.Append('<'); - for (int i = 0; i < type.TypeArguments.Count; i++) - { - if (i > 0) - builder.Append(','); - type.TypeArguments[i].AcceptVisitor(this); - } - builder.Append('>'); - return type; - } - - public override IType VisitTupleType(TupleType type) - { - type.UnderlyingType.AcceptVisitor(this); - return type; - } - - public override IType VisitFunctionPointerType(FunctionPointerType type) - { - builder.Append("method "); - if (type.CallingConvention != SignatureCallingConvention.Default) - { - builder.Append(type.CallingConvention.ToILSyntax()); - builder.Append(' '); - } - type.ReturnType.AcceptVisitor(this); - builder.Append(" *("); - bool first = true; - foreach (var p in type.ParameterTypes) - { - if (first) - first = false; - else - builder.Append(", "); - - p.AcceptVisitor(this); - } - builder.Append(')'); - return type; - } - - public override IType VisitOtherType(IType type) - { - WriteType(type); - return type; - } - - private void WriteType(IType type) - { - if (includeNamespace) - EscapeName(builder, type.FullName); - else - EscapeName(builder, type.Name); - if (type.TypeParameterCount > 0) - { - builder.Append('`'); - builder.Append(type.TypeParameterCount); - } - } - - public override IType VisitTypeDefinition(ITypeDefinition type) - { - switch (type.KnownTypeCode) - { - case KnownTypeCode.Object: - builder.Append("object"); - break; - case KnownTypeCode.Boolean: - builder.Append("bool"); - break; - case KnownTypeCode.Char: - builder.Append("char"); - break; - case KnownTypeCode.SByte: - builder.Append("int8"); - break; - case KnownTypeCode.Byte: - builder.Append("uint8"); - break; - case KnownTypeCode.Int16: - builder.Append("int16"); - break; - case KnownTypeCode.UInt16: - builder.Append("uint16"); - break; - case KnownTypeCode.Int32: - builder.Append("int32"); - break; - case KnownTypeCode.UInt32: - builder.Append("uint32"); - break; - case KnownTypeCode.Int64: - builder.Append("int64"); - break; - case KnownTypeCode.UInt64: - builder.Append("uint64"); - break; - case KnownTypeCode.Single: - builder.Append("float32"); - break; - case KnownTypeCode.Double: - builder.Append("float64"); - break; - case KnownTypeCode.String: - builder.Append("string"); - break; - case KnownTypeCode.Void: - builder.Append("void"); - break; - case KnownTypeCode.IntPtr: - builder.Append("native int"); - break; - case KnownTypeCode.UIntPtr: - builder.Append("native uint"); - break; - case KnownTypeCode.TypedReference: - builder.Append("typedref"); - break; - default: - WriteType(type); - break; - } - return type; - } - } - #endregion - - /// - /// Escape characters that cannot be displayed in the UI. - /// - public static StringBuilder EscapeName(StringBuilder sb, string name) - { - foreach (char ch in name) - { - if (char.IsWhiteSpace(ch) || char.IsControl(ch) || char.IsSurrogate(ch)) - sb.AppendFormat("\\u{0:x4}", (int)ch); - else - sb.Append(ch); - } - return sb; - } + WriteCommentLine(output, $"{TypeToString(method.DeclaringTypeDefinition, includeNamespace: true)}.{method.Name}"); + } + + public virtual void WriteCommentLine(ITextOutput output, string comment) + { + output.WriteLine("// " + comment); + } + + #region TypeToString + /// + /// Converts a type definition, reference or specification into a string. This method is used by tree nodes and search results. + /// + public virtual string TypeToString(IType type, bool includeNamespace) + { + var visitor = new TypeToStringVisitor(includeNamespace); + type.AcceptVisitor(visitor); + return visitor.ToString(); + } + + class TypeToStringVisitor : TypeVisitor + { + readonly bool includeNamespace; + readonly StringBuilder builder; + + public override string ToString() + { + return builder.ToString(); + } + + public TypeToStringVisitor(bool includeNamespace) + { + this.includeNamespace = includeNamespace; + builder = new StringBuilder(); + } + + public override IType VisitArrayType(ArrayType type) + { + base.VisitArrayType(type); + builder.Append('['); + builder.Append(',', type.Dimensions - 1); + builder.Append(']'); + return type; + } + + public override IType VisitByReferenceType(ByReferenceType type) + { + base.VisitByReferenceType(type); + builder.Append('&'); + return type; + } + + public override IType VisitModOpt(ModifiedType type) + { + type.ElementType.AcceptVisitor(this); + builder.Append(" modopt("); + type.Modifier.AcceptVisitor(this); + builder.Append(")"); + return type; + } + + public override IType VisitModReq(ModifiedType type) + { + type.ElementType.AcceptVisitor(this); + builder.Append(" modreq("); + type.Modifier.AcceptVisitor(this); + builder.Append(")"); + return type; + } + + public override IType VisitPointerType(PointerType type) + { + base.VisitPointerType(type); + builder.Append('*'); + return type; + } + + public override IType VisitTypeParameter(ITypeParameter type) + { + base.VisitTypeParameter(type); + EscapeName(builder, type.Name); + return type; + } + + public override IType VisitParameterizedType(ParameterizedType type) + { + type.GenericType.AcceptVisitor(this); + builder.Append('<'); + for (int i = 0; i < type.TypeArguments.Count; i++) + { + if (i > 0) + builder.Append(','); + type.TypeArguments[i].AcceptVisitor(this); + } + builder.Append('>'); + return type; + } + + public override IType VisitTupleType(TupleType type) + { + type.UnderlyingType.AcceptVisitor(this); + return type; + } + + public override IType VisitFunctionPointerType(FunctionPointerType type) + { + builder.Append("method "); + if (type.CallingConvention != SignatureCallingConvention.Default) + { + builder.Append(type.CallingConvention.ToILSyntax()); + builder.Append(' '); + } + type.ReturnType.AcceptVisitor(this); + builder.Append(" *("); + bool first = true; + foreach (var p in type.ParameterTypes) + { + if (first) + first = false; + else + builder.Append(", "); + + p.AcceptVisitor(this); + } + builder.Append(')'); + return type; + } + + public override IType VisitOtherType(IType type) + { + WriteType(type); + return type; + } + + private void WriteType(IType type) + { + if (includeNamespace) + EscapeName(builder, type.FullName); + else + EscapeName(builder, type.Name); + if (type.TypeParameterCount > 0) + { + builder.Append('`'); + builder.Append(type.TypeParameterCount); + } + } + + public override IType VisitTypeDefinition(ITypeDefinition type) + { + switch (type.KnownTypeCode) + { + case KnownTypeCode.Object: + builder.Append("object"); + break; + case KnownTypeCode.Boolean: + builder.Append("bool"); + break; + case KnownTypeCode.Char: + builder.Append("char"); + break; + case KnownTypeCode.SByte: + builder.Append("int8"); + break; + case KnownTypeCode.Byte: + builder.Append("uint8"); + break; + case KnownTypeCode.Int16: + builder.Append("int16"); + break; + case KnownTypeCode.UInt16: + builder.Append("uint16"); + break; + case KnownTypeCode.Int32: + builder.Append("int32"); + break; + case KnownTypeCode.UInt32: + builder.Append("uint32"); + break; + case KnownTypeCode.Int64: + builder.Append("int64"); + break; + case KnownTypeCode.UInt64: + builder.Append("uint64"); + break; + case KnownTypeCode.Single: + builder.Append("float32"); + break; + case KnownTypeCode.Double: + builder.Append("float64"); + break; + case KnownTypeCode.String: + builder.Append("string"); + break; + case KnownTypeCode.Void: + builder.Append("void"); + break; + case KnownTypeCode.IntPtr: + builder.Append("native int"); + break; + case KnownTypeCode.UIntPtr: + builder.Append("native uint"); + break; + case KnownTypeCode.TypedReference: + builder.Append("typedref"); + break; + default: + WriteType(type); + break; + } + return type; + } + } + #endregion + + /// + /// Escape characters that cannot be displayed in the UI. + /// + public static StringBuilder EscapeName(StringBuilder sb, string name) + { + foreach (char ch in name) + { + if (char.IsWhiteSpace(ch) || char.IsControl(ch) || char.IsSurrogate(ch)) + sb.AppendFormat("\\u{0:x4}", (int) ch); + else + sb.Append(ch); + } + return sb; + } } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Decompilers/ILSpy/PlainTextOutput2.cs b/src/BUTR.CrashReport.Decompilers/ILSpy/PlainTextOutput2.cs index c76bcc3..2e058ad 100644 --- a/src/BUTR.CrashReport.Decompilers/ILSpy/PlainTextOutput2.cs +++ b/src/BUTR.CrashReport.Decompilers/ILSpy/PlainTextOutput2.cs @@ -1,4 +1,4 @@ -using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.Disassembler; using ICSharpCode.Decompiler.Metadata; @@ -9,7 +9,7 @@ using System.Reflection.Metadata; using System.Text; -namespace BUTR.CrashReport.ILSpy; +namespace BUTR.CrashReport.Decompilers.ILSpy; internal class PlainTextOutput2 : ITextOutput { diff --git a/src/BUTR.CrashReport.Decompilers/ILSpy/StringBuilderExtensions.cs b/src/BUTR.CrashReport.Decompilers/ILSpy/StringBuilderExtensions.cs index 0191648..ed5d8f8 100644 --- a/src/BUTR.CrashReport.Decompilers/ILSpy/StringBuilderExtensions.cs +++ b/src/BUTR.CrashReport.Decompilers/ILSpy/StringBuilderExtensions.cs @@ -1,7 +1,7 @@ using System; using System.Text; -namespace BUTR.CrashReport.ILSpy; +namespace BUTR.CrashReport.Decompilers.ILSpy; internal static class StringBuilderExtensions { diff --git a/src/BUTR.CrashReport.Decompilers/ILSpy/TextWriterExtensions.cs b/src/BUTR.CrashReport.Decompilers/ILSpy/TextWriterExtensions.cs index dcce62d..e0981c5 100644 --- a/src/BUTR.CrashReport.Decompilers/ILSpy/TextWriterExtensions.cs +++ b/src/BUTR.CrashReport.Decompilers/ILSpy/TextWriterExtensions.cs @@ -1,7 +1,7 @@ using System.IO; using System.Text; -namespace BUTR.CrashReport.ILSpy; +namespace BUTR.CrashReport.Decompilers.ILSpy; internal static class TextWriterExtensions { diff --git a/src/BUTR.CrashReport.Decompilers/Utils/AssemblyNameFormatter.cs b/src/BUTR.CrashReport.Decompilers/Utils/AssemblyNameFormatter.cs index eb0d826..000b4a0 100644 --- a/src/BUTR.CrashReport.Decompilers/Utils/AssemblyNameFormatter.cs +++ b/src/BUTR.CrashReport.Decompilers/Utils/AssemblyNameFormatter.cs @@ -3,22 +3,10 @@ using System.IO; using System.Text; -namespace BUTR.CrashReport.Utils; +namespace BUTR.CrashReport.Decompilers.Utils; -/// -/// -/// public static class AssemblyNameFormatter { - /// - /// - /// - /// - /// - /// - /// - /// - /// public static string ComputeDisplayName(string? name, string? version, string? cultureName, string? publicKeyToken) { if (name == string.Empty) @@ -58,7 +46,7 @@ public static string ComputeDisplayName(string? name, string? version, string? c private static void AppendQuoted(this StringBuilder sb, string s) { - bool needsQuoting = false; + var needsQuoting = false; const char quoteChar = '\"'; //@todo: App-compat: You can use double or single quotes to quote a name, and Fusion (or rather the IdentityAuthority) picks one @@ -69,12 +57,14 @@ private static void AppendQuoted(this StringBuilder sb, string s) if (needsQuoting) sb.Append(quoteChar); - for (int i = 0; i < s.Length; i++) + for (var i = 0; i < s.Length; i++) { - bool addedEscape = false; - foreach (KeyValuePair kv in EscapeSequences) + var addedEscape = false; + foreach (var kv in EscapeSequences) { - string escapeReplacement = kv.Value; + var key = kv.Key; + var escapeReplacement = kv.Value; + if (s[i] != escapeReplacement[0]) continue; if (s.Length - i < escapeReplacement.Length) @@ -82,7 +72,7 @@ private static void AppendQuoted(this StringBuilder sb, string s) if (s.AsSpan(i, escapeReplacement.Length).SequenceEqual(escapeReplacement.AsSpan())) { sb.Append('\\'); - sb.Append(kv.Key); + sb.Append(key); addedEscape = true; } } @@ -95,37 +85,26 @@ private static void AppendQuoted(this StringBuilder sb, string s) sb.Append(quoteChar); } - /// - /// - /// - /// - /// public static string GetVersion(Version version) { var sb = new StringBuilder(); var canonicalizedVersion = version.CanonicalizeVersion(); - if (canonicalizedVersion.Major != ushort.MaxValue) - { - sb.Append(canonicalizedVersion.Major); - if (canonicalizedVersion.Minor != ushort.MaxValue) - { - sb.Append('.'); - sb.Append(canonicalizedVersion.Minor); + if (canonicalizedVersion.Major == ushort.MaxValue) return sb.ToString(); + sb.Append(canonicalizedVersion.Major); + + if (canonicalizedVersion.Minor == ushort.MaxValue) return sb.ToString(); + sb.Append('.'); + sb.Append(canonicalizedVersion.Minor); + + if (canonicalizedVersion.Build == ushort.MaxValue) return sb.ToString(); + sb.Append('.'); + sb.Append(canonicalizedVersion.Build); + + if (canonicalizedVersion.Revision == ushort.MaxValue) return sb.ToString(); + sb.Append('.'); + sb.Append(canonicalizedVersion.Revision); - if (canonicalizedVersion.Build != ushort.MaxValue) - { - sb.Append('.'); - sb.Append(canonicalizedVersion.Build); - - if (canonicalizedVersion.Revision != ushort.MaxValue) - { - sb.Append('.'); - sb.Append(canonicalizedVersion.Revision); - } - } - } - } return sb.ToString(); } @@ -143,13 +122,13 @@ private static Version CanonicalizeVersion(this Version version) } private static readonly KeyValuePair[] EscapeSequences = - { + [ new('\\', "\\"), new(',', ","), new('=', "="), new('\'', "'"), new('\"', "\""), new('n', Environment.NewLine), - new('t', "\t"), - }; + new('t', "\t") + ]; } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Decompilers/Utils/FileSystemName.cs b/src/BUTR.CrashReport.Decompilers/Utils/FileSystemName.cs index bd399f8..0041c56 100644 --- a/src/BUTR.CrashReport.Decompilers/Utils/FileSystemName.cs +++ b/src/BUTR.CrashReport.Decompilers/Utils/FileSystemName.cs @@ -1,6 +1,6 @@ using System; -namespace BUTR.CrashReport.Utils; +namespace BUTR.CrashReport.Decompilers.Utils; /// /// Provides methods for matching file system names. @@ -23,7 +23,7 @@ public static class FileSystemName /// The name to check against the expression. /// to ignore case (default); if the match should be case-sensitive. /// if the given expression matches the given name; otherwise, . - public static bool MatchesSimpleExpression(ReadOnlySpan expression, ReadOnlySpan name, bool ignoreCase = true) + private static bool MatchesSimpleExpression(ReadOnlySpan expression, ReadOnlySpan name, bool ignoreCase = true) { return MatchPattern(expression, name, ignoreCase, useExtendedWildcards: false); } diff --git a/src/BUTR.CrashReport.Decompilers/Utils/MethodCopier.cs b/src/BUTR.CrashReport.Decompilers/Utils/MethodCopier.cs index 6b524fd..ae74900 100644 --- a/src/BUTR.CrashReport.Decompilers/Utils/MethodCopier.cs +++ b/src/BUTR.CrashReport.Decompilers/Utils/MethodCopier.cs @@ -3,16 +3,17 @@ using AsmResolver.DotNet.Dynamic; using System; +using System.Diagnostics; using System.IO; using System.Reflection; using TypeAttributes = AsmResolver.PE.DotNet.Metadata.Tables.Rows.TypeAttributes; -namespace BUTR.CrashReport.Utils; +namespace BUTR.CrashReport.Decompilers.Utils; internal static class MethodCopier { - public static Stream? GetAssemblyCopy(MethodBase method, out int metadataToken) + public static MemoryStream? GetAssemblyCopy(MethodBase method, out int metadataToken) { metadataToken = 0; MethodDefinition? methodDefinition = null; @@ -23,14 +24,20 @@ internal static class MethodCopier module = ModuleDefinition.FromModule(typeof(MethodDecompiler).Module); methodDefinition = new DynamicMethodDefinition(module, method); } - catch (Exception) { /* ignore */ } + catch (Exception e) + { + Trace.TraceError(e.ToString()); + } try { module = ModuleDefinition.FromModule(method.Module); methodDefinition = module.LookupMember(method.MetadataToken) as MethodDefinition; } - catch (Exception) { /* ignore */ } + catch (Exception e) + { + Trace.TraceError(e.ToString()); + } if (module is null || methodDefinition is null) return null; @@ -42,7 +49,7 @@ internal static class MethodCopier cloner.AddListener(new InjectTypeClonerListener(destinationModule)); cloner.AddListener(new AssignTokensClonerListener(destinationModule)); var result = cloner.Clone(); - + var clonedMethodDefinition = result.GetClonedMember(methodDefinition); metadataToken = clonedMethodDefinition.MetadataToken.ToInt32(); diff --git a/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.AsmResolver.cs b/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.AsmResolver.cs index 8bc571e..cfe5c94 100644 --- a/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.AsmResolver.cs +++ b/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.AsmResolver.cs @@ -3,11 +3,13 @@ using AsmResolver.DotNet.Dynamic; using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Reflection; -namespace BUTR.CrashReport.Utils; +namespace BUTR.CrashReport.Decompilers.Utils; partial class MethodDecompiler { @@ -19,7 +21,10 @@ private static bool TryGetMethodDefinition(MethodBase method, [NotNullWhen(true) methodDefinition = new DynamicMethodDefinition(module, method); return true; } - catch (Exception) { /* ignore */ } + catch (Exception e) + { + Trace.TraceError(e.ToString()); + } try { @@ -27,13 +32,16 @@ private static bool TryGetMethodDefinition(MethodBase method, [NotNullWhen(true) methodDefinition = module.LookupMember(method.MetadataToken) as MethodDefinition; return methodDefinition is not null; } - catch (Exception) { /* ignore */ } + catch (Exception e) + { + Trace.TraceError(e.ToString()); + } module = null; methodDefinition = null; return false; } - + /// /// Gets the IL representation of the methods /// @@ -43,13 +51,33 @@ public static string[] DecompileILCode(MethodBase? method) if (method is null) return Array.Empty(); + try + { + if (!TryCopyMethod(method, out var stream, out var methodHandle)) return Array.Empty(); + + using var _ = stream; + using var ms = stream as MemoryStream ?? new MemoryStream(); + if (stream is not MemoryStream) stream.CopyTo(ms); + + var moduleDefinition = ModuleDefinition.FromBytes(ms.ToArray()); + var methodDefinition = moduleDefinition.LookupMember(method.MetadataToken); + return ToLines(methodDefinition.CilMethodBody?.Instructions); + } + catch (Exception e) + { + Trace.TraceError(e.ToString()); + } + try { if (!TryGetMethodDefinition(method, out _, out var methodDefinition)) return Array.Empty(); return ToLines(methodDefinition.CilMethodBody?.Instructions); } - catch (Exception) { /* ignore */ } + catch (Exception e) + { + Trace.TraceError(e.ToString()); + } return Array.Empty(); } diff --git a/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.ILSpy.cs b/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.ILSpy.cs index 0b07dbf..f5a0448 100644 --- a/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.ILSpy.cs +++ b/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.ILSpy.cs @@ -1,4 +1,4 @@ -using BUTR.CrashReport.ILSpy; +using BUTR.CrashReport.Decompilers.ILSpy; using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.CSharp; @@ -8,25 +8,54 @@ using ICSharpCode.Decompiler.TypeSystem.Implementation; using System; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading; -namespace BUTR.CrashReport.Utils; +namespace BUTR.CrashReport.Decompilers.Utils; partial class MethodDecompiler { + /// + /// Gets the extended IL representation of the methods + /// + public static string[] DecompileILCodeExtended(MethodBase? method) + { + if (method is null) return Array.Empty(); + + try + { + if (!TryCopyMethod(method, out var stream, out var methodHandle)) return Array.Empty(); + + using var _ = stream; + using var peFile = new PEFile("Assembly", stream); + + var output = new PlainTextOutput2(); + var disassembler = ILLanguage.CreateDisassembler(output, CancellationToken.None); + disassembler.DisassembleMethod(peFile, methodHandle); + + return output.ToString()!.Split([Environment.NewLine], StringSplitOptions.None); + } + catch (Exception e) + { + Trace.TraceError(e.ToString()); + } + + return Array.Empty(); + } + /// /// Gets the C# + IL representation of the methods /// public static string[] DecompileILWithCSharpCode(MethodBase? method) { if (method is null) return Array.Empty(); - + try { if (!TryCopyMethod(method, out var stream, out var methodHandle)) return Array.Empty(); - + using var _ = stream; using var peFile = new PEFile("Assembly", stream); @@ -34,16 +63,16 @@ public static string[] DecompileILWithCSharpCode(MethodBase? method) var disassembler = CSharpILMixedLanguage.CreateDisassembler(output, CancellationToken.None); disassembler.DisassembleMethod(peFile, methodHandle); - return output.ToString()!.Split(new[] {Environment.NewLine}, StringSplitOptions.None); + return output.ToString()!.Split([Environment.NewLine], StringSplitOptions.None); } catch (Exception e) { - return new[] { e.ToString() }; + Trace.TraceError(e.ToString()); } return Array.Empty(); } - + /// /// Gets the C# representation of the methods /// @@ -72,11 +101,11 @@ public static string[] DecompileCSharpCode(MethodBase? method) AggressiveInlining = true, }); - return output.ToString()!.Split(new[] {Environment.NewLine}, StringSplitOptions.None); + return output.ToString()!.Split([Environment.NewLine], StringSplitOptions.None); } catch (Exception e) { - return new[] { e.ToString() }; + Trace.TraceError(e.ToString()); } return Array.Empty(); diff --git a/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.Iced.cs b/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.Iced.cs index bc8b94c..8d1a4b1 100644 --- a/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.Iced.cs +++ b/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.Iced.cs @@ -1,4 +1,5 @@ extern alias iced; + using iced::Iced.Intel; using System; @@ -9,11 +10,11 @@ using System.Runtime.InteropServices; using System.Text; -using static BUTR.CrashReport.Utils.MonoModUtils; +using static BUTR.CrashReport.Decompilers.Utils.MonoModUtils; using Decoder = iced::Iced.Intel.Decoder; -namespace BUTR.CrashReport.Utils; +namespace BUTR.CrashReport.Decompilers.Utils; partial class MethodDecompiler { @@ -24,7 +25,8 @@ public static string[] DecompileNativeCode(MethodBase? method, int nativeILOffse { static IEnumerable GetLines(MethodBase method, int nativeILOffset) { - var nativeCodePtr = GetNativeMethodBody!(CurrentPlatformTriple!(), method); + var nativeCodePtr = GetNativeMethodBody(method); + if (nativeCodePtr == IntPtr.Zero) yield break; var length = (uint) nativeILOffset + 16; var bytecode = new byte[length]; @@ -58,14 +60,16 @@ static IEnumerable GetLines(MethodBase method, int nativeILOffset) if (method is null) return Array.Empty(); if (nativeILOffset == StackFrame.OFFSET_UNKNOWN) return Array.Empty(); - if (CurrentPlatformTriple is null || GetNativeMethodBody is null) return Array.Empty(); try { return GetLines(method, nativeILOffset).ToArray(); } - catch (Exception) { /* ignore */ } - + catch (Exception e) + { + Trace.TraceError(e.ToString()); + } + return Array.Empty(); } } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.System.cs b/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.System.cs index c578c7f..604812d 100644 --- a/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.System.cs +++ b/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.System.cs @@ -20,7 +20,7 @@ using System.Text; using System.Threading; -namespace BUTR.CrashReport.Utils; +namespace BUTR.CrashReport.Decompilers.Utils; partial class MethodDecompiler { diff --git a/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.SystemReflectionMetadata.cs b/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.SystemReflectionMetadata.cs index 4d49a86..3c0cef9 100644 --- a/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.SystemReflectionMetadata.cs +++ b/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.SystemReflectionMetadata.cs @@ -1,11 +1,12 @@ using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; -namespace BUTR.CrashReport.Utils; +namespace BUTR.CrashReport.Decompilers.Utils; partial class MethodDecompiler { @@ -21,7 +22,10 @@ private static bool TryCopyMethod(MethodBase method, [NotNullWhen(true)] out Str stream = File.OpenRead(assembly.Location); return true; } - catch (Exception) { /* ignore */ } + catch (Exception e) + { + Trace.TraceError(e.ToString()); + } } try @@ -33,7 +37,10 @@ private static bool TryCopyMethod(MethodBase method, [NotNullWhen(true)] out Str return true; } } - catch (Exception) { /* ignore */ } + catch (Exception e) + { + Trace.TraceError(e.ToString()); + } stream = null; methodHandle = default; diff --git a/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.cs b/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.cs index 10e3d74..c1b73a9 100644 --- a/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.cs +++ b/src/BUTR.CrashReport.Decompilers/Utils/MethodDecompiler.cs @@ -1,4 +1,4 @@ -namespace BUTR.CrashReport.Utils; +namespace BUTR.CrashReport.Decompilers.Utils; /// /// Can provide C#, IL and Native representation of a method. diff --git a/src/BUTR.CrashReport.Decompilers/Utils/MonoModUtils.cs b/src/BUTR.CrashReport.Decompilers/Utils/MonoModUtils.cs index c05aa3e..419c956 100644 --- a/src/BUTR.CrashReport.Decompilers/Utils/MonoModUtils.cs +++ b/src/BUTR.CrashReport.Decompilers/Utils/MonoModUtils.cs @@ -1,21 +1,82 @@ -using HarmonyLib.BUTR.Extensions; - -using System; +using System; +using System.Diagnostics; using System.Reflection; -namespace BUTR.CrashReport.Utils; +using static HarmonyLib.BUTR.Extensions.AccessTools2; + +namespace BUTR.CrashReport.Decompilers.Utils; +/// +/// Wrapper for MonoMod methods, since MonoMod.Core is not a dependency for us +/// public static class MonoModUtils { - public delegate object GetCurrentPlatformTripleDelegate(); - public static readonly GetCurrentPlatformTripleDelegate? CurrentPlatformTriple = - AccessTools2.GetPropertyGetterDelegate("MonoMod.Core.Platforms.PlatformTriple:Current"); + private delegate object GetCurrentRuntimeDelegate(); + private static readonly GetCurrentRuntimeDelegate? CurrentRuntimeMethod = GetPropertyGetterDelegate( + "MonoMod.RuntimeDetour.DetourHelper:Runtime", logErrorInTrace: false); + + private delegate MethodBase GetIdentifiableOldDelegate(object instance, MethodBase method); + private static readonly GetIdentifiableOldDelegate? GetIdentifiableOldMethod = GetDelegate( + "MonoMod.RuntimeDetour.IDetourRuntimePlatform:GetIdentifiable", logErrorInTrace: false); + + private delegate IntPtr GetNativeStartDelegate(object instance, MethodBase method); + private static readonly GetNativeStartDelegate? GetNativeStartMethod = GetDelegate( + "MonoMod.RuntimeDetour.IDetourRuntimePlatform:GetNativeStart", logErrorInTrace: false); + + + private delegate object GetCurrentPlatformTripleDelegate(); + private static readonly GetCurrentPlatformTripleDelegate? CurrentPlatformTripleMethod = GetPropertyGetterDelegate( + "MonoMod.Core.Platforms.PlatformTriple:Current", logErrorInTrace: false); + + private delegate MethodBase GetIdentifiableDelegate(object instance, MethodBase method); + private static readonly GetIdentifiableDelegate? GetIdentifiableMethod = GetDelegate( + "MonoMod.Core.Platforms.PlatformTriple:GetIdentifiable", logErrorInTrace: false); + + private delegate IntPtr GetNativeMethodBodyDelegate(object instance, MethodBase method); + private static readonly GetNativeMethodBodyDelegate? GetNativeMethodBodyMethod = GetDelegate( + "MonoMod.Core.Platforms.PlatformTriple:GetNativeMethodBody", logErrorInTrace: false); + + /// + /// + /// + /// + public static MethodBase? GetIdentifiable(MethodBase method) + { + try + { + if (CurrentRuntimeMethod?.Invoke() is { } runtime) + return GetIdentifiableOldMethod?.Invoke(runtime, method); + + if (CurrentPlatformTripleMethod?.Invoke() is { } platformTriple) + return GetIdentifiableMethod?.Invoke(platformTriple, method); + } + catch (Exception e) + { + Trace.TraceError(e.ToString()); + } + + return null; + } + + /// + /// + /// + /// + public static IntPtr GetNativeMethodBody(MethodBase method) + { + try + { + if (CurrentRuntimeMethod?.Invoke() is { } runtine) + return GetNativeStartMethod?.Invoke(runtine, method) ?? IntPtr.Zero; - public delegate MethodBase GetIdentifiableDelegate(object instance, MethodBase method); - public static readonly GetIdentifiableDelegate? GetIdentifiable = - AccessTools2.GetDelegate("MonoMod.Core.Platforms.PlatformTriple:GetIdentifiable"); + if (CurrentPlatformTripleMethod?.Invoke() is { } platformTriple) + return GetNativeMethodBodyMethod?.Invoke(platformTriple, method) ?? IntPtr.Zero; + } + catch (Exception e) + { + Trace.TraceError(e.ToString()); + } - public delegate IntPtr GetNativeMethodBodyDelegate(object instance, MethodBase method); - public static readonly GetNativeMethodBodyDelegate? GetNativeMethodBody = - AccessTools2.GetDelegate("MonoMod.Core.Platforms.PlatformTriple:GetNativeMethodBody"); + return IntPtr.Zero; + } } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Decompilers/Utils/ReferenceImporter.cs b/src/BUTR.CrashReport.Decompilers/Utils/ReferenceImporter.cs index ce064d9..54f3a85 100644 --- a/src/BUTR.CrashReport.Decompilers/Utils/ReferenceImporter.cs +++ b/src/BUTR.CrashReport.Decompilers/Utils/ReferenceImporter.cs @@ -2,32 +2,33 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; -namespace BUTR.CrashReport.Utils; +namespace BUTR.CrashReport.Decompilers.Utils; /// -/// +/// /// public record AssemblyTypeReferenceInternal { /// - /// + /// /// - /// + /// public required string Name { get; set; } /// - /// + /// /// - /// + /// public required string Namespace { get; set; } /// - /// + /// /// - /// + /// public required string FullName { get; set; } } @@ -41,6 +42,7 @@ public static class ReferenceImporter /// public static Dictionary GetImportedTypeReferences(Dictionary AvailableAssemblies) => AvailableAssemblies.ToDictionary(x => x.Key, x => { + // TODO: Can we do that with the built-in Reflection API? foreach (var assemblyModule in x.Value.Modules) { try @@ -53,9 +55,32 @@ public static Dictionary GetImpor FullName = y.FullName, }).ToArray(); } - catch (Exception) { /* ignore */ } - + catch (Exception e) + { + Trace.TraceError(x.Key.ToString()); + Trace.TraceError(e.ToString()); + } + } + + try + { + var assembly = AssemblyDefinition.FromFile(x.Value.Location); + foreach (var module in assembly.Modules) + { + return module.GetImportedTypeReferences().Select(y => new AssemblyTypeReferenceInternal + { + Name = y.Name ?? string.Empty, + Namespace = y.Namespace ?? string.Empty, + FullName = y.FullName, + }).ToArray(); + } + } + catch (Exception e) + { + Trace.TraceError(x.Key.ToString()); + Trace.TraceError(e.ToString()); } + return Array.Empty(); }); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/AssemblyIdModel.cs b/src/BUTR.CrashReport.Models/AssemblyIdModel.cs new file mode 100644 index 0000000..62d7de5 --- /dev/null +++ b/src/BUTR.CrashReport.Models/AssemblyIdModel.cs @@ -0,0 +1,57 @@ +using BUTR.CrashReport.Models.Utils; + +using System; +using System.Reflection; + +namespace BUTR.CrashReport.Models; + +/// +/// Represents an assembly identity. +/// +public sealed record AssemblyIdModel : IEquatable, IEquatable +{ + /// + /// Creates a new instance of from an . + /// + /// + /// + public static AssemblyIdModel FromAssembly(AssemblyName assemblyName) => new() + { + Name = assemblyName.Name, + Version = assemblyName.Version.ToString(), + PublicKeyToken = AssemblyUtils.PublicKeyAsString(assemblyName.GetPublicKeyToken()), + }; + + /// + /// + /// + /// + public required string Name { get; set; } + + /// + /// + /// + /// A string that represents the major, minor, build, and revision numbers of the assembly. + public required string? Version { get; set; } + + /// + /// + /// + /// A hex string that contains the public key token. + public required string? PublicKeyToken { get; set; } + + /// + public bool Equals(AssemblyIdModel? other) => other is not null && + Name == other.Name && + ((Version is null || other.Version is null) || Version == other.Version) && + PublicKeyToken == other.PublicKeyToken; + + /// + public bool Equals(AssemblyModel? other) => other is not null && other.Id.Equals(this); + + /// + public bool Equals(AssemblyName? other) => other is not null && + Name == other.Name && + (Version is null || Version == other.Version.ToString()) && + PublicKeyToken == AssemblyUtils.PublicKeyAsString(other.GetPublicKeyToken()); +} \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/AssemblyModel.cs b/src/BUTR.CrashReport.Models/AssemblyModel.cs index 70cc713..7e2267e 100644 --- a/src/BUTR.CrashReport.Models/AssemblyModel.cs +++ b/src/BUTR.CrashReport.Models/AssemblyModel.cs @@ -14,28 +14,21 @@ public record AssemblyModel public required string? ModuleId { get; set; } /// - /// + /// Is null if not from a module. /// - /// - public required string Name { get; set; } + /// + public required string? LoaderPluginId { get; set; } /// - /// + /// /// - /// A string that represents the major, minor, build, and revision numbers of the assembly. - public required string Version { get; set; } + public required AssemblyIdModel Id { get; set; } /// /// /// /// - public required string? Culture { get; set; } - - /// - /// - /// - /// A hex string that contains the public key token. - public required string? PublicKeyToken { get; set; } + public required string? CultureName { get; set; } /// /// @@ -61,16 +54,16 @@ public record AssemblyModel /// /// The list of imported type references from the assembly. /// - public required IReadOnlyList ImportedTypeReferences { get; set; } = new List(); + public required IList ImportedTypeReferences { get; set; } = new List(); /// /// The list of imported assembly references from the assembly. /// - public required IReadOnlyList ImportedAssemblyReferences { get; set; } = new List(); + public required IList ImportedAssemblyReferences { get; set; } = new List(); /// /// /// /// - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); + public required IList AdditionalMetadata { get; set; } = new List(); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/AssemblyModelType.cs b/src/BUTR.CrashReport.Models/AssemblyModelType.cs index d3b99fc..4db26dc 100644 --- a/src/BUTR.CrashReport.Models/AssemblyModelType.cs +++ b/src/BUTR.CrashReport.Models/AssemblyModelType.cs @@ -42,4 +42,19 @@ public enum AssemblyModelType /// Custom module assembly /// Module = 32, + + /// + /// Loader assembly + /// + Loader = 64, + + /// + /// Loader plugin assembly + /// + LoaderPlugin = 128, + + /// + /// Assembly is protected from disassembly + /// + ProtectedFromDisassembly = 256, } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/BUTR.CrashReport.Models.csproj b/src/BUTR.CrashReport.Models/BUTR.CrashReport.Models.csproj index 1f7a5b6..71244d4 100644 --- a/src/BUTR.CrashReport.Models/BUTR.CrashReport.Models.csproj +++ b/src/BUTR.CrashReport.Models/BUTR.CrashReport.Models.csproj @@ -19,14 +19,18 @@ - - - - - - - + + + + + + + + + + + diff --git a/src/BUTR.CrashReport.Models/CapabilityModuleOrPluginModel.cs b/src/BUTR.CrashReport.Models/CapabilityModuleOrPluginModel.cs new file mode 100644 index 0000000..fd3c4fe --- /dev/null +++ b/src/BUTR.CrashReport.Models/CapabilityModuleOrPluginModel.cs @@ -0,0 +1,22 @@ +namespace BUTR.CrashReport.Models; + +/// +/// Represents the functionality that is used by the module or plugin. +/// +public record CapabilityModuleOrPluginModel +{ + /// + /// The name of the capability. + /// + public string Name { get; } + + /// + /// An optional description of the capability. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Creates a new instance of . + /// + public CapabilityModuleOrPluginModel(string name) => Name = name; +} \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/CrashReportMetadataModel.cs b/src/BUTR.CrashReport.Models/CrashReportMetadataModel.cs index a13b076..05052b7 100644 --- a/src/BUTR.CrashReport.Models/CrashReportMetadataModel.cs +++ b/src/BUTR.CrashReport.Models/CrashReportMetadataModel.cs @@ -7,6 +7,26 @@ namespace BUTR.CrashReport.Models; /// public record CrashReportMetadataModel { + /// + /// The game name. + /// + public required string? GameName { get; set; } + + /// + /// The game version. + /// + public required string GameVersion { get; set; } + + /// + /// The loader plugin provider name that was used to launch the game. + /// + public required string? LoaderPluginProviderName { get; set; } + + /// + /// The loader plugin provider version that was used to launch the game. + /// + public required string? LoaderPluginProviderVersion { get; set; } + /// /// The launcher type that was used to launch the game. Usually it's the process name. /// @@ -26,5 +46,5 @@ public record CrashReportMetadataModel /// /// /// - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); + public required IList AdditionalMetadata { get; set; } = new List(); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/CrashReportModel.cs b/src/BUTR.CrashReport.Models/CrashReportModel.cs index 97e03d5..c3467c9 100644 --- a/src/BUTR.CrashReport.Models/CrashReportModel.cs +++ b/src/BUTR.CrashReport.Models/CrashReportModel.cs @@ -18,11 +18,6 @@ public record CrashReportModel /// public required byte Version { get; set; } - /// - /// The game version. - /// - public required string GameVersion { get; set; } - /// /// The exception that caused the crash. /// @@ -36,36 +31,50 @@ public record CrashReportModel /// /// The list of modules that are loaded in the process. /// - public required IReadOnlyList Modules { get; set; } = new List(); + public required IList Modules { get; set; } = new List(); /// /// The list of involved modules in the crash. /// - public required IReadOnlyList InvolvedModules { get; set; } = new List(); + public required IList InvolvedModules { get; set; } = new List(); /// /// The enhanced stack trace frames. /// - public required IReadOnlyList EnhancedStacktrace { get; set; } = new List(); + public required IList EnhancedStacktrace { get; set; } = new List(); /// /// The list of assemblies that are present. /// - public required IReadOnlyList Assemblies { get; set; } = new List(); + public required IList Assemblies { get; set; } = new List(); + + + /* + /// + /// The list of MonoMod detours that are present. + /// MonoMod does not keep a list of detours. If you have a library that does, here it could be exposed. + /// + public required IList MonoModDetours { get; set; } = new List(); + */ /// /// The list of Harmony patches that are present. /// - public required IReadOnlyList HarmonyPatches { get; set; } = new List(); + public required IList HarmonyPatches { get; set; } = new List(); /// - /// The list of MonoMod detours that are present. + /// The list of loader plugins that are present. + /// + public required IList LoaderPlugins { get; set; } = new List(); + + /// + /// The list of involved loader plugins in the crash. /// - public required IReadOnlyList MonoModDetours { get; set; } = new List(); + public required IList InvolvedLoaderPlugins { get; set; } = new List(); /// /// Additional metadata associated with the model. /// /// A key:value list of metadatas. - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); + public required IList AdditionalMetadata { get; set; } = new List(); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/ModuleDependencyMetadataModel.cs b/src/BUTR.CrashReport.Models/DependencyMetadataModel.cs similarity index 67% rename from src/BUTR.CrashReport.Models/ModuleDependencyMetadataModel.cs rename to src/BUTR.CrashReport.Models/DependencyMetadataModel.cs index deee5db..4ec8736 100644 --- a/src/BUTR.CrashReport.Models/ModuleDependencyMetadataModel.cs +++ b/src/BUTR.CrashReport.Models/DependencyMetadataModel.cs @@ -5,18 +5,18 @@ namespace BUTR.CrashReport.Models; /// /// Represents the dependency metadata for a module. /// -public record ModuleDependencyMetadataModel +public record DependencyMetadataModel { /// - /// + /// Is null if not from a module. + /// Is null if not from a plugin /// - /// - public required string ModuleId { get; set; } + public required string ModuleOrPluginId { get; set; } /// /// The dependency type. /// - public required ModuleDependencyMetadataModelType Type { get; set; } + public required DependencyMetadataModelType Type { get; set; } /// /// Whether the dependency is required. @@ -37,5 +37,5 @@ public record ModuleDependencyMetadataModel /// /// /// - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); + public required IList AdditionalMetadata { get; set; } = new List(); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/ModuleDependencyMetadataModelType.cs b/src/BUTR.CrashReport.Models/DependencyMetadataModelType.cs similarity index 90% rename from src/BUTR.CrashReport.Models/ModuleDependencyMetadataModelType.cs rename to src/BUTR.CrashReport.Models/DependencyMetadataModelType.cs index 6a3923f..42b4b83 100644 --- a/src/BUTR.CrashReport.Models/ModuleDependencyMetadataModelType.cs +++ b/src/BUTR.CrashReport.Models/DependencyMetadataModelType.cs @@ -3,7 +3,7 @@ /// /// Represents the type of teh dependency metadata. /// -public enum ModuleDependencyMetadataModelType +public enum DependencyMetadataModelType { /// /// The depencency will load before this module. diff --git a/src/BUTR.CrashReport.Models/EnhancedStacktraceFrameModel.cs b/src/BUTR.CrashReport.Models/EnhancedStacktraceFrameModel.cs index f7d4012..8eba651 100644 --- a/src/BUTR.CrashReport.Models/EnhancedStacktraceFrameModel.cs +++ b/src/BUTR.CrashReport.Models/EnhancedStacktraceFrameModel.cs @@ -41,13 +41,13 @@ public record EnhancedStacktraceFrameModel public required MethodSimple? OriginalMethod { get; set; } /// - /// The list of Harmony patch methods that are applied to the method. + /// The list of patch methods that are applied to the method. /// - public required IReadOnlyList PatchMethods { get; set; } = new List(); + public required IList PatchMethods { get; set; } = new List(); /// /// /// /// - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); + public required IList AdditionalMetadata { get; set; } = new List(); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/ExceptionModel.cs b/src/BUTR.CrashReport.Models/ExceptionModel.cs index 0fb5f4a..17bd3b0 100644 --- a/src/BUTR.CrashReport.Models/ExceptionModel.cs +++ b/src/BUTR.CrashReport.Models/ExceptionModel.cs @@ -7,12 +7,23 @@ namespace BUTR.CrashReport.Models; /// public record ExceptionModel { + /// + /// The assembly identity of the assembly. Is associated with the source of the exception. + /// + public required AssemblyIdModel? SourceAssemblyId { get; set; } + /// /// Is associated with the source of the exception. /// /// public required string? SourceModuleId { get; set; } + /// + /// Is associated with the source of the exception. + /// + /// + public required string? SourceLoaderPluginId { get; set; } + /// /// The type full name of the exception. /// @@ -40,5 +51,5 @@ public record ExceptionModel /// /// /// - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); + public required IList AdditionalMetadata { get; set; } = new List(); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/HarmonyPatchModel.cs b/src/BUTR.CrashReport.Models/HarmonyPatchModel.cs index 2811ed5..c1c4ff9 100644 --- a/src/BUTR.CrashReport.Models/HarmonyPatchModel.cs +++ b/src/BUTR.CrashReport.Models/HarmonyPatchModel.cs @@ -10,12 +10,24 @@ public sealed record HarmonyPatchModel /// /// The type of the patch. /// - public required HarmonyPatchModelType Type { get; set; } + public required HarmonyPatchType Type { get; set; } /// - /// The of the assembly that contains the patch. + /// Is null if not from a module. /// - public required string? AssemblyName { get; set; } + /// + public required string? ModuleId { get; set; } + + /// + /// Is null if not from a module. + /// + /// + public required string? LoaderPluginId { get; set; } + + /// + /// The of the assembly that contains the patch. + /// + public required AssemblyIdModel? AssemblyId { get; set; } /// /// @@ -45,17 +57,17 @@ public sealed record HarmonyPatchModel /// /// /// - public required IReadOnlyList Before { get; set; } = new List(); + public required IList Before { get; set; } = new List(); /// /// /// /// - public required IReadOnlyList After { get; set; } = new List(); + public required IList After { get; set; } = new List(); /// /// /// /// - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); + public required IList AdditionalMetadata { get; set; } = new List(); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/HarmonyPatchModelType.cs b/src/BUTR.CrashReport.Models/HarmonyPatchType.cs similarity index 83% rename from src/BUTR.CrashReport.Models/HarmonyPatchModelType.cs rename to src/BUTR.CrashReport.Models/HarmonyPatchType.cs index bd9f375..0602546 100644 --- a/src/BUTR.CrashReport.Models/HarmonyPatchModelType.cs +++ b/src/BUTR.CrashReport.Models/HarmonyPatchType.cs @@ -3,25 +3,25 @@ /// /// Represents the type of a Harmony patch. /// -public enum HarmonyPatchModelType +public enum HarmonyPatchType { /// /// /// - Prefix, + Prefix = 1, /// /// /// - Postfix, + Postfix = 2, /// /// /// - Finalizer, + Finalizer = 3, /// /// /// - Transpiler, + Transpiler = 4, } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/HarmonyPatchesModel.cs b/src/BUTR.CrashReport.Models/HarmonyPatchesModel.cs index d6272a5..60cfae8 100644 --- a/src/BUTR.CrashReport.Models/HarmonyPatchesModel.cs +++ b/src/BUTR.CrashReport.Models/HarmonyPatchesModel.cs @@ -20,11 +20,11 @@ public sealed record HarmonyPatchesModel /// /// The list of Harmony patches. /// - public required IReadOnlyList Patches { get; set; } = new List(); + public required IList Patches { get; set; } = new List(); /// /// /// /// - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); + public required IList AdditionalMetadata { get; set; } = new List(); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/InvolvedModuleModel.cs b/src/BUTR.CrashReport.Models/InvolvedModuleOrPluginModel.cs similarity index 78% rename from src/BUTR.CrashReport.Models/InvolvedModuleModel.cs rename to src/BUTR.CrashReport.Models/InvolvedModuleOrPluginModel.cs index a788bfd..a545899 100644 --- a/src/BUTR.CrashReport.Models/InvolvedModuleModel.cs +++ b/src/BUTR.CrashReport.Models/InvolvedModuleOrPluginModel.cs @@ -5,13 +5,13 @@ namespace BUTR.CrashReport.Models; /// /// Represents an involved module info. /// -public record InvolvedModuleModel +public record InvolvedModuleOrPluginModel { /// /// /// /// - public required string ModuleId { get; set; } + public required string ModuleOrLoaderPluginId { get; set; } /// /// @@ -23,5 +23,5 @@ public record InvolvedModuleModel /// /// /// - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); + public required IList AdditionalMetadata { get; set; } = new List(); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/LoaderPluginModel.cs b/src/BUTR.CrashReport.Models/LoaderPluginModel.cs new file mode 100644 index 0000000..b9d127f --- /dev/null +++ b/src/BUTR.CrashReport.Models/LoaderPluginModel.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; + +namespace BUTR.CrashReport.Models; + +/// +/// Represents a plugin from a Loader +/// +public sealed record LoaderPluginModel +{ + /// + /// The unique identifier of the plugin + /// + public required string Id { get; set; } + + /// + /// The name of the plugin + /// + public required string Name { get; set; } + + /// + /// The version of the plugin + /// + public required string? Version { get; set; } + + /// + /// The information for updating the module. + /// + public required UpdateInfoModuleOrLoaderPlugin? UpdateInfo { get; set; } + + /// + /// The plugin dependencies of the plugin + /// + public required IList Dependencies { get; set; } = new List(); + + /// + /// The capabilities, if there are any. + /// + public required IList Capabilities { get; set; } = new List(); + + /// + /// + /// + /// + public required IList AdditionalMetadata { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/LogEntry.cs b/src/BUTR.CrashReport.Models/LogEntry.cs index 9bd9065..eb58fd0 100644 --- a/src/BUTR.CrashReport.Models/LogEntry.cs +++ b/src/BUTR.CrashReport.Models/LogEntry.cs @@ -20,7 +20,7 @@ public record LogEntry /// /// The level of the log entry. /// - public required string Level { get; set; } + public required LogLevel Level { get; set; } /// /// The message of the log entry. diff --git a/src/BUTR.CrashReport.Models/LogLevel.cs b/src/BUTR.CrashReport.Models/LogLevel.cs new file mode 100644 index 0000000..76b9d02 --- /dev/null +++ b/src/BUTR.CrashReport.Models/LogLevel.cs @@ -0,0 +1,47 @@ +namespace BUTR.CrashReport.Models; + +/// +/// Specifies the meaning and relative importance of a log event. +/// +public enum LogLevel +{ + /// + /// Unknown log level. + /// + None = 0, + + /// + /// Anything and everything you might want to know about + /// a running block of code. + /// + Verbose = 1, + + /// + /// Internal system events that aren't necessarily + /// observable from the outside. + /// + Debug = 2, + + /// + /// The lifeblood of operational intelligence - things + /// happen. + /// + Information = 3, + + /// + /// Service is degraded or endangered. + /// + Warning = 4, + + /// + /// Functionality is unavailable, invariants are broken + /// or data is lost. + /// + Error = 5, + + /// + /// If you have a pager, it goes off when one of these + /// occurs. + /// + Fatal = 6, +} \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/LogSource.cs b/src/BUTR.CrashReport.Models/LogSource.cs index d780ed4..3fd7c93 100644 --- a/src/BUTR.CrashReport.Models/LogSource.cs +++ b/src/BUTR.CrashReport.Models/LogSource.cs @@ -15,11 +15,11 @@ public record LogSource /// /// The log entries associated with the log source. /// - public required IReadOnlyList Logs { get; set; } = new List(); + public required IList Logs { get; set; } = new List(); /// /// /// /// - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); + public required IList AdditionalMetadata { get; set; } = new List(); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/MethodExecuting.cs b/src/BUTR.CrashReport.Models/MethodExecuting.cs index 0985d80..e730ee6 100644 --- a/src/BUTR.CrashReport.Models/MethodExecuting.cs +++ b/src/BUTR.CrashReport.Models/MethodExecuting.cs @@ -10,5 +10,5 @@ public record MethodExecuting : MethodSimple /// /// The native code of the method that was compiled by the JIT. /// - public required IReadOnlyList NativeInstructions { get; set; } = new List(); + public required IList NativeInstructions { get; set; } = new List(); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/MethodHarmonyPatch.cs b/src/BUTR.CrashReport.Models/MethodHarmonyPatch.cs new file mode 100644 index 0000000..ff162e2 --- /dev/null +++ b/src/BUTR.CrashReport.Models/MethodHarmonyPatch.cs @@ -0,0 +1,39 @@ +using System.Diagnostics.CodeAnalysis; + +namespace BUTR.CrashReport.Models; + +/// +/// Represents a Harmony patch method. +/// +public record MethodHarmonyPatch : MethodSimple +{ + /// + /// The type of the patch. + /// + public required HarmonyPatchType PatchType { get; set; } + + /// + /// Main constructor + /// + public MethodHarmonyPatch() { } + + /// + /// The copy constructor + /// + [SetsRequiredMembers] + public MethodHarmonyPatch(MethodSimple methodSimple, HarmonyPatchType patchType) + { + AssemblyId = methodSimple.AssemblyId; + ModuleId = methodSimple.ModuleId; + LoaderPluginId = methodSimple.LoaderPluginId; + MethodDeclaredTypeName = methodSimple.MethodDeclaredTypeName; + MethodName = methodSimple.MethodName; + MethodFullDescription = methodSimple.MethodFullDescription; + MethodParameters = methodSimple.MethodParameters; + ILInstructions = methodSimple.ILInstructions; + CSharpILMixedInstructions = methodSimple.CSharpILMixedInstructions; + CSharpInstructions = methodSimple.CSharpInstructions; + AdditionalMetadata = methodSimple.AdditionalMetadata; + PatchType = patchType; + } +} \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/MethodSimple.cs b/src/BUTR.CrashReport.Models/MethodSimple.cs index f2c6a47..b7a10ef 100644 --- a/src/BUTR.CrashReport.Models/MethodSimple.cs +++ b/src/BUTR.CrashReport.Models/MethodSimple.cs @@ -7,12 +7,23 @@ namespace BUTR.CrashReport.Models; /// public record MethodSimple { + /// + /// The assembly identity of the assembly that contains the method. + /// + public required AssemblyIdModel? AssemblyId { get; set; } + /// /// /// /// public required string? ModuleId { get; set; } + /// + /// + /// + /// + public required string? LoaderPluginId { get; set; } + /// /// /// @@ -34,26 +45,26 @@ public record MethodSimple /// /// The list of types that are part of the method signature. /// - public required IReadOnlyList MethodParameters { get; set; } = new List(); + public required IList MethodParameters { get; set; } = new List(); /// /// The Common Intermediate Language (CIL/IL) representation of the method. /// - public required IReadOnlyList ILInstructions { get; set; } = new List(); - + public required IList ILInstructions { get; set; } = new List(); + /// /// The C# and Common Intermediate Language (CIL/IL) representation of the method. /// - public required IReadOnlyList CSharpILMixedInstructions { get; set; } = new List(); - + public required IList CSharpILMixedInstructions { get; set; } = new List(); + /// /// The C# representation the method. /// - public required IReadOnlyList CSharpInstructions { get; set; } = new List(); + public required IList CSharpInstructions { get; set; } = new List(); /// /// /// /// - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); + public required IList AdditionalMetadata { get; set; } = new List(); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/ModuleModel.cs b/src/BUTR.CrashReport.Models/ModuleModel.cs index 81a688f..36411f6 100644 --- a/src/BUTR.CrashReport.Models/ModuleModel.cs +++ b/src/BUTR.CrashReport.Models/ModuleModel.cs @@ -48,23 +48,28 @@ public sealed record ModuleModel public required string? Url { get; set; } /// - /// The Key:Value pair for updating the module. The Key can be 'NexusMods:%MODID%' or 'GitHub:%USER%/%REPO%'. + /// The information for updating the module. /// - public required string? UpdateInfo { get; set; } + public required UpdateInfoModuleOrLoaderPlugin? UpdateInfo { get; set; } /// - /// The dependencies of the module. + /// The dependencies of the module, if there are any. /// - public required IReadOnlyList DependencyMetadatas { get; set; } = new List(); + public required IList DependencyMetadatas { get; set; } = new List(); /// - /// The submodules of the module. + /// The submodules of the module, if there are any. /// - public required IReadOnlyList SubModules { get; set; } = new List(); + public required IList SubModules { get; set; } = new List(); + + /// + /// The capabilities, if there are any. + /// + public required IList Capabilities { get; set; } = new List(); /// /// /// /// - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); + public required IList AdditionalMetadata { get; set; } = new List(); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/ModuleSubModuleModel.cs b/src/BUTR.CrashReport.Models/ModuleSubModuleModel.cs index 1a70fe0..70d8afc 100644 --- a/src/BUTR.CrashReport.Models/ModuleSubModuleModel.cs +++ b/src/BUTR.CrashReport.Models/ModuleSubModuleModel.cs @@ -15,7 +15,7 @@ public record ModuleSubModuleModel /// /// The main assembly of the SubModule. /// - public required string AssemblyName { get; set; } + public required AssemblyIdModel? AssemblyId { get; set; } /// /// The entry point of the assembly. Can be a method or a type full name. @@ -26,5 +26,5 @@ public record ModuleSubModuleModel /// /// /// - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); + public required IList AdditionalMetadata { get; set; } = new List(); } \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/MonoModDetourModel.cs b/src/BUTR.CrashReport.Models/MonoModDetourModel.cs index 00b56ee..5523305 100644 --- a/src/BUTR.CrashReport.Models/MonoModDetourModel.cs +++ b/src/BUTR.CrashReport.Models/MonoModDetourModel.cs @@ -1,3 +1,4 @@ +/* using System.Collections.Generic; namespace BUTR.CrashReport.Models; @@ -11,11 +12,11 @@ public sealed record MonoModDetourModel /// The type of the detour. /// public required MonoModDetourModelType Type { get; set; } - + /// - /// The of the assembly that contains the patch. + /// The method that is doing the detour. /// - public required string? AssemblyName { get; set; } + public required MethodSimple? Method { get; set; } /// /// @@ -33,17 +34,18 @@ public sealed record MonoModDetourModel /// /// /// - public required IReadOnlyList Before { get; set; } = new List(); + public required IList Before { get; set; } = new List(); /// /// /// /// - public required IReadOnlyList After { get; set; } = new List(); + public required IList After { get; set; } = new List(); /// /// /// /// - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); -} \ No newline at end of file + public required IList AdditionalMetadata { get; set; } = new List(); +} +*/ \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/MonoModDetourModelType.cs b/src/BUTR.CrashReport.Models/MonoModDetourModelType.cs index e8893ef..c8a3142 100644 --- a/src/BUTR.CrashReport.Models/MonoModDetourModelType.cs +++ b/src/BUTR.CrashReport.Models/MonoModDetourModelType.cs @@ -1,3 +1,4 @@ +/* namespace BUTR.CrashReport.Models; /// @@ -14,4 +15,5 @@ public enum MonoModDetourModelType /// IL Hook /// ILHook, -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/MonoModDetoursModel.cs b/src/BUTR.CrashReport.Models/MonoModDetoursModel.cs index 8899eeb..75d151b 100644 --- a/src/BUTR.CrashReport.Models/MonoModDetoursModel.cs +++ b/src/BUTR.CrashReport.Models/MonoModDetoursModel.cs @@ -1,3 +1,4 @@ +/* using System.Collections.Generic; namespace BUTR.CrashReport.Models; @@ -20,11 +21,12 @@ public sealed record MonoModDetoursModel /// /// The list of MonoMod detours. /// - public required IReadOnlyList Detours { get; set; } = new List(); + public required IList Detours { get; set; } = new List(); /// /// /// /// - public required IReadOnlyList AdditionalMetadata { get; set; } = new List(); -} \ No newline at end of file + public required IList AdditionalMetadata { get; set; } = new List(); +} +*/ \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/UpdateInfoModuleOrLoaderPlugin.cs b/src/BUTR.CrashReport.Models/UpdateInfoModuleOrLoaderPlugin.cs new file mode 100644 index 0000000..80eca02 --- /dev/null +++ b/src/BUTR.CrashReport.Models/UpdateInfoModuleOrLoaderPlugin.cs @@ -0,0 +1,20 @@ +namespace BUTR.CrashReport.Models; + +/// +/// Represents the module or loader plugin update information. +/// +public sealed record UpdateInfoModuleOrLoaderPlugin +{ + /// + /// The provider of the update. Can be 'NexusMods' or 'GitHub', for example. + /// + public required string Provider { get; set; } + + /// + /// The value of the update. Can be the mod ID for NexusMods or the 'user/repo' for GitHub, for example. + /// + public required string Value { get; set; } + + /// + public override string ToString() => $"{Provider}:{Value}"; +} \ No newline at end of file diff --git a/src/BUTR.CrashReport.Models/Utils/AssemblyUtils.cs b/src/BUTR.CrashReport.Models/Utils/AssemblyUtils.cs new file mode 100644 index 0000000..3a703ea --- /dev/null +++ b/src/BUTR.CrashReport.Models/Utils/AssemblyUtils.cs @@ -0,0 +1,18 @@ +using System; +using System.Globalization; + +namespace BUTR.CrashReport.Models.Utils; + +/// +/// Provides the assembly utilities. +/// +public static class AssemblyUtils +{ + /// + /// Gets the public key token as string. + /// + /// The public key token. + /// The public key token as string. + public static string PublicKeyAsString(byte[]? publicKeyToken) => + string.Join(string.Empty, Array.ConvertAll(publicKeyToken ?? Array.Empty(), x => x.ToString("x2", CultureInfo.InvariantCulture))); +} \ No newline at end of file diff --git a/src/BUTR.CrashReport.sln b/src/BUTR.CrashReport.sln index ac23394..e681e60 100644 --- a/src/BUTR.CrashReport.sln +++ b/src/BUTR.CrashReport.sln @@ -30,6 +30,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BUTR.CrashReport.Models", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BUTR.CrashReport.Decompilers", "BUTR.CrashReport.Decompilers\BUTR.CrashReport.Decompilers.csproj", "{CE6956D6-1C78-45E3-995E-93CE49BDA524}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BUTR.CrashReport.BepInEx5.Source", "BUTR.CrashReport.BepInEx5.Source\BUTR.CrashReport.BepInEx5.Source.csproj", "{3A6C3D77-82EA-4B83-BE30-22D0EE3A230F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BUTR.CrashReport.BepInEx6.Source", "BUTR.CrashReport.BepInEx6.Source\BUTR.CrashReport.BepInEx6.Source.csproj", "{D8AB3D2C-6026-4ACA-BFBB-A341E4D913AD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -60,6 +64,14 @@ Global {CE6956D6-1C78-45E3-995E-93CE49BDA524}.Debug|Any CPU.Build.0 = Debug|Any CPU {CE6956D6-1C78-45E3-995E-93CE49BDA524}.Release|Any CPU.ActiveCfg = Release|Any CPU {CE6956D6-1C78-45E3-995E-93CE49BDA524}.Release|Any CPU.Build.0 = Release|Any CPU + {3A6C3D77-82EA-4B83-BE30-22D0EE3A230F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A6C3D77-82EA-4B83-BE30-22D0EE3A230F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A6C3D77-82EA-4B83-BE30-22D0EE3A230F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A6C3D77-82EA-4B83-BE30-22D0EE3A230F}.Release|Any CPU.Build.0 = Release|Any CPU + {D8AB3D2C-6026-4ACA-BFBB-A341E4D913AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8AB3D2C-6026-4ACA-BFBB-A341E4D913AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8AB3D2C-6026-4ACA-BFBB-A341E4D913AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8AB3D2C-6026-4ACA-BFBB-A341E4D913AD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -71,9 +83,10 @@ Global {B1100D38-D4C9-4B83-96BC-2252449A89E5} = {8013DEFB-F940-4891-9F74-BFE1A8106506} {80698068-7B95-487A-9DD8-E02DE27127AC} = {8013DEFB-F940-4891-9F74-BFE1A8106506} {51A4BB52-A8E0-4BC3-8476-23E272413954} = {8013DEFB-F940-4891-9F74-BFE1A8106506} - {8D496C6F-420E-4F8C-A0FE-C835AAE4585D} = {8013DEFB-F940-4891-9F74-BFE1A8106506} {36A642D2-9075-470D-B5B4-40EA20E0C16C} = {8013DEFB-F940-4891-9F74-BFE1A8106506} {03DE899E-3EE2-4EA4-8D5D-188C1AB83365} = {8013DEFB-F940-4891-9F74-BFE1A8106506} {CE6956D6-1C78-45E3-995E-93CE49BDA524} = {8013DEFB-F940-4891-9F74-BFE1A8106506} + {3A6C3D77-82EA-4B83-BE30-22D0EE3A230F} = {8013DEFB-F940-4891-9F74-BFE1A8106506} + {D8AB3D2C-6026-4ACA-BFBB-A341E4D913AD} = {8013DEFB-F940-4891-9F74-BFE1A8106506} EndGlobalSection EndGlobal diff --git a/src/BUTR.CrashReport/BUTR.CrashReport.csproj b/src/BUTR.CrashReport/BUTR.CrashReport.csproj index ef70fb4..84461ff 100644 --- a/src/BUTR.CrashReport/BUTR.CrashReport.csproj +++ b/src/BUTR.CrashReport/BUTR.CrashReport.csproj @@ -10,16 +10,14 @@ Debug;Release - false + true true true false - $(PkgBUTR_ILRepack)\tools\net461\ILRepack.exe - - - + + @@ -32,30 +30,31 @@ - + + - + + - - + + - + - + $(ILRepackExcludeAssemblies);$(ProjectDir)$(OutputPath)0Harmony.dll; - $(ILRepackExcludeAssemblies);$(ProjectDir)$(OutputPath)BUTR.CrashReport.Decompilers.dll; $(ILRepackExcludeAssemblies);$(ProjectDir)$(OutputPath)BUTR.CrashReport.Models.dll; diff --git a/src/BUTR.CrashReport/CrashReportInfo.cs b/src/BUTR.CrashReport/CrashReportInfo.cs index 2ad0c90..ad93e9c 100644 --- a/src/BUTR.CrashReport/CrashReportInfo.cs +++ b/src/BUTR.CrashReport/CrashReportInfo.cs @@ -1,4 +1,6 @@ -using HarmonyLib; +using BUTR.CrashReport.Interfaces; +using BUTR.CrashReport.Models; +using BUTR.CrashReport.Utils; using System; using System.Collections.Generic; @@ -6,153 +8,60 @@ using System.Linq; using System.Reflection; -using static BUTR.CrashReport.Utils.MethodDecompiler; -using static BUTR.CrashReport.Utils.MonoModUtils; -using static BUTR.CrashReport.Utils.ReferenceImporter; +using static BUTR.CrashReport.Decompilers.Utils.ReferenceImporter; namespace BUTR.CrashReport; /// -/// -/// -public record AssemblyTypeReference -{ - /// - /// - /// - /// - public required string Name { get; set; } - - /// - /// - /// - /// - public required string Namespace { get; set; } - - /// - /// - /// - /// - public required string FullName { get; set; } -} - -/// -/// Represents a Harmony patch. -/// -public record MethodEntry -{ - /// - /// The Harmony patch method. - /// - public required MethodBase Method { get; set; } - - /// - /// - /// - /// - public required IModuleInfo? ModuleInfo { get; set; } - - /// - /// - /// - /// - public required string[] ILInstructions { get; set; } - - /// - /// - /// - /// - public required string[] CSharpILMixedInstructions { get; set; } - - /// - /// - /// - /// - public required string[] CSharpInstructions { get; set; } -} - -/// -/// +/// The initial crash report info to be converted into the POCO /// -public record StacktraceEntry +public class CrashReportInfo { /// - /// - /// - /// - public required MethodBase Method { get; set; } - - /// - /// - /// - /// - public required MethodEntry? OriginalMethod { get; set; } - - /// - /// - /// - /// - public required bool MethodFromStackframeIssue { get; set; } - - /// - /// The module that holds the method. Can be null. - /// - public required IModuleInfo? ModuleInfo { get; set; } - - /// - /// - /// - /// - public required int? ILOffset { get; set; } - - /// - /// - /// - /// - public required int? NativeOffset { get; set; } - - /// - /// - /// - /// - public required string StackFrameDescription { get; set; } - - /// - /// + /// Converts the CrashReportInfo into a CrashReportModel /// - /// - public required string[] NativeInstructions { get; set; } + public static CrashReportModel ToModel(CrashReportInfo crashReport, + ICrashReportMetadataProvider crashReportMetadataProvider, + IModelConverter modelConverter, + IModuleProvider moduleProvider, + ILoaderPluginProvider loaderPluginProvider, + IAssemblyUtilities assemblyUtilities, + IPathAnonymizer pathAnonymizer) + { + var assemblies = CrashReportModelUtils.GetAssemblies(crashReport, assemblyUtilities, pathAnonymizer); + var modules = modelConverter.ToModuleModels(crashReport.LoadedModules, assemblies); + var plugins = modelConverter.ToLoaderPluginModels(crashReport.LoadedLoaderPlugins, assemblies); + var metadata = crashReportMetadataProvider.GetCrashReportMetadataModel(crashReport); + metadata.Runtime ??= System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription; + return new CrashReportModel + { + Id = crashReport.Id, + Version = crashReport.Version, + Exception = CrashReportModelUtils.GetRecursiveException(crashReport, assemblies), + EnhancedStacktrace = CrashReportModelUtils.GetEnhancedStacktrace(crashReport, assemblies), + InvolvedModules = CrashReportModelUtils.GetInvolvedModules(crashReport), + Modules = modules, + Assemblies = assemblies, + HarmonyPatches = CrashReportModelUtils.GetHarmonyPatches(crashReport, assemblies, moduleProvider, loaderPluginProvider), + //MonoModDetours = Array.Empty(), + LoaderPlugins = plugins, + InvolvedLoaderPlugins = CrashReportModelUtils.GetInvolvedPlugins(crashReport), + Metadata = metadata, + AdditionalMetadata = Array.Empty(), + }; + } /// - /// - /// - /// - public required string[] ILInstructions { get; set; } - - /// - /// - /// - /// - public required string[] CSharpILMixedInstructions { get; set; } - - /// - /// + /// Creates the CrashReportInfo based on initial crash report data. /// - /// - public required string[] CSharpInstructions { get; set; } + public static CrashReportInfo Create(Exception exception, Dictionary additionalMetadata, + IStacktraceFilter stacktraceFilter, + IAssemblyUtilities assemblyUtilities, + IModuleProvider moduleProvider, + ILoaderPluginProvider loaderPluginProvider, + IHarmonyProvider harmonyProvider) => + new(exception, additionalMetadata, stacktraceFilter, assemblyUtilities, moduleProvider, loaderPluginProvider, harmonyProvider); - /// - /// - /// - /// - public required MethodEntry[] PatchMethods { get; set; } -} - -/// -/// The initial crash report info to be converted into the POCO -/// -public class CrashReportInfo -{ /// /// /// @@ -187,6 +96,12 @@ public class CrashReportInfo /// public ICollection LoadedModules { get; } + /// + /// + /// + /// + public ICollection LoadedLoaderPlugins { get; } + /// /// Lookup dictionary for available assemblies. /// @@ -201,7 +116,7 @@ public class CrashReportInfo /// /// /// - public Dictionary LoadedHarmonyPatches { get; } = new(); + public Dictionary LoadedHarmonyPatches { get; } = new(); /// /// Additional metadata about the crash. @@ -211,16 +126,18 @@ public class CrashReportInfo /// /// Creates the CrashReportInfo based on initial crash report data. /// - /// The exception that caused the crash. - /// The interface implementation of the needed basic functions. - /// Any additional metadata to be passed to the CrashReportInfo. - public CrashReportInfo(Exception exception, ICrashReportHelper crashReportHelper, Dictionary additionalMetadata) + private CrashReportInfo(Exception exception, Dictionary additionalMetadata, + IStacktraceFilter stacktraceFilter, IAssemblyUtilities assemblyUtilities, + IModuleProvider moduleProvider, ILoaderPluginProvider loaderPluginProvider, IHarmonyProvider harmonyProvider) { + var assemblies = assemblyUtilities.Assemblies().ToArray(); + Exception = exception.Demystify(); AdditionalMetadata = additionalMetadata; - LoadedModules = crashReportHelper.GetLoadedModules().ToArray(); + LoadedModules = moduleProvider.GetLoadedModules(); + LoadedLoaderPlugins = loaderPluginProvider.GetLoadedLoaderPlugins(); - AvailableAssemblies = crashReportHelper.Assemblies().ToDictionary(x => x.GetName(), x => x); + AvailableAssemblies = assemblies.ToDictionary(x => x.GetName(), x => x); ImportedTypeReferences = GetImportedTypeReferences(AvailableAssemblies).ToDictionary(x => x.Key, x => x.Value.Select(y => new AssemblyTypeReference { Name = y.Name, @@ -228,149 +145,14 @@ public CrashReportInfo(Exception exception, ICrashReportHelper crashReportHelper FullName = y.FullName }).ToArray()); - Stacktrace = GetAllInvolvedModules(Exception, crashReportHelper).ToArray(); - FilteredStacktrace = crashReportHelper.Filter(Stacktrace).ToArray(); + Stacktrace = CrashReportUtils.GetAllInvolvedModules(Exception, assemblies, moduleProvider, loaderPluginProvider, harmonyProvider).ToArray(); + FilteredStacktrace = stacktraceFilter.Filter(Stacktrace).ToArray(); - foreach (var originalMethod in Harmony.GetAllPatchedMethods()) + foreach (var originalMethod in harmonyProvider.GetAllPatchedMethods()) { - var patches = Harmony.GetPatchInfo(originalMethod); + var patches = harmonyProvider.GetPatchInfo(originalMethod); if (originalMethod is null || patches is null) continue; LoadedHarmonyPatches.Add(originalMethod, patches); } } - - private static IEnumerable GetAllInvolvedModules(Exception ex, ICrashReportHelper crashReportHelper) - { - static IEnumerable<(MethodBase, IModuleInfo)> GetPatches(Patches? info, ICrashReportHelper moduleHelper) - { - if (info is null) - yield break; - - var patchMethods = info.Prefixes.OrderBy(t => t.priority).Select(t => t.PatchMethod) - .Concat(info.Postfixes.OrderBy(t => t.priority).Select(t => t.PatchMethod)) - .Concat(info.Transpilers.OrderBy(t => t.priority).Select(t => t.PatchMethod)) - .Concat(info.Finalizers.OrderBy(t => t.priority).Select(t => t.PatchMethod)); - - foreach (var method in patchMethods) - { - if (method.DeclaringType is { } declaringType && moduleHelper.GetModuleByType(declaringType) is { } moduleInfo) - yield return (method, moduleInfo); - } - } - - static IModuleInfo? GetModuleInfoIfMod(MethodBase? method, ICrashReportHelper moduleHelper) - { - if (method is null) - return null; - - if (method.DeclaringType is { Assembly.IsDynamic: false }) - return moduleHelper.GetModuleByType(method.DeclaringType); - - // The lambda methods don't have an owner, so we can't know who's the lambda creator - if (method.DeclaringType is null && method.Name == "lambda_method") - return null; - - // Patches contain as their name the full name of the method, including type and namespace - // This is not possible - if (method.DeclaringType is null && method.Name.Contains('.')) - { - var methodName = method.Name.Split('(')[0]; - var patchPostfix = methodName.Split(new[] { "_Patch" }, StringSplitOptions.None); - - if (!patchPostfix.Last().All(char.IsDigit)) - return null; - - var fullMethodName = string.Join("", patchPostfix.Take(patchPostfix.Length - 1)); - var foundMethod = moduleHelper.Assemblies().Where(x => !x.IsDynamic) - .SelectMany(x => x.DefinedTypes) - .Where(x => !x.IsAbstract) - .SelectMany(x => x.DeclaredMethods) - .Where(x => x.DeclaringType is not null) - .FirstOrDefault(x => fullMethodName == $"{x.DeclaringType!.FullName}.{x.Name}"); - - if (foundMethod is null) - return null; - - return moduleHelper.GetModuleByType(foundMethod.DeclaringType); - } - - return null; - } - - - var inner = ex.InnerException; - if (inner is not null) - { - foreach (var modInfo in GetAllInvolvedModules(inner, crashReportHelper)) - yield return modInfo; - } - - var trace = new EnhancedStackTrace(ex); - foreach (var frame in trace.GetFrames()) - { - if (!frame.HasMethod()) continue; - - MethodBase method; - var methodFromStackframeIssue = false; - try - { - method = Harmony.GetMethodFromStackframe(frame); - } - // NullReferenceException means the method was not found. Harmony doesn't handle this case gracefully - catch (NullReferenceException) - { - method = frame.GetMethod()!; - } - // The given generic instantiation was invalid. - // From what I understand, this will occur with generic methods - // Also when static constructors throw errors, Harmony resolution will fail - catch (Exception) - { - methodFromStackframeIssue = true; - method = frame.GetMethod()!; - } - - var methods = new List(); - var identifiableMethod = method is MethodInfo mi ? CurrentPlatformTriple?.Invoke() is { } cpt && GetIdentifiable?.Invoke(cpt, mi) is MethodInfo v ? v : mi : null; - var original = identifiableMethod is not null ? Harmony.GetOriginalMethod(identifiableMethod) : null; - var patches = original is not null ? Harmony.GetPatchInfo(original) : null; - - foreach (var (methodBase, extendedModuleInfo) in GetPatches(patches, crashReportHelper)) - { - methods.Add(new() - { - Method = methodBase, - ModuleInfo = extendedModuleInfo, - ILInstructions = DecompileILCode(methodBase), - CSharpILMixedInstructions = DecompileILWithCSharpCode(methodBase), - CSharpInstructions = DecompileCSharpCode(methodBase), - }); - } - - var ilOffset = frame.GetILOffset(); - var nativeILOffset = frame.GetNativeOffset(); - yield return new() - { - Method = identifiableMethod!, - OriginalMethod = original is not null ? new() - { - Method = original, - ModuleInfo = GetModuleInfoIfMod(original, crashReportHelper), - ILInstructions = DecompileILCode(original), - CSharpILMixedInstructions = DecompileILWithCSharpCode(original), - CSharpInstructions = DecompileCSharpCode(original), - } : null, - MethodFromStackframeIssue = methodFromStackframeIssue, - ModuleInfo = GetModuleInfoIfMod(identifiableMethod, crashReportHelper), - ILOffset = ilOffset != StackFrame.OFFSET_UNKNOWN ? ilOffset : null, - NativeOffset = nativeILOffset != StackFrame.OFFSET_UNKNOWN ? nativeILOffset : null, - StackFrameDescription = frame.ToString(), - NativeInstructions = DecompileNativeCode(identifiableMethod, nativeILOffset), - ILInstructions = DecompileILCode(identifiableMethod), - CSharpILMixedInstructions = DecompileILWithCSharpCode(identifiableMethod), - CSharpInstructions = DecompileCSharpCode(identifiableMethod), - PatchMethods = methods.ToArray(), - }; - } - } } \ No newline at end of file diff --git a/src/BUTR.CrashReport/Extensions/AssemblyImportedReferenceModelExtensions.cs b/src/BUTR.CrashReport/Extensions/AssemblyImportedReferenceModelExtensions.cs index 518660c..0d2d439 100644 --- a/src/BUTR.CrashReport/Extensions/AssemblyImportedReferenceModelExtensions.cs +++ b/src/BUTR.CrashReport/Extensions/AssemblyImportedReferenceModelExtensions.cs @@ -1,5 +1,5 @@ +using BUTR.CrashReport.Decompilers.Utils; using BUTR.CrashReport.Models; -using BUTR.CrashReport.Utils; using System; using System.Globalization; diff --git a/src/BUTR.CrashReport/Extensions/AssemblyModelExtensions.cs b/src/BUTR.CrashReport/Extensions/AssemblyModelExtensions.cs index e568566..24784ab 100644 --- a/src/BUTR.CrashReport/Extensions/AssemblyModelExtensions.cs +++ b/src/BUTR.CrashReport/Extensions/AssemblyModelExtensions.cs @@ -1,5 +1,5 @@ +using BUTR.CrashReport.Decompilers.Utils; using BUTR.CrashReport.Models; -using BUTR.CrashReport.Utils; namespace BUTR.CrashReport.Extensions; @@ -13,5 +13,5 @@ public static class AssemblyModelExtensions /// /// public static string GetFullName(this AssemblyModel model) => - AssemblyNameFormatter.ComputeDisplayName(model.Name, model.Version, model.Culture, model.PublicKeyToken); + AssemblyNameFormatter.ComputeDisplayName(model.Id.Name, model.Id.Version, model.CultureName, model.Id.PublicKeyToken); } \ No newline at end of file diff --git a/src/BUTR.CrashReport/Extensions/ModuleModelExtensions.cs b/src/BUTR.CrashReport/Extensions/ModuleModelExtensions.cs index 29e8e1e..d53aaa6 100644 --- a/src/BUTR.CrashReport/Extensions/ModuleModelExtensions.cs +++ b/src/BUTR.CrashReport/Extensions/ModuleModelExtensions.cs @@ -1,5 +1,5 @@ -using BUTR.CrashReport.Models; -using BUTR.CrashReport.Utils; +using BUTR.CrashReport.Decompilers.Utils; +using BUTR.CrashReport.Models; using System.Collections.Generic; using System.Linq; @@ -30,4 +30,24 @@ public static bool ContainsAssemblyReferences(this ModuleModel model, IEnumerabl public static bool ContainsTypeReferences(this ModuleModel model, IEnumerable assemblies, string[] typeReferences) => assemblies.Where(x => x.ModuleId == model.Id) .SelectMany(x => x.ImportedTypeReferences) .Any(x => typeReferences.Any(y => FileSystemName.MatchesSimpleExpression(y, x.FullName))); + + /// + /// Gets whether the module contains an assembly reference. + /// + /// + /// The list of available assemblies + /// The assembly references to search for. Supports wildcard + public static bool ContainsAssemblyReferences(this LoaderPluginModel model, IEnumerable assemblies, string[] assemblyReferences) => assemblies.Where(x => x.LoaderPluginId == model.Id) + .SelectMany(x => x.ImportedAssemblyReferences) + .Any(x => assemblyReferences.Any(y => FileSystemName.MatchesSimpleExpression(y, x.Name))); + + /// + /// Gets whether the module contains an type reference. + /// + /// + /// The list of available assemblies + /// The type references to search for. Supports wildcard + public static bool ContainsTypeReferences(this LoaderPluginModel model, IEnumerable assemblies, string[] typeReferences) => assemblies.Where(x => x.LoaderPluginId == model.Id) + .SelectMany(x => x.ImportedTypeReferences) + .Any(x => typeReferences.Any(y => FileSystemName.MatchesSimpleExpression(y, x.FullName))); } \ No newline at end of file diff --git a/src/BUTR.CrashReport/ICrashReportHelper.cs b/src/BUTR.CrashReport/ICrashReportHelper.cs deleted file mode 100644 index 58583fa..0000000 --- a/src/BUTR.CrashReport/ICrashReportHelper.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; - -namespace BUTR.CrashReport; - -/// -/// Exposes the basic function needed for the Crash Report Creator -/// -public interface ICrashReportHelper -{ - /// - /// Filters out stack trace frames that are not relevant to the crash report. - /// - /// - /// - IEnumerable Filter(ICollection stacktraceEntries); - - /// - /// Provides the implementation for getting the loaded modules in the process. - /// - /// - IEnumerable GetLoadedModules(); - - /// - /// Provides the implementation for getting the module based on the type. - /// - /// - /// - IModuleInfo? GetModuleByType(Type? type); - - /// - /// Provides the implementation for getting the assemblies present in the process. - /// - /// - IEnumerable Assemblies(); -} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Interfaces/IAssemblyUtilities.cs b/src/BUTR.CrashReport/Interfaces/IAssemblyUtilities.cs new file mode 100644 index 0000000..e8f7aa4 --- /dev/null +++ b/src/BUTR.CrashReport/Interfaces/IAssemblyUtilities.cs @@ -0,0 +1,32 @@ +using BUTR.CrashReport.Models; + +using System.Collections.Generic; +using System.Reflection; + +namespace BUTR.CrashReport.Interfaces; + +/// +/// Provides functionality related to assemblies. +/// +public interface IAssemblyUtilities +{ + /// + /// Provides the implementation for getting the assemblies present in the process. + /// + IEnumerable Assemblies(); + + /// + /// Gets the module for the assembly if there is one + /// + IModuleInfo? GetAssemblyModule(CrashReportInfo crashReport, Assembly assembly); + + /// + /// Gets the module for the assembly if there is one + /// + ILoaderPluginInfo? GetAssemblyPlugin(CrashReportInfo crashReport, Assembly assembly); + + /// + /// Gets the type of the assembly + /// + AssemblyModelType GetAssemblyType(AssemblyModelType type, CrashReportInfo crashReport, Assembly assembly); +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Interfaces/ICrashReportMetadataProvider.cs b/src/BUTR.CrashReport/Interfaces/ICrashReportMetadataProvider.cs new file mode 100644 index 0000000..8e5694d --- /dev/null +++ b/src/BUTR.CrashReport/Interfaces/ICrashReportMetadataProvider.cs @@ -0,0 +1,14 @@ +using BUTR.CrashReport.Models; + +namespace BUTR.CrashReport.Interfaces; + +/// +/// Provides metadata for a crash report. +/// +public interface ICrashReportMetadataProvider +{ + /// + /// Gets the metadata for a crash report. + /// + CrashReportMetadataModel GetCrashReportMetadataModel(CrashReportInfo crashReport); +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Interfaces/IHarmonyProvider.cs b/src/BUTR.CrashReport/Interfaces/IHarmonyProvider.cs new file mode 100644 index 0000000..a2d5a27 --- /dev/null +++ b/src/BUTR.CrashReport/Interfaces/IHarmonyProvider.cs @@ -0,0 +1,35 @@ +using BUTR.CrashReport.Models; + +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; + +namespace BUTR.CrashReport.Interfaces; + +/// +/// Provides information about Harmony patches. +/// +public interface IHarmonyProvider +{ + /// + /// Returns all patched methods. + /// + IEnumerable GetAllPatchedMethods(); + + /// + /// Returns the patch information for a given method. + /// + /// + HarmonyPatches? GetPatchInfo(MethodBase originalMethod); + + /// + /// Returns the original method for a given patch method. + /// + /// The patch method + MethodBase? GetOriginalMethod(MethodInfo replacement); + + /// + /// Returns the method from a stackframe. + /// + MethodBase? GetMethodFromStackframe(StackFrame frame); +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Interfaces/ILoaderPluginProvider.cs b/src/BUTR.CrashReport/Interfaces/ILoaderPluginProvider.cs new file mode 100644 index 0000000..a31510a --- /dev/null +++ b/src/BUTR.CrashReport/Interfaces/ILoaderPluginProvider.cs @@ -0,0 +1,22 @@ +using BUTR.CrashReport.Models; + +using System; +using System.Collections.Generic; + +namespace BUTR.CrashReport.Interfaces; + +/// +/// Represents the loader plugin information. +/// +public interface ILoaderPluginProvider +{ + /// + /// Provides the implementation for getting the loaded loader plugin in the process. + /// + ICollection GetLoadedLoaderPlugins(); + + /// + /// Provides the implementation for getting the loader plugin based on the type. + /// + ILoaderPluginInfo? GetLoaderPluginByType(Type? type); +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Interfaces/IModelConverter.cs b/src/BUTR.CrashReport/Interfaces/IModelConverter.cs new file mode 100644 index 0000000..f24fff1 --- /dev/null +++ b/src/BUTR.CrashReport/Interfaces/IModelConverter.cs @@ -0,0 +1,21 @@ +using BUTR.CrashReport.Models; + +using System.Collections.Generic; + +namespace BUTR.CrashReport.Interfaces; + +/// +/// Converts the data interfaces to models. +/// +public interface IModelConverter +{ + /// + /// Converts the loaded modules to module models. + /// + List ToModuleModels(ICollection loadedModules, ICollection assemblies); + + /// + /// Converts the loaded assemblies to assembly models. + /// + List ToLoaderPluginModels(ICollection loadedLoaderPlugins, ICollection assemblies); +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Interfaces/IModuleProvider.cs b/src/BUTR.CrashReport/Interfaces/IModuleProvider.cs new file mode 100644 index 0000000..f1776a9 --- /dev/null +++ b/src/BUTR.CrashReport/Interfaces/IModuleProvider.cs @@ -0,0 +1,23 @@ +using BUTR.CrashReport.Models; + +using System; +using System.Collections.Generic; + +namespace BUTR.CrashReport.Interfaces; + +/// +/// Provides the implementation for getting the module information. +/// +public interface IModuleProvider +{ + /// + /// Provides the implementation for getting the loaded modules in the process. + /// + /// + ICollection GetLoadedModules(); + + /// + /// Provides the implementation for getting the module based on the type. + /// + IModuleInfo? GetModuleByType(Type? type); +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Interfaces/IPathAnonymizer.cs b/src/BUTR.CrashReport/Interfaces/IPathAnonymizer.cs new file mode 100644 index 0000000..2be9745 --- /dev/null +++ b/src/BUTR.CrashReport/Interfaces/IPathAnonymizer.cs @@ -0,0 +1,12 @@ +namespace BUTR.CrashReport.Interfaces; + +/// +/// Anonymizes paths. +/// +public interface IPathAnonymizer +{ + /// + /// Tries to handle the path and return an anonymized version of it. + /// + bool TryHandlePath(string path, out string anonymizedPath); +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Interfaces/IStacktraceFilter.cs b/src/BUTR.CrashReport/Interfaces/IStacktraceFilter.cs new file mode 100644 index 0000000..d8ac760 --- /dev/null +++ b/src/BUTR.CrashReport/Interfaces/IStacktraceFilter.cs @@ -0,0 +1,16 @@ +using BUTR.CrashReport.Models; + +using System.Collections.Generic; + +namespace BUTR.CrashReport.Interfaces; + +/// +/// Represents a filter that can be used to filter out irrelevant stack trace frames from a crash report. +/// +public interface IStacktraceFilter +{ + /// + /// Filters out stack trace frames that are not relevant to the crash report. + /// + IEnumerable Filter(ICollection stacktraceEntries); +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Models/AssemblyTypeReference.cs b/src/BUTR.CrashReport/Models/AssemblyTypeReference.cs new file mode 100644 index 0000000..27a6cc6 --- /dev/null +++ b/src/BUTR.CrashReport/Models/AssemblyTypeReference.cs @@ -0,0 +1,25 @@ +namespace BUTR.CrashReport.Models; + +/// +/// +/// +public record AssemblyTypeReference +{ + /// + /// + /// + /// + public required string Name { get; set; } + + /// + /// + /// + /// + public required string Namespace { get; set; } + + /// + /// + /// + /// + public required string FullName { get; set; } +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Models/HarmonyPatch.cs b/src/BUTR.CrashReport/Models/HarmonyPatch.cs new file mode 100644 index 0000000..ec59ad4 --- /dev/null +++ b/src/BUTR.CrashReport/Models/HarmonyPatch.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Reflection; + +namespace BUTR.CrashReport.Models; + +/// +/// +/// +public record HarmonyPatch +{ + /// + /// + /// + public required string Owner { get; set; } + + /// + /// + /// + public required int Index { get; set; } + + /// + /// + /// + public required int Priority { get; set; } + + /// + /// + /// + public required IList Before { get; set; } = new List(); + + /// + /// + /// + public required IList After { get; set; } = new List(); + + /// + /// + /// + public required MethodInfo PatchMethod { get; set; } + + /// + /// + /// + public required HarmonyPatchType Type { get; set; } +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Models/HarmonyPatches.cs b/src/BUTR.CrashReport/Models/HarmonyPatches.cs new file mode 100644 index 0000000..ff1bbb9 --- /dev/null +++ b/src/BUTR.CrashReport/Models/HarmonyPatches.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace BUTR.CrashReport.Models; + +/// +/// +/// +public record HarmonyPatches +{ + /// + /// + /// + public required IList Prefixes { get; set; } + + /// + /// + /// + public required IList Postfixes { get; set; } + + /// + /// + /// + public required IList Finalizers { get; set; } + + /// + /// + /// + public required IList Transpilers { get; set; } +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Models/ILoaderPluginInfo.cs b/src/BUTR.CrashReport/Models/ILoaderPluginInfo.cs new file mode 100644 index 0000000..3ed5f30 --- /dev/null +++ b/src/BUTR.CrashReport/Models/ILoaderPluginInfo.cs @@ -0,0 +1,25 @@ +namespace BUTR.CrashReport.Models; + +/// +/// Represents a loader plugin. +/// +public interface ILoaderPluginInfo +{ + /// + /// + /// + /// + string Id { get; } + + /// + /// + /// + /// + string? Version { get; } + + /// + /// + /// + /// + string? UpdateInfo { get; } +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/IModuleInfo.cs b/src/BUTR.CrashReport/Models/IModuleInfo.cs similarity index 96% rename from src/BUTR.CrashReport/IModuleInfo.cs rename to src/BUTR.CrashReport/Models/IModuleInfo.cs index 82ed0f4..9373b39 100644 --- a/src/BUTR.CrashReport/IModuleInfo.cs +++ b/src/BUTR.CrashReport/Models/IModuleInfo.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace BUTR.CrashReport; +namespace BUTR.CrashReport.Models; /// /// Represents a module. diff --git a/src/BUTR.CrashReport/IModuleSubModuleInfo.cs b/src/BUTR.CrashReport/Models/IModuleSubModuleInfo.cs similarity index 50% rename from src/BUTR.CrashReport/IModuleSubModuleInfo.cs rename to src/BUTR.CrashReport/Models/IModuleSubModuleInfo.cs index ccb9049..1e041ee 100644 --- a/src/BUTR.CrashReport/IModuleSubModuleInfo.cs +++ b/src/BUTR.CrashReport/Models/IModuleSubModuleInfo.cs @@ -1,4 +1,4 @@ -namespace BUTR.CrashReport; +namespace BUTR.CrashReport.Models; /// /// Represents a SubModule @@ -6,10 +6,10 @@ public interface IModuleSubModuleInfo { /// - /// + /// /// - /// - string AssemblyName { get; } + /// + string AssemblyFile { get; } /// /// The assemblies that are linked to the submodule diff --git a/src/BUTR.CrashReport/Models/MethodEntry.cs b/src/BUTR.CrashReport/Models/MethodEntry.cs new file mode 100644 index 0000000..675ee87 --- /dev/null +++ b/src/BUTR.CrashReport/Models/MethodEntry.cs @@ -0,0 +1,44 @@ +using System.Reflection; + +namespace BUTR.CrashReport.Models; + +/// +/// Represents a method. +/// +public abstract record MethodEntry +{ + /// + /// The Harmony patch method. + /// + public required MethodBase Method { get; set; } + + /// + /// + /// + /// + public required IModuleInfo? ModuleInfo { get; set; } + + /// + /// + /// + /// + public required ILoaderPluginInfo? LoaderPluginInfo { get; set; } + + /// + /// + /// + /// + public required string[] ILInstructions { get; set; } + + /// + /// + /// + /// + public required string[] CSharpILMixedInstructions { get; set; } + + /// + /// + /// + /// + public required string[] CSharpInstructions { get; set; } +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Models/MethodEntryHarmony.cs b/src/BUTR.CrashReport/Models/MethodEntryHarmony.cs new file mode 100644 index 0000000..02b4f6e --- /dev/null +++ b/src/BUTR.CrashReport/Models/MethodEntryHarmony.cs @@ -0,0 +1,12 @@ +namespace BUTR.CrashReport.Models; + +/// +/// Represents a harmony patch. +/// +public record MethodEntryHarmony : MethodEntry +{ + /// + /// The harmony patch. + /// + public required HarmonyPatch Patch { get; set; } +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Models/MethodEntrySimple.cs b/src/BUTR.CrashReport/Models/MethodEntrySimple.cs new file mode 100644 index 0000000..f626909 --- /dev/null +++ b/src/BUTR.CrashReport/Models/MethodEntrySimple.cs @@ -0,0 +1,6 @@ +namespace BUTR.CrashReport.Models; + +/// +/// Represents a method entry. +/// +public record MethodEntrySimple : MethodEntry; \ No newline at end of file diff --git a/src/BUTR.CrashReport/Models/StacktraceEntry.cs b/src/BUTR.CrashReport/Models/StacktraceEntry.cs new file mode 100644 index 0000000..fcbf190 --- /dev/null +++ b/src/BUTR.CrashReport/Models/StacktraceEntry.cs @@ -0,0 +1,85 @@ +using System.Reflection; + +namespace BUTR.CrashReport.Models; + +/// +/// +/// +public record StacktraceEntry +{ + /// + /// + /// + /// + public required MethodBase Method { get; set; } + + /// + /// + /// + /// + public required MethodEntrySimple? OriginalMethod { get; set; } + + /// + /// + /// + /// + public required bool MethodFromStackframeIssue { get; set; } + + /// + /// The module that holds the method. Can be null. + /// + public required IModuleInfo? ModuleInfo { get; set; } + + /// + /// The loader plugin that holds the method. Can be null. + /// + public required ILoaderPluginInfo? LoaderPluginInfo { get; set; } + + /// + /// + /// + /// + public required int? ILOffset { get; set; } + + /// + /// + /// + /// + public required int? NativeOffset { get; set; } + + /// + /// + /// + /// + public required string StackFrameDescription { get; set; } + + /// + /// + /// + /// + public required string[] NativeInstructions { get; set; } + + /// + /// + /// + /// + public required string[] ILInstructions { get; set; } + + /// + /// + /// + /// + public required string[] CSharpILMixedInstructions { get; set; } + + /// + /// + /// + /// + public required string[] CSharpInstructions { get; set; } + + /// + /// + /// + /// + public required MethodEntry[] PatchMethods { get; set; } +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Utils/Anonymizer.cs b/src/BUTR.CrashReport/Utils/Anonymizer.cs index 5645702..808e4db 100644 --- a/src/BUTR.CrashReport/Utils/Anonymizer.cs +++ b/src/BUTR.CrashReport/Utils/Anonymizer.cs @@ -3,7 +3,7 @@ namespace BUTR.CrashReport.Utils; /// -/// Provides various anonymization methods. +/// Provides various built-in anonymization methods. /// public static class Anonymizer { @@ -17,9 +17,6 @@ public static string AnonymizePath(string path) if (path.IndexOf("steamapps", StringComparison.OrdinalIgnoreCase) is var idxSteam and not -1) return path.Substring(idxSteam); - if (path.IndexOf("Mount & Blade II Bannerlord", StringComparison.OrdinalIgnoreCase) is var idxRoot and not -1) - return path.Substring(idxRoot); - if (path.IndexOf("Windows", StringComparison.OrdinalIgnoreCase) is var idxWindows and not -1) return path.Substring(idxWindows); diff --git a/src/BUTR.CrashReport/Utils/CrashReportModelUtils.cs b/src/BUTR.CrashReport/Utils/CrashReportModelUtils.cs new file mode 100644 index 0000000..60387b0 --- /dev/null +++ b/src/BUTR.CrashReport/Utils/CrashReportModelUtils.cs @@ -0,0 +1,335 @@ +using BUTR.CrashReport.Extensions; +using BUTR.CrashReport.Interfaces; +using BUTR.CrashReport.Models; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography; + +namespace BUTR.CrashReport.Utils; + +/// +/// Exposes the code we use to create the +/// +public static class CrashReportModelUtils +{ + /// + /// Returns the + /// + public static ExceptionModel GetRecursiveException(CrashReportInfo crashReport, IReadOnlyCollection assemblies) + { + static ExceptionModel GetRecursiveExceptionInternal(IReadOnlyCollection assemblies, Exception ex) + { + // TODO: Check if there are collisions + var assembly = assemblies.FirstOrDefault(y => y.Id.Name == ex.Source); + return new ExceptionModel + { + SourceAssemblyId = assembly?.Id, + SourceModuleId = assembly?.ModuleId, + SourceLoaderPluginId = assembly?.LoaderPluginId, + Type = ex.GetType().FullName ?? string.Empty, + Message = ex.Message, + CallStack = ex.StackTrace ?? string.Empty, + InnerException = ex.InnerException is not null ? GetRecursiveExceptionInternal(assemblies, ex.InnerException) : null, + AdditionalMetadata = Array.Empty(), + }; + } + + return GetRecursiveExceptionInternal(assemblies, crashReport.Exception); + } + + /// + /// Returns the + /// + public static List GetEnhancedStacktrace(CrashReportInfo crashReport, IReadOnlyCollection assemblies) + { + var enhancedStacktraceFrameModels = new List(); + foreach (var stacktrace in crashReport.Stacktrace.GroupBy(x => x.StackFrameDescription)) + { + foreach (var entry in stacktrace) + { + var methods = new List(entry.PatchMethods.Length); + foreach (var patchMethod in entry.PatchMethods) + { + var patchAssemblyName = entry.Method.DeclaringType?.Assembly.GetName(); + var patchAssembly = patchAssemblyName is not null ? assemblies.FirstOrDefault(x => x.Id.Equals(patchAssemblyName)) : null; + var methodSimple = new MethodSimple + { + AssemblyId = patchAssembly?.Id, + ModuleId = patchMethod.ModuleInfo?.Id, + LoaderPluginId = patchMethod.LoaderPluginInfo?.Id, + MethodDeclaredTypeName = patchMethod.Method.DeclaringType?.FullName, + MethodName = patchMethod.Method.Name, + MethodFullDescription = patchMethod.Method.FullDescription(), + MethodParameters = patchMethod.Method.GetParameters().Select(x => x.ParameterType.FullName ?? string.Empty).ToArray(), + ILInstructions = patchMethod.ILInstructions, + CSharpILMixedInstructions = patchMethod.CSharpILMixedInstructions, + CSharpInstructions = patchMethod.CSharpInstructions, + AdditionalMetadata = Array.Empty(), + }; + methods.Add(patchMethod switch + { + MethodEntryHarmony meh => new MethodHarmonyPatch(methodSimple, meh.Patch.Type), + _ => methodSimple + }); + } + + var executingAssemblyName = entry.Method.DeclaringType?.Assembly.GetName(); + var executingAssembly = executingAssemblyName is not null ? assemblies.FirstOrDefault(x => x.Id.Equals(executingAssemblyName)) : null; + + var originalAssemblyName = entry.OriginalMethod?.Method.DeclaringType?.Assembly.GetName(); + var originalAssembly = originalAssemblyName is not null ? assemblies.FirstOrDefault(x => x.Id.Equals(originalAssemblyName)) : null; + + // Do not reverse engineer copyrighted or flagged original assemblies + static bool IsProtected(AssemblyModel? assembly) => assembly is not null && + ((assembly.Type & AssemblyModelType.GameCore) != 0 || + (assembly.Type & AssemblyModelType.GameModule) != 0 || + (assembly.Type & AssemblyModelType.ProtectedFromDisassembly) != 0); + + var skipDisassemblyForOriginal = IsProtected(originalAssembly); + var skipDisassemblyForExecuting = IsProtected(originalAssembly) || IsProtected(executingAssembly); + + enhancedStacktraceFrameModels.Add(new() + { + FrameDescription = entry.StackFrameDescription, + ExecutingMethod = new() + { + AssemblyId = executingAssembly?.Id, + ModuleId = entry.ModuleInfo?.Id, + LoaderPluginId = entry.LoaderPluginInfo?.Id, + MethodDeclaredTypeName = entry.Method.DeclaringType?.FullName, + MethodName = entry.Method.Name, + MethodFullDescription = entry.Method.FullDescription(), + MethodParameters = entry.Method.GetParameters().Select(x => x.ParameterType.FullName ?? string.Empty).ToArray(), + NativeInstructions = entry.NativeInstructions, + ILInstructions = entry.ILInstructions, + CSharpILMixedInstructions = skipDisassemblyForExecuting ? Array.Empty() : entry.CSharpILMixedInstructions, + CSharpInstructions = skipDisassemblyForExecuting ? Array.Empty() : entry.CSharpInstructions, + AdditionalMetadata = Array.Empty(), + }, + OriginalMethod = entry.OriginalMethod is not null ? new() + { + AssemblyId = originalAssembly?.Id, + ModuleId = entry.OriginalMethod.ModuleInfo?.Id, + LoaderPluginId = entry.OriginalMethod.LoaderPluginInfo?.Id, + MethodDeclaredTypeName = entry.OriginalMethod.Method.DeclaringType?.FullName, + MethodName = entry.OriginalMethod.Method.Name, + MethodFullDescription = entry.OriginalMethod.Method.FullDescription(), + MethodParameters = entry.OriginalMethod.Method.GetParameters().Select(x => x.ParameterType.FullName ?? string.Empty).ToArray(), + ILInstructions = entry.OriginalMethod.ILInstructions, + CSharpILMixedInstructions = skipDisassemblyForOriginal ? Array.Empty() : entry.OriginalMethod.CSharpILMixedInstructions, + CSharpInstructions = skipDisassemblyForOriginal ? Array.Empty() : entry.OriginalMethod.CSharpInstructions, + AdditionalMetadata = Array.Empty() + } : null, + PatchMethods = methods, + ILOffset = entry.ILOffset, + NativeOffset = entry.NativeOffset, + MethodFromStackframeIssue = entry.MethodFromStackframeIssue, + AdditionalMetadata = Array.Empty(), + }); + } + } + return enhancedStacktraceFrameModels; + } + + /// + /// Returns the + /// + public static List GetInvolvedModules(CrashReportInfo crashReport) + { + var involvedModels = new List(); + foreach (var stacktrace in crashReport.FilteredStacktrace.GroupBy(m => m.ModuleInfo)) + { + var module = stacktrace.Key; + if (module is null) continue; + + involvedModels.Add(new() + { + ModuleOrLoaderPluginId = module.Id, + EnhancedStacktraceFrameName = stacktrace.Last().StackFrameDescription, + AdditionalMetadata = Array.Empty(), + }); + } + return involvedModels; + } + + /// + /// Returns the + /// + public static List GetInvolvedPlugins(CrashReportInfo crashReport) + { + var involvedPluginModels = new List(); + foreach (var stacktrace in crashReport.FilteredStacktrace.GroupBy(m => m.LoaderPluginInfo)) + { + var loaderPlugin = stacktrace.Key; + if (loaderPlugin is null) continue; + + involvedPluginModels.Add(new() + { + ModuleOrLoaderPluginId = loaderPlugin.Id, + EnhancedStacktraceFrameName = stacktrace.Last().StackFrameDescription, + AdditionalMetadata = Array.Empty(), + }); + } + return involvedPluginModels; + } + + /// + /// Returns the + /// + public static List GetAssemblies(CrashReportInfo crashReport, IAssemblyUtilities assemblyUtilities, IPathAnonymizer pathAnonymizer) + { + static bool IsGAC(Assembly assembly) + { + try + { + return assembly.GlobalAssemblyCache; + } + catch (Exception) { return false; } + } + static ProcessorArchitecture GetProcessorArchitecture(AssemblyName assemblyName) + { + try + { + return assemblyName.ProcessorArchitecture; + } + catch (Exception) { return ProcessorArchitecture.None; } + } + static bool IsProtectedFromDisassembly(Assembly assembly) + { + var attibutes = assembly.GetCustomAttributes(); + return attibutes.Any(x => x.Key == "ProtectedFromDisassembly" && !string.Equals(x.Value, "false", StringComparison.OrdinalIgnoreCase)); + } + + static string CalculateMD5(string filename) + { + using var md5 = MD5.Create(); + using var stream = File.OpenRead(filename); + var hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + + var assemblyModels = new List(crashReport.AvailableAssemblies.Count); + + var systemAssemblyDirectory = Path.GetDirectoryName(typeof(object).Assembly.Location); + foreach (var kv in crashReport.AvailableAssemblies) + { + var assemblyName = kv.Key; + var assembly = kv.Value; + + var type = AssemblyModelType.Unclassified; + + // TODO: On unity the system folder is the unity root folder. + // With unity thre is not system folder, so everything will classified as Game + // TODO: BepInEx detection + var isSystem = + (!assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location) && Path.GetDirectoryName(assembly.Location)?.Equals(systemAssemblyDirectory, StringComparison.Ordinal) == true) || + (assembly.GetCustomAttribute()?.Product == "Microsoft® .NET Framework") || + (assembly.GetCustomAttribute()?.Product == "Microsoft® .NET"); + + if (isSystem) type |= AssemblyModelType.System; + if (assembly.IsDynamic) type |= AssemblyModelType.Dynamic; + if (IsGAC(assembly)) type |= AssemblyModelType.GAC; + if (IsProtectedFromDisassembly(assembly)) type |= AssemblyModelType.ProtectedFromDisassembly; + + var module = assemblyUtilities.GetAssemblyModule(crashReport, assembly); + if (module is not null) type |= AssemblyModelType.Module; + var loaderPlugin = assemblyUtilities.GetAssemblyPlugin(crashReport, assembly); + if (loaderPlugin is not null) type |= AssemblyModelType.LoaderPlugin; + + type = assemblyUtilities.GetAssemblyType(type, crashReport, assembly); + + var anonymizedPath = !assembly.IsDynamic ? assembly.Location : string.Empty; + if (!assembly.IsDynamic && !pathAnonymizer.TryHandlePath(anonymizedPath, out anonymizedPath)) + anonymizedPath = Anonymizer.AnonymizePath(anonymizedPath); + + assemblyModels.Add(new() + { + Id = AssemblyIdModel.FromAssembly(assemblyName), + ModuleId = module?.Id, + LoaderPluginId = loaderPlugin?.Id, + CultureName = assemblyName.CultureName, + Architecture = GetProcessorArchitecture(assemblyName).ToString(), + Hash = assembly.IsDynamic || string.IsNullOrWhiteSpace(assembly.Location) || !File.Exists(assembly.Location) ? string.Empty : CalculateMD5(assembly.Location), + AnonymizedPath = assembly.IsDynamic ? "DYNAMIC" : string.IsNullOrWhiteSpace(assembly.Location) ? "EMPTY" : !File.Exists(assembly.Location) ? "MISSING" : anonymizedPath, + Type = type, + ImportedTypeReferences = (type & AssemblyModelType.System) == 0 + ? crashReport.ImportedTypeReferences.TryGetValue(assemblyName, out var values) ? values.Select(x => new AssemblyImportedTypeReferenceModel + { + Namespace = x.Namespace, + Name = x.Name, + FullName = x.FullName, + }).ToArray() : Array.Empty() + : Array.Empty(), + ImportedAssemblyReferences = (type & AssemblyModelType.System) == 0 + ? assembly.GetReferencedAssemblies().Select(AssemblyImportedReferenceModelExtensions.Create).ToArray() + : Array.Empty(), + AdditionalMetadata = Array.Empty(), + }); + } + + return assemblyModels; + } + + /// + /// Returns the + /// + public static List GetHarmonyPatches(CrashReportInfo crashReport, IReadOnlyCollection assemblies, IModuleProvider moduleProvider, ILoaderPluginProvider loaderPluginProvider) + { + var builder = new List(crashReport.LoadedHarmonyPatches.Count); + + static void AppendPatches(ICollection builder, HarmonyPatchType type, IEnumerable patches, IReadOnlyCollection assemblies, IModuleProvider moduleProvider, ILoaderPluginProvider loaderPluginProvider) + { + foreach (var patch in patches) + { + var assemblyId = patch.PatchMethod.DeclaringType?.Assembly.GetName() is { } asmName ? AssemblyIdModel.FromAssembly(asmName) : null; + var module = moduleProvider.GetModuleByType(patch.PatchMethod.DeclaringType); + var loaderPlugin = loaderPluginProvider.GetLoaderPluginByType(patch.PatchMethod.DeclaringType); + + builder.Add(new() + { + Type = type, + AssemblyId = assemblyId, + ModuleId = module?.Id, + LoaderPluginId = loaderPlugin?.Id, + Owner = patch.Owner, + Namespace = $"{patch.PatchMethod.DeclaringType!.FullName}.{patch.PatchMethod.Name}", + Index = patch.Index, + Priority = patch.Priority, + Before = patch.Before, + After = patch.After, + AdditionalMetadata = Array.Empty(), + }); + } + } + + foreach (var kv in crashReport.LoadedHarmonyPatches) + { + var originalMethod = kv.Key; + var patches = kv.Value; + + var patchBuilder = new List(patches.Prefixes.Count + patches.Postfixes.Count + patches.Finalizers.Count + patches.Transpilers.Count); + + AppendPatches(patchBuilder, HarmonyPatchType.Prefix, patches.Prefixes, assemblies, moduleProvider, loaderPluginProvider); + AppendPatches(patchBuilder, HarmonyPatchType.Postfix, patches.Postfixes, assemblies, moduleProvider, loaderPluginProvider); + AppendPatches(patchBuilder, HarmonyPatchType.Finalizer, patches.Finalizers, assemblies, moduleProvider, loaderPluginProvider); + AppendPatches(patchBuilder, HarmonyPatchType.Transpiler, patches.Transpilers, assemblies, moduleProvider, loaderPluginProvider); + + if (patchBuilder.Count > 0) + { + builder.Add(new() + { + OriginalMethodDeclaredTypeName = originalMethod.DeclaringType?.FullName, + OriginalMethodName = originalMethod.Name, + Patches = patchBuilder, + AdditionalMetadata = Array.Empty(), + }); + } + } + + return builder; + } +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Utils/CrashReportUtils.cs b/src/BUTR.CrashReport/Utils/CrashReportUtils.cs new file mode 100644 index 0000000..5fa0bd5 --- /dev/null +++ b/src/BUTR.CrashReport/Utils/CrashReportUtils.cs @@ -0,0 +1,275 @@ +using BUTR.CrashReport.Interfaces; +using BUTR.CrashReport.Models; + +using HarmonyLib; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +using static BUTR.CrashReport.Decompilers.Utils.MethodDecompiler; +using static BUTR.CrashReport.Decompilers.Utils.MonoModUtils; + +using HarmonyPatch = BUTR.CrashReport.Models.HarmonyPatch; + +namespace BUTR.CrashReport.Utils; + +/// +/// Exposes the code we use to create the +/// +public static class CrashReportUtils +{ + /// + /// + /// + public record StackframePatchData + { + /// + /// + /// + public required MethodBase? Original { get; set; } + + /// + /// + /// + public required MethodInfo? Replacement { get; set; } + + /// + /// + /// + public required List Patches { get; set; } + + /// + /// + /// + public required bool Issues { get; set; } + + /// + /// Deconstructs the object. + /// + public void Deconstruct(out MethodBase? original, out MethodInfo? replacement, out List patches, out bool issues) + { + original = Original; + replacement = Replacement; + patches = Patches; + issues = Issues; + } + } + + /// + /// Gets all involved modules in the exception stacktrace. + /// + public static IEnumerable<(HarmonyPatch, IModuleInfo?, ILoaderPluginInfo?)> GetHarmonyPatchMethods(HarmonyPatches? patches, IModuleProvider moduleProvider, ILoaderPluginProvider loaderPluginProvider) + { + if (patches is null) + yield break; + + var patchMethods = patches.Prefixes.OrderBy(t => t.Priority) + .Concat(patches.Postfixes.OrderBy(t => t.Priority)) + .Concat(patches.Transpilers.OrderBy(t => t.Priority)) + .Concat(patches.Finalizers.OrderBy(t => t.Priority)); + + foreach (var patch in patchMethods) + { + var method = patch.PatchMethod; + if (method.DeclaringType is not { } declaringType) + continue; + + var moduleInfo = moduleProvider.GetModuleByType(declaringType); + var loaderPluginInfo = loaderPluginProvider.GetLoaderPluginByType(declaringType); + + if (moduleInfo is not null || loaderPluginInfo is not null) + yield return (patch, moduleInfo, loaderPluginInfo); + } + } + + /// + /// Gets the module info if the method is from a mod. + /// + public static IModuleInfo? GetModuleInfoIfMod(MethodBase? method, IEnumerable assemblies, IModuleProvider moduleProvider) + { + if (method is null) + return null; + + if (method.DeclaringType is { Assembly.IsDynamic: false }) + return moduleProvider.GetModuleByType(method.DeclaringType); + + // The lambda methods don't have an owner, so we can't know who's the lambda creator + if (method.DeclaringType is null && method.Name == "lambda_method") + return null; + + // Patches contain as their name the full name of the method, including type and namespace + // This is not possible + if (method.DeclaringType is null && method.Name.Contains('.')) + { + var methodName = method.Name.Split('(')[0]; + var patchPostfix = methodName.Split(["_Patch"], StringSplitOptions.None); + + if (!patchPostfix.Last().All(char.IsDigit)) + return null; + + var fullMethodName = string.Join("", patchPostfix.Take(patchPostfix.Length - 1)); + var foundMethod = assemblies.Where(x => !x.IsDynamic) + .SelectMany(AccessTools.GetTypesFromAssembly) + .Where(x => !x.IsAbstract) + .Where(x => !string.IsNullOrEmpty(x.DeclaringType?.FullName) && fullMethodName.StartsWith(x.DeclaringType!.FullName)) + .SelectMany(x => x.GetMethods()) + .Where(x => x.DeclaringType is not null) + .FirstOrDefault(x => fullMethodName == $"{x.DeclaringType!.FullName}.{x.Name}"); + + if (foundMethod is null) + return null; + + return moduleProvider.GetModuleByType(foundMethod.DeclaringType); + } + + return null; + } + + /// + /// Gets the loader plugin if the method is from a mod. + /// + public static ILoaderPluginInfo? GetLoaderPluginIfMod(MethodBase? method, IEnumerable assemblies, ILoaderPluginProvider loaderPluginProvider) + { + if (method is null) + return null; + + if (method.DeclaringType is { Assembly.IsDynamic: false }) + return loaderPluginProvider.GetLoaderPluginByType(method.DeclaringType); + + // The lambda methods don't have an owner, so we can't know who's the lambda creator + if (method.DeclaringType is null && method.Name == "lambda_method") + return null; + + // Patches contain as their name the full name of the method, including type and namespace + // This is not possible + if (method.DeclaringType is null && method.Name.Contains('.')) + { + var methodName = method.Name.Split('(')[0]; + var patchPostfix = methodName.Split(["_Patch"], StringSplitOptions.None); + + if (!patchPostfix.Last().All(char.IsDigit)) + return null; + + var fullMethodName = string.Join("", patchPostfix.Take(patchPostfix.Length - 1)); + var foundMethod = assemblies.Where(x => !x.IsDynamic) + .SelectMany(AccessTools.GetTypesFromAssembly) + .Where(x => !x.IsAbstract) + .Where(x => !string.IsNullOrEmpty(x.DeclaringType?.FullName) && fullMethodName.StartsWith(x.DeclaringType!.FullName)) + .SelectMany(x => x.GetMethods()) + .FirstOrDefault(x => fullMethodName == $"{x.DeclaringType!.FullName}.{x.Name}"); + + if (foundMethod is null) + return null; + + return loaderPluginProvider.GetLoaderPluginByType(foundMethod.DeclaringType); + } + + return null; + } + + /// + /// Gets the Harmony data from the stackframe. + /// + public static StackframePatchData GetHarmonyData(StackFrame frame, IHarmonyProvider harmonyProvider, IModuleProvider moduleProvider, ILoaderPluginProvider loaderPluginProvider) + { + MethodBase? method; + var methodFromStackframeIssue = false; + try + { + method = harmonyProvider.GetMethodFromStackframe(frame); + } + // NullReferenceException means the method was not found. Harmony doesn't handle this case gracefully + catch (NullReferenceException e) + { + Trace.TraceError(e.ToString()); + method = frame.GetMethod()!; + } + // The given generic instantiation was invalid. + // From what I understand, this will occur with generic methods + // Also when static constructors throw errors, Harmony resolution will fail + catch (Exception e) + { + Trace.TraceError(e.ToString()); + methodFromStackframeIssue = true; + method = frame.GetMethod()!; + } + + var methods = new List(); + var identifiableMethod = method is MethodInfo mi ? GetIdentifiable(mi) is MethodInfo v ? v : mi : null; + var original = identifiableMethod is not null ? harmonyProvider.GetOriginalMethod(identifiableMethod) : null; + var patches = original is not null ? harmonyProvider.GetPatchInfo(original) : null; + + foreach (var (patch, moduleInfo, loaderPluginInfo) in GetHarmonyPatchMethods(patches, moduleProvider, loaderPluginProvider)) + { + methods.Add(new MethodEntryHarmony + { + Patch = patch, + Method = patch.PatchMethod, + ModuleInfo = moduleInfo, + LoaderPluginInfo = loaderPluginInfo, + ILInstructions = DecompileILCode(patch.PatchMethod), + CSharpILMixedInstructions = DecompileILWithCSharpCode(patch.PatchMethod), + CSharpInstructions = DecompileCSharpCode(patch.PatchMethod), + }); + } + + return new() + { + Original = original, + Replacement = identifiableMethod, + Patches = methods, + Issues = methodFromStackframeIssue, + }; + } + + /// + /// Gets all involved modules in the exception stacktrace. + /// + public static IEnumerable GetAllInvolvedModules(Exception ex, ICollection assemblies, IModuleProvider moduleProvider, ILoaderPluginProvider loaderPluginProvider, IHarmonyProvider harmonyProvider) + { + var inner = ex.InnerException; + if (inner is not null) + { + foreach (var modInfo in GetAllInvolvedModules(inner, assemblies, moduleProvider, loaderPluginProvider, harmonyProvider)) + yield return modInfo; + } + + var trace = new EnhancedStackTrace(ex); + foreach (var frame in trace.GetFrames()) + { + if (!frame.HasMethod()) continue; + + var (original, identifiableMethod, patches, methodFromStackframeIssue) = GetHarmonyData(frame, harmonyProvider, moduleProvider, loaderPluginProvider); + + var ilOffset = frame.GetILOffset(); + var nativeILOffset = frame.GetNativeOffset(); + yield return new() + { + Method = identifiableMethod!, + OriginalMethod = original is not null ? new() + { + Method = original, + ModuleInfo = GetModuleInfoIfMod(original, assemblies, moduleProvider), + LoaderPluginInfo = GetLoaderPluginIfMod(original, assemblies, loaderPluginProvider), + ILInstructions = DecompileILCode(original), + CSharpILMixedInstructions = DecompileILWithCSharpCode(original), + CSharpInstructions = DecompileCSharpCode(original), + } : null, + MethodFromStackframeIssue = methodFromStackframeIssue, + ModuleInfo = GetModuleInfoIfMod(identifiableMethod, assemblies, moduleProvider), + LoaderPluginInfo = GetLoaderPluginIfMod(identifiableMethod, assemblies, loaderPluginProvider), + ILOffset = ilOffset != StackFrame.OFFSET_UNKNOWN ? ilOffset : null, + NativeOffset = nativeILOffset != StackFrame.OFFSET_UNKNOWN ? nativeILOffset : null, + StackFrameDescription = frame.ToString(), + NativeInstructions = DecompileNativeCode(identifiableMethod, nativeILOffset), + ILInstructions = DecompileILCode(identifiableMethod), + CSharpILMixedInstructions = DecompileILWithCSharpCode(identifiableMethod), + CSharpInstructions = DecompileCSharpCode(identifiableMethod), + PatchMethods = patches.ToArray(), + }; + } + } +} \ No newline at end of file diff --git a/src/BUTR.CrashReport/Utils/HarmonyUtils.cs b/src/BUTR.CrashReport/Utils/HarmonyUtils.cs new file mode 100644 index 0000000..d7ebc9b --- /dev/null +++ b/src/BUTR.CrashReport/Utils/HarmonyUtils.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace BUTR.CrashReport.Utils; + +internal static class HarmonyUtils +{ + public static Type? GetReturnedType(MethodBase? methodOrConstructor) => methodOrConstructor switch + { + ConstructorInfo => typeof(void), + MethodInfo method => method.ReturnType, + _ => null + }; + + public static string Join(this IEnumerable enumeration, Func? converter = null, string delimiter = ", ") + { + converter ??= t => t!.ToString(); + return enumeration.Aggregate("", (prev, curr) => prev + (prev.Length > 0 ? delimiter : "") + converter(curr)); + } + + public static string FullDescription(this Type? type) + { + if (type is null) + return "null"; + + var ns = type.Namespace; + if (string.IsNullOrEmpty(ns) is false) ns += "."; + var result = ns + type.Name; + + if (type.IsGenericType) + { + result += "<"; + var subTypes = type.GetGenericArguments(); + for (var i = 0; i < subTypes.Length; i++) + { + if (!result.EndsWith("<", StringComparison.Ordinal)) + result += ", "; + result += subTypes[i].FullDescription(); + } + result += ">"; + } + return result; + } + + public static string FullDescription(this MethodBase? member) + { + if (member is null) return "null"; + var returnType = GetReturnedType(member); + + var result = new StringBuilder(); + if (member.IsStatic) _ = result.Append("static "); + if (member.IsAbstract) _ = result.Append("abstract "); + if (member.IsVirtual) _ = result.Append("virtual "); + _ = result.Append($"{returnType.FullDescription()} "); + if (member.DeclaringType is not null) + _ = result.Append($"{member.DeclaringType.FullDescription()}::"); + var parameterString = member.GetParameters().Join(p => $"{p.ParameterType.FullDescription()} {p.Name}"); + _ = result.Append($"{member.Name}({parameterString})"); + return result.ToString(); + } +} \ No newline at end of file diff --git a/src/nuget.config b/src/nuget.config index eb3fc3b..776bd66 100644 --- a/src/nuget.config +++ b/src/nuget.config @@ -4,6 +4,7 @@ +