Skip to content

Instantly share code, notes, and snippets.

@kameko
Created April 30, 2021 23:11
Show Gist options
  • Save kameko/61478e79d80302b678ca8b261c122dac to your computer and use it in GitHub Desktop.
Save kameko/61478e79d80302b678ca8b261c122dac to your computer and use it in GitHub Desktop.
ExceptionListener
namespace Your.Namespace.Here
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
public static class ExceptionListenerExtensions
{
public static Task TellIfTaskFaulted(this Task task, ExceptionListener listener)
{
listener.TellTaskFault(task);
return task;
}
public static Task ContinueAndTellIfTaskFaulted(this Task task, ExceptionListener listener)
{
return task.ContinueWith(cont => listener.TellTaskFault(cont));
}
}
/// <summary>
/// Allows the main thread of the program to let other threads inform it
/// that an unhandled exception was encountered in the system, allowing
/// the main thread to inspect the exception and safely shut down.
/// </summary>
public sealed class ExceptionListener : IDisposable
{
/// <summary>
/// The amount of exceptions stored in the system.
/// </summary>
public int ExceptionCount => exceptions.Count;
/// <summary>
/// The CancellationToken that is canceled if an exception is thrown.
/// </summary>
public CancellationToken CancellationToken => listen_cts.Token;
/// <summary>
/// Raised when the system is informed of an exception. The "Wait" methods
/// will not finish blocking until this event has finished executing.
/// If this event, ironically, throws an unhandlex exception, then that
/// exception will be added to the AggregateException and this event will
/// not be raised again.
/// </summary>
public event Action<Exception, ExceptionListener> OnExceptionRaised;
/// <summary>
/// Raised when a warning is mentioned to the system.
/// </summary>
public event Action<Exception> OnWarn;
private CancellationTokenSource listen_cts;
private List<Exception> exceptions;
/// <summary>
/// Allows the main thread of the program to let other threads inform it
/// that an unhandled exception was encountered in the system, allowing
/// the main thread to inspect the exception and safely shut down.
/// </summary>
public ExceptionListener()
{
OnExceptionRaised += delegate { };
OnWarn += delegate { };
listen_cts = new CancellationTokenSource();
exceptions = new List<Exception>();
}
/// <summary>
/// Reset the listener, clearing all exceptions stored.
/// </summary>
public void Reset()
{
listen_cts.Cancel();
listen_cts = new CancellationTokenSource();
exceptions.Clear();
}
/// <summary>
/// Inform the listener of a warning event.
/// </summary>
/// <param name="e"></param>
public void Warn(Exception e)
{
try
{
OnWarn.Invoke(e);
}
catch (Exception ei)
{
Tell(ei);
}
}
/// <summary>
/// Tell the listener that an unhandled exception was encountered.
/// </summary>
/// <param name="e"></param>
public void Tell(Exception e)
{
exceptions.Add(e);
InvokeOnExceptionRaised(e);
listen_cts.Cancel();
}
/// <summary>
/// Checks if the provided Task has faulted, and if so, tells the system.
/// </summary>
/// <param name="task"></param>
public void TellTaskFault(Task task)
{
if (task.IsFaulted)
{
Tell(task.Exception!);
}
}
/// <summary>
/// Get all stored exceptions, if any.
/// </summary>
/// <returns></returns>
public AggregateException? GetExceptions()
{
if (exceptions.Count > 0)
{
return new AggregateException(exceptions);
}
return null;
}
/// <summary>
/// Throw an AggregateException storing all stored exceptions.
/// If no exceptions are stored, this does not throw.
/// </summary>
public void ThrowException()
{
if (exceptions.Count > 0)
{
throw new AggregateException(exceptions).Flatten();
}
}
/// <summary>
/// Get the first exception stored.
/// </summary>
/// <returns></returns>
public Exception? GetFirstException()
{
return exceptions.FirstOrDefault();
}
/// <summary>
/// Get the last exception stored.
/// </summary>
/// <returns></returns>
public Exception? GetLastException()
{
return exceptions.LastOrDefault();
}
/// <summary>
/// Block this thread until an unhandled exception is thrown. This does not throw.
/// </summary>
/// <param name="other_token"></param>
public void Wait(CancellationToken other_token = default)
{
var token = CancellationTokenSource.CreateLinkedTokenSource(listen_cts.Token, other_token).Token;
while (!token.IsCancellationRequested)
{
Thread.Sleep(16);
}
}
/// <summary>
/// Asynchronously wait until an unhandled exception is thrown. This does not throw.
/// </summary>
/// <param name="other_token"></param>
/// <returns></returns>
public async Task WaitAsync(CancellationToken other_token = default)
{
try
{
var token = CancellationTokenSource.CreateLinkedTokenSource(listen_cts.Token, other_token).Token;
await Task.Delay(-1, token);
}
catch (TaskCanceledException)
{
// ignore
}
}
/// <summary>
/// Block this thread until an unhandled exception is thrown, then re-throw all
/// exceptions as an AggregateException, if any are stored.
/// </summary>
/// <param name="other_token"></param>
public void WaitAndThrow(CancellationToken other_token = default)
{
Wait(other_token);
ThrowException();
}
/// <summary>
/// Asynchronously wait until an unhandled exception is thrown, then re-throw all
/// exceptions as an AggregateException, if any are stored.
/// </summary>
/// <param name="other_token"></param>
/// <returns></returns>
public async Task WaitAndThrowAsync(CancellationToken other_token = default)
{
await WaitAsync(other_token);
ThrowException();
}
public void Dispose()
{
listen_cts.Cancel();
}
private void InvokeOnExceptionRaised(Exception e)
{
try
{
OnExceptionRaised.Invoke(e, this);
}
catch (Exception ee)
{
exceptions.Add(ee);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment