Skip to content

Commit

Permalink
Apply WinUI 3 exception handler in Sentry core (#1863)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattjohnsonpint authored Aug 19, 2022
1 parent 9f98ec1 commit 900697b
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 75 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- Add `Distribution` properties ([#1851](https://github.com/getsentry/sentry-dotnet/pull/1851))
- Add and configure options for the iOS SDK ([#1849](https://github.com/getsentry/sentry-dotnet/pull/1849))
- Set default `Release` and `Distribution` for iOS and Android ([#1856](https://github.com/getsentry/sentry-dotnet/pull/1856))
- Apply WinUI 3 exception handler in Sentry core ([#1863](https://github.com/getsentry/sentry-dotnet/pull/1863))

### Fixes

Expand Down
7 changes: 7 additions & 0 deletions samples/Sentry.Samples.Maui/MainPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@
Clicked="OnUnhandledExceptionClicked"
HorizontalOptions="Center" />

<Button
x:Name="ThrowBackgroundUnhandledBtn"
Text="Throw Unhandled .NET Exception on Background Thread (Crash)"
SemanticProperties.Hint="Throws an unhandled .NET exception on a background thread, crashing the app."
Clicked="OnBackgroundThreadUnhandledExceptionClicked"
HorizontalOptions="Center" />

<Button
x:Name="JavaCrashBtn"
Text="Throw Java Exception (Crash)"
Expand Down
5 changes: 5 additions & 0 deletions samples/Sentry.Samples.Maui/MainPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ private void OnUnhandledExceptionClicked(object sender, EventArgs e)
SentrySdk.CauseCrash(CrashType.Managed);
}

private void OnBackgroundThreadUnhandledExceptionClicked(object sender, EventArgs e)
{
SentrySdk.CauseCrash(CrashType.ManagedBackgroundThread);
}

private void OnCapturedExceptionClicked(object sender, EventArgs e)
{
try
Expand Down
76 changes: 1 addition & 75 deletions src/Sentry.Maui/Internal/SentryMauiInitializer.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Options;
using Sentry.Protocol;

namespace Sentry.Maui.Internal;

internal class SentryMauiInitializer : IMauiInitializeService
{
private Exception? _lastFirstChanceException;

public void Initialize(IServiceProvider services)
{
var options = services.GetRequiredService<IOptions<SentryMauiOptions>>().Value;
var disposer = services.GetRequiredService<Disposer>();

var disposable = SentrySdk.Init(options);

// Register the return value from initializing the SDK with the disposer.
Expand All @@ -24,74 +19,5 @@ public void Initialize(IServiceProvider services)
// Bind MAUI events
var binder = services.GetRequiredService<MauiEventsBinder>();
binder.BindMauiEvents();

// Register with the WinUI unhandled exception handler when needed
RegisterApplicationUnhandledExceptionForWinUI();
}

private void RegisterApplicationUnhandledExceptionForWinUI()
{
// We need to manually attach to the unhandled exception handler on Windows
// Note that stack traces will be empty until the following issue is resolved:
// https://github.com/microsoft/microsoft-ui-xaml/issues/7160

// We'll do this at runtime via reflection so that we don't have to specifically
// build a target Windows just for this feature.

if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Not running on Windows
return;
}

// Locate the Microsoft.WinUI assembly from the AppDomain
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
var assembly = Array.Find(assemblies, x => x.GetName().Name == "Microsoft.WinUI");
if (assembly == null)
{
// Not in a WinUI app
return;
}

// Reflection equivalent of:
// Microsoft.UI.Xaml.Application.Current.UnhandledException += WinUIUnhandledExceptionHandler;
//
EventHandler handler = WinUIUnhandledExceptionHandler!;
var applicationType = assembly.GetType("Microsoft.UI.Xaml.Application")!;
var application = applicationType.GetProperty("Current")!.GetValue(null);
var eventInfo = applicationType.GetEvent("UnhandledException")!;
var typedHandler = Delegate.CreateDelegate(eventInfo.EventHandlerType!, handler.Target, handler.Method);
eventInfo.AddEventHandler(application, typedHandler);

// Workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/7160
AppDomain.CurrentDomain.FirstChanceException += (_, e) => _lastFirstChanceException = e.Exception;
}

private void WinUIUnhandledExceptionHandler(object sender, object e)
{
var eventArgsType = e.GetType();
var handled = (bool)eventArgsType.GetProperty("Handled")!.GetValue(e)!;
var exception = (Exception)eventArgsType.GetProperty("Exception")!.GetValue(e)!;

// Workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/7160
if (exception.StackTrace is null)
{
exception = _lastFirstChanceException!;
}

CaptureUnhandledException(handled, exception, "Microsoft.UI.Xaml.Application.UnhandledException");
}

private static void CaptureUnhandledException(bool handled, Exception exception, string mechanism)
{
// Set some useful data and capture the exception
exception.Data[Mechanism.HandledKey] = handled;
exception.Data[Mechanism.MechanismKey] = mechanism;
SentrySdk.CaptureException(exception);
if (!handled)
{
// We're crashing, so flush events to Sentry right away
SentrySdk.Close();
}
}
}
135 changes: 135 additions & 0 deletions src/Sentry/Integrations/WinUIUnhandledExceptionIntegration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#if NET5_0_OR_GREATER
using System;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using Sentry.Extensibility;

namespace Sentry.Integrations
{
// This integration hooks unhandled exceptions in WinUI 3.
// The primary hook is Microsoft.UI.Xaml.Application.Current.UnhandledException
//
// There are some quirks to be aware of:
//
// - By default, important details (message, stack trace, etc.) are stripped away.
// We can work around this by catching first-chance exceptions.
// See: https://github.com/microsoft/microsoft-ui-xaml/issues/7160
//
// - Exceptions from background threads are not caught here.
// However, they are caught by System.AppDomain.CurrentDomain.UnhandledException,
// which we already hook in our AppDomainUnhandledExceptionIntegration
// See: https://github.com/microsoft/microsoft-ui-xaml/issues/5221
//
// Note that we use reflection in this integration to get at WinUI code.
// If we ever add a Windows platform target (net6.0-windows, etc.), we could refactor to avoid reflection.
//
// This integration is for WinUI 3. It does NOT work for UWP (WinUI 2).
// For UWP, the calling application will need to hook the event handler.
// See https://docs.sentry.io/platforms/dotnet/guides/uwp/
// (We can't do it automatically without a separate UWP class library,
// due to a security exception when attempting to attach the event dynamically.)

internal class WinUIUnhandledExceptionIntegration : ISdkIntegration
{
private static readonly byte[] WinUIPublicKeyToken = Convert.FromHexString("de31ebe4ad15742b");
private static readonly Assembly? WinUIAssembly = GetWinUIAssembly();

private Exception? _lastFirstChanceException;
private IHub _hub = null!;
private SentryOptions _options = null!;

public static bool IsApplicable => WinUIAssembly != null;

public void Register(IHub hub, SentryOptions options)
{
if (!IsApplicable)
{
return;
}

_hub = hub;
_options = options;

// Hook the main event handler
AttachEventHandler();

// First part of workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/7160
AppDomain.CurrentDomain.FirstChanceException += (_, e) => _lastFirstChanceException = e.Exception;
}

private static Assembly? GetWinUIAssembly()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Not running on Windows
return null;
}

// Attempt to locate the Microsoft.WinUI assembly from the AppDomain
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
return Array.Find(assemblies, x =>
{
// check by name and public key token
var assemblyName = x.GetName();
return assemblyName.Name == "Microsoft.WinUI" &&
assemblyName.GetPublicKeyToken()?.SequenceEqual(WinUIPublicKeyToken) is true;
});
}

private void AttachEventHandler()
{
try
{
// Reflection equivalent of:
// Microsoft.UI.Xaml.Application.Current.UnhandledException += WinUIUnhandledExceptionHandler;
//
EventHandler handler = WinUIUnhandledExceptionHandler!;
var applicationType = WinUIAssembly!.GetType("Microsoft.UI.Xaml.Application")!;
var application = applicationType.GetProperty("Current")!.GetValue(null);
var eventInfo = applicationType.GetEvent("UnhandledException")!;
var typedHandler = Delegate.CreateDelegate(eventInfo.EventHandlerType!, handler.Target, handler.Method);
eventInfo.AddEventHandler(application, typedHandler);
}
catch (Exception ex)
{
_options.LogError("Could not attach WinUIUnhandledExceptionHandler.", ex);
}
}

private void WinUIUnhandledExceptionHandler(object sender, object e)
{
bool handled;
Exception exception;
try
{
var eventArgsType = e.GetType();
handled = (bool)eventArgsType.GetProperty("Handled")!.GetValue(e)!;
exception = (Exception)eventArgsType.GetProperty("Exception")!.GetValue(e)!;
}
catch (Exception ex)
{
_options.LogError("Could not get exception details in WinUIUnhandledExceptionHandler.", ex);
return;
}

// Second part of workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/7160
if (exception.StackTrace is null)
{
exception = _lastFirstChanceException!;
}

// Set some useful data and capture the exception
exception.Data[Protocol.Mechanism.HandledKey] = handled;
exception.Data[Protocol.Mechanism.MechanismKey] = "Microsoft.UI.Xaml.UnhandledException";
_hub.CaptureException(exception);

if (!handled)
{
// We're crashing, so flush events to Sentry right away
_hub.FlushAsync(_options.ShutdownTimeout).GetAwaiter().GetResult();
}
}
}
}
#endif
7 changes: 7 additions & 0 deletions src/Sentry/SentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,13 @@ public SentryOptions()
#endif
};

#if NET5_0_OR_GREATER
if (WinUIUnhandledExceptionIntegration.IsApplicable)
{
this.AddIntegration(new WinUIUnhandledExceptionIntegration());
}
#endif

#if ANDROID
Android = new AndroidOptions(this);
#elif __IOS__
Expand Down

0 comments on commit 900697b

Please sign in to comment.