Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,24 @@ public GeneralUpdateBootstrap AddListenerException(

public GeneralUpdateBootstrap AddListenerUpdateInfo(
Action<object, UpdateInfoEventArgs> cb) => AddListener(cb);

public GeneralUpdateBootstrap AddListenerProgress(
Action<object, ProgressEventArgs> cb) => AddListener(cb);

/// <summary>
/// Batch-register an event listener implementing <see cref="IUpdateEventListener"/>.
/// All 7 event handlers are registered at once.
/// </summary>
public GeneralUpdateBootstrap AddEventListener<TListener>() where TListener : IUpdateEventListener, new()
{
var listener = new TListener();
AddListener<MultiAllDownloadCompletedEventArgs>((s, e) => listener.OnAllDownloadCompleted(e));
AddListener<MultiDownloadCompletedEventArgs>((s, e) => listener.OnDownloadCompleted(e));
AddListener<MultiDownloadErrorEventArgs>((s, e) => listener.OnDownloadError(e));
AddListener<MultiDownloadStatisticsEventArgs>((s, e) => listener.OnDownloadStatistics(e));
AddListener<UpdateInfoEventArgs>((s, e) => listener.OnUpdateInfo(e));
AddListener<ExceptionEventArgs>((s, e) => listener.OnException(e));
AddListener<ProgressEventArgs>((s, e) => listener.OnProgress(e));
return this;
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
using System;
using System.Collections.Generic;
using GeneralUpdate.Core.Download;
using GeneralUpdate.Core.Event;
using IProgress = System.IProgress<GeneralUpdate.Core.Download.Models.DownloadProgress>;

namespace GeneralUpdate.Core.Download.Progress;

/// <summary>Bridges IProgress to event-based callbacks for download status.</summary>
/// <summary>Bridges IProgress to EventManager for backward-compatible event listeners.</summary>
public class DownloadProgressReporter : IProgress
{
private readonly Action<Download.Models.DownloadProgress>? _onProgress;
private readonly Action<Models.DownloadProgress>? _onProgress;
private readonly Action? _onCompleted;
private readonly Action? _onAllCompleted;

public DownloadProgressReporter(
Action<Download.Models.DownloadProgress>? onProgress = null,
Action<Models.DownloadProgress>? onProgress = null,
Action? onCompleted = null,
Action? onAllCompleted = null)
{
Expand All @@ -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)>()));
}
}

/// <summary>
/// Create an IProgress that dispatches progress events to EventManager.
/// </summary>
public static IProgress CreateEventBridge()
=> new DownloadProgressReporter();
}
93 changes: 41 additions & 52 deletions src/c#/GeneralUpdate.Core/Event/EventManager.cs
Original file line number Diff line number Diff line change
@@ -1,92 +1,81 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using GeneralUpdate.Core;

namespace GeneralUpdate.Core.Event
{
/// <summary>
/// Thread-safe event manager using ConcurrentDictionary.
/// Supports add/remove/dispatch without lock contention.
/// </summary>
public class EventManager : IDisposable
{
private static readonly Lazy<EventManager> _lazy = new(() => new EventManager());
private Dictionary<Type, Delegate> _dicDelegates = new();
private bool _disposed = false;
private ConcurrentDictionary<Type, Delegate> _dicDelegates = new();
private bool _disposed;

private EventManager() { }

public static EventManager Instance => _lazy.Value;

public void AddListener<TEventArgs>(Action<object, TEventArgs> listener) where TEventArgs : EventArgs
{
try
{
if (listener == null) throw new ArgumentNullException(nameof(listener));
var delegateType = typeof(Action<object, TEventArgs>);
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<object, TEventArgs>);
_dicDelegates.AddOrUpdate(type,
_ => listener,
(_, existing) => Delegate.Combine(existing, listener));
}

public void RemoveListener<TEventArgs>(Action<object, TEventArgs> listener) where TEventArgs : EventArgs
{
try
if (listener == null) throw new ArgumentNullException(nameof(listener));
var type = typeof(Action<object, TEventArgs>);
if (_dicDelegates.TryGetValue(type, out var existing))
{
if (listener == null) throw new ArgumentNullException(nameof(listener));
var delegateType = typeof(Action<object, TEventArgs>);
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<TEventArgs>(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<object, TEventArgs>);
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<object, TEventArgs>);
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<object, TEventArgs>)existingDelegate)?.Invoke(sender, eventArgs);
try
{
((Action<object, TEventArgs>)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;
}
}
}
}
}
40 changes: 31 additions & 9 deletions src/c#/GeneralUpdate.Core/Event/IUpdateEventListener.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,47 @@
using System;
using GeneralUpdate.Core.Download;
using GeneralUpdate.Core.Download.Models;
using GeneralUpdate.Core.Event;

namespace GeneralUpdate.Core.Event;

/// <summary>Batch event registration — implement once, register once.</summary>
/// <summary>
/// Batch registration interface for all update event types.
/// Implement this interface and register via
/// <c>new GeneralUpdateBootstrap().AddEventListener&lt;MyListener&gt;()</c>.
/// </summary>
public interface IUpdateEventListener
{
/// <summary>All downloads completed.</summary>
void OnAllDownloadCompleted(MultiAllDownloadCompletedEventArgs args);

/// <summary>Single download completed.</summary>
void OnDownloadCompleted(MultiDownloadCompletedEventArgs args);

/// <summary>Download error.</summary>
void OnDownloadError(MultiDownloadErrorEventArgs args);

/// <summary>Download statistics updated.</summary>
void OnDownloadStatistics(MultiDownloadStatisticsEventArgs args);

/// <summary>Update information available.</summary>
void OnUpdateInfo(UpdateInfoEventArgs args);

/// <summary>Exception occurred.</summary>
void OnException(ExceptionEventArgs args);
void OnProgress(DownloadProgress progress);

/// <summary>Real-time download progress.</summary>
void OnProgress(ProgressEventArgs args);
}

/// <summary>Progress event args for AddListenerProgress.</summary>
public class ProgressEventArgs : EventArgs
/// <summary>
/// Base class that implements IUpdateEventListener with no-op methods.
/// Inherit from this and override only the events you need.
/// </summary>
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) { }
}
17 changes: 17 additions & 0 deletions src/c#/GeneralUpdate.Core/Event/ProgressEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using GeneralUpdate.Core.Download.Models;

namespace GeneralUpdate.Core.Event;

/// <summary>
/// Progress event args — wraps a DownloadProgress snapshot.
/// </summary>
public class ProgressEventArgs : EventArgs
{
public DownloadProgress Progress { get; }

public ProgressEventArgs(DownloadProgress progress)
{
Progress = progress;
}
}
2 changes: 1 addition & 1 deletion tests/CoreTest/Event/EventListenerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
}

Expand Down
Loading