Skip to content

Commit

Permalink
Merge pull request #528 from MindscapeHQ/sean/portable-pdb-support
Browse files Browse the repository at this point in the history
Add Portable PDB support to Raygun4Net and Raygun4NetCore
  • Loading branch information
xenolightning authored May 22, 2024
2 parents f893105 + 159a5ee commit cd3a86c
Show file tree
Hide file tree
Showing 14 changed files with 440 additions and 38 deletions.
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;
}
}
}
}
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" />
</ItemGroup>

</Project>
Loading

0 comments on commit cd3a86c

Please sign in to comment.