Initial commit
This commit is contained in:
@@ -0,0 +1,555 @@
|
||||
using Appccelerate.StateMachine;
|
||||
using Appccelerate.StateMachine.Machine;
|
||||
using RobotNet10.Script;
|
||||
using RobotNet10.ScriptEngine.Shared;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
|
||||
namespace RobotNet10.ScriptEngine.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a periodic task with state machine management.
|
||||
/// </summary>
|
||||
public class ScriptTask : IDisposable
|
||||
{
|
||||
private readonly PassiveStateMachine<ScriptTaskState, TaskTrigger> _stateMachine;
|
||||
private readonly ScriptTaskModel _model;
|
||||
private readonly ScriptGlobals _globals;
|
||||
private readonly ILogger _logger;
|
||||
private Thread? _timerThread;
|
||||
private volatile bool _timerThreadRunning;
|
||||
private bool _isExecuting;
|
||||
private bool _isPaused;
|
||||
private bool _disposed;
|
||||
private Exception? _lastError;
|
||||
private readonly object _lockObject = new();
|
||||
private ScriptTaskState _currentState;
|
||||
private long _executionCount;
|
||||
|
||||
/// <summary>
|
||||
/// Task triggers for state machine transitions.
|
||||
/// </summary>
|
||||
public enum TaskTrigger
|
||||
{
|
||||
Start,
|
||||
Pause,
|
||||
Resume,
|
||||
Stop,
|
||||
PausingCompleted,
|
||||
ResumingCompleted,
|
||||
StoppingCompleted,
|
||||
ErrorOccurred,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the task.
|
||||
/// </summary>
|
||||
public string Name => _model.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the interval in seconds between task executions.
|
||||
/// </summary>
|
||||
public int Interval => _model.Interval;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the task should auto-start when engine starts.
|
||||
/// </summary>
|
||||
public bool AutoStart => _model.AutoStart;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current state of the task.
|
||||
/// </summary>
|
||||
public ScriptTaskState State => _currentState;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last error that occurred during task execution.
|
||||
/// </summary>
|
||||
public Exception? LastError => _lastError;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the task is currently executing.
|
||||
/// </summary>
|
||||
public bool IsExecuting => _isExecuting;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of times the task has been executed.
|
||||
/// </summary>
|
||||
public long ExecutionCount => _executionCount;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ScriptTask class.
|
||||
/// </summary>
|
||||
/// <param name="model">The task model containing task metadata and runner.</param>
|
||||
/// <param name="globals">The script globals dictionary.</param>
|
||||
public ScriptTask(
|
||||
ScriptTaskModel model,
|
||||
ScriptGlobals globals)
|
||||
{
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
_globals = globals ?? throw new ArgumentNullException(nameof(globals));
|
||||
|
||||
// Get logger from globals.ScriptRobotNet
|
||||
if (_globals.RobotNet.TryGetValue("get_Logger", out object? getLogger) &&
|
||||
getLogger is Func<ILogger> getLoggerFunc)
|
||||
{
|
||||
_logger = getLoggerFunc.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to get Logger from ScriptRobotNet globals for task '{model.Name}'");
|
||||
}
|
||||
|
||||
var builder = new StateMachineDefinitionBuilder<ScriptTaskState, TaskTrigger>();
|
||||
|
||||
// Configure state machine transitions according to StateMachine_Design.md
|
||||
ConfigureStateMachine(builder);
|
||||
|
||||
_stateMachine = builder
|
||||
.WithInitialState(ScriptTaskState.Idle)
|
||||
.Build()
|
||||
.CreatePassiveStateMachine();
|
||||
|
||||
_currentState = ScriptTaskState.Idle;
|
||||
_stateMachine.Start();
|
||||
}
|
||||
|
||||
private void ConfigureStateMachine(StateMachineDefinitionBuilder<ScriptTaskState, TaskTrigger> builder)
|
||||
{
|
||||
// Idle state
|
||||
builder.In(ScriptTaskState.Idle)
|
||||
.On(TaskTrigger.Start)
|
||||
.Goto(ScriptTaskState.Running);
|
||||
|
||||
// Running state - configure entry/exit actions separately
|
||||
builder.In(ScriptTaskState.Running)
|
||||
.ExecuteOnEntry(() => { _currentState = ScriptTaskState.Running; OnEnterRunning(); })
|
||||
.On(TaskTrigger.Pause)
|
||||
.Goto(ScriptTaskState.Pausing)
|
||||
.On(TaskTrigger.Stop)
|
||||
.Goto(ScriptTaskState.Stopping)
|
||||
.On(TaskTrigger.ErrorOccurred)
|
||||
.Goto(ScriptTaskState.Error)
|
||||
.Execute(() => { _currentState = ScriptTaskState.Error; OnEnterError(); });
|
||||
|
||||
// Pausing state
|
||||
builder.In(ScriptTaskState.Pausing)
|
||||
.ExecuteOnEntry(() => { _currentState = ScriptTaskState.Pausing; OnEnterPausing(); })
|
||||
.ExecuteOnExit(() => OnExitPausing())
|
||||
.On(TaskTrigger.PausingCompleted)
|
||||
.Goto(ScriptTaskState.Paused)
|
||||
.On(TaskTrigger.ErrorOccurred)
|
||||
.Goto(ScriptTaskState.Error)
|
||||
.Execute(() => { _currentState = ScriptTaskState.Error; OnEnterError(); });
|
||||
|
||||
// Paused state
|
||||
builder.In(ScriptTaskState.Paused)
|
||||
.ExecuteOnEntry(() => { _currentState = ScriptTaskState.Paused; OnEnterPaused(); })
|
||||
.ExecuteOnExit(() => OnExitPaused())
|
||||
.On(TaskTrigger.Resume)
|
||||
.Goto(ScriptTaskState.Resuming)
|
||||
.On(TaskTrigger.Stop)
|
||||
.Goto(ScriptTaskState.Stopping);
|
||||
|
||||
// Resuming state
|
||||
builder.In(ScriptTaskState.Resuming)
|
||||
.ExecuteOnEntry(() => { _currentState = ScriptTaskState.Resuming; OnEnterResuming(); })
|
||||
.ExecuteOnExit(() => OnExitResuming())
|
||||
.On(TaskTrigger.ResumingCompleted)
|
||||
.Goto(ScriptTaskState.Running)
|
||||
.On(TaskTrigger.ErrorOccurred)
|
||||
.Goto(ScriptTaskState.Error)
|
||||
.Execute(() => { _currentState = ScriptTaskState.Error; OnEnterError(); });
|
||||
|
||||
// Stopping state
|
||||
builder.In(ScriptTaskState.Stopping)
|
||||
.ExecuteOnEntry(() => { _currentState = ScriptTaskState.Stopping; OnEnterStopping(); })
|
||||
.On(TaskTrigger.StoppingCompleted)
|
||||
.Goto(ScriptTaskState.Stopped)
|
||||
.On(TaskTrigger.ErrorOccurred)
|
||||
.Goto(ScriptTaskState.Error)
|
||||
.Execute(() => { _currentState = ScriptTaskState.Error; OnEnterError(); });
|
||||
|
||||
// Stopped state
|
||||
builder.In(ScriptTaskState.Stopped)
|
||||
.ExecuteOnEntry(() => { _currentState = ScriptTaskState.Stopped; OnEnterStopped(); })
|
||||
.On(TaskTrigger.Start)
|
||||
.Goto(ScriptTaskState.Running);
|
||||
|
||||
// Error state
|
||||
builder.In(ScriptTaskState.Error)
|
||||
.ExecuteOnEntry(() => { _currentState = ScriptTaskState.Error; OnEnterError(); })
|
||||
.ExecuteOnExit(() => OnExitError())
|
||||
.On(TaskTrigger.Start)
|
||||
.Goto(ScriptTaskState.Running);
|
||||
}
|
||||
|
||||
#region State Machine Event Handlers
|
||||
|
||||
private void OnEnterRunning()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
// Start high-priority thread with SpinWait
|
||||
StartTimerThread();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartTimerThread()
|
||||
{
|
||||
if (_timerThread == null || !_timerThread.IsAlive)
|
||||
{
|
||||
_timerThreadRunning = true;
|
||||
_timerThread = new Thread(TimerThreadProc)
|
||||
{
|
||||
IsBackground = false,
|
||||
Priority = ThreadPriority.Highest,
|
||||
Name = $"TaskTimer-{_model.Name}"
|
||||
};
|
||||
_timerThread.Start();
|
||||
_logger.LogInfo($"Task '{_model.Name}' started running with high-priority thread (interval: {_model.Interval}ms).");
|
||||
}
|
||||
}
|
||||
|
||||
private void TimerThreadProc()
|
||||
{
|
||||
_isExecuting = true;
|
||||
Thread.BeginThreadAffinity();
|
||||
try
|
||||
{
|
||||
// Convert interval from seconds to milliseconds
|
||||
var intervalMs = _model.Interval;
|
||||
var intervalTicks = intervalMs * TimeSpan.TicksPerMillisecond;
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var nextExecutionTime = stopwatch.ElapsedTicks + intervalTicks;
|
||||
var spinWait = new SpinWait();
|
||||
long currentTicks = 0;
|
||||
|
||||
while (_timerThreadRunning)
|
||||
{
|
||||
currentTicks = stopwatch.ElapsedTicks;
|
||||
|
||||
// Check if it's time to execute
|
||||
if (currentTicks >= nextExecutionTime)
|
||||
{
|
||||
if (_isPaused) continue;
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_executionCount++;
|
||||
}
|
||||
|
||||
// Execute the script runner with globals directly
|
||||
var result = _model.Runner(_globals).GetAwaiter().GetResult();
|
||||
|
||||
// Handle async result if needed
|
||||
if (result is Task taskResult)
|
||||
{
|
||||
taskResult.GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_lastError = ex;
|
||||
_logger.LogError($"Task '{_model.Name}' execution error: {ex.Message}");
|
||||
_stateMachine.Fire(TaskTrigger.ErrorOccurred);
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate next execution time
|
||||
nextExecutionTime = currentTicks + intervalTicks;
|
||||
}
|
||||
spinWait.SpinOnce();
|
||||
spinWait.SpinOnce();
|
||||
spinWait.SpinOnce();
|
||||
spinWait.SpinOnce();
|
||||
spinWait.SpinOnce();
|
||||
spinWait.SpinOnce();
|
||||
spinWait.SpinOnce();
|
||||
spinWait.SpinOnce();
|
||||
spinWait.SpinOnce();
|
||||
spinWait.SpinOnce();
|
||||
spinWait.Reset();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Thread.EndThreadAffinity();
|
||||
_isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnterPausing()
|
||||
{
|
||||
// Wait for current execution to complete if running
|
||||
// Then fire PausingCompleted trigger
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await WaitForExecutionComplete();
|
||||
_stateMachine.Fire(TaskTrigger.PausingCompleted);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnExitPausing()
|
||||
{
|
||||
// Set paused flag - timer continues but ExecuteTask will skip execution
|
||||
_isPaused = true;
|
||||
}
|
||||
|
||||
private void OnEnterPaused()
|
||||
{
|
||||
_isPaused = true;
|
||||
_logger.LogInfo($"Task '{_model.Name}' paused (timer continues, execution skipped).");
|
||||
}
|
||||
|
||||
private void OnExitPaused()
|
||||
{
|
||||
// Clear paused flag when exiting paused state
|
||||
_isPaused = false;
|
||||
}
|
||||
|
||||
private void OnEnterResuming()
|
||||
{
|
||||
// Clear paused flag immediately
|
||||
_isPaused = false;
|
||||
// Fire ResumingCompleted immediately (no async operation needed)
|
||||
_stateMachine.Fire(TaskTrigger.ResumingCompleted);
|
||||
}
|
||||
|
||||
private void OnExitResuming()
|
||||
{
|
||||
// Ensure paused flag is cleared
|
||||
_isPaused = false;
|
||||
}
|
||||
|
||||
private void OnEnterStopping()
|
||||
{
|
||||
// Stop timer thread, wait for current execution to complete
|
||||
lock (_lockObject)
|
||||
{
|
||||
// Stop the timer thread loop
|
||||
_timerThreadRunning = false;
|
||||
}
|
||||
|
||||
// Wait for timer thread to finish (with timeout)
|
||||
if (_timerThread != null && _timerThread.IsAlive)
|
||||
{
|
||||
if (!_timerThread.Join(TimeSpan.FromSeconds(2)))
|
||||
{
|
||||
_logger.LogWarning($"Task '{_model.Name}' timer thread did not stop within timeout. Proceeding anyway.");
|
||||
}
|
||||
}
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// Wait for execution to complete, but only timeout if actually executing
|
||||
await WaitForExecutionComplete();
|
||||
|
||||
// If still executing after timeout, it's ScriptRunner blocking
|
||||
// Otherwise, proceed even if state machine didn't transition
|
||||
_stateMachine.Fire(TaskTrigger.StoppingCompleted);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnEnterStopped()
|
||||
{
|
||||
_logger.LogInfo($"Task '{_model.Name}' stopped.");
|
||||
}
|
||||
|
||||
private void OnEnterError()
|
||||
{
|
||||
_logger.LogError($"Task '{_model.Name}' entered error state. Last error: {_lastError?.Message}");
|
||||
}
|
||||
|
||||
private void OnExitError()
|
||||
{
|
||||
_lastError = null;
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Control Methods
|
||||
|
||||
/// <summary>
|
||||
/// Starts the task (transitions from Idle/Stopped/Error to Running).
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
_stateMachine.Fire(TaskTrigger.Start);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Failed to start task '{_model.Name}': {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pauses the task (transitions from Running to Pausing → Paused).
|
||||
/// </summary>
|
||||
public void Pause()
|
||||
{
|
||||
try
|
||||
{
|
||||
_stateMachine.Fire(TaskTrigger.Pause);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Failed to pause task '{_model.Name}': {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resumes the task (transitions from Paused to Resuming → Running).
|
||||
/// </summary>
|
||||
public void Resume()
|
||||
{
|
||||
try
|
||||
{
|
||||
_stateMachine.Fire(TaskTrigger.Resume);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Failed to resume task '{_model.Name}': {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the task (transitions from Running/Paused to Stopping → Stopped).
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
try
|
||||
{
|
||||
_stateMachine.Fire(TaskTrigger.Stop);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"Failed to stop task '{_model.Name}': {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Task Execution
|
||||
|
||||
private async Task WaitForExecutionComplete()
|
||||
{
|
||||
// Wait for current execution to complete (max 30 seconds)
|
||||
// Only timeout if task is actually executing (ScriptRunner running)
|
||||
var timeout = TimeSpan.FromSeconds(30);
|
||||
var startTime = DateTime.UtcNow;
|
||||
|
||||
while (_isExecuting && (DateTime.UtcNow - startTime) < timeout)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
if (_isExecuting)
|
||||
{
|
||||
// Task is still executing - ScriptRunner is blocking
|
||||
_logger.LogWarning($"Task '{_model.Name}' execution timeout while waiting for completion. ScriptRunner may be blocking.");
|
||||
}
|
||||
// If task is not executing, it's effectively stopped (even if state machine didn't transition)
|
||||
// No need to log - proceed silently
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the task. Can be called from any state.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
// Stop the task if it's running
|
||||
try
|
||||
{
|
||||
if (_currentState == ScriptTaskState.Running ||
|
||||
_currentState == ScriptTaskState.Paused ||
|
||||
_currentState == ScriptTaskState.Pausing ||
|
||||
_currentState == ScriptTaskState.Resuming)
|
||||
{
|
||||
_stateMachine.Fire(TaskTrigger.Stop);
|
||||
// Wait a bit for stopping to complete
|
||||
var stopTimeout = TimeSpan.FromSeconds(2);
|
||||
var startTime = DateTime.UtcNow;
|
||||
while ((_currentState == ScriptTaskState.Running ||
|
||||
_currentState == ScriptTaskState.Paused ||
|
||||
_currentState == ScriptTaskState.Pausing ||
|
||||
_currentState == ScriptTaskState.Resuming ||
|
||||
_currentState == ScriptTaskState.Stopping) &&
|
||||
(DateTime.UtcNow - startTime) < stopTimeout)
|
||||
{
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors during stop
|
||||
}
|
||||
|
||||
// Ensure timer thread is stopped
|
||||
lock (_lockObject)
|
||||
{
|
||||
_timerThreadRunning = false;
|
||||
}
|
||||
|
||||
// Wait for timer thread to finish (with timeout)
|
||||
if (_timerThread != null && _timerThread.IsAlive)
|
||||
{
|
||||
if (!_timerThread.Join(TimeSpan.FromSeconds(2)))
|
||||
{
|
||||
_logger.LogWarning($"Task '{_model.Name}' timer thread did not stop within timeout during dispose.");
|
||||
}
|
||||
}
|
||||
_timerThread = null;
|
||||
|
||||
// Wait for execution to complete
|
||||
try
|
||||
{
|
||||
WaitForExecutionComplete().Wait(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
// Stop state machine
|
||||
try
|
||||
{
|
||||
_stateMachine.Stop();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user