Initial commit
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
using RobotNet10.ScriptEngine.HubContexts;
|
||||
|
||||
namespace RobotNet10.ScriptEngine.Models;
|
||||
|
||||
public class LoggerMission(Guid id, ConsoleHubContext hubContext) : RobotNet10.Script.ILogger
|
||||
{
|
||||
private string log = "";
|
||||
private readonly Mutex mutexLog = new();
|
||||
|
||||
public string GetLog()
|
||||
{
|
||||
mutexLog.WaitOne();
|
||||
var result = log;
|
||||
log = ""; // Clear log after reading
|
||||
mutexLog.ReleaseMutex();
|
||||
return result;
|
||||
}
|
||||
|
||||
public void LogError(string message)
|
||||
{
|
||||
hubContext.LogErrorToMission(id, message);
|
||||
|
||||
mutexLog.WaitOne();
|
||||
// Format: [LEVEL] YYYY-MM-DDTHH:mm:ss.fffZ | message
|
||||
log += $"[ERROR] {DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ} | {message}{Environment.NewLine}";
|
||||
mutexLog.ReleaseMutex();
|
||||
}
|
||||
|
||||
public void LogInfo(string message)
|
||||
{
|
||||
hubContext.LogInfoToMission(id, message);
|
||||
|
||||
mutexLog.WaitOne();
|
||||
// Format: [LEVEL] YYYY-MM-DDTHH:mm:ss.fffZ | message
|
||||
log += $"[INFO] {DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ} | {message}{Environment.NewLine}";
|
||||
mutexLog.ReleaseMutex();
|
||||
}
|
||||
|
||||
public void LogWarning(string message)
|
||||
{
|
||||
hubContext.LogWarningToMission(id, message);
|
||||
|
||||
mutexLog.WaitOne();
|
||||
// Format: [LEVEL] YYYY-MM-DDTHH:mm:ss.fffZ | message
|
||||
log += $"[WARN] {DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ} | {message}{Environment.NewLine}";
|
||||
mutexLog.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using RobotNet10.ScriptEngine.HubContexts;
|
||||
|
||||
namespace RobotNet10.ScriptEngine.Models;
|
||||
|
||||
public class LoggerTask(string name, ConsoleHubContext hubContext) : RobotNet10.Script.ILogger
|
||||
{
|
||||
public string GetLog() => string.Empty;
|
||||
|
||||
public void LogError(string message) => hubContext.LogErrorToTask(name, message);
|
||||
|
||||
public void LogInfo(string message) => hubContext.LogInfoToTask(name, message);
|
||||
|
||||
public void LogWarning(string message) => hubContext.LogWarningToTask(name, message);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using RobotNet10.Script;
|
||||
using RobotNet10.Script.IO;
|
||||
using RobotNet10.ScriptEngine.IO;
|
||||
using RobotNet10.ScriptEngine.Shared;
|
||||
|
||||
namespace RobotNet10.ScriptEngine.Models;
|
||||
|
||||
public class ScriptEngineGlobals(ILogger logger, IServiceScopeFactory scopeFactory) : IScriptGlobals
|
||||
{
|
||||
public RobotNet10.Script.ILogger Logger => logger;
|
||||
|
||||
public Guid CreateMission(string name, params object[] args)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var missionManager = scope.ServiceProvider.GetRequiredService<MissionManager>();
|
||||
var result = missionManager.CreateMission(name, args);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return result.Data;
|
||||
}
|
||||
throw new InvalidOperationException($"Failed to create mission '{name}': {result.Message}");
|
||||
}
|
||||
|
||||
public bool CancelMission(Guid id, string reason)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var missionManager = scope.ServiceProvider.GetRequiredService<MissionManager>();
|
||||
var mission = missionManager.GetMission(id);
|
||||
if (mission == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
mission.Cancel(reason);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void DisableTask(string name)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var taskManager = scope.ServiceProvider.GetRequiredService<TaskManager>();
|
||||
var result = taskManager.DisableTask(name);
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to disable task '{name}': {result.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void EnableTask(string name)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var taskManager = scope.ServiceProvider.GetRequiredService<TaskManager>();
|
||||
var result = taskManager.EnableTask(name);
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to enable task '{name}': {result.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// IO Connection Factory Methods
|
||||
|
||||
public IHttpConnection CreateHttpConnection(string baseUrl, int timeoutSeconds = 30)
|
||||
{
|
||||
return new HttpConnection(baseUrl, TimeSpan.FromSeconds(timeoutSeconds));
|
||||
}
|
||||
|
||||
public IModbusTcpConnection CreateModbusTcpConnection(string ipAddress, int port = 502, byte slaveId = 1)
|
||||
{
|
||||
return new ModbusTcpConnection(ipAddress, port, slaveId);
|
||||
}
|
||||
|
||||
public IProfiNetConnection CreateProfiNetConnection(string ipAddress, int slot = 1, int subslot = 1)
|
||||
{
|
||||
return new ProfiNetConnection(ipAddress, slot, subslot);
|
||||
}
|
||||
|
||||
public ICcLinkIeConnection CreateCcLinkIeConnection(string ipAddress, int stationNumber = 1)
|
||||
{
|
||||
return new CcLinkIeConnection(ipAddress, stationNumber);
|
||||
}
|
||||
|
||||
public IOpcUaConnection CreateOpcUaConnection(string endpointUrl)
|
||||
{
|
||||
return new OpcUaConnection(endpointUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RobotNet10.ScriptEngine.Models;
|
||||
|
||||
public record ScriptGlobals(
|
||||
IDictionary<string, object?> RobotNet,
|
||||
IDictionary<string, object?> AppApis,
|
||||
IDictionary<string, object?> GlobalVariables,
|
||||
IDictionary<string, object?> MissionParameters
|
||||
);
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using RobotNet10.Script;
|
||||
|
||||
namespace RobotNet10.ScriptEngine.Models;
|
||||
|
||||
public record ScriptMissionModel(string Name, IEnumerable<ScriptMissionParameterModel> Parameters, string Code, int TotalScore, bool IsMultipleRun, bool AutoStart, ScriptRunner<IAsyncEnumerable<MissionStatus>> Runner);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace RobotNet10.ScriptEngine.Models;
|
||||
|
||||
public record ScriptMissionParameterModel(string Name, Type Type, object? DefaultValue = null);
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
|
||||
namespace RobotNet10.ScriptEngine.Models;
|
||||
|
||||
public record ScriptTaskModel(string Name, int Interval, bool AutoStart, string Code, ScriptRunner<object> Runner);
|
||||
@@ -0,0 +1,8 @@
|
||||
using RobotNet10.ScriptEngine.Shared;
|
||||
|
||||
namespace RobotNet10.ScriptEngine.Models;
|
||||
|
||||
public record ScriptVariableModel(string Name, Type Type, object? DefaultValue, bool PublicRead, bool PublicWrite)
|
||||
{
|
||||
public string TypeName { get; } = ScriptHelpers.ToString(Type);
|
||||
}
|
||||
Reference in New Issue
Block a user