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

Apply WinUI 3 exception handler in Sentry core #1863

Merged
merged 8 commits into from
Aug 19, 2022
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
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()
mattjohnsonpint marked this conversation as resolved.
Show resolved Hide resolved
{
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