IW4M-Admin/Application/Misc/ScriptPluginTimerHelper.cs

161 lines
4.3 KiB
C#

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<ScriptPluginTimerHelper> 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;
}
_logger.LogDebug("-Releasing OnTick for timer");
_onDependentAction?.Release(1);
}
private void OnTick()
{
try
{
if (!_onRunningTick.IsSet)
{
_logger.LogDebug("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?.Wait();
_logger.LogDebug("+Running OnTick for timer");
var start = DateTime.Now;
_jsAction.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined });
_logger.LogDebug("OnTick took {Time}ms", (DateTime.Now - start).TotalMilliseconds);
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; }
}