From cdbe45bd536e9e66b25776425e4fd004f268adcb Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Sun, 24 May 2026 18:30:10 +0800 Subject: [PATCH] =?UTF-8?q?Batch=205:=20Event=20system=20=E2=80=94=20threa?= =?UTF-8?q?d=20safety,=20IUpdateEventListener,=20Progress=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite EventManager with ConcurrentDictionary for thread safety Each handler invoked individually so one handler's exception doesn't prevent others from being called. - Add IUpdateEventListener interface for batch registration of all 7 event types at once via AddEventListener() - Add UpdateEventListenerBase abstract class for optional overrides - Add ProgressEventArgs and AddListenerProgress to Bootstrap - Wire DownloadProgressReporter to fire Progress/Completed/Error events through EventManager automatically - Fix test to match new OnProgress signature Closes #369 --- .../Bootstrap/GeneralUpdateBootstrap.cs | 20 ++++ .../Progress/DownloadProgressReporter.cs | 40 ++++++-- .../GeneralUpdate.Core/Event/EventManager.cs | 93 ++++++++----------- .../Event/IUpdateEventListener.cs | 40 ++++++-- .../Event/ProgressEventArgs.cs | 17 ++++ tests/CoreTest/Event/EventListenerTests.cs | 2 +- 6 files changed, 143 insertions(+), 69 deletions(-) create mode 100644 src/c#/GeneralUpdate.Core/Event/ProgressEventArgs.cs diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index a42d7293..61e57009 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -329,4 +329,24 @@ public GeneralUpdateBootstrap AddListenerException( public GeneralUpdateBootstrap AddListenerUpdateInfo( Action cb) => AddListener(cb); + + public GeneralUpdateBootstrap AddListenerProgress( + Action cb) => AddListener(cb); + + /// + /// Batch-register an event listener implementing . + /// All 7 event handlers are registered at once. + /// + public GeneralUpdateBootstrap AddEventListener() where TListener : IUpdateEventListener, new() + { + var listener = new TListener(); + AddListener((s, e) => listener.OnAllDownloadCompleted(e)); + AddListener((s, e) => listener.OnDownloadCompleted(e)); + AddListener((s, e) => listener.OnDownloadError(e)); + AddListener((s, e) => listener.OnDownloadStatistics(e)); + AddListener((s, e) => listener.OnUpdateInfo(e)); + AddListener((s, e) => listener.OnException(e)); + AddListener((s, e) => listener.OnProgress(e)); + return this; + } } diff --git a/src/c#/GeneralUpdate.Core/Download/Progress/DownloadProgressReporter.cs b/src/c#/GeneralUpdate.Core/Download/Progress/DownloadProgressReporter.cs index 8ffb65eb..557ece23 100644 --- a/src/c#/GeneralUpdate.Core/Download/Progress/DownloadProgressReporter.cs +++ b/src/c#/GeneralUpdate.Core/Download/Progress/DownloadProgressReporter.cs @@ -1,17 +1,20 @@ using System; +using System.Collections.Generic; +using GeneralUpdate.Core.Download; +using GeneralUpdate.Core.Event; using IProgress = System.IProgress; namespace GeneralUpdate.Core.Download.Progress; -/// Bridges IProgress to event-based callbacks for download status. +/// Bridges IProgress to EventManager for backward-compatible event listeners. public class DownloadProgressReporter : IProgress { - private readonly Action? _onProgress; + private readonly Action? _onProgress; private readonly Action? _onCompleted; private readonly Action? _onAllCompleted; public DownloadProgressReporter( - Action? onProgress = null, + Action? onProgress = null, Action? onCompleted = null, Action? onAllCompleted = null) { @@ -20,14 +23,37 @@ public DownloadProgressReporter( _onAllCompleted = onAllCompleted; } - public void Report(Download.Models.DownloadProgress value) + public void Report(Models.DownloadProgress value) { _onProgress?.Invoke(value); - if (value.Status == Download.Models.DownloadStatus.Completed) + + // Fire progress event via EventManager + EventManager.Instance.Dispatch(this, new ProgressEventArgs(value)); + + if (value.Status == Models.DownloadStatus.Completed) { _onCompleted?.Invoke(); - if (value.Percentage >= 100) - _onAllCompleted?.Invoke(); + EventManager.Instance.Dispatch(this, + new MultiDownloadCompletedEventArgs(value.AssetName ?? "unknown", true)); + } + + if (value.Status == Models.DownloadStatus.Failed) + { + EventManager.Instance.Dispatch(this, + new MultiDownloadErrorEventArgs(new Exception("Download failed"), value.AssetName ?? "unknown")); + } + + if (value.Percentage >= 100) + { + _onAllCompleted?.Invoke(); + EventManager.Instance.Dispatch(this, + new MultiAllDownloadCompletedEventArgs(true, new List<(object, string)>())); } } + + /// + /// Create an IProgress that dispatches progress events to EventManager. + /// + public static IProgress CreateEventBridge() + => new DownloadProgressReporter(); } diff --git a/src/c#/GeneralUpdate.Core/Event/EventManager.cs b/src/c#/GeneralUpdate.Core/Event/EventManager.cs index 6267b209..aeb8cded 100644 --- a/src/c#/GeneralUpdate.Core/Event/EventManager.cs +++ b/src/c#/GeneralUpdate.Core/Event/EventManager.cs @@ -1,15 +1,20 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; +using System.Threading; using GeneralUpdate.Core; namespace GeneralUpdate.Core.Event { + /// + /// Thread-safe event manager using ConcurrentDictionary. + /// Supports add/remove/dispatch without lock contention. + /// public class EventManager : IDisposable { private static readonly Lazy _lazy = new(() => new EventManager()); - private Dictionary _dicDelegates = new(); - private bool _disposed = false; + private ConcurrentDictionary _dicDelegates = new(); + private bool _disposed; private EventManager() { } @@ -17,76 +22,60 @@ private EventManager() { } public void AddListener(Action listener) where TEventArgs : EventArgs { - try - { - if (listener == null) throw new ArgumentNullException(nameof(listener)); - var delegateType = typeof(Action); - if (_dicDelegates.ContainsKey(delegateType)) - { - _dicDelegates[delegateType] = Delegate.Combine(_dicDelegates[delegateType], listener); - } - else - { - _dicDelegates.Add(delegateType, listener); - } - } - catch (Exception e) - { - GeneralTracer.Error("The AddListener method in the EventManager class throws an exception.", e); - } + if (listener == null) throw new ArgumentNullException(nameof(listener)); + var type = typeof(Action); + _dicDelegates.AddOrUpdate(type, + _ => listener, + (_, existing) => Delegate.Combine(existing, listener)); } public void RemoveListener(Action listener) where TEventArgs : EventArgs { - try + if (listener == null) throw new ArgumentNullException(nameof(listener)); + var type = typeof(Action); + if (_dicDelegates.TryGetValue(type, out var existing)) { - if (listener == null) throw new ArgumentNullException(nameof(listener)); - var delegateType = typeof(Action); - if (_dicDelegates.TryGetValue(delegateType, out var existingDelegate)) - { - _dicDelegates[delegateType] = Delegate.Remove(existingDelegate, listener); - } - } - catch (Exception e) - { - GeneralTracer.Error("The RemoveListener method in the EventManager class throws an exception.", e); + var updated = Delegate.Remove(existing, listener); + if (updated == null) + _dicDelegates.TryRemove(type, out _); + else + _dicDelegates.TryUpdate(type, updated, existing); } } public void Dispatch(object sender, TEventArgs eventArgs) where TEventArgs : EventArgs { - try + if (sender == null) throw new ArgumentNullException(nameof(sender)); + if (eventArgs == null) throw new ArgumentNullException(nameof(eventArgs)); + + var type = typeof(Action); + if (_dicDelegates.TryGetValue(type, out var existingDelegate)) { - if (sender == null) throw new ArgumentNullException(nameof(sender)); - if (eventArgs == null) throw new ArgumentNullException(nameof(eventArgs)); - var delegateType = typeof(Action); - if (_dicDelegates.TryGetValue(delegateType, out var existingDelegate)) + // Invoke each handler individually so one handler's exception + // doesn't prevent others from being called. + foreach (var handler in existingDelegate.GetInvocationList()) { - ((Action)existingDelegate)?.Invoke(sender, eventArgs); + try + { + ((Action)handler).Invoke(sender, eventArgs); + } + catch (Exception e) + { + GeneralTracer.Error("EventManager.Dispatch handler threw an exception.", e); + } } } - catch (Exception e) - { - GeneralTracer.Error("The Dispatch method in the EventManager class throws an exception.", e); - } } public void Clear() => _dicDelegates.Clear(); public void Dispose() { - try - { - if (!this._disposed) - { - _dicDelegates.Clear(); - _disposed = true; - } - } - catch (Exception e) + if (!_disposed) { - GeneralTracer.Error("The Dispose method in the EventManager class throws an exception.", e); + _dicDelegates.Clear(); + _disposed = true; } } } -} \ No newline at end of file +} diff --git a/src/c#/GeneralUpdate.Core/Event/IUpdateEventListener.cs b/src/c#/GeneralUpdate.Core/Event/IUpdateEventListener.cs index cd57122e..3a047b8f 100644 --- a/src/c#/GeneralUpdate.Core/Event/IUpdateEventListener.cs +++ b/src/c#/GeneralUpdate.Core/Event/IUpdateEventListener.cs @@ -1,25 +1,47 @@ -using System; using GeneralUpdate.Core.Download; -using GeneralUpdate.Core.Download.Models; -using GeneralUpdate.Core.Event; namespace GeneralUpdate.Core.Event; -/// Batch event registration — implement once, register once. +/// +/// Batch registration interface for all update event types. +/// Implement this interface and register via +/// new GeneralUpdateBootstrap().AddEventListener<MyListener>(). +/// public interface IUpdateEventListener { + /// All downloads completed. void OnAllDownloadCompleted(MultiAllDownloadCompletedEventArgs args); + + /// Single download completed. void OnDownloadCompleted(MultiDownloadCompletedEventArgs args); + + /// Download error. void OnDownloadError(MultiDownloadErrorEventArgs args); + + /// Download statistics updated. void OnDownloadStatistics(MultiDownloadStatisticsEventArgs args); + + /// Update information available. void OnUpdateInfo(UpdateInfoEventArgs args); + + /// Exception occurred. void OnException(ExceptionEventArgs args); - void OnProgress(DownloadProgress progress); + + /// Real-time download progress. + void OnProgress(ProgressEventArgs args); } -/// Progress event args for AddListenerProgress. -public class ProgressEventArgs : EventArgs +/// +/// Base class that implements IUpdateEventListener with no-op methods. +/// Inherit from this and override only the events you need. +/// +public abstract class UpdateEventListenerBase : IUpdateEventListener { - public DownloadProgress Progress { get; } - public ProgressEventArgs(DownloadProgress progress) => Progress = progress; + public virtual void OnAllDownloadCompleted(MultiAllDownloadCompletedEventArgs args) { } + public virtual void OnDownloadCompleted(MultiDownloadCompletedEventArgs args) { } + public virtual void OnDownloadError(MultiDownloadErrorEventArgs args) { } + public virtual void OnDownloadStatistics(MultiDownloadStatisticsEventArgs args) { } + public virtual void OnUpdateInfo(UpdateInfoEventArgs args) { } + public virtual void OnException(ExceptionEventArgs args) { } + public virtual void OnProgress(ProgressEventArgs args) { } } diff --git a/src/c#/GeneralUpdate.Core/Event/ProgressEventArgs.cs b/src/c#/GeneralUpdate.Core/Event/ProgressEventArgs.cs new file mode 100644 index 00000000..f9fc926f --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Event/ProgressEventArgs.cs @@ -0,0 +1,17 @@ +using System; +using GeneralUpdate.Core.Download.Models; + +namespace GeneralUpdate.Core.Event; + +/// +/// Progress event args — wraps a DownloadProgress snapshot. +/// +public class ProgressEventArgs : EventArgs +{ + public DownloadProgress Progress { get; } + + public ProgressEventArgs(DownloadProgress progress) + { + Progress = progress; + } +} diff --git a/tests/CoreTest/Event/EventListenerTests.cs b/tests/CoreTest/Event/EventListenerTests.cs index 4aac595b..73b86495 100644 --- a/tests/CoreTest/Event/EventListenerTests.cs +++ b/tests/CoreTest/Event/EventListenerTests.cs @@ -36,7 +36,7 @@ public void OnUpdateInfo(UpdateInfoEventArgs args) public void OnException(ExceptionEventArgs args) => ExceptionCalls++; - public void OnProgress(DownloadProgress progress) + public void OnProgress(ProgressEventArgs args) => ProgressCalls++; }