diff --git a/src/Sentry/Integrations/AppDomainProcessExitIntegration.cs b/src/Sentry/Integrations/AppDomainProcessExitIntegration.cs new file mode 100644 index 0000000000..f90dfb4ead --- /dev/null +++ b/src/Sentry/Integrations/AppDomainProcessExitIntegration.cs @@ -0,0 +1,35 @@ +using System; +using System.Diagnostics; +using Sentry.Internal; + +namespace Sentry.Integrations +{ + internal class AppDomainProcessExitIntegration : IInternalSdkIntegration + { + private readonly IAppDomain _appDomain; + private IHub _hub; + + public AppDomainProcessExitIntegration(IAppDomain appDomain = null) + { + _appDomain = appDomain ?? AppDomainAdapter.Instance; + } + + public void Register(IHub hub, SentryOptions options) + { + Debug.Assert(hub != null); + _hub = hub; + _appDomain.ProcessExit += HandleProcessExit; + } + + public void Unregister(IHub hub) + { + _appDomain.ProcessExit -= HandleProcessExit; + _hub = null; + } + + internal void HandleProcessExit(object sender, EventArgs e) + { + (_hub as IDisposable)?.Dispose(); + } + } +} diff --git a/src/Sentry/Integrations/AppDomainUnhandledExceptionIntegration.cs b/src/Sentry/Integrations/AppDomainUnhandledExceptionIntegration.cs index 5fecd7d834..c2ae81448a 100644 --- a/src/Sentry/Integrations/AppDomainUnhandledExceptionIntegration.cs +++ b/src/Sentry/Integrations/AppDomainUnhandledExceptionIntegration.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Threading.Tasks; using Sentry.Internal; namespace Sentry.Integrations diff --git a/src/Sentry/Internal/AppDomainAdapter.cs b/src/Sentry/Internal/AppDomainAdapter.cs index e9e6cc142a..e28e5af4c6 100644 --- a/src/Sentry/Internal/AppDomainAdapter.cs +++ b/src/Sentry/Internal/AppDomainAdapter.cs @@ -5,16 +5,26 @@ namespace Sentry.Internal internal interface IAppDomain { event UnhandledExceptionEventHandler UnhandledException; + + event EventHandler ProcessExit; } internal sealed class AppDomainAdapter : IAppDomain { public static AppDomainAdapter Instance { get; } = new AppDomainAdapter(); - private AppDomainAdapter() => AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; - + private AppDomainAdapter() + { + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + } + public event UnhandledExceptionEventHandler UnhandledException; + public event EventHandler ProcessExit; + + private void OnProcessExit(object sender, EventArgs e) => ProcessExit?.Invoke(sender, e); + private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) => UnhandledException?.Invoke(this, e); } diff --git a/src/Sentry/PublicAPI.Unshipped.txt b/src/Sentry/PublicAPI.Unshipped.txt index 5f282702bb..c48ed54124 100644 --- a/src/Sentry/PublicAPI.Unshipped.txt +++ b/src/Sentry/PublicAPI.Unshipped.txt @@ -1 +1 @@ - \ No newline at end of file +static Sentry.SentryOptionsExtensions.DisableAppDomainProcessExitFlush(this Sentry.SentryOptions options) -> void \ No newline at end of file diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 5948d8af69..a8b47e2261 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -378,7 +378,8 @@ public SentryOptions() Integrations = ImmutableList.Create( - new AppDomainUnhandledExceptionIntegration()); + new AppDomainUnhandledExceptionIntegration(), + new AppDomainProcessExitIntegration()); InAppExclude = ImmutableList.Create( diff --git a/src/Sentry/SentryOptionsExtensions.cs b/src/Sentry/SentryOptionsExtensions.cs index 82c2f6aff9..ccff0a99a9 100644 --- a/src/Sentry/SentryOptionsExtensions.cs +++ b/src/Sentry/SentryOptionsExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Linq; using Sentry.Extensibility; @@ -29,9 +30,14 @@ public static void DisableDuplicateEventDetection(this SentryOptions options) /// Disables the capture of errors through /// /// The SentryOptions to remove the integration from. - public static void DisableAppDomainUnhandledExceptionCapture(this SentryOptions options) - => options.Integrations = options.Integrations.RemoveAll(p => p.GetType() == typeof(AppDomainUnhandledExceptionIntegration)); + public static void DisableAppDomainUnhandledExceptionCapture(this SentryOptions options) => options.RemoveIntegration(); + /// + /// Disables the capture of errors through + /// + /// The SentryOptions to remove the integration from. + public static void DisableAppDomainProcessExitFlush(this SentryOptions options) => options.RemoveIntegration(); + /// /// Add an integration /// @@ -40,6 +46,15 @@ public static void DisableAppDomainUnhandledExceptionCapture(this SentryOptions public static void AddIntegration(this SentryOptions options, ISdkIntegration integration) => options.Integrations = options.Integrations.Add(integration); + /// + /// Removes all integrations of type . + /// + /// The type of the integration(s) to remove. + /// The SentryOptions to remove the integration(s) from. + /// + internal static void RemoveIntegration(this SentryOptions options) where TIntegration : ISdkIntegration + => options.Integrations = options.Integrations.RemoveAll(p => p.GetType() == typeof(TIntegration)); + /// /// Add prefix to exclude from 'InApp' stack trace list /// @@ -159,5 +174,6 @@ internal static void SetupLogging(this SentryOptions options) options.DiagnosticLogger = null; } } + } } diff --git a/test/Sentry.Tests/AppDomainProcessExitIntegrationTests.cs b/test/Sentry.Tests/AppDomainProcessExitIntegrationTests.cs new file mode 100644 index 0000000000..8c82ef2504 --- /dev/null +++ b/test/Sentry.Tests/AppDomainProcessExitIntegrationTests.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading.Tasks; +using NSubstitute; +using Sentry.Integrations; +using Sentry.Internal; +using Xunit; + +namespace Sentry.Tests +{ + public class AppDomainProcessExitIntegrationTests + { + private class Fixture + { + public IHub Hub { get; set; } = Substitute.For(); + + public IAppDomain AppDomain { get; set; } = Substitute.For(); + + public Fixture() => Hub.IsEnabled.Returns(true); + + public AppDomainProcessExitIntegration GetSut() => new AppDomainProcessExitIntegration(AppDomain); + } + + private readonly Fixture _fixture = new Fixture(); + + public SentryOptions SentryOptions { get; set; } = new SentryOptions(); + + [Fact] + public void Handle_WithException_CaptureEvent() + { + var sut = _fixture.GetSut(); + sut.Register(_fixture.Hub, SentryOptions); + + sut.HandleProcessExit(this, EventArgs.Empty); + + (_fixture.Hub as IDisposable).Received(1).Dispose(); + } + + [Fact] + public void Register_ProcessExit_Subscribes() + { + var sut = _fixture.GetSut(); + sut.Register(_fixture.Hub, SentryOptions); + + _fixture.AppDomain.Received().ProcessExit += sut.HandleProcessExit; + } + + [Fact] + public void Unregister_ProcessExit_Unsubscribes() + { + var sut = _fixture.GetSut(); + + sut.Register(_fixture.Hub, SentryOptions); + sut.Unregister(_fixture.Hub); + + _fixture.AppDomain.Received(1).ProcessExit -= sut.HandleProcessExit; + } + } +}