diff --git a/src/YouTubeDownloadTool/Utils/RefCountedFileLock.cs b/src/YouTubeDownloadTool/Utils/RefCountedFileLock.cs index e82b9c2..5857325 100644 --- a/src/YouTubeDownloadTool/Utils/RefCountedFileLock.cs +++ b/src/YouTubeDownloadTool/Utils/RefCountedFileLock.cs @@ -5,12 +5,12 @@ namespace YouTubeDownloadTool; internal sealed class RefCountedFileLock { private readonly FileStream stream; - private readonly RefCounter referenceCounter; + private readonly RefCountingDisposer referenceCounter; private RefCountedFileLock(FileStream stream) { this.stream = stream; - referenceCounter = new RefCounter(stream.Dispose); + referenceCounter = new RefCountingDisposer(stream); } public static RefCountedFileLock? CreateIfExists(string filePath) @@ -27,7 +27,7 @@ private RefCountedFileLock(FileStream stream) public IDisposable Lease() => referenceCounter.Lease(); - public string FilePath => referenceCounter.IsDisposed + public string FilePath => referenceCounter.IsClosed ? throw new ObjectDisposedException(nameof(RefCountedFileLock)) : stream.Name; } diff --git a/src/YouTubeDownloadTool/Utils/RefCounter.cs b/src/YouTubeDownloadTool/Utils/RefCounter.cs deleted file mode 100644 index 495b5bf..0000000 --- a/src/YouTubeDownloadTool/Utils/RefCounter.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace YouTubeDownloadTool; - -internal sealed class RefCounter -{ - private Action? action; - private int referenceCount; - - public RefCounter(Action action) - { - this.action = action ?? throw new ArgumentNullException(nameof(action)); - } - - public bool IsDisposed => Volatile.Read(ref action) is null; - - public IDisposable Lease() - { - Increment(); - return new RefCounterLease(this); - } - - private void Increment() - { - if (Interlocked.Increment(ref referenceCount) <= 0 || IsDisposed) - { - throw new ObjectDisposedException(nameof(RefCountedFileLock)); - } - } - - private void Decrement() - { - if (Interlocked.Decrement(ref referenceCount) == 0) - { - Interlocked.Exchange(ref action, null)?.Invoke(); - } - } - - private sealed class RefCounterLease : IDisposable - { - private RefCounter? refCounter; - - public RefCounterLease(RefCounter refCounter) - { - this.refCounter = refCounter; - } - - public void Dispose() - { - Interlocked.Exchange(ref refCounter, null)?.Decrement(); - } - } -} diff --git a/src/YouTubeDownloadTool/Utils/RefCountingDisposer.cs b/src/YouTubeDownloadTool/Utils/RefCountingDisposer.cs new file mode 100644 index 0000000..1c39d00 --- /dev/null +++ b/src/YouTubeDownloadTool/Utils/RefCountingDisposer.cs @@ -0,0 +1,126 @@ +namespace YouTubeDownloadTool; + +/// +/// Tracks how many references there are to a disposable object, and disposes it when there are none remaining. +/// +public sealed class RefCountingDisposer +{ + private readonly IDisposable disposable; + private uint refCount = 1; + private readonly object lockObject = new(); + + /// + /// Begins tracking an initial reference to . The reference count starts as 1. + /// If the next call is , the reference count will go to 0 and will be disposed. An additional call will be needed for each call, if any. + /// + /// + /// A which can track further references and which will dispose when the last reference has been released. + /// + public RefCountingDisposer(IDisposable disposable) + { + this.disposable = disposable ?? throw new ArgumentNullException(nameof(disposable)); + } + + /// + /// + /// Reflects that an additional reference to the tracked object has been made. This will require an additional call + /// to before the tracked object will be disposed by this + /// instance. + /// + /// + /// is thrown if all references have already been released and there is no + /// longer anything to track. + /// + /// + /// + /// Thrown if all references have already been released and there is no longer anything to track. + /// + public void AddRef() + { + lock (lockObject) + { + if (refCount == 0) + throw new InvalidOperationException($"{nameof(AddRef)} must not be called after all references have been released."); + + refCount++; + } + } + + /// + /// Reflects that a reference to the tracked object has been released. If the last remaining reference is released, + /// the tracked object will be disposed and future calls to and will + /// throw . + /// + /// + /// Thrown if all references have already been released and there is no longer anything to track. + /// + public void Release() + { + bool dispose; + + lock (lockObject) + { + if (refCount == 0) + throw new InvalidOperationException($"{nameof(Release)} must not be called after all references have been released."); + + refCount--; + + dispose = refCount == 0; + } + + if (dispose) disposable.Dispose(); + } + + /// + /// + /// Indicates whether all references have been released. When , the tracked object is either + /// disposed already or in the process of being disposed on a different thread. + /// + /// + /// ⚠️ Subsequent calls to and may still throw even if this property + /// returns . Another thread may have executed the final call in the + /// meantime. + /// + /// + public bool IsClosed => Volatile.Read(ref refCount) == 0; + + /// + /// + /// Reflects that an additional reference to the tracked object has been made and returns a lease object which + /// releases that reference when disposed. is idempotent and thread-safe on the + /// returned object. + /// + /// + /// is thrown if all references have already been released and there is no + /// longer anything to track. If unbalanced calls to are made separately, may also throw due to all references + /// already having been released. + /// + /// + /// + /// Thrown if all references have already been released and there is no longer anything to track. + /// + public IDisposable Lease() + { + AddRef(); + return new RefLease(this); + } + + private sealed class RefLease : IDisposable + { + private RefCountingDisposer? disposer; + + public RefLease(RefCountingDisposer disposer) + { + this.disposer = disposer; + } + + public void Dispose() + { + Interlocked.Exchange(ref disposer, null)?.Release(); + } + } +}