using Appccelerate.StateMachine; using Appccelerate.StateMachine.Machine; using RobotNet10.Script; using RobotNet10.ScriptEngine.Shared; using System.Diagnostics; using System.Threading; namespace RobotNet10.ScriptEngine.Models; /// /// Represents a periodic task with state machine management. /// public class ScriptTask : IDisposable { private readonly PassiveStateMachine _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; /// /// Task triggers for state machine transitions. /// public enum TaskTrigger { Start, Pause, Resume, Stop, PausingCompleted, ResumingCompleted, StoppingCompleted, ErrorOccurred, } /// /// Gets the name of the task. /// public string Name => _model.Name; /// /// Gets the interval in seconds between task executions. /// public int Interval => _model.Interval; /// /// Gets whether the task should auto-start when engine starts. /// public bool AutoStart => _model.AutoStart; /// /// Gets the current state of the task. /// public ScriptTaskState State => _currentState; /// /// Gets the last error that occurred during task execution. /// public Exception? LastError => _lastError; /// /// Gets whether the task is currently executing. /// public bool IsExecuting => _isExecuting; /// /// Gets the number of times the task has been executed. /// public long ExecutionCount => _executionCount; /// /// Initializes a new instance of the ScriptTask class. /// /// The task model containing task metadata and runner. /// The script globals dictionary. 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 getLoggerFunc) { _logger = getLoggerFunc.Invoke(); } else { throw new InvalidOperationException($"Failed to get Logger from ScriptRobotNet globals for task '{model.Name}'"); } var builder = new StateMachineDefinitionBuilder(); // 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 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 /// /// Starts the task (transitions from Idle/Stopped/Error to Running). /// public void Start() { try { _stateMachine.Fire(TaskTrigger.Start); } catch (Exception ex) { _logger.LogError($"Failed to start task '{_model.Name}': {ex.Message}"); throw; } } /// /// Pauses the task (transitions from Running to Pausing → Paused). /// public void Pause() { try { _stateMachine.Fire(TaskTrigger.Pause); } catch (Exception ex) { _logger.LogError($"Failed to pause task '{_model.Name}': {ex.Message}"); throw; } } /// /// Resumes the task (transitions from Paused to Resuming → Running). /// public void Resume() { try { _stateMachine.Fire(TaskTrigger.Resume); } catch (Exception ex) { _logger.LogError($"Failed to resume task '{_model.Name}': {ex.Message}"); throw; } } /// /// Stops the task (transitions from Running/Paused to Stopping → Stopped). /// 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 /// /// Disposes the task. Can be called from any state. /// 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 }