Initial commit

This commit is contained in:
2026-07-13 09:25:40 +07:00
parent c08ff54676
commit bccfb156d7
1938 changed files with 641646 additions and 0 deletions

View File

@@ -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
}