Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Portable PDB support to Raygun4Net and Raygun4NetCore #528

Merged
merged 16 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 89 additions & 13 deletions Mindscape.Raygun4Net.Core/Builders/RaygunErrorMessageBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.PortableExecutable;
using Mindscape.Raygun4Net.Diagnostics;
using Mindscape.Raygun4Net.Messages;

namespace Mindscape.Raygun4Net.Builders
{
public class RaygunErrorMessageBuilder : RaygunErrorMessageBuilderBase
{
private static readonly ConcurrentDictionary<string, PEDebugInformation> DebugInformationCache = new();
public static Func<string, PEReader> AssemblyReaderProvider { get; set; } = PortableExecutableReaderExtensions.GetFileSystemPEReader;

public static RaygunErrorMessage Build(Exception exception)
{
RaygunErrorMessage message = new RaygunErrorMessage();
Expand All @@ -21,6 +27,13 @@ public static RaygunErrorMessage Build(Exception exception)

message.StackTrace = BuildStackTrace(exception);

if (message.StackTrace != null)
{
// If we have a stack trace then grab the debug info images, and put them into an array
// for the outgoing payload
message.Images = GetDebugInfoForStackFrames(message.StackTrace).ToArray();
}

if (exception.Data != null)
{
IDictionary data = new Dictionary<object, object>();
Expand Down Expand Up @@ -105,31 +118,47 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac
return lines.ToArray();
}

foreach (StackFrame frame in frames)
foreach (var frame in frames)
{
MethodBase method = frame.GetMethod();
var method = frame.GetMethod();

if (method != null)
{
int lineNumber = frame.GetFileLineNumber();
string methodName = null;
string file = null;
string className = null;
var lineNumber = 0;
var ilOffset = StackFrame.OFFSET_UNKNOWN;
var methodToken = StackFrame.OFFSET_UNKNOWN;
PEDebugInformation debugInfo = null;

if (lineNumber == 0)
try
{
lineNumber = frame.GetILOffset();
file = frame.GetFileName();
lineNumber = frame.GetFileLineNumber();
methodName = GenerateMethodName(method);
className = method.ReflectedType != null ? method.ReflectedType.FullName : "(unknown)";
ilOffset = frame.GetILOffset();
debugInfo = TryGetDebugInformation(method.Module.Name);

// This might fail in medium trust environments or for array methods,
// so don't crash the entire send process - just move on with what we have
methodToken = method.MetadataToken;
}
catch (Exception ex)
{
Debug.WriteLine("Exception retrieving stack frame details: {0}", ex);
}

var methodName = GenerateMethodName(method);

string file = frame.GetFileName();

string className = method.ReflectedType != null ? method.ReflectedType.FullName : "(unknown)";

var line = new RaygunErrorStackTraceLineMessage
{
FileName = file,
LineNumber = lineNumber,
MethodName = methodName,
ClassName = className
ClassName = className,
ILOffset = ilOffset,
MethodToken = methodToken,
ImageSignature = debugInfo?.Signature
};

lines.Add(line);
Expand All @@ -138,5 +167,52 @@ public static RaygunErrorStackTraceLineMessage[] BuildStackTrace(StackTrace stac

return lines.ToArray();
}

private static IEnumerable<PEDebugInformation> GetDebugInfoForStackFrames(IEnumerable<RaygunErrorStackTraceLineMessage> frames)
{
if (DebugInformationCache.IsEmpty)
{
return Enumerable.Empty<PEDebugInformation>();
}

var imageMap = DebugInformationCache.Values.Where(x => x != null).ToDictionary(k => k.Signature);
var imageSet = new HashSet<PEDebugInformation>();

foreach (var stackFrame in frames)
{
if (stackFrame.ImageSignature != null && imageMap.TryGetValue(stackFrame.ImageSignature, out var image))
{
imageSet.Add(image);
}
}

return imageSet;
}

private static PEDebugInformation TryGetDebugInformation(string moduleName)
{
if (DebugInformationCache.TryGetValue(moduleName, out var cachedInfo))
{
return cachedInfo;
}

try
{
// Attempt to read out the Debug Info from the PE
var peReader = AssemblyReaderProvider(moduleName);

// If we got this far, the assembly/module exists, so whatever the result
// put it in the cache to prevent reading the disk over and over
peReader.TryGetDebugInformation(out var debugInfo);
DebugInformationCache.TryAdd(moduleName, debugInfo);
return debugInfo;
}
catch (Exception ex)
{
Debug.WriteLine($"Could not load debug information: {ex}");
}

return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the module is not found and returns null, we could consider adding it to the dictionary too to avoid the lookup attempt every time.

Copy link
Contributor Author

@xenolightning xenolightning May 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I originally had this code as that something went wrong and we shouldn't add it, and maybe we could try again.

But when there's no exceptions, maybe we do add a null?

}
}
}
}
27 changes: 27 additions & 0 deletions Mindscape.Raygun4Net.Core/Diagnostics/PEDebugInformation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;

namespace Mindscape.Raygun4Net.Diagnostics;

public sealed class PEDebugInformation
{
/// <summary>
/// The signature of the PE and PDB linking them together - usually a GUID
/// </summary>
public string Signature { get; internal set; }

/// <summary>
/// Checksum of the PE & PDB. Format: {algorithm}:{hash:X}
/// </summary>
public string Checksum { get; internal set; }

/// <summary>
/// The full location of the PDB at build time
/// </summary>
public string File { get; internal set; }

/// <summary>
/// The generated Timestamp of the code at build time stored as hex
/// </summary>
public string Timestamp { get; internal set; }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection.PortableExecutable;

namespace Mindscape.Raygun4Net.Diagnostics;

internal static class PortableExecutableReaderExtensions
{
public static PEReader GetFileSystemPEReader(string moduleName)
{
try
{
// Read into memory to avoid any premature stream closures
var bytes = ImmutableArray.Create(File.ReadAllBytes(moduleName));
return new PEReader(bytes);
}
catch (Exception ex)
{
Debug.WriteLine($"Could not open module [{moduleName}] from disk: {ex}");
return null;
}
}

public static bool TryGetDebugInformation(this PEReader peReader, out PEDebugInformation debugInformation)
{
try
{
debugInformation = GetDebugInformation(peReader);
return true;
}
catch (Exception ex)
{
Debug.WriteLine($"Error reading PE Debug Data: {ex}");
}

debugInformation = null;
return false;
}

private static PEDebugInformation GetDebugInformation(this PEReader peReader)
{
var debugInfo = new PEDebugInformation
{
Timestamp = $"{peReader.PEHeaders.CoffHeader.TimeDateStamp:X8}"
};

foreach (var entry in peReader.ReadDebugDirectory())
{
if (entry.Type == DebugDirectoryEntryType.CodeView)
{
// Read the CodeView data
var codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry);

debugInfo.File = codeViewData.Path;
debugInfo.Signature = codeViewData.Guid.ToString();
}

if (entry.Type == DebugDirectoryEntryType.PdbChecksum)
{
var checksumEntry = peReader.ReadPdbChecksumDebugDirectoryData(entry);
var checksumHex = BitConverter.ToString(checksumEntry.Checksum.ToArray()).Replace("-", "").ToUpperInvariant();
debugInfo.Checksum = $"{checksumEntry.AlgorithmName}:{checksumHex}";
}
}

return debugInfo;
}
}
5 changes: 4 additions & 1 deletion Mindscape.Raygun4Net.Core/Messages/RaygunErrorMessage.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections;
using Mindscape.Raygun4Net.Diagnostics;

namespace Mindscape.Raygun4Net.Messages
{
Expand All @@ -15,12 +16,14 @@ public class RaygunErrorMessage
public string Message { get; set; }

public RaygunErrorStackTraceLineMessage[] StackTrace { get; set; }

public PEDebugInformation[] Images { get; set; }

public override string ToString()
{
// This exists because Reflection in Xamarin can't seem to obtain the Getter methods unless the getter is used somewhere in the code.
// The getter of all properties is required to serialize the Raygun messages to JSON.
return string.Format("[RaygunErrorMessage: InnerError={0}, InnerErrors={1}, Data={2}, ClassName={3}, Message={4}, StackTrace={5}]", InnerError, InnerErrors, Data, ClassName, Message, StackTrace);
return string.Format("[RaygunErrorMessage: InnerError={0}, InnerErrors={1}, Data={2}, ClassName={3}, Message={4}, StackTrace={5}, Images={6}]", InnerError, InnerErrors, Data, ClassName, Message, StackTrace, Images);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ public class RaygunErrorStackTraceLineMessage

public string Raw { get; set; }

public int ILOffset { get; set; }

public int MethodToken { get; set; }

public string ImageSignature { get; set; }

public override string ToString()
{
// This exists because Reflection in Xamarin can't seem to obtain the Getter methods unless the getter is used somewhere in the code.
// The getter of all properties is required to serialize the Raygun messages to JSON.
return string.Format("[RaygunErrorStackTraceLineMessage: LineNumber={0}, ClassName={1}, FileName={2}, MethodName={3}, Raw={4}]", LineNumber, ClassName, FileName, MethodName, Raw);
return string.Format("[RaygunErrorStackTraceLineMessage: LineNumber={0}, ClassName={1}, FileName={2}, MethodName={3}, Raw={4}, ILOffset={5}, MethodToken={6}, PdbSignature={7}]",
LineNumber, ClassName, FileName, MethodName, Raw, ILOffset, MethodToken, ImageSignature);
}
}
}
6 changes: 5 additions & 1 deletion Mindscape.Raygun4Net.Core/Mindscape.Raygun4Net.Core.csproj
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net462</TargetFramework>
<OutputType>Library</OutputType>
<RootNamespace>Mindscape.Raygun4Net</RootNamespace>
<AssemblyName>Mindscape.Raygun4Net</AssemblyName>
<AssemblyTitle>Raygun4Net.Core</AssemblyTitle>
<TargetFramework>net462</TargetFramework>
</PropertyGroup>

<PropertyGroup>
Expand Down Expand Up @@ -46,4 +46,8 @@
<None Include="..\128x128-transparent.png" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.Reflection.Metadata" Version="6.0.1" />
xenolightning marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>

</Project>
Loading