You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
424 lines
19 KiB
424 lines
19 KiB
5 years ago
|
#if NET20 || NET30 || NET35 || !NET_4_6
|
||
|
|
||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||
|
// See the LICENSE file in the project root for more information.
|
||
|
|
||
|
using System.Collections.Generic;
|
||
|
using System.Collections.ObjectModel;
|
||
|
using System.Diagnostics;
|
||
|
using System.Diagnostics.Contracts;
|
||
|
using System.Linq;
|
||
|
using System.Runtime.ExceptionServices;
|
||
|
using LinqInternal.Threading;
|
||
|
|
||
|
namespace System.Threading.Tasks
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// An exception holder manages a list of exceptions for one particular task.
|
||
|
/// It offers the ability to aggregate, but more importantly, also offers intrinsic
|
||
|
/// support for propagating unhandled exceptions that are never observed. It does
|
||
|
/// this by aggregating and throwing if the holder is ever GC'd without the holder's
|
||
|
/// contents ever having been requested (e.g. by a Task.Wait, Task.get_Exception, etc).
|
||
|
/// This behavior is prominent in .NET 4 but is suppressed by default beyond that release.
|
||
|
/// </summary>
|
||
|
internal class TaskExceptionHolder
|
||
|
{
|
||
|
/// <summary>Whether we should propagate exceptions on the finalizer.</summary>
|
||
|
private static readonly bool _failFastOnUnobservedException = ShouldFailFastOnUnobservedException();
|
||
|
|
||
|
/// <summary>An event handler used to notify of domain unload.</summary>
|
||
|
private static EventHandler _adUnloadEventHandler;
|
||
|
|
||
|
/// <summary>Whether the AppDomain has started to unload.</summary>
|
||
|
private static volatile bool _domainUnloadStarted;
|
||
|
|
||
|
/// <summary>The task with which this holder is associated.</summary>
|
||
|
private readonly Task _task;
|
||
|
|
||
|
/// <summary>An exception that triggered the task to cancel.</summary>
|
||
|
private ExceptionDispatchInfo _cancellationException;
|
||
|
|
||
|
/// <summary>
|
||
|
/// The lazily-initialized list of faulting exceptions. Volatile
|
||
|
/// so that it may be read to determine whether any exceptions were stored.
|
||
|
/// </summary>
|
||
|
private volatile List<ExceptionDispatchInfo> _faultExceptions;
|
||
|
|
||
|
/// <summary>Whether the holder was "observed" and thus doesn't cause finalization behavior.</summary>
|
||
|
private volatile bool _isHandled;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Creates a new holder; it will be registered for finalization.
|
||
|
/// </summary>
|
||
|
/// <param name="task">The task this holder belongs to.</param>
|
||
|
internal TaskExceptionHolder(Task task)
|
||
|
{
|
||
|
Contract.Requires(task != null, "Expected a non-null task.");
|
||
|
_task = task;
|
||
|
EnsureAppDomainUnloadCallbackRegistered();
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// A finalizer that repropagates unhandled exceptions.
|
||
|
/// </summary>
|
||
|
~TaskExceptionHolder()
|
||
|
{
|
||
|
// Raise unhandled exceptions only when we know that neither the process or nor the appdomain is being torn down.
|
||
|
// We need to do this filtering because all TaskExceptionHolders will be finalized during shutdown or unload
|
||
|
// regardles of reachability of the task (i.e. even if the user code was about to observe the task's exception),
|
||
|
// which can otherwise lead to spurious crashes during shutdown.
|
||
|
if (_faultExceptions != null && !_isHandled &&
|
||
|
!Environment.HasShutdownStarted && !GCMonitor.FinalizingForUnload && !_domainUnloadStarted)
|
||
|
{
|
||
|
// We don't want to crash the finalizer thread if any ThreadAbortExceptions
|
||
|
// occur in the list or in any nested AggregateExceptions.
|
||
|
// (Don't rethrow ThreadAbortExceptions.)
|
||
|
foreach (var edi in _faultExceptions)
|
||
|
{
|
||
|
var exp = edi.SourceException;
|
||
|
var aggExp = exp as AggregateException;
|
||
|
if (aggExp != null)
|
||
|
{
|
||
|
var flattenedAggExp = aggExp.Flatten();
|
||
|
foreach (var innerExp in flattenedAggExp.InnerExceptions)
|
||
|
{
|
||
|
if (innerExp is ThreadAbortException)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if (exp is ThreadAbortException)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
var exceptionToThrow = CreateAggregateException();
|
||
|
var ueea = new UnobservedTaskExceptionEventArgs(exceptionToThrow);
|
||
|
TaskScheduler.PublishUnobservedTaskException(_task, ueea);
|
||
|
|
||
|
// Now, if we are still unobserved and we're configured to crash on unobserved, throw the exception.
|
||
|
// We need to publish the event above even if we're not going to crash, hence
|
||
|
// why this check doesn't come at the beginning of the method.
|
||
|
if (_failFastOnUnobservedException && !ueea.Observed)
|
||
|
{
|
||
|
throw exceptionToThrow;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>Gets whether the exception holder is currently storing any exceptions for faults.</summary>
|
||
|
internal bool ContainsFaultList
|
||
|
{
|
||
|
get { return _faultExceptions != null; }
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Add an exception to the holder. This will ensure the holder is
|
||
|
/// in the proper state (handled/unhandled) depending on the list's contents.
|
||
|
/// </summary>
|
||
|
/// <param name="representsCancellation">
|
||
|
/// Whether the exception represents a cancellation request (true) or a fault (false).
|
||
|
/// </param>
|
||
|
/// <param name="exceptionObject">
|
||
|
/// An exception object (either an Exception, an ExceptionDispatchInfo,
|
||
|
/// an IEnumerable{Exception}, or an IEnumerable{ExceptionDispatchInfo})
|
||
|
/// to add to the list.
|
||
|
/// </param>
|
||
|
/// <remarks>
|
||
|
/// Must be called under lock.
|
||
|
/// </remarks>
|
||
|
internal void Add(object exceptionObject, bool representsCancellation)
|
||
|
{
|
||
|
Contract.Requires(exceptionObject != null, "TaskExceptionHolder.Add(): Expected a non-null exceptionObject");
|
||
|
Contract.Requires(
|
||
|
exceptionObject is Exception || exceptionObject is IEnumerable<Exception> ||
|
||
|
exceptionObject is ExceptionDispatchInfo || exceptionObject is IEnumerable<ExceptionDispatchInfo>,
|
||
|
"TaskExceptionHolder.Add(): Expected Exception, IEnumerable<Exception>, ExceptionDispatchInfo, or IEnumerable<ExceptionDispatchInfo>");
|
||
|
|
||
|
if (representsCancellation)
|
||
|
{
|
||
|
SetCancellationException(exceptionObject);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
AddFaultException(exceptionObject);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Allocates a new aggregate exception and adds the contents of the list to
|
||
|
/// it. By calling this method, the holder assumes exceptions to have been
|
||
|
/// "observed", such that the finalization check will be subsequently skipped.
|
||
|
/// </summary>
|
||
|
/// <param name="calledFromFinalizer">Whether this is being called from a finalizer.</param>
|
||
|
/// <param name="includeThisException">An extra exception to be included (optionally).</param>
|
||
|
/// <returns>The aggregate exception to throw.</returns>
|
||
|
internal AggregateException CreateExceptionObject(bool calledFromFinalizer, Exception includeThisException)
|
||
|
{
|
||
|
var exceptions = _faultExceptions;
|
||
|
Debug.Assert(exceptions != null, "Expected an initialized list.");
|
||
|
Debug.Assert(exceptions.Count > 0, "Expected at least one exception.");
|
||
|
|
||
|
// Mark as handled and aggregate the exceptions.
|
||
|
MarkAsHandled(calledFromFinalizer);
|
||
|
|
||
|
// If we're only including the previously captured exceptions,
|
||
|
// return them immediately in an aggregate.
|
||
|
if (includeThisException == null)
|
||
|
{
|
||
|
return new AggregateException(exceptions.Select(exceptionDispatchInfo => exceptionDispatchInfo.SourceException));
|
||
|
}
|
||
|
|
||
|
// Otherwise, the caller wants a specific exception to be included,
|
||
|
// so return an aggregate containing that exception and the rest.
|
||
|
var combinedExceptions = new Exception[exceptions.Count + 1];
|
||
|
for (var i = 0; i < combinedExceptions.Length - 1; i++)
|
||
|
{
|
||
|
combinedExceptions[i] = exceptions[i].SourceException;
|
||
|
}
|
||
|
combinedExceptions[combinedExceptions.Length - 1] = includeThisException;
|
||
|
return new AggregateException(combinedExceptions);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gets the ExceptionDispatchInfo representing the singular exception
|
||
|
/// that was the cause of the task's cancellation.
|
||
|
/// </summary>
|
||
|
/// <returns>
|
||
|
/// The ExceptionDispatchInfo for the cancellation exception. May be null.
|
||
|
/// </returns>
|
||
|
internal ExceptionDispatchInfo GetCancellationExceptionDispatchInfo()
|
||
|
{
|
||
|
var edi = _cancellationException;
|
||
|
Debug.Assert(edi == null || edi.SourceException is OperationCanceledException,
|
||
|
"Expected the EDI to be for an OperationCanceledException");
|
||
|
return edi;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Wraps the exception dispatch infos into a new read-only collection. By calling this method,
|
||
|
/// the holder assumes exceptions to have been "observed", such that the finalization
|
||
|
/// check will be subsequently skipped.
|
||
|
/// </summary>
|
||
|
internal ReadOnlyCollection<ExceptionDispatchInfo> GetExceptionDispatchInfos()
|
||
|
{
|
||
|
var exceptions = _faultExceptions;
|
||
|
Debug.Assert(exceptions != null, "Expected an initialized list.");
|
||
|
Debug.Assert(exceptions.Count > 0, "Expected at least one exception.");
|
||
|
MarkAsHandled(false);
|
||
|
return new ReadOnlyCollection<ExceptionDispatchInfo>(exceptions);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// A private helper method that ensures the holder is considered
|
||
|
/// handled, i.e. it is not registered for finalization.
|
||
|
/// </summary>
|
||
|
/// <param name="calledFromFinalizer">Whether this is called from the finalizer thread.</param>
|
||
|
internal void MarkAsHandled(bool calledFromFinalizer)
|
||
|
{
|
||
|
if (!_isHandled)
|
||
|
{
|
||
|
if (!calledFromFinalizer)
|
||
|
{
|
||
|
GC.SuppressFinalize(this);
|
||
|
}
|
||
|
|
||
|
_isHandled = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static void AppDomainUnloadCallback(object sender, EventArgs e)
|
||
|
{
|
||
|
_domainUnloadStarted = true;
|
||
|
}
|
||
|
|
||
|
private static void EnsureAppDomainUnloadCallbackRegistered()
|
||
|
{
|
||
|
if (Volatile.Read(ref _adUnloadEventHandler) == null)
|
||
|
{
|
||
|
EventHandler handler = AppDomainUnloadCallback;
|
||
|
if (Interlocked.CompareExchange(ref _adUnloadEventHandler, handler, null) == null)
|
||
|
{
|
||
|
AppDomain.CurrentDomain.DomainUnload += handler;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static bool ShouldFailFastOnUnobservedException()
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/// <summary>Adds the exception to the fault list.</summary>
|
||
|
/// <param name="exceptionObject">The exception to store.</param>
|
||
|
/// <remarks>
|
||
|
/// Must be called under lock.
|
||
|
/// </remarks>
|
||
|
private void AddFaultException(object exceptionObject)
|
||
|
{
|
||
|
Contract.Requires(exceptionObject != null, "AddFaultException(): Expected a non-null exceptionObject");
|
||
|
|
||
|
// Initialize the exceptions list if necessary. The list should be non-null iff it contains exceptions.
|
||
|
var exceptions = _faultExceptions;
|
||
|
if (exceptions == null)
|
||
|
{
|
||
|
_faultExceptions = exceptions = new List<ExceptionDispatchInfo>(1);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Debug.Assert(exceptions.Count > 0, "Expected existing exceptions list to have > 0 exceptions.");
|
||
|
}
|
||
|
|
||
|
// Handle Exception by capturing it into an ExceptionDispatchInfo and storing that
|
||
|
var exception = exceptionObject as Exception;
|
||
|
if (exception != null)
|
||
|
{
|
||
|
exceptions.Add(ExceptionDispatchInfo.Capture(exception));
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Handle ExceptionDispatchInfo by storing it into the list
|
||
|
var edi = exceptionObject as ExceptionDispatchInfo;
|
||
|
if (edi != null)
|
||
|
{
|
||
|
exceptions.Add(edi);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Handle enumerables of exceptions by capturing each of the contained exceptions into an EDI and storing it
|
||
|
var exColl = exceptionObject as IEnumerable<Exception>;
|
||
|
if (exColl != null)
|
||
|
{
|
||
|
#if DEBUG
|
||
|
var numExceptions = 0;
|
||
|
#endif
|
||
|
foreach (var exc in exColl)
|
||
|
{
|
||
|
#if DEBUG
|
||
|
Debug.Assert(exc != null, "No exceptions should be null");
|
||
|
numExceptions++;
|
||
|
#endif
|
||
|
exceptions.Add(ExceptionDispatchInfo.Capture(exc));
|
||
|
}
|
||
|
#if DEBUG
|
||
|
Debug.Assert(numExceptions > 0, "Collection should contain at least one exception.");
|
||
|
#endif
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Handle enumerables of EDIs by storing them directly
|
||
|
var ediColl = exceptionObject as IEnumerable<ExceptionDispatchInfo>;
|
||
|
if (ediColl != null)
|
||
|
{
|
||
|
exceptions.AddRange(ediColl);
|
||
|
#if DEBUG
|
||
|
Debug.Assert(exceptions.Count > 0, "There should be at least one dispatch info.");
|
||
|
foreach (var tmp in exceptions)
|
||
|
{
|
||
|
Debug.Assert(tmp != null, "No dispatch infos should be null");
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
// Anything else is a programming error
|
||
|
else
|
||
|
{
|
||
|
throw new ArgumentException("(Internal)Expected an Exception or an IEnumerable<Exception>", "exceptionObject");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If all of the exceptions are ThreadAbortExceptions and/or
|
||
|
// AppDomainUnloadExceptions, we do not want the finalization
|
||
|
// probe to propagate them, so we consider the holder to be
|
||
|
// handled. If a subsequent exception comes in of a different
|
||
|
// kind, we will reactivate the holder.
|
||
|
for (var i = 0; i < exceptions.Count; i++)
|
||
|
{
|
||
|
var t = exceptions[i].SourceException.GetType();
|
||
|
if (t != typeof(ThreadAbortException) && t != typeof(AppDomainUnloadedException))
|
||
|
{
|
||
|
MarkAsUnhandled();
|
||
|
break;
|
||
|
}
|
||
|
else if (i == exceptions.Count - 1)
|
||
|
{
|
||
|
MarkAsHandled(false);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private AggregateException CreateAggregateException()
|
||
|
{
|
||
|
// We will only propagate if this is truly unhandled. The reason this could
|
||
|
// ever occur is somewhat subtle: if a Task's exceptions are observed in some
|
||
|
// other finalizer, and the Task was finalized before the holder, the holder
|
||
|
// will have been marked as handled before even getting here.
|
||
|
|
||
|
// Give users a chance to keep this exception from crashing the process
|
||
|
|
||
|
// First, publish the unobserved exception and allow users to observe it
|
||
|
return new AggregateException(
|
||
|
"A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread.",
|
||
|
_faultExceptions.Select(exceptionDispatchInfo => exceptionDispatchInfo.SourceException));
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// A private helper method that ensures the holder is considered
|
||
|
/// unhandled, i.e. it is registered for finalization.
|
||
|
/// </summary>
|
||
|
private void MarkAsUnhandled()
|
||
|
{
|
||
|
// If a thread partially observed this thread's exceptions, we
|
||
|
// should revert back to "not handled" so that subsequent exceptions
|
||
|
// must also be seen. Otherwise, some could go missing. We also need
|
||
|
// to reregister for finalization.
|
||
|
if (_isHandled)
|
||
|
{
|
||
|
GC.ReRegisterForFinalize(this);
|
||
|
_isHandled = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>Sets the cancellation exception.</summary>
|
||
|
/// <param name="exceptionObject">The cancellation exception.</param>
|
||
|
/// <remarks>
|
||
|
/// Must be called under lock.
|
||
|
/// </remarks>
|
||
|
private void SetCancellationException(object exceptionObject)
|
||
|
{
|
||
|
Contract.Requires(exceptionObject != null, "Expected exceptionObject to be non-null.");
|
||
|
|
||
|
Debug.Assert(_cancellationException == null,
|
||
|
"Expected SetCancellationException to be called only once.");
|
||
|
// Breaking this assumption will overwrite a previously OCE,
|
||
|
// and implies something may be wrong elsewhere, since there should only ever be one.
|
||
|
|
||
|
Debug.Assert(_faultExceptions == null,
|
||
|
"Expected SetCancellationException to be called before any faults were added.");
|
||
|
// Breaking this assumption shouldn't hurt anything here, but it implies something may be wrong elsewhere.
|
||
|
// If this changes, make sure to only conditionally mark as handled below.
|
||
|
|
||
|
// Store the cancellation exception
|
||
|
var oce = exceptionObject as OperationCanceledException;
|
||
|
if (oce != null)
|
||
|
{
|
||
|
_cancellationException = ExceptionDispatchInfo.Capture(oce);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
var edi = exceptionObject as ExceptionDispatchInfo;
|
||
|
Debug.Assert(edi != null && edi.SourceException is OperationCanceledException,
|
||
|
"Expected an OCE or an EDI that contained an OCE");
|
||
|
_cancellationException = edi;
|
||
|
}
|
||
|
|
||
|
// This is just cancellation, and there are no faults, so mark the holder as handled.
|
||
|
MarkAsHandled(false);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endif
|