|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
#nullable enable
namespace Microsoft.Extensions.StackTrace.Sources;
internal sealed class ExceptionDetailsProvider
{
private readonly IFileProvider _fileProvider;
private readonly ILogger? _logger;
private readonly int _sourceCodeLineCount;
public ExceptionDetailsProvider(IFileProvider fileProvider, ILogger? logger, int sourceCodeLineCount)
{
_fileProvider = fileProvider;
_logger = logger;
_sourceCodeLineCount = sourceCodeLineCount;
}
public IEnumerable<ExceptionDetails> GetDetails(Exception exception)
{
var exceptions = FlattenAndReverseExceptionTree(exception);
foreach (var ex in exceptions)
{
yield return new ExceptionDetails(ex, GetStackFrames(ex));
}
}
private IEnumerable<StackFrameSourceCodeInfo> GetStackFrames(Exception original)
{
var stackFrames = StackTraceHelper.GetFrames(original, out var exception)
.Select(frame => GetStackFrameSourceCodeInfo(
frame.MethodDisplayInfo?.ToString(),
frame.FilePath,
frame.LineNumber));
if (exception != null)
{
_logger?.FailedToReadStackTraceInfo(exception);
}
return stackFrames;
}
private static IEnumerable<Exception> FlattenAndReverseExceptionTree(Exception? ex)
{
// ReflectionTypeLoadException is special because the details are in
// the LoaderExceptions property
var typeLoadException = ex as ReflectionTypeLoadException;
if (typeLoadException != null)
{
var typeLoadExceptions = new List<Exception>();
foreach (var loadException in typeLoadException.LoaderExceptions)
{
typeLoadExceptions.AddRange(FlattenAndReverseExceptionTree(loadException));
}
typeLoadExceptions.Add(typeLoadException);
return typeLoadExceptions;
}
var list = new List<Exception>();
if (ex is AggregateException aggregateException)
{
list.Add(ex);
foreach (var innerException in aggregateException.Flatten().InnerExceptions)
{
list.Add(innerException);
}
}
else
{
while (ex != null)
{
list.Add(ex);
ex = ex.InnerException;
}
list.Reverse();
}
return list;
}
// make it internal to enable unit testing
internal StackFrameSourceCodeInfo GetStackFrameSourceCodeInfo(string? method, string? filePath, int lineNumber)
{
var stackFrame = new StackFrameSourceCodeInfo
{
Function = method,
File = filePath,
Line = lineNumber
};
if (string.IsNullOrEmpty(stackFrame.File))
{
return stackFrame;
}
IEnumerable<string>? lines = null;
if (File.Exists(stackFrame.File))
{
lines = File.ReadLines(stackFrame.File);
}
else
{
// Handle relative paths and embedded files
var fileInfo = _fileProvider.GetFileInfo(stackFrame.File);
if (fileInfo.Exists)
{
// ReadLines doesn't accept a stream. Use ReadLines as its more efficient
// relative to reading lines via stream reader
if (!string.IsNullOrEmpty(fileInfo.PhysicalPath))
{
lines = File.ReadLines(fileInfo.PhysicalPath);
}
else
{
lines = ReadLines(fileInfo);
}
}
}
if (lines != null)
{
ReadFrameContent(stackFrame, lines, stackFrame.Line, stackFrame.Line);
}
return stackFrame;
}
// make it internal to enable unit testing
internal void ReadFrameContent(
StackFrameSourceCodeInfo frame,
IEnumerable<string> allLines,
int errorStartLineNumberInFile,
int errorEndLineNumberInFile)
{
// Get the line boundaries in the file to be read and read all these lines at once into an array.
var preErrorLineNumberInFile = Math.Max(errorStartLineNumberInFile - _sourceCodeLineCount, 1);
var postErrorLineNumberInFile = errorEndLineNumberInFile + _sourceCodeLineCount;
var codeBlock = allLines
.Skip(preErrorLineNumberInFile - 1)
.Take(postErrorLineNumberInFile - preErrorLineNumberInFile + 1)
.ToArray();
var numOfErrorLines = (errorEndLineNumberInFile - errorStartLineNumberInFile) + 1;
var errorStartLineNumberInArray = errorStartLineNumberInFile - preErrorLineNumberInFile;
frame.PreContextLine = preErrorLineNumberInFile;
frame.PreContextCode = codeBlock.Take(errorStartLineNumberInArray).ToArray();
frame.ContextCode = codeBlock
.Skip(errorStartLineNumberInArray)
.Take(numOfErrorLines)
.ToArray();
frame.PostContextCode = codeBlock
.Skip(errorStartLineNumberInArray + numOfErrorLines)
.ToArray();
}
private static IEnumerable<string> ReadLines(IFileInfo fileInfo)
{
using (var reader = new StreamReader(fileInfo.CreateReadStream()))
{
string? line;
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
}
}
|