mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-16 00:42:51 +08:00
Improves exception rendering for async methods
This commit is contained in:
parent
ff4215f431
commit
a0e20f299c
@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Security.Authentication;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Spectre.Console.Examples;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -43,6 +44,18 @@ public static class Program
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await DoMagicAsync(42, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AnsiConsole.WriteLine();
|
||||
AnsiConsole.Write(new Rule("Async").LeftAligned());
|
||||
AnsiConsole.WriteLine();
|
||||
AnsiConsole.WriteException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DoMagic(int foo, string[,] bar)
|
||||
@ -61,4 +74,23 @@ public static class Program
|
||||
{
|
||||
throw new InvalidCredentialException("The credentials are invalid.");
|
||||
}
|
||||
|
||||
private static async Task DoMagicAsync(int foo, string[,] bar)
|
||||
{
|
||||
try
|
||||
{
|
||||
await CheckCredentialsAsync(foo, bar);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException("Whaaat?", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CheckCredentialsAsync(int qux, string[,] corgi)
|
||||
{
|
||||
await Task.Delay(0);
|
||||
throw new InvalidCredentialException("The credentials are invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,14 +55,29 @@ internal static class ExceptionFormatter
|
||||
|
||||
// Stack frames
|
||||
var stackTrace = new StackTrace(ex, fNeedFileInfo: true);
|
||||
foreach (var frame in stackTrace.GetFrames().Where(f => f != null).Cast<StackFrame>())
|
||||
var frames = stackTrace
|
||||
.GetFrames()
|
||||
.FilterStackFrames()
|
||||
.ToList();
|
||||
|
||||
foreach (var frame in frames)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
// Method
|
||||
var shortenMethods = (settings.Format & ExceptionFormats.ShortenMethods) != 0;
|
||||
var method = frame.GetMethod();
|
||||
var methodName = method.GetName();
|
||||
if (method == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var methodName = GetMethodName(ref method, out var isAsync);
|
||||
if (isAsync)
|
||||
{
|
||||
builder.Append("async ");
|
||||
}
|
||||
|
||||
builder.Append(Emphasize(methodName, new[] { '.' }, styles.Method, shortenMethods, settings));
|
||||
builder.AppendWithStyle(styles.Parenthesis, "(");
|
||||
AppendParameters(builder, method, settings);
|
||||
@ -161,4 +176,158 @@ internal static class ExceptionFormatter
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static bool ShowInStackTrace(StackFrame frame)
|
||||
{
|
||||
// NET 6 has an attribute of StackTraceHiddenAttribute that we can use to clean up the stack trace
|
||||
// cleanly. If the user is on an older version we'll fall back to all the stack frames being included.
|
||||
#if NET6_0_OR_GREATER
|
||||
var mb = frame.GetMethod();
|
||||
if (mb == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((mb.MethodImplementationFlags & MethodImplAttributes.AggressiveInlining) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (mb.IsDefined(typeof(StackTraceHiddenAttribute), false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var declaringType = mb.DeclaringType;
|
||||
if (declaringType?.IsDefined(typeof(StackTraceHiddenAttribute), false) == true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// if we can't get the attributes then fall back to including it.
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IEnumerable<StackFrame> FilterStackFrames(this IEnumerable<StackFrame?> frames)
|
||||
{
|
||||
var allFrames = frames.ToArray();
|
||||
var numberOfFrames = allFrames.Length;
|
||||
|
||||
for (var i = 0; i < numberOfFrames; i++)
|
||||
{
|
||||
var thisFrame = allFrames[i];
|
||||
if (thisFrame == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// always include the last frame
|
||||
if (i == numberOfFrames - 1)
|
||||
{
|
||||
yield return thisFrame;
|
||||
}
|
||||
else if (ShowInStackTrace(thisFrame))
|
||||
{
|
||||
yield return thisFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetMethodName(ref MethodBase method, out bool isAsync)
|
||||
{
|
||||
var declaringType = method.DeclaringType;
|
||||
|
||||
if (declaringType?.IsDefined(typeof(CompilerGeneratedAttribute), false) == true)
|
||||
{
|
||||
isAsync = typeof(IAsyncStateMachine).IsAssignableFrom(declaringType);
|
||||
if (isAsync || typeof(IEnumerator).IsAssignableFrom(declaringType))
|
||||
{
|
||||
TryResolveStateMachineMethod(ref method, out declaringType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
isAsync = false;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder(256);
|
||||
|
||||
var fullName = method.DeclaringType?.FullName;
|
||||
if (fullName != null)
|
||||
{
|
||||
// See https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L247-L253
|
||||
builder.Append(fullName.Replace('+', '.'));
|
||||
builder.Append('.');
|
||||
}
|
||||
|
||||
builder.Append(method.Name);
|
||||
if (method.IsGenericMethod)
|
||||
{
|
||||
builder.Append('[');
|
||||
builder.Append(string.Join(",", method.GetGenericArguments().Select(t => t.Name)));
|
||||
builder.Append(']');
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static bool TryResolveStateMachineMethod(ref MethodBase method, out Type declaringType)
|
||||
{
|
||||
// https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L400-L455
|
||||
declaringType = method.DeclaringType ?? throw new ArgumentException("Method must have a declaring type.", nameof(method));
|
||||
|
||||
var parentType = declaringType.DeclaringType;
|
||||
if (parentType == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
static IEnumerable<MethodInfo> GetDeclaredMethods(IReflect type) => type.GetMethods(
|
||||
BindingFlags.Public |
|
||||
BindingFlags.NonPublic |
|
||||
BindingFlags.Static |
|
||||
BindingFlags.Instance |
|
||||
BindingFlags.DeclaredOnly);
|
||||
|
||||
var methods = GetDeclaredMethods(parentType);
|
||||
|
||||
foreach (var candidateMethod in methods)
|
||||
{
|
||||
var attributes = candidateMethod.GetCustomAttributes<StateMachineAttribute>(false);
|
||||
|
||||
bool foundAttribute = false, foundIteratorAttribute = false;
|
||||
foreach (var asma in attributes)
|
||||
{
|
||||
if (asma.StateMachineType != declaringType)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foundAttribute = true;
|
||||
#if NET6_0_OR_GREATER
|
||||
foundIteratorAttribute |= asma is IteratorStateMachineAttribute or AsyncIteratorStateMachineAttribute;
|
||||
#else
|
||||
foundIteratorAttribute |= asma is IteratorStateMachineAttribute;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (!foundAttribute)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
method = candidateMethod;
|
||||
declaringType = candidateMethod.DeclaringType!;
|
||||
return foundIteratorAttribute;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
namespace Spectre.Console;
|
||||
|
||||
internal static class MethodExtensions
|
||||
{
|
||||
public static string GetName(this MethodBase? method)
|
||||
{
|
||||
if (method is null)
|
||||
{
|
||||
return "<unknown method>";
|
||||
}
|
||||
|
||||
var builder = new StringBuilder(256);
|
||||
|
||||
var fullName = method.DeclaringType?.FullName;
|
||||
if (fullName != null)
|
||||
{
|
||||
// See https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L247-L253
|
||||
builder.Append(fullName.Replace('+', '.'));
|
||||
builder.Append('.');
|
||||
}
|
||||
|
||||
builder.Append(method.Name);
|
||||
|
||||
if (method.IsGenericMethod)
|
||||
{
|
||||
builder.Append('[');
|
||||
builder.Append(string.Join(",", method.GetGenericArguments().Select(t => t.Name)));
|
||||
builder.Append(']');
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user