using System; using System.Threading; using Jint.Native; using Jint.Runtime; using Microsoft.Extensions.Logging; using SharedLibraryCore.Interfaces; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace IW4MAdmin.Application.Misc; public class ScriptPluginTimerHelper : IScriptPluginTimerHelper { private Timer _timer; private Action _actions; private Delegate _jsAction; private string _actionName; private const int DefaultDelay = 0; private const int DefaultInterval = 1000; private readonly ILogger _logger; private readonly ManualResetEventSlim _onRunningTick = new(); private SemaphoreSlim _onDependentAction; public ScriptPluginTimerHelper(ILogger logger) { _logger = logger; } ~ScriptPluginTimerHelper() { if (_timer != null) { Stop(); } _onRunningTick.Dispose(); } public void Start(int delay, int interval) { if (_actions is null) { throw new InvalidOperationException("Timer action must be defined before starting"); } if (delay < 0) { throw new ArgumentException("Timer delay must be >= 0"); } if (interval < 20) { throw new ArgumentException("Timer interval must be at least 20ms"); } Stop(); _logger.LogDebug("Starting script timer..."); _onRunningTick.Set(); _timer ??= new Timer(callback => _actions(), null, delay, interval); IsRunning = true; } public void Start(int interval) { Start(DefaultDelay, interval); } public void Start() { Start(DefaultDelay, DefaultInterval); } public void Stop() { if (_timer == null) { return; } _logger.LogDebug("Stopping script timer..."); _timer.Change(Timeout.Infinite, Timeout.Infinite); _timer.Dispose(); _timer = null; IsRunning = false; } public void OnTick(Delegate action, string actionName) { if (string.IsNullOrEmpty(actionName)) { throw new ArgumentException("actionName must be provided", nameof(actionName)); } if (action is null) { throw new ArgumentException("action must be provided", nameof(action)); } _logger.LogDebug("Adding new action with name {ActionName}", actionName); _jsAction = action; _actionName = actionName; _actions = OnTick; } private void ReleaseThreads() { _onRunningTick.Set(); if (_onDependentAction?.CurrentCount != 0) { return; } _onDependentAction?.Release(1); } private void OnTick() { try { if (!_onRunningTick.IsSet) { _logger.LogWarning("Previous {OnTick} is still running, so we are skipping this one", nameof(OnTick)); return; } _onRunningTick.Reset(); // the js engine is not thread safe so we need to ensure we're not executing OnTick and OnEventAsync simultaneously _onDependentAction?.WaitAsync().Wait(); _jsAction.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined }); ReleaseThreads(); } catch (Exception ex) when (ex.InnerException is JavaScriptException jsex) { _logger.LogError(jsex, "Could not execute timer tick for script action {ActionName} [@{LocationInfo}]", _actionName, jsex.Location); ReleaseThreads(); } catch (Exception ex) { _logger.LogError(ex, "Could not execute timer tick for script action {ActionName}", _actionName); _onRunningTick.Set(); ReleaseThreads(); } } public void SetDependency(SemaphoreSlim dependentSemaphore) { _onDependentAction = dependentSemaphore; } public bool IsRunning { get; private set; } }