From 1ad51076549564cafa8a42625ad1c8123c0fd771 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Feb 2024 22:51:00 -0800 Subject: [PATCH] Implement Dispatcher.UnhandledException and Dispatcher.UnhandledExceptionFilter (#14432) * Fix AvaloniaSynchronizationContext always using UIThread * Fix DispatcherOperation TaskSource initialization which caused some exception to be swallowed * Implement Dispatcher.UnhandledException and Dispatcher.UnhandledExceptionFilter * Add new internal Dispatcher.Send method without async semantics, use it in SyncContext * Add tests, partially ported from WPF * Make Input events go through Dispatcher.UIThread.Send * Make Dispatcher.ExceptionDataKey internal, so it can be used in XPF * Some Dispatcher.Send fixes * Fix Headless tests after SynchronizationContext changes (it relied on global UIThread) * Fix Send InvokeImpl usage * Do not wrap AvaloniaSynchronizationContext.Ensure in the Send --------- Co-authored-by: Nikita Tsukanov --- src/Avalonia.Base/Input/MouseDevice.cs | 20 +- src/Avalonia.Base/Input/PenDevice.cs | 21 +- .../AvaloniaSynchronizationContext.cs | 41 +- .../Threading/Dispatcher.Exceptions.cs | 141 +++++++ .../Threading/Dispatcher.Invoke.cs | 34 ++ src/Avalonia.Base/Threading/Dispatcher.cs | 5 +- .../Threading/DispatcherEventArgs.cs | 19 + .../Threading/DispatcherOperation.cs | 17 +- .../DispatcherUnhandledExceptionEventArgs.cs | 55 +++ ...atcherUnhandledExceptionFilterEventArgs.cs | 65 +++ src/Avalonia.Controls/TopLevel.cs | 12 +- .../HeadlessUnitTestSession.cs | 25 +- .../DispatcherTests.Exception.cs | 387 ++++++++++++++++++ .../DispatcherTests.cs | 4 +- 14 files changed, 795 insertions(+), 51 deletions(-) create mode 100644 src/Avalonia.Base/Threading/Dispatcher.Exceptions.cs create mode 100644 src/Avalonia.Base/Threading/DispatcherEventArgs.cs create mode 100644 src/Avalonia.Base/Threading/DispatcherUnhandledExceptionEventArgs.cs create mode 100644 src/Avalonia.Base/Threading/DispatcherUnhandledExceptionFilterEventArgs.cs create mode 100644 tests/Avalonia.Base.UnitTests/DispatcherTests.Exception.cs diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index db333dbd8b..0cabf02566 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -194,13 +194,19 @@ namespace Avalonia.Input var e = new PointerReleasedEventArgs(source, _pointer, (Visual)root, p, timestamp, props, inputModifiers, _lastMouseDownButton); - if (_pointer.CapturedGestureRecognizer is GestureRecognizer gestureRecognizer) - gestureRecognizer.PointerReleasedInternal(e); - else - source?.RaiseEvent(e); - _pointer.Capture(null); - _pointer.CaptureGestureRecognizer(null); - _lastMouseDownButton = default; + try + { + if (_pointer.CapturedGestureRecognizer is GestureRecognizer gestureRecognizer) + gestureRecognizer.PointerReleasedInternal(e); + else + source?.RaiseEvent(e); + } + finally + { + _pointer.Capture(null); + _pointer.CaptureGestureRecognizer(null); + _lastMouseDownButton = default; + } return e.Handled; } diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs index 09bf18d3fd..85352f88dd 100644 --- a/src/Avalonia.Base/Input/PenDevice.cs +++ b/src/Avalonia.Base/Input/PenDevice.cs @@ -143,13 +143,20 @@ namespace Avalonia.Input var e = new PointerReleasedEventArgs(source, pointer, (Visual)root, p, timestamp, properties, inputModifiers, _lastMouseDownButton); - if (pointer.CapturedGestureRecognizer is GestureRecognizer gestureRecognizer) - gestureRecognizer.PointerReleasedInternal(e); - else - source.RaiseEvent(e); - pointer.Capture(null); - pointer.CaptureGestureRecognizer(null); - _lastMouseDownButton = default; + try + { + if (pointer.CapturedGestureRecognizer is GestureRecognizer gestureRecognizer) + gestureRecognizer.PointerReleasedInternal(e); + else + source.RaiseEvent(e); + } + finally + { + pointer.Capture(null); + pointer.CaptureGestureRecognizer(null); + _lastMouseDownButton = default; + } + return e.Handled; } diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index 1efaa01442..a643445e99 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -13,25 +13,33 @@ namespace Avalonia.Threading internal readonly DispatcherPriority Priority; private readonly NonPumpingLockHelper.IHelperImpl? _nonPumpingHelper = AvaloniaLocator.Current.GetService(); - - public AvaloniaSynchronizationContext(): this(Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) - { - - } - + private readonly Dispatcher _dispatcher; + // This constructor is here to enforce STA behavior for unit tests - internal AvaloniaSynchronizationContext(bool isStaThread) + internal AvaloniaSynchronizationContext(Dispatcher dispatcher, DispatcherPriority priority, bool isStaThread = false) { + _dispatcher = dispatcher; + Priority = priority; if (_nonPumpingHelper != null && isStaThread) SetWaitNotificationRequired(); } + public AvaloniaSynchronizationContext() + : this(Dispatcher.UIThread, DispatcherPriority.Default, Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) + { + } + public AvaloniaSynchronizationContext(DispatcherPriority priority) + : this(Dispatcher.UIThread, priority, false) { - Priority = priority; } - + + public AvaloniaSynchronizationContext(Dispatcher dispatcher, DispatcherPriority priority) + : this(dispatcher, priority, false) + { + } + /// /// Controls if SynchronizationContext should be installed in InstallIfNeeded. Used by Designer. /// @@ -53,18 +61,19 @@ namespace Avalonia.Threading /// public override void Post(SendOrPostCallback d, object? state) { - Dispatcher.UIThread.Post(d, state, Priority); + _dispatcher.Post(d, state, Priority); } /// public override void Send(SendOrPostCallback d, object? state) { - if (Dispatcher.UIThread.CheckAccess()) - d(state); + if (_dispatcher.CheckAccess()) + // Same-thread, use send priority to avoid any reentrancy. + _dispatcher.Send(d, state, DispatcherPriority.Send); else - Dispatcher.UIThread.InvokeAsync(() => d(state), DispatcherPriority.Send).GetAwaiter().GetResult(); + _dispatcher.Send(d, state, Priority); } - + #if !NET6_0_OR_GREATER [PrePrepareMethod] #endif @@ -72,8 +81,8 @@ namespace Avalonia.Threading { if ( _nonPumpingHelper != null - && Dispatcher.UIThread.CheckAccess() - && Dispatcher.UIThread.DisabledProcessingCount > 0) + && _dispatcher.CheckAccess() + && _dispatcher.DisabledProcessingCount > 0) return _nonPumpingHelper.Wait(waitHandles, waitAll, millisecondsTimeout); return base.Wait(waitHandles, waitAll, millisecondsTimeout); } diff --git a/src/Avalonia.Base/Threading/Dispatcher.Exceptions.cs b/src/Avalonia.Base/Threading/Dispatcher.Exceptions.cs new file mode 100644 index 0000000000..2e9718342f --- /dev/null +++ b/src/Avalonia.Base/Threading/Dispatcher.Exceptions.cs @@ -0,0 +1,141 @@ +using System; + +namespace Avalonia.Threading; + +public partial class Dispatcher +{ + internal static readonly object ExceptionDataKey = new(); + private DispatcherUnhandledExceptionFilterEventHandler? _unhandledExceptionFilter; + + // Pre-allocated arguments for exception handling. + // This helps avoid allocations in the handler code, a potential + // source of secondary exceptions (i.e. in Out-Of-Memory cases). + private DispatcherUnhandledExceptionEventArgs _unhandledExceptionEventArgs; + private DispatcherUnhandledExceptionFilterEventArgs _exceptionFilterEventArgs; + + /// + /// Occurs when a thread exception is thrown and uncaught during execution of a delegate by way of or . + /// + /// + /// This event is raised when an exception that was thrown during execution of a delegate by way of or is uncaught. + /// A handler can mark the exception as handled, which will prevent the internal exception handler from being called. + /// Event handlers for this event must be written with care to avoid creating secondary exceptions and to catch any that occur. It is recommended to avoid allocating memory or doing any resource intensive operations in the handler. + /// + public event DispatcherUnhandledExceptionEventHandler? UnhandledException; + + /// + /// Occurs when a thread exception is thrown and uncaught during execution of a delegate by way of or when in the filter stage. + /// + /// + /// This event is raised during the filter stage for an exception that is raised during execution of a delegate by way of or and is uncaught. + /// The call stack is not unwound at this point (first-chance exception). + /// Event handlers for this event must be written with care to avoid creating secondary exceptions and to catch any that occur. It is recommended to avoid allocating memory or doing any resource intensive operations in the handler. + /// The event provides a means to not raise the event. The event is raised first, + /// and If is set to false, the event will not be raised. + /// + public event DispatcherUnhandledExceptionFilterEventHandler? UnhandledExceptionFilter + { + add + { + _unhandledExceptionFilter += value; + } + remove + { + _unhandledExceptionFilter -= value; + } + } + + /// Exception filter returns true if exception should be caught. + internal bool ExceptionFilter(Exception e) + { + // see whether this dispatcher has already seen the exception. + // This can happen when the dispatcher is re-entered via + // PushFrame (or similar). + if (!e.Data.Contains(ExceptionDataKey)) + { + // first time we've seen this exception - add data to the exception + e.Data.Add(ExceptionDataKey, null); + } + else + { + // we've seen this exception before - don't catch it + return false; + } + + // By default, Request catch if there's anyone signed up to catch it; + var requestCatch = UnhandledException is not null; + + // The app can hook up an ExceptionFilter to avoid catching it. + // ExceptionFilter will run REGARDLESS of whether there are exception handlers. + if (_unhandledExceptionFilter != null) + { + // The default requestCatch value that is passed in the args + // should be returned unchanged if filters don't set them explicitly. + _exceptionFilterEventArgs.Initialize(e, requestCatch); + var bSuccess = false; + try + { + _unhandledExceptionFilter(this, _exceptionFilterEventArgs); + bSuccess = true; + } + finally + { + if (bSuccess) + { + requestCatch = _exceptionFilterEventArgs.RequestCatch; + } + + // For bSuccess is false, + // To be in line with default behavior of structured exception handling, + // we would want to set requestCatch to false, however, this means one + // poorly programmed filter will knock out all dispatcher exception handling. + // If an exception filter fails, we run with whatever value is set thus far. + } + } + + return requestCatch; + } + + internal bool CatchException(Exception e) + { + var handled = false; + + if (UnhandledException != null) + { + _unhandledExceptionEventArgs.Initialize(e, false); + + var bSuccess = false; + try + { + UnhandledException(this, _unhandledExceptionEventArgs); + handled = _unhandledExceptionEventArgs.Handled; + bSuccess = true; + } + finally + { + if (!bSuccess) + handled = false; + } + } + + return handled; + } + + /// Returns true, if exception was handled. + internal bool TryCatchWhen(Exception e) + { + if (ExceptionFilter(e)) + { + if (!CatchException(e)) + { + return false; + } + } + else + { + return false; + } + + return true; + } +} diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs index 30c77bf7dd..dbf95fb609 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs @@ -625,6 +625,40 @@ public partial class Dispatcher _ = action ?? throw new ArgumentNullException(nameof(action)); InvokeAsyncImpl(new SendOrPostCallbackDispatcherOperation(this, priority, action, arg, true), CancellationToken.None); } + + /// + /// Sends an action that will be invoked on the dispatcher thread. + /// + /// The method. + /// The argument of method to call. + /// The priority with which to invoke the method. If null, Send is default. + /// + /// When on the same thread with Send priority, callback is executed immediately, without changing synchronization context. + /// + internal void Send(SendOrPostCallback action, object? arg, DispatcherPriority? priority = null) + { + _ = action ?? throw new ArgumentNullException(nameof(action)); + priority ??= DispatcherPriority.Send; + + if (priority == DispatcherPriority.Send && CheckAccess()) + { + try + { + action(arg); + } + catch (Exception ex) when (ExceptionFilter(ex)) + { + if (!CatchException(ex)) + throw; + } + } + else + { + InvokeImpl(new SendOrPostCallbackDispatcherOperation(this, priority.Value, action, arg, true), + CancellationToken.None, + default); + } + } /// /// Returns a task awaitable that would invoke continuation on specified dispatcher priority diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 46c6699b5a..ec5f7307ef 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -39,6 +39,9 @@ public partial class Dispatcher : IDispatcher MaximumInputStarvationTimeInExplicitProcessingExplicitMode; if (_backgroundProcessingImpl != null) _backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing; + + _unhandledExceptionEventArgs = new DispatcherUnhandledExceptionEventArgs(this); + _exceptionFilterEventArgs = new DispatcherUnhandledExceptionFilterEventArgs(this); } public static Dispatcher UIThread => s_uiThread ??= CreateUIThreadDispatcher(); @@ -86,6 +89,6 @@ public partial class Dispatcher : IDispatcher { DispatcherPriority.Validate(priority, nameof(priority)); var index = priority - DispatcherPriority.MinValue; - return _priorityContexts[index] ??= new(priority); + return _priorityContexts[index] ??= new(this, priority); } } diff --git a/src/Avalonia.Base/Threading/DispatcherEventArgs.cs b/src/Avalonia.Base/Threading/DispatcherEventArgs.cs new file mode 100644 index 0000000000..161a055552 --- /dev/null +++ b/src/Avalonia.Base/Threading/DispatcherEventArgs.cs @@ -0,0 +1,19 @@ +using System; + +namespace Avalonia.Threading; + +/// +/// Provides event data for Dispatcher related events. +/// +public abstract class DispatcherEventArgs : EventArgs +{ + /// + /// The Dispatcher associated with this event. + /// + public Dispatcher Dispatcher { get; } + + internal DispatcherEventArgs(Dispatcher dispatcher) + { + Dispatcher = dispatcher; + } +} diff --git a/src/Avalonia.Base/Threading/DispatcherOperation.cs b/src/Avalonia.Base/Threading/DispatcherOperation.cs index f387ff0d6e..e32b7e809a 100644 --- a/src/Avalonia.Base/Threading/DispatcherOperation.cs +++ b/src/Avalonia.Base/Threading/DispatcherOperation.cs @@ -283,13 +283,17 @@ public class DispatcherOperation { lock (Dispatcher.InstanceLock) { + // Ensure TaskSource created. + _ = GetTaskCore(); Status = DispatcherOperationStatus.Completed; if (TaskSource is TaskCompletionSource tcs) tcs.SetException(e); } - if (ThrowOnUiThread) + if (ThrowOnUiThread && !Dispatcher.TryCatchWhen(e)) + { throw; + } } } @@ -312,10 +316,11 @@ public class DispatcherOperation { if (Status == DispatcherOperationStatus.Aborted) return s_abortedTask; + if (TaskSource is TaskCompletionSource tcs) + return tcs.Task; if (Status == DispatcherOperationStatus.Completed) return Task.CompletedTask; - if (TaskSource is not TaskCompletionSource tcs) - TaskSource = tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + TaskSource = tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); return tcs.Task; } } @@ -401,13 +406,17 @@ internal sealed class SendOrPostCallbackDispatcherOperation : DispatcherOperatio { lock (Dispatcher.InstanceLock) { + // Ensure TaskSource created. + _ = GetTaskCore(); Status = DispatcherOperationStatus.Completed; if (TaskSource is TaskCompletionSource tcs) tcs.SetException(e); } - if (ThrowOnUiThread) + if (ThrowOnUiThread && !Dispatcher.TryCatchWhen(e)) + { throw; + } } } } diff --git a/src/Avalonia.Base/Threading/DispatcherUnhandledExceptionEventArgs.cs b/src/Avalonia.Base/Threading/DispatcherUnhandledExceptionEventArgs.cs new file mode 100644 index 0000000000..079176a855 --- /dev/null +++ b/src/Avalonia.Base/Threading/DispatcherUnhandledExceptionEventArgs.cs @@ -0,0 +1,55 @@ +using System; +using System.Diagnostics; +using Avalonia.Interactivity; + +namespace Avalonia.Threading; + +/// +/// Represents the method that will handle the event. +/// +public delegate void DispatcherUnhandledExceptionEventHandler(object sender, DispatcherUnhandledExceptionEventArgs e); + +/// +/// Provides data for the event. +/// +public sealed class DispatcherUnhandledExceptionEventArgs : DispatcherEventArgs +{ + private Exception _exception; + private bool _handled; + + internal DispatcherUnhandledExceptionEventArgs(Dispatcher dispatcher) : base(dispatcher) + { + _exception = null!; + } + + /// + /// Gets the exception that was raised when executing code by way of the dispatcher. + /// + public Exception Exception => _exception; + + /// + /// Gets or sets whether the exception event has been handled. + /// + public bool Handled + { + get + { + return _handled; + } + set + { + // Only allow to be set true. + if (value) + { + _handled = value; + } + } + } + + internal void Initialize(Exception exception, bool handled) + { + Debug.Assert(exception != null); + _exception = exception; + _handled = handled; + } +} diff --git a/src/Avalonia.Base/Threading/DispatcherUnhandledExceptionFilterEventArgs.cs b/src/Avalonia.Base/Threading/DispatcherUnhandledExceptionFilterEventArgs.cs new file mode 100644 index 0000000000..83b71039a8 --- /dev/null +++ b/src/Avalonia.Base/Threading/DispatcherUnhandledExceptionFilterEventArgs.cs @@ -0,0 +1,65 @@ +using System; +using System.Diagnostics; + +namespace Avalonia.Threading; + +/// +/// Provides data for the event. +/// +public delegate void DispatcherUnhandledExceptionFilterEventHandler(object sender, + DispatcherUnhandledExceptionFilterEventArgs e); + +/// +/// Represents the method that will handle the event. +/// +public sealed class DispatcherUnhandledExceptionFilterEventArgs : DispatcherEventArgs +{ + private Exception? _exception; + private bool _requestCatch; + + internal DispatcherUnhandledExceptionFilterEventArgs(Dispatcher dispatcher) + : base(dispatcher) + { + } + + /// + /// Gets the exception that was raised when executing code by way of the dispatcher. + /// + public Exception Exception => _exception!; + + /// + /// Gets or sets whether the exception should be caught and the event handlers called.. + /// + /// + /// A filter handler can set this property to false to request that + /// the exception not be caught, to avoid the callstack getting + /// unwound up to the Dispatcher. + ///

+ /// A previous handler in the event multicast might have already set this + /// property to false, signalling that the exception will not be caught. + /// We let the "don't catch" behavior override all others because + /// it most likely means a debugging scenario. + /// + public bool RequestCatch + { + get + { + return _requestCatch; + } + set + { + // Only allow to be set false. + if (!value) + { + _requestCatch = value; + } + } + } + + internal void Initialize(Exception exception, bool requestCatch) + { + Debug.Assert(exception != null); + _exception = exception; + _requestCatch = requestCatch; + } +} diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 1bafecea64..23841a8130 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -825,12 +825,16 @@ namespace Avalonia.Controls { if (PlatformImpl != null) { - if (e is RawPointerEventArgs pointerArgs) + Dispatcher.UIThread.Send(static state => { - pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position); - } + var (topLevel, e) = (ValueTuple)state!; + if (e is RawPointerEventArgs pointerArgs) + { + pointerArgs.InputHitTestResult = topLevel.InputHitTest(pointerArgs.Position); + } - _inputManager?.ProcessInput(e); + topLevel._inputManager?.ProcessInput(e); + }, (this, e)); } else { diff --git a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs index 91e47d661a..a67e99a55b 100644 --- a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs +++ b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs @@ -86,19 +86,23 @@ public sealed class HeadlessUnitTestSession : IDisposable using var application = EnsureApplication(); var task = action(); - task.ContinueWith((_, s) => ((CancellationTokenSource)s!).Cancel(), cts, - TaskScheduler.FromCurrentSynchronizationContext()); - - if (cts.IsCancellationRequested) + if (task.Status != TaskStatus.RanToCompletion) { - tcs.TrySetCanceled(cts.Token); - return; + task.ContinueWith((_, s) => + ((CancellationTokenSource)s!).Cancel(), cts, + TaskScheduler.FromCurrentSynchronizationContext()); + + if (cts.IsCancellationRequested) + { + tcs.TrySetCanceled(cts.Token); + return; + } + + var frame = new DispatcherFrame(); + using var innerCts = cts.Token.Register(() => frame.Continue = false, true); + Dispatcher.UIThread.PushFrame(frame); } - var frame = new DispatcherFrame(); - using var innerCts = cts.Token.Register(() => frame.Continue = false, true); - Dispatcher.UIThread.PushFrame(frame); - var result = task.GetAwaiter().GetResult(); tcs.TrySetResult(result); } @@ -128,6 +132,7 @@ public sealed class HeadlessUnitTestSession : IDisposable { scope.Dispose(); Dispatcher.ResetForUnitTests(); + SynchronizationContext.SetSynchronizationContext(null); }); } diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.Exception.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.Exception.cs new file mode 100644 index 0000000000..8da7815e22 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.Exception.cs @@ -0,0 +1,387 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls.Platform; +using Avalonia.Threading; +using Xunit; + +namespace Avalonia.Base.UnitTests; + +// Some of these exceptions are based from https://github.com/dotnet/wpf-test/blob/05797008bb4975ceeb71be36c47f01688f535d53/src/Test/ElementServices/FeatureTests/Untrusted/Dispatcher/UnhandledExceptionTest.cs#L30 +public partial class DispatcherTests +{ + private const string ExpectedExceptionText = "Exception thrown inside Dispatcher.Invoke / Dispatcher.BeginInvoke."; + + private int _numberOfHandlerOnUnhandledEventInvoked; + private int _numberOfHandlerOnUnhandledEventFilterInvoked; + + public DispatcherTests() + { + _numberOfHandlerOnUnhandledEventInvoked = 0; + _numberOfHandlerOnUnhandledEventFilterInvoked = 0; + } + + [Fact] + public void DispatcherHandlesExceptionWithPost() + { + var impl = new ManagedDispatcherImpl(null); + var disp = new Dispatcher(impl); + + var handled = false; + var executed = false; + disp.UnhandledException += (sender, args) => + { + handled = true; + args.Handled = true; + }; + disp.Post(() => ThrowAnException()); + disp.Post(() => executed = true); + + disp.RunJobs(); + + Assert.True(handled); + Assert.True(executed); + } + + [Fact] + public void SyncContextExceptionCanBeHandledWithPost() + { + var impl = new ManagedDispatcherImpl(null); + var disp = new Dispatcher(impl); + + var syncContext = disp.GetContextWithPriority(DispatcherPriority.Background); + + var handled = false; + var executed = false; + disp.UnhandledException += (sender, args) => + { + handled = true; + args.Handled = true; + }; + + syncContext.Post(_ => ThrowAnException(), null); + syncContext.Post(_ => executed = true, null); + + disp.RunJobs(); + + Assert.True(handled); + Assert.True(executed); + } + + [Fact] + public void CanRemoveDispatcherExceptionHandler() + { + var impl = new ManagedDispatcherImpl(null); + var dispatcher = new Dispatcher(impl); + var caughtCorrectException = false; + + dispatcher.UnhandledExceptionFilter += + HandlerOnUnhandledExceptionFilterRequestCatch; + dispatcher.UnhandledException += + HandlerOnUnhandledExceptionNotHandled; + + dispatcher.UnhandledExceptionFilter -= + HandlerOnUnhandledExceptionFilterRequestCatch; + dispatcher.UnhandledException -= + HandlerOnUnhandledExceptionNotHandled; + + try + { + dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); + dispatcher.RunJobs(); + } + catch (Exception e) + { + caughtCorrectException = e.Message == ExpectedExceptionText; + } + finally + { + Verification(caughtCorrectException, 0, 0); + } + } + + [Fact] + public void CanHandleExceptionWithUnhandledException() + { + var impl = new ManagedDispatcherImpl(null); + var dispatcher = new Dispatcher(impl); + + dispatcher.UnhandledExceptionFilter += + HandlerOnUnhandledExceptionFilterRequestCatch; + + dispatcher.UnhandledException += + HandlerOnUnhandledExceptionHandled; + var caughtCorrectException = true; + try + { + dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); + dispatcher.RunJobs(); + } + catch (Exception) + { + // should be no exception here. + caughtCorrectException = false; + } + finally + { + Verification(caughtCorrectException, 1, 1); + } + } + + [Fact] + public void InvokeMethodDoesntTriggerUnhandledException() + { + var impl = new ManagedDispatcherImpl(null); + var dispatcher = new Dispatcher(impl); + + dispatcher.UnhandledExceptionFilter += + HandlerOnUnhandledExceptionFilterRequestCatch; + + dispatcher.UnhandledException += + HandlerOnUnhandledExceptionHandled; + var caughtCorrectException = false; + try + { + // Since both Invoke and InvokeAsync can throw exception, there is no need to pass them to the UnhandledException. + dispatcher.Invoke(ThrowAnException, DispatcherPriority.Normal); + dispatcher.RunJobs(); + } + catch (Exception e) + { + // should be no exception here. + caughtCorrectException = e.Message == ExpectedExceptionText; + } + finally + { + Verification(caughtCorrectException, 0, 0); + } + } + + [Fact] + public void InvokeAsyncMethodDoesntTriggerUnhandledException() + { + var impl = new ManagedDispatcherImpl(null); + var dispatcher = new Dispatcher(impl); + + dispatcher.UnhandledExceptionFilter += + HandlerOnUnhandledExceptionFilterRequestCatch; + + dispatcher.UnhandledException += + HandlerOnUnhandledExceptionHandled; + var caughtCorrectException = false; + try + { + // Since both Invoke and InvokeAsync can throw exception, there is no need to pass them to the UnhandledException. + var op = dispatcher.InvokeAsync(ThrowAnException, DispatcherPriority.Normal); + op.Wait(); + dispatcher.RunJobs(); + } + catch (Exception e) + { + // should be no exception here. + caughtCorrectException = e.Message == ExpectedExceptionText; + } + finally + { + Verification(caughtCorrectException, 0, 0); + } + } + + [Fact] + public void CanRethrowExceptionWithUnhandledException() + { + var impl = new ManagedDispatcherImpl(null); + var dispatcher = new Dispatcher(impl); + + dispatcher.UnhandledExceptionFilter += + HandlerOnUnhandledExceptionFilterRequestCatch; + + dispatcher.UnhandledException += + HandlerOnUnhandledExceptionNotHandled; + var caughtCorrectException = false; + try + { + dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); + dispatcher.RunJobs(); + } + catch (Exception e) + { + caughtCorrectException = e.Message == ExpectedExceptionText; + } + finally + { + Verification(caughtCorrectException, 1, 1); + } + } + + [Fact] + public void MultipleUnhandledExceptionFilterCannotResetRequestCatchFlag() + { + var impl = new ManagedDispatcherImpl(null); + var dispatcher = new Dispatcher(impl); + + dispatcher.UnhandledExceptionFilter += + HandlerOnUnhandledExceptionFilterNotRequestCatch; + dispatcher.UnhandledExceptionFilter += + HandlerOnUnhandledExceptionFilterRequestCatch; + + dispatcher.UnhandledException += + HandlerOnUnhandledExceptionNotHandled; + dispatcher.UnhandledException += + HandlerOnUnhandledExceptionHandled; + var caughtCorrectException = false; + try + { + dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); + dispatcher.RunJobs(); + } + catch (Exception e) + { + caughtCorrectException = e.Message == ExpectedExceptionText; + } + finally + { + Verification(caughtCorrectException, 0, 2); + } + } + + [Fact] + public void MultipleUnhandledExceptionCannotResetHandleFlag() + { + var impl = new ManagedDispatcherImpl(null); + var dispatcher = new Dispatcher(impl); + + dispatcher.UnhandledExceptionFilter += + HandlerOnUnhandledExceptionFilterRequestCatch; + + dispatcher.UnhandledException += + HandlerOnUnhandledExceptionHandled; + dispatcher.UnhandledException += + HandlerOnUnhandledExceptionNotHandled; + var caughtCorrectException = true; + + try + { + dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); + dispatcher.RunJobs(); + } + catch (Exception e) + { + // should be no exception here. + caughtCorrectException = false; + } + finally + { + Verification(caughtCorrectException, 1, 1); + } + } + + [Fact] + public void CanPushFrameAndShutdownDispatcherFromUnhandledException() + { + var impl = new ManagedDispatcherImpl(null); + var dispatcher = new Dispatcher(impl); + + dispatcher.UnhandledExceptionFilter += + HandlerOnUnhandledExceptionFilterNotRequestCatchPushFrame; + + dispatcher.UnhandledException += + HandlerOnUnhandledExceptionHandledPushFrame; + var caughtCorrectException = false; + try + { + dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); + dispatcher.RunJobs(); + } + catch (Exception e) + { + caughtCorrectException = e.Message == ExpectedExceptionText; + } + finally + { + Verification(caughtCorrectException, 0, 1); + } + } + + private void Verification(bool caughtCorrectException, int numberOfHandlerOnUnhandledEventShouldInvoke, + int numberOfHandlerOnUnhandledEventFilterShouldInvoke) + { + Assert.True( + _numberOfHandlerOnUnhandledEventInvoked >= numberOfHandlerOnUnhandledEventShouldInvoke, + "Number of handler invoked on UnhandledException is invalid"); + + Assert.True( + _numberOfHandlerOnUnhandledEventFilterInvoked >= numberOfHandlerOnUnhandledEventFilterShouldInvoke, + "Number of handler invoked on UnhandledExceptionFilter is invalid"); + + Assert.True(caughtCorrectException, "Wrong exception caught."); + } + + private void HandlerOnUnhandledExceptionFilterRequestCatch(object sender, + DispatcherUnhandledExceptionFilterEventArgs args) + { + args.RequestCatch = true; + + _numberOfHandlerOnUnhandledEventFilterInvoked += 1; + Assert.Equal(ExpectedExceptionText, args.Exception.Message); + } + + private void HandlerOnUnhandledExceptionFilterNotRequestCatchPushFrame(object sender, + DispatcherUnhandledExceptionFilterEventArgs args) + { + HandlerOnUnhandledExceptionFilterNotRequestCatch(sender, args); + var frame = new DispatcherFrame(); + args.Dispatcher.InvokeAsync(() => frame.Continue = false, DispatcherPriority.Background); + args.Dispatcher.PushFrame(frame); + } + + private void HandlerOnUnhandledExceptionFilterNotRequestCatch(object sender, + DispatcherUnhandledExceptionFilterEventArgs args) + { + args.RequestCatch = false; + _numberOfHandlerOnUnhandledEventFilterInvoked += 1; + + Assert.Equal(ExpectedExceptionText, args.Exception.Message); + } + + private void HandlerOnUnhandledExceptionHandledPushFrame(object sender, DispatcherUnhandledExceptionEventArgs args) + { + Assert.Equal(ExpectedExceptionText, args.Exception.Message); + Assert.False(_numberOfHandlerOnUnhandledEventFilterInvoked == 0, + "UnhandledExceptionFilter should be invoked before UnhandledException."); + + args.Handled = true; + _numberOfHandlerOnUnhandledEventInvoked += 1; + + var dispatcher = args.Dispatcher; + var frame = new DispatcherFrame(); + dispatcher.BeginInvokeShutdown(DispatcherPriority.Background); + dispatcher.PushFrame(frame); + } + + private void HandlerOnUnhandledExceptionHandled(object sender, DispatcherUnhandledExceptionEventArgs args) + { + Assert.Equal(ExpectedExceptionText, args.Exception.Message); + Assert.False(_numberOfHandlerOnUnhandledEventFilterInvoked == 0, + "UnhandledExceptionFilter should be invoked before UnhandledException."); + + args.Handled = true; + _numberOfHandlerOnUnhandledEventInvoked += 1; + } + + private void HandlerOnUnhandledExceptionNotHandled(object sender, DispatcherUnhandledExceptionEventArgs args) + { + Assert.Equal(ExpectedExceptionText, args.Exception.Message); + Assert.False(_numberOfHandlerOnUnhandledEventFilterInvoked == 0, + "UnhandledExceptionFilter should be invoked before UnhandledException."); + + args.Handled = false; + _numberOfHandlerOnUnhandledEventInvoked += 1; + } + + private void ThrowAnException() + { + throw new Exception(ExpectedExceptionText); + } +} + diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs index 6b7ed7f257..fdc2ff9274 100644 --- a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs +++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs @@ -10,7 +10,7 @@ using Avalonia.Utilities; using Xunit; namespace Avalonia.Base.UnitTests; -public class DispatcherTests +public partial class DispatcherTests { class SimpleDispatcherImpl : IDispatcherImpl, IDispatcherImplWithPendingInput { @@ -402,7 +402,7 @@ public class DispatcherTests Assert.Throws(() => Dispatcher.UIThread.RunJobs()); } - var avaloniaContext = new AvaloniaSynchronizationContext(true); + var avaloniaContext = new AvaloniaSynchronizationContext(Dispatcher.UIThread, DispatcherPriority.Default, true); SynchronizationContext.SetSynchronizationContext(avaloniaContext); var waitHandle = new ManualResetEvent(true);