diff --git a/sample/StackTrace/Program.cs b/sample/StackTrace/Program.cs index dda43af..2eb42de 100644 --- a/sample/StackTrace/Program.cs +++ b/sample/StackTrace/Program.cs @@ -16,9 +16,11 @@ class Program } catch (Exception ex) { + Console.WriteLine(ex); exception = ex.Demystify(); } + Console.WriteLine(); Console.WriteLine(exception); } @@ -64,7 +66,12 @@ class Program [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] static async Task MethodAsync(TValue value) { - return await MethodAsync(1); + return await MethodLocalAsync(); + + async Task MethodLocalAsync() + { + return await MethodAsync(1); + } } [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] diff --git a/src/Ben.Demystifier/EnhancedStackTrace.Frames.cs b/src/Ben.Demystifier/EnhancedStackTrace.Frames.cs index 968d637..fcd7cf3 100644 --- a/src/Ben.Demystifier/EnhancedStackTrace.Frames.cs +++ b/src/Ben.Demystifier/EnhancedStackTrace.Frames.cs @@ -12,6 +12,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace System.Diagnostics @@ -162,7 +163,7 @@ namespace System.Diagnostics // ResolveStateMachineMethod may have set declaringType to null if (type != null) { - var declaringTypeName = TypeNameHelper.GetTypeDisplayName(type, fullName: true, includeGenericParameterNames: true); + var declaringTypeName = TypeNameHelper.GetTypeDisplayName(type, fullName: true, includeGenericParameterNames: true); methodDisplayInfo.DeclaringTypeName = declaringTypeName; } @@ -297,6 +298,17 @@ namespace System.Diagnostics candidateConstructors = dt.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); if (TryResolveSourceMethod(candidateConstructors, kind, matchHint, ref method, ref type, out ordinal)) return true; + if (methodName == ".cctor") + { + candidateConstructors = dt.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); + foreach (var cctor in candidateConstructors) + { + method = cctor; + type = dt; + return true; + } + } + return false; } @@ -321,7 +333,6 @@ namespace System.Diagnostics } } - var rawIL = nethodBody?.GetILAsByteArray(); if (rawIL == null) continue; @@ -342,23 +353,15 @@ namespace System.Diagnostics return true; } } - else if (reader.Operand is Type t) - { - if (t == type) - { - method = candidateMethod; - type = method.DeclaringType; - return true; - } - } } } + return false; } private static void GetOrdinal(MethodBase method, ref int? ordinal) { - var lamdaStart = method.Name.IndexOf((char) GeneratedNameKind.LambdaMethod + "__") + 3; + var lamdaStart = method.Name.IndexOf((char)GeneratedNameKind.LambdaMethod + "__") + 3; if (lamdaStart > 3) { var secondStart = method.Name.IndexOf("_", lamdaStart) + 1; @@ -584,7 +587,28 @@ namespace System.Diagnostics { return false; } - if (type == typeof(Task) && method.Name == "ExecuteWithThreadLocal") + if (type == typeof(Task)) + { + switch (method.Name) + { + case "ExecuteWithThreadLocal": + case "Execute": + case "ExecutionContextCallback": + case "ExecuteEntry": + case "InnerInvoke": + return false; + } + } + if (type == typeof(ExecutionContext)) + { + switch (method.Name) + { + case "RunInternal": + case "Run": + return false; + } + } + if (type.Namespace == "System.Threading" && (type.Name?.StartsWith("_") ?? false)) { return false; } diff --git a/src/Ben.Demystifier/ExceptionExtentions.cs b/src/Ben.Demystifier/ExceptionExtentions.cs index f14ee45..b4ab1b9 100644 --- a/src/Ben.Demystifier/ExceptionExtentions.cs +++ b/src/Ben.Demystifier/ExceptionExtentions.cs @@ -1,6 +1,7 @@ // Copyright (c) Ben A Adams. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic.Enumerable; using System.Reflection; namespace System.Diagnostics @@ -20,6 +21,14 @@ namespace System.Diagnostics stackTraceString.SetValue(exception, stackTrace.ToString()); } + if (exception is AggregateException aggEx) + { + foreach (var ex in EnumerableIList.Create(aggEx.InnerExceptions)) + { + ex.Demystify(); + } + } + exception.InnerException?.Demystify(); } catch diff --git a/src/Ben.Demystifier/Internal/ILReader.cs b/src/Ben.Demystifier/Internal/ILReader.cs index b347270..6e8e0d5 100644 --- a/src/Ben.Demystifier/Internal/ILReader.cs +++ b/src/Ben.Demystifier/Internal/ILReader.cs @@ -19,7 +19,7 @@ namespace System.Diagnostics.Internal public OpCode OpCode { get; private set; } public int MetadataToken { get; private set; } - public object Operand { get; private set; } + public MemberInfo Operand { get; private set; } public bool Read(MethodBase methodInfo) { @@ -35,13 +35,13 @@ namespace System.Diagnostics.Internal OpCode ReadOpCode() { byte instruction = ReadByte(); - if (instruction != 254) + if (instruction < 254) return singleByteOpCode[instruction]; else return doubleByteOpCode[ReadByte()]; } - object ReadOperand(OpCode code, MethodBase methodInfo) + MemberInfo ReadOperand(OpCode code, MethodBase methodInfo) { MetadataToken = 0; switch (code.OperandType) @@ -50,11 +50,23 @@ namespace System.Diagnostics.Internal MetadataToken = ReadInt(); Type[] methodArgs = null; if (methodInfo.GetType() != typeof(ConstructorInfo) && !methodInfo.GetType().IsSubclassOf(typeof(ConstructorInfo))) + { methodArgs = methodInfo.GetGenericArguments(); + } Type[] typeArgs = null; if (methodInfo.DeclaringType != null) + { typeArgs = methodInfo.DeclaringType.GetGenericArguments(); - return methodInfo.Module.ResolveMember(MetadataToken, typeArgs, methodArgs); + } + try + { + return methodInfo.Module.ResolveMember(MetadataToken, typeArgs, methodArgs); + } + catch + { + // Can return System.ArgumentException : Token xxx is not a valid MemberInfo token in the scope of module xxx.dll + return null; + } } return null; } diff --git a/test/Ben.Demystifier.Test/AggregateException.cs b/test/Ben.Demystifier.Test/AggregateException.cs new file mode 100644 index 0000000..a33d319 --- /dev/null +++ b/test/Ben.Demystifier.Test/AggregateException.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Xunit; + +namespace Demystify +{ + public class AggregateException + { + [Fact] + public void DemystifiesAggregateExceptions() + { + Exception demystifiedException = null; + + try + { + var tasks = new List + { + Task.Run(async () => await Throw1()), + Task.Run(async () => await Throw2()), + Task.Run(async () => await Throw3()) + }; + + Task.WaitAll(tasks.ToArray()); + } + catch (Exception ex) + { + demystifiedException = ex.Demystify(); + } + + // Assert + var stackTrace = demystifiedException.ToString(); + stackTrace = ReplaceLineEndings.Replace(stackTrace, ""); + var trace = string.Join("", stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) + // Remove items that vary between test runners + .Where(s => + s != " at Task Demystify.DynamicCompilation.DoesNotPreventStackTrace()+()=>{}" && + !s.Contains("System.Threading.Tasks.Task.WaitAll") + ) + .Skip(1) + .ToArray()) + // Remove Full framework back arrow + .Replace("<---", ""); + + var expected = string.Join("", new[] { + " at async Task Demystify.AggregateException.Throw1()", + " at async void Demystify.AggregateException.DemystifiesAggregateExceptions()+(?)=>{}", + " --- End of inner exception stack trace ---", + " at void Demystify.AggregateException.DemystifiesAggregateExceptions()", + "---> (Inner Exception #0) System.ArgumentException: Value does not fall within the expected range.", + " at async Task Demystify.AggregateException.Throw1()", + " at async void Demystify.AggregateException.DemystifiesAggregateExceptions()+(?)=>{}", + "---> (Inner Exception #1) System.NullReferenceException: Object reference not set to an instance of an object.", + " at async Task Demystify.AggregateException.Throw2()", + " at async void Demystify.AggregateException.DemystifiesAggregateExceptions()+(?)=>{}", + "---> (Inner Exception #2) System.InvalidOperationException: Operation is not valid due to the current state of the object.", + " at async Task Demystify.AggregateException.Throw3()", + " at async void Demystify.AggregateException.DemystifiesAggregateExceptions()+(?)=>{}"}); + + Assert.Equal(expected, trace); + } + + async Task Throw1() + { + await Task.Delay(1).ConfigureAwait(false); + throw new ArgumentException(); + } + + async Task Throw2() + { + await Task.Delay(1).ConfigureAwait(false); + throw new NullReferenceException(); + } + + async Task Throw3() + { + await Task.Delay(1).ConfigureAwait(false); + throw new InvalidOperationException(); + } + + + private Regex ReplaceLineEndings = new Regex(" in [^\n\r]+"); + } +} diff --git a/test/Ben.Demystifier.Test/Ben.Demystifier.Test.csproj b/test/Ben.Demystifier.Test/Ben.Demystifier.Test.csproj index c5e256c..9b4760b 100644 --- a/test/Ben.Demystifier.Test/Ben.Demystifier.Test.csproj +++ b/test/Ben.Demystifier.Test/Ben.Demystifier.Test.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + netcoreapp2.0;net46 false diff --git a/test/Ben.Demystifier.Test/DynamicCompilation.cs b/test/Ben.Demystifier.Test/DynamicCompilation.cs index a6767f1..26fa6d9 100644 --- a/test/Ben.Demystifier.Test/DynamicCompilation.cs +++ b/test/Ben.Demystifier.Test/DynamicCompilation.cs @@ -38,7 +38,7 @@ namespace Demystify // Assert var stackTrace = demystifiedException.ToString(); stackTrace = ReplaceLineEndings.Replace(stackTrace, ""); - var trace = stackTrace.Split(Environment.NewLine) + var trace = stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.None) // Remove items that vary between test runners .Where(s => s != " at void System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, object state)" && diff --git a/test/Ben.Demystifier.Test/MixedStack.cs b/test/Ben.Demystifier.Test/MixedStack.cs index a86ec4f..880a3ce 100644 --- a/test/Ben.Demystifier.Test/MixedStack.cs +++ b/test/Ben.Demystifier.Test/MixedStack.cs @@ -22,16 +22,21 @@ namespace Demystify stackFrame => stackFrame.MethodInfo.ToString() ) // Remove Framework method that can be optimized out (inlined) - .Where(methodName => methodName != "System.Collections.Generic.List+Enumerator.MoveNext()") - // Don't include this method as call stack shared between multiple tests - .SkipLast(1); + .Where(methodName => !methodName.StartsWith("bool System.Collections.Generic.List+")); + + var count = methodNames.Count(); + methodNames = methodNames.Take(count - 1); - foreach (var method in methodNames) - { - Console.WriteLine(method.ToString()); - } // Assert - Assert.Equal (ExpectedCallStack, methodNames.ToList()); + var expected = ExpectedCallStack.ToArray(); + var trace = methodNames.ToArray(); + + Assert.Equal(expected.Length, trace.Length); + + for (var i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], trace[i]); + } } @@ -52,7 +57,6 @@ namespace Demystify static List ExpectedCallStack = new List() { - "bool System.Collections.Generic.List+Enumerator.MoveNextRare()", "IEnumerable Demystify.MixedStack.Iterator()+MoveNext()", "string string.Join(string separator, IEnumerable values)", "string Demystify.MixedStack+GenericClass.GenericMethod(ref V value)", diff --git a/test/Ben.Demystifier.Test/NonThrownException.cs b/test/Ben.Demystifier.Test/NonThrownException.cs index 679a1c9..865c68b 100644 --- a/test/Ben.Demystifier.Test/NonThrownException.cs +++ b/test/Ben.Demystifier.Test/NonThrownException.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Xunit; @@ -28,13 +29,12 @@ namespace Demystify // Assert var stackTrace = demystifiedException.ToString(); stackTrace = ReplaceLineEndings.Replace(stackTrace, ""); - var trace = stackTrace.Split(Environment.NewLine); + var trace = stackTrace.Split(new[]{Environment.NewLine}, StringSplitOptions.None); Assert.Equal( new[] { "System.Exception: Exception of type 'System.Exception' was thrown. ---> System.Exception: Exception of type 'System.Exception' was thrown.", " at Task Demystify.NonThrownException.DoesNotPreventThrowStackTrace()+()=>{}", - " at void System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, object state)", " at async Task Demystify.NonThrownException.DoesNotPreventThrowStackTrace()", " --- End of inner exception stack trace ---"}, trace); @@ -52,13 +52,12 @@ namespace Demystify // Assert stackTrace = demystifiedException.ToString(); stackTrace = ReplaceLineEndings.Replace(stackTrace, ""); - trace = stackTrace.Split(Environment.NewLine); + trace = stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.None); Assert.Equal( new[] { "System.Exception: Exception of type 'System.Exception' was thrown. ---> System.Exception: Exception of type 'System.Exception' was thrown.", " at Task Demystify.NonThrownException.DoesNotPreventThrowStackTrace()+()=>{}", - " at void System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, object state)", " at async Task Demystify.NonThrownException.DoesNotPreventThrowStackTrace()", " --- End of inner exception stack trace ---", " at async Task Demystify.NonThrownException.DoesNotPreventThrowStackTrace()" @@ -78,11 +77,14 @@ namespace Demystify // Assert var stackTrace = est.ToString(); stackTrace = ReplaceLineEndings.Replace(stackTrace, ""); - var trace = stackTrace.Split(Environment.NewLine); + var trace = stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.None) + // Remove Full framework entries + .Where(s => !s.StartsWith(" at bool System.Threading._ThreadPoolWaitCallbac") && + !s.StartsWith(" at void System.Threading.Tasks.Task.System.Thre")); + Assert.Equal( new[] { - " at void System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, object state)", " at bool System.Threading.ThreadPoolWorkQueue.Dispatch()"}, trace); }