diff --git a/Dalamud/Utility/DisposeSafety.cs b/Dalamud/Utility/DisposeSafety.cs new file mode 100644 index 000000000..909c4e932 --- /dev/null +++ b/Dalamud/Utility/DisposeSafety.cs @@ -0,0 +1,392 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reactive.Disposables; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace Dalamud.Utility; + +/// +/// Utilities for disposing stuff. +/// +public static class DisposeSafety +{ + /// + /// Interface that marks a disposable that it can call back on dispose. + /// + public interface IDisposeCallback : IDisposable + { + /// + /// Event to be fired before object dispose. First parameter is the object iself. + /// + event Action? BeforeDispose; + + /// + /// Event to be fired after object dispose. First parameter is the object iself. + /// + event Action? AfterDispose; + } + + /// + /// Returns a proxy that on dispose will dispose the result of the given + /// .
+ /// If any exception has occurred, it will be ignored. + ///
+ /// The task. + /// A disposable type. + /// The proxy . + public static IDisposable ToDisposableIgnoreExceptions(this Task task) + where T : IDisposable + { + return Disposable.Create(() => task.ContinueWith(r => + { + _ = r.Exception; + if (r.IsCompleted) + { + try + { + r.Dispose(); + } + catch + { + // ignore + } + } + })); + } + + /// + /// Transforms into a , disposing the content as necessary. + /// + /// The task. + /// Ignore all exceptions. + /// A disposable type. + /// A wrapper for the task. + public static Task ToContentDisposedTask(this Task task, bool ignoreAllExceptions = false) + where T : IDisposable => task.ContinueWith( + r => + { + if (!r.IsCompletedSuccessfully) + return ignoreAllExceptions ? Task.CompletedTask : r; + try + { + r.Result.Dispose(); + } + catch (Exception e) + { + if (!ignoreAllExceptions) + { + return Task.FromException( + new AggregateException( + new[] { e }.Concat( + (IEnumerable)r.Exception?.InnerExceptions + ?? new[] { new OperationCanceledException() }))); + } + } + + return Task.CompletedTask; + }).Unwrap(); + + /// + /// Returns a proxy that on dispose will dispose all the elements of the given + /// of s. + /// + /// The disposables. + /// The disposable types. + /// The proxy . + /// Error. + public static IDisposable AggregateToDisposable(this IEnumerable? disposables) + where T : IDisposable + { + if (disposables is not T[] array) + array = disposables?.ToArray() ?? Array.Empty(); + + return Disposable.Create(() => + { + List exceptions = null; + foreach (var d in array) + { + try + { + d?.Dispose(); + } + catch (Exception de) + { + exceptions ??= new(); + exceptions.Add(de); + } + } + + if (exceptions is not null) + throw new AggregateException(exceptions); + }); + } + + /// + /// Utility class for managing finalizing stuff. + /// + public class ScopedFinalizer : IDisposeCallback, IAsyncDisposable + { + private readonly List objects = new(); + + /// + public event Action? BeforeDispose; + + /// + public event Action? AfterDispose; + + /// + public void EnsureCapacity(int capacity) => this.objects.EnsureCapacity(capacity); + + /// + /// The parameter. + [return: NotNullIfNotNull(nameof(d))] + public T? Add(T? d) where T : IDisposable + { + if (d is not null) + this.objects.Add(this.CheckAdd(d)); + + return d; + } + + /// + [return: NotNullIfNotNull(nameof(d))] + public Action? Add(Action? d) + { + if (d is not null) + this.objects.Add(this.CheckAdd(d)); + + return d; + } + + /// + [return: NotNullIfNotNull(nameof(d))] + public Func? Add(Func? d) + { + if (d is not null) + this.objects.Add(this.CheckAdd(d)); + + return d; + } + + /// + public GCHandle Add(GCHandle d) + { + if (d != default) + this.objects.Add(this.CheckAdd(d)); + + return d; + } + + /// + /// Queue all the given to be disposed later. + /// + /// Disposables. + public void AddRange(IEnumerable ds) => + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + + /// + /// Queue all the given to be run later. + /// + /// Actions. + public void AddRange(IEnumerable ds) => + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + + /// + /// Queue all the given returning to be run later. + /// + /// Func{Task}s. + public void AddRange(IEnumerable?> ds) => + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + + /// + /// Queue all the given to be disposed later. + /// + /// GCHandles. + public void AddRange(IEnumerable ds) => + this.objects.AddRange(ds.Select(d => (object)this.CheckAdd(d))); + + /// + /// Cancel all pending disposals. + /// + /// Use this after successful initialization of multiple disposables. + public void Cancel() + { + foreach (var o in this.objects) + this.CheckRemove(o); + this.objects.Clear(); + } + + /// + /// This for method chaining. + public ScopedFinalizer WithEnsureCapacity(int capacity) + { + this.EnsureCapacity(capacity); + return this; + } + + /// + /// This for method chaining. + public ScopedFinalizer With(IDisposable d) + { + this.Add(d); + return this; + } + + /// + /// This for method chaining. + public ScopedFinalizer With(Action d) + { + this.Add(d); + return this; + } + + /// + /// This for method chaining. + public ScopedFinalizer With(Func d) + { + this.Add(d); + return this; + } + + /// + /// This for method chaining. + public ScopedFinalizer With(GCHandle d) + { + this.Add(d); + return this; + } + + /// + public void Dispose() + { + this.BeforeDispose?.InvokeSafely(this); + + List? exceptions = null; + while (this.objects.Any()) + { + var obj = this.objects[^1]; + this.objects.RemoveAt(this.objects.Count - 1); + + try + { + switch (obj) + { + case IDisposable x: + x.Dispose(); + break; + case Action a: + a.Invoke(); + break; + case Func a: + a.Invoke().Wait(); + break; + case GCHandle a: + a.Free(); + break; + } + } + catch (Exception ex) + { + exceptions ??= new(); + exceptions.Add(ex); + } + } + + this.objects.TrimExcess(); + + if (exceptions is not null) + { + var exs = exceptions.Count == 1 ? exceptions[0] : new AggregateException(exceptions); + try + { + this.AfterDispose?.Invoke(this, exs); + } + catch + { + // whatever + } + + throw exs; + } + } + + /// + public async ValueTask DisposeAsync() + { + this.BeforeDispose?.InvokeSafely(this); + + List? exceptions = null; + while (this.objects.Any()) + { + var obj = this.objects[^1]; + this.objects.RemoveAt(this.objects.Count - 1); + + try + { + switch (obj) + { + case IAsyncDisposable x: + await x.DisposeAsync(); + break; + case IDisposable x: + x.Dispose(); + break; + case Func a: + await a.Invoke(); + break; + case Action a: + a.Invoke(); + break; + case GCHandle a: + a.Free(); + break; + } + } + catch (Exception ex) + { + exceptions ??= new(); + exceptions.Add(ex); + } + } + + this.objects.TrimExcess(); + + if (exceptions is not null) + { + var exs = exceptions.Count == 1 ? exceptions[0] : new AggregateException(exceptions); + try + { + this.AfterDispose?.Invoke(this, exs); + } + catch + { + // whatever + } + + throw exs; + } + } + + private T CheckAdd(T item) + { + if (item is IDisposeCallback dc) + dc.BeforeDispose += this.OnItemDisposed; + + return item; + } + + private void CheckRemove(object item) + { + if (item is IDisposeCallback dc) + dc.BeforeDispose -= this.OnItemDisposed; + } + + private void OnItemDisposed(IDisposeCallback obj) + { + obj.BeforeDispose -= this.OnItemDisposed; + this.objects.Remove(obj); + } + } +}