using System; using System.Threading.Tasks; using System.Timers; using Artemis.Core.Modules; using Artemis.Core.Services; using Humanizer; using Ninject; using Serilog; namespace Artemis.Core { /// /// Represents a registration for a timed plugin update /// public class TimedUpdateRegistration : IDisposable { private readonly object _lock = new(); private bool _disposed; private DateTime _lastEvent; private readonly ILogger _logger; private Timer? _timer; internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Action action, string? name) { _logger = CoreService.Kernel.Get(); Feature = feature; Interval = interval; Action = action; Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}"; Feature.Enabled += FeatureOnEnabled; Feature.Disabled += FeatureOnDisabled; if (Feature.IsEnabled) Start(); } internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func asyncAction, string? name) { _logger = CoreService.Kernel.Get(); Feature = feature; Interval = interval; AsyncAction = asyncAction; Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}"; Feature.Enabled += FeatureOnEnabled; Feature.Disabled += FeatureOnDisabled; if (Feature.IsEnabled) Start(); } /// /// Gets the plugin feature this registration is associated with /// public PluginFeature Feature { get; } /// /// Gets the interval at which the update should occur /// public TimeSpan Interval { get; } /// /// Gets the action that gets called each time the update event fires /// public Action? Action { get; } /// /// Gets the task that gets called each time the update event fires /// public Func? AsyncAction { get; } /// /// Gets the name of this timed update /// public string Name { get; } /// /// Starts calling the or at the configured /// Note: Called automatically when the plugin enables /// public void Start() { if (_disposed) throw new ObjectDisposedException("TimedUpdateRegistration"); lock (_lock) { if (!Feature.IsEnabled) throw new ArtemisPluginException("Cannot start a timed update for a disabled plugin feature"); if (_timer != null) return; _lastEvent = DateTime.Now; _timer = new Timer(Interval.TotalMilliseconds); _timer.Elapsed += TimerOnElapsed; _timer.Start(); } } /// /// Stops calling the or at the configured /// Note: Called automatically when the plugin disables /// public void Stop() { if (_disposed) throw new ObjectDisposedException("TimedUpdateRegistration"); lock (_lock) { if (_timer == null) return; _timer.Elapsed -= TimerOnElapsed; _timer.Stop(); _timer.Dispose(); _timer = null; } } /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// /// to release both managed and unmanaged resources; /// to release only unmanaged resources. /// protected virtual void Dispose(bool disposing) { if (disposing) { Stop(); Feature.Enabled -= FeatureOnEnabled; Feature.Disabled -= FeatureOnDisabled; _disposed = true; } } private void TimerOnElapsed(object? sender, ElapsedEventArgs e) { if (!Feature.IsEnabled) return; lock (_lock) { Feature.Profiler.StartMeasurement(ToString()); TimeSpan interval = DateTime.Now - _lastEvent; _lastEvent = DateTime.Now; // Modules don't always want to update, honor that if (Feature is Module module && !module.IsUpdateAllowed) return; try { if (Action != null) { Action(interval.TotalSeconds); } else if (AsyncAction != null) { Task task = AsyncAction(interval.TotalSeconds); task.Wait(); } } catch (Exception exception) { _logger.Error(exception, "{timedUpdate} uncaught exception in plugin {plugin}", this, Feature.Plugin); } finally { Feature.Profiler.StopMeasurement(ToString()); } } } private void FeatureOnEnabled(object? sender, EventArgs e) { Start(); } private void FeatureOnDisabled(object? sender, EventArgs e) { Stop(); } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// public sealed override string ToString() { if (Interval.TotalSeconds >= 1) return $"{Name} ({Interval.TotalSeconds} sec)"; return $"{Name} ({Interval.TotalMilliseconds} ms)"; } } }