Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View File

@@ -0,0 +1,606 @@
using Appccelerate.StateMachine;
using Appccelerate.StateMachine.Machine;
using RobotNet10.Script;
using RobotNet10.ScriptEngine.Shared;
namespace RobotNet10.ScriptEngine.Models;
/// <summary>
/// Represents a mission instance with state machine management.
/// </summary>
public class ScriptMission : IDisposable
{
private readonly PassiveStateMachine<ScriptMissionState, MissionTrigger> _stateMachine;
private readonly ScriptMissionModel _model;
private readonly ScriptGlobals _globals;
private readonly ILogger _logger;
private readonly CancellationTokenSource _internalCts;
private CancellationTokenSource? _executionCts;
private Task? _executionTask;
private bool _isPaused;
private bool _isCanceling;
private bool _disposed;
private Exception? _lastError;
private readonly Lock _lockObject = new();
private ScriptMissionState _currentState;
private int _currentScore;
/// <summary>
/// Mission triggers for state machine transitions.
/// </summary>
public enum MissionTrigger
{
Start,
Cancel,
Pause,
Resume,
CompleteCanceling,
CompletePausing,
CompleteResuming,
CompleteRunning,
ErrorOccurred,
}
/// <summary>
/// Gets the unique identifier of the mission instance.
/// </summary>
public Guid Id { get; }
/// <summary>
/// Gets the name of the mission.
/// </summary>
public string Name => _model.Name;
/// <summary>
/// Gets the total score for progress tracking.
/// </summary>
public int TotalScore => _model.TotalScore;
/// <summary>
/// Gets the current score.
/// </summary>
public int CurrentScore => _currentScore;
/// <summary>
/// Gets the current state of the mission.
/// </summary>
public ScriptMissionState State => _currentState;
/// <summary>
/// Gets the last error that occurred during mission execution.
/// </summary>
public Exception? LastError => _lastError;
/// <summary>
/// Gets the log message from mission execution.
/// </summary>
public string LogMessage => _logger.GetLog();
/// <summary>
/// Gets the log message from ILogger.
/// </summary>
public string GetLog()
{
return _logger.GetLog();
}
/// <summary>
/// Gets whether the mission is currently executing.
/// </summary>
public bool IsExecuting => _executionTask != null && !_executionTask.IsCompleted;
/// <summary>
/// Initializes a new instance of the ScriptMission class.
/// </summary>
/// <param name="id">The unique identifier for this mission instance.</param>
/// <param name="model">The mission model containing mission metadata and runner.</param>
/// <param name="globals">The script globals dictionary.</param>
public ScriptMission(
Guid id,
ScriptMissionModel model,
ScriptGlobals globals)
{
Id = id;
_model = model ?? throw new ArgumentNullException(nameof(model));
_globals = globals ?? throw new ArgumentNullException(nameof(globals));
_internalCts = new CancellationTokenSource();
// 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 mission '{model.Name}'");
}
var builder = new StateMachineDefinitionBuilder<ScriptMissionState, MissionTrigger>();
// Configure state machine transitions according to StateMachine_Design.md
ConfigureStateMachine(builder);
_stateMachine = builder
.WithInitialState(ScriptMissionState.Idle)
.Build()
.CreatePassiveStateMachine();
_currentState = ScriptMissionState.Idle;
_stateMachine.Start();
}
private void ConfigureStateMachine(StateMachineDefinitionBuilder<ScriptMissionState, MissionTrigger> builder)
{
// Idle state
builder.In(ScriptMissionState.Idle)
.On(MissionTrigger.Start)
.Goto(ScriptMissionState.Running);
// Running state
builder.In(ScriptMissionState.Running)
.ExecuteOnEntry(() => { _currentState = ScriptMissionState.Running; OnEnterRunning(); })
.On(MissionTrigger.Cancel)
.Goto(ScriptMissionState.Canceling)
.On(MissionTrigger.Pause)
.Goto(ScriptMissionState.Pausing)
.On(MissionTrigger.CompleteRunning)
.Goto(ScriptMissionState.Completed)
.On(MissionTrigger.ErrorOccurred)
.Goto(ScriptMissionState.Error)
.Execute(() => { _currentState = ScriptMissionState.Error; OnEnterError(); });
// Canceling state
builder.In(ScriptMissionState.Canceling)
.ExecuteOnEntry(() => { _currentState = ScriptMissionState.Canceling; OnEnterCanceling(); })
.On(MissionTrigger.CompleteCanceling)
.Goto(ScriptMissionState.Canceled)
.On(MissionTrigger.ErrorOccurred)
.Goto(ScriptMissionState.Error)
.Execute(() => { _currentState = ScriptMissionState.Error; OnEnterError(); });
// Pausing state
builder.In(ScriptMissionState.Pausing)
.ExecuteOnEntry(() => { _currentState = ScriptMissionState.Pausing; OnEnterPausing(); })
.On(MissionTrigger.CompletePausing)
.Goto(ScriptMissionState.Paused)
.On(MissionTrigger.ErrorOccurred)
.Goto(ScriptMissionState.Error)
.Execute(() => { _currentState = ScriptMissionState.Error; OnEnterError(); });
// Paused state
builder.In(ScriptMissionState.Paused)
.ExecuteOnEntry(() => { _currentState = ScriptMissionState.Paused; OnEnterPaused(); })
.On(MissionTrigger.Resume)
.Goto(ScriptMissionState.Resuming)
.On(MissionTrigger.Cancel)
.Goto(ScriptMissionState.Canceling);
// Resuming state
builder.In(ScriptMissionState.Resuming)
.ExecuteOnEntry(() => { _currentState = ScriptMissionState.Resuming; OnEnterResuming(); })
.On(MissionTrigger.CompleteResuming)
.Goto(ScriptMissionState.Running)
.On(MissionTrigger.ErrorOccurred)
.Goto(ScriptMissionState.Error)
.Execute(() => { _currentState = ScriptMissionState.Error; OnEnterError(); });
// Canceled state (terminal)
builder.In(ScriptMissionState.Canceled)
.ExecuteOnEntry(() => { _currentState = ScriptMissionState.Canceled; OnEnterCanceled(); });
// Completed state (terminal)
builder.In(ScriptMissionState.Completed)
.ExecuteOnEntry(() => { _currentState = ScriptMissionState.Completed; OnEnterCompleted(); });
// Error state (terminal)
builder.In(ScriptMissionState.Error)
.ExecuteOnEntry(() => { _currentState = ScriptMissionState.Error; OnEnterError(); });
}
#region State Machine Event Handlers
private void OnEnterRunning()
{
lock (_lockObject)
{
if (_executionTask == null || _executionTask.IsCompleted)
{
// Create new execution task
_executionCts = CancellationTokenSource.CreateLinkedTokenSource(_internalCts.Token);
_isPaused = false;
_isCanceling = false;
_currentScore = 0;
// Update CancellationToken parameters in MissionParameters to link with mission's cancellation token
// This allows script runner to receive cancellation when Cancel() is called
foreach (var paramModel in _model.Parameters)
{
if (paramModel.Type == typeof(CancellationToken))
{
// Link CancellationToken parameter with mission's internal cancellation token
_globals.MissionParameters[paramModel.Name] = _executionCts.Token;
}
}
// Use standard thread pool
_executionTask = Task.Run(() => ExecuteMissionAsync(_executionCts.Token), _executionCts.Token);
_logger.LogInfo($"Mission '{_model.Name}' started running.");
}
}
}
private void OnEnterCanceling()
{
lock (_lockObject)
{
_isCanceling = true;
_executionCts?.Cancel();
_logger.LogInfo($"Mission '{_model.Name}' is canceling...");
}
Task.Run(async () =>
{
// Wait for execution to complete cancellation
if (_executionTask != null)
{
try
{
await _executionTask;
}
catch (OperationCanceledException)
{
// Expected when canceling
}
catch (Exception ex)
{
_logger.LogError($"Mission '{_model.Name}' cancellation error: {ex.Message}");
}
}
_stateMachine.Fire(MissionTrigger.CompleteCanceling);
});
}
private void OnEnterPausing()
{
lock (_lockObject)
{
_isPaused = true;
_logger.LogInfo($"Mission '{_model.Name}' is pausing...");
}
Task.Run(async () =>
{
// Wait for current step to complete (check in NextStepHandler)
await Task.Delay(100); // Small delay to allow current step to check pause flag
_stateMachine.Fire(MissionTrigger.CompletePausing);
});
}
private void OnEnterPaused()
{
_logger.LogInfo($"Mission '{_model.Name}' is paused.");
}
private void OnEnterResuming()
{
lock (_lockObject)
{
_isPaused = false;
_logger.LogInfo($"Mission '{_model.Name}' is resuming...");
}
Task.Run(async () =>
{
// Small delay to ensure state transition
await Task.Delay(50);
_stateMachine.Fire(MissionTrigger.CompleteResuming);
});
}
private void OnEnterCanceled()
{
_logger.LogInfo($"Mission '{_model.Name}' was canceled. Final score: {_currentScore}/{TotalScore}");
}
private void OnEnterCompleted()
{
_logger.LogInfo($"Mission '{_model.Name}' completed successfully. Final score: {_currentScore}/{TotalScore}");
}
private void OnEnterError()
{
_logger.LogError($"Mission '{_model.Name}' entered error state. Last error: {_lastError?.Message}");
}
#endregion
#region Public Control Methods
/// <summary>
/// Starts the mission (transitions from Idle to Running).
/// </summary>
public void Start()
{
try
{
_stateMachine.Fire(MissionTrigger.Start);
}
catch (Exception ex)
{
_logger.LogError($"Failed to start mission '{_model.Name}': {ex.Message}");
throw;
}
}
/// <summary>
/// Cancels the mission (transitions from Running/Paused to Canceling → Canceled).
/// </summary>
public void Cancel(string reason)
{
try
{
_logger.LogWarning($"Cancellation requested with reason: {reason}");
_stateMachine.Fire(MissionTrigger.Cancel);
}
catch (Exception ex)
{
_logger.LogError($"Failed to cancel mission '{_model.Name}': {ex.Message}");
throw;
}
}
/// <summary>
/// Pauses the mission (transitions from Running to Pausing → Paused).
/// </summary>
public void Pause()
{
try
{
_stateMachine.Fire(MissionTrigger.Pause);
}
catch (Exception ex)
{
_logger.LogError($"Failed to pause mission '{_model.Name}': {ex.Message}");
throw;
}
}
/// <summary>
/// Resumes the mission (transitions from Paused to Resuming → Running).
/// </summary>
public void Resume()
{
try
{
_stateMachine.Fire(MissionTrigger.Resume);
}
catch (Exception ex)
{
_logger.LogError($"Failed to resume mission '{_model.Name}': {ex.Message}");
throw;
}
}
/// <summary>
/// Waits for the mission to reach a terminal state (Completed, Canceled, or Error).
/// </summary>
public void WaitForStop(int timeoutMs = 10000)
{
var startTime = DateTime.UtcNow;
// Only wait if mission is actually executing - if not executing, state machine issue, proceed anyway
while (IsExecuting &&
_currentState != ScriptMissionState.Completed &&
_currentState != ScriptMissionState.Canceled &&
_currentState != ScriptMissionState.Error)
{
if ((DateTime.UtcNow - startTime).TotalMilliseconds > timeoutMs)
{
// Timeout - mission is still executing (ScriptRunner blocking)
_logger.LogWarning($"Mission '{_model.Name}' is still executing after {timeoutMs}ms timeout. ScriptRunner may be blocking.");
break;
}
Thread.Sleep(50); // Check every 50ms
}
// If mission is not executing, it's effectively stopped (even if state machine didn't transition)
if (!IsExecuting)
{
return; // Mission is not executing, proceed
}
}
#endregion
#region Mission Execution
private async Task ExecuteMissionAsync(CancellationToken cancellationToken)
{
try
{
// Execute the script runner with globals
// The CancellationToken parameter in MissionParameters is already linked to _executionCts.Token
// so the script runner will receive cancellation when Cancel() is called
var result = await _model.Runner(_globals, cancellationToken);
if (result is IAsyncEnumerable<MissionStatus> statusEnumerable)
{
var enumerator = statusEnumerable.GetAsyncEnumerator(cancellationToken);
try
{
while (await enumerator.MoveNextAsync())
{
var status = enumerator.Current;
// Update score and log message
_currentScore += status.Score;
var progress = TotalScore > 0 ? 100.0 * _currentScore / TotalScore : 0.0;
_logger.LogInfo($"Mission '{_model.Name}' progress: {progress:0.##}% - {status.Message}");
// Check for pause/resume/cancel/stop via NextStepHandler
if (!await NextStepHandlerAsync(cancellationToken))
{
// Mission was canceled or stopped
return;
}
}
}
finally
{
await enumerator.DisposeAsync();
}
// Mission completed successfully
_stateMachine.Fire(MissionTrigger.CompleteRunning);
}
else
{
_logger.LogError($"Mission '{_model.Name}' runner did not return IAsyncEnumerable<MissionStatus>.");
_lastError = new InvalidOperationException("Mission runner must return IAsyncEnumerable<MissionStatus>");
_stateMachine.Fire(MissionTrigger.ErrorOccurred);
}
}
catch (OperationCanceledException)
{
// Expected when canceling
if (_isCanceling)
{
// Cancellation was requested, state machine will handle transition
return;
}
else
{
_lastError = new OperationCanceledException("Mission execution was canceled");
_stateMachine.Fire(MissionTrigger.ErrorOccurred);
}
}
catch (Exception ex)
{
_lastError = ex;
_logger.LogError($"Mission '{_model.Name}' execution error: {ex.Message}");
_stateMachine.Fire(MissionTrigger.ErrorOccurred);
}
}
/// <summary>
/// Handles state transitions during mission execution (pause/resume/cancel/stop).
/// Returns false if execution should stop, true if execution should continue.
/// </summary>
private async Task<bool> NextStepHandlerAsync(CancellationToken cancellationToken)
{
// Check for cancellation
if (cancellationToken.IsCancellationRequested || _isCanceling)
{
return false;
}
// Check for pause - wait until resumed or canceled
while (_isPaused && !cancellationToken.IsCancellationRequested && !_isCanceling)
{
// Use async delay to avoid blocking
await Task.Delay(1000, cancellationToken);
}
// Check again after pause
if (cancellationToken.IsCancellationRequested || _isCanceling)
{
return false;
}
return true;
}
#endregion
#region IDisposable
/// <summary>
/// Disposes the mission. Can be called from any state.
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}
lock (_lockObject)
{
if (_disposed)
{
return;
}
_disposed = true;
}
// Cancel execution if running
try
{
if (_currentState == ScriptMissionState.Running ||
_currentState == ScriptMissionState.Paused ||
_currentState == ScriptMissionState.Pausing ||
_currentState == ScriptMissionState.Resuming)
{
_internalCts.Cancel();
_executionCts?.Cancel();
// Wait a bit for cancellation to complete
var cancelTimeout = TimeSpan.FromSeconds(2);
var startTime = DateTime.UtcNow;
while ((_currentState == ScriptMissionState.Running ||
_currentState == ScriptMissionState.Paused ||
_currentState == ScriptMissionState.Pausing ||
_currentState == ScriptMissionState.Resuming ||
_currentState == ScriptMissionState.Canceling) &&
(DateTime.UtcNow - startTime) < cancelTimeout)
{
Thread.Sleep(50);
}
}
}
catch
{
// Ignore errors during cancellation
}
// Wait for execution task to complete
try
{
if (_executionTask != null && !_executionTask.IsCompleted)
{
_executionTask.Wait(TimeSpan.FromSeconds(5));
}
}
catch
{
// Ignore errors
}
// Dispose resources
try
{
_internalCts?.Dispose();
_executionCts?.Dispose();
}
catch
{
// Ignore errors
}
// Stop state machine
try
{
_stateMachine.Stop();
}
catch
{
// Ignore errors
}
GC.SuppressFinalize(this);
}
#endregion
}