608 lines
22 KiB
C#
608 lines
22 KiB
C#
using Appccelerate.StateMachine;
|
|
using Appccelerate.StateMachine.Machine;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using RobotNet10.Script;
|
|
using RobotNet10.ScriptEngine.Helpers;
|
|
using RobotNet10.ScriptEngine.HubContexts;
|
|
using RobotNet10.ScriptEngine.Models;
|
|
using RobotNet10.ScriptEngine.Shared;
|
|
using RobotNet10.Shared;
|
|
using System.Collections.Concurrent;
|
|
|
|
namespace RobotNet10.ScriptEngine;
|
|
|
|
/// <summary>
|
|
/// State enum for TaskManager.
|
|
/// </summary>
|
|
public enum TaskManagerState
|
|
{
|
|
Idle = 0,
|
|
Running,
|
|
Stopping,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Triggers for TaskManager state machine.
|
|
/// </summary>
|
|
public enum TaskManagerTrigger
|
|
{
|
|
Start,
|
|
Stop,
|
|
StoppingCompleted,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Manages script tasks with state machine support.
|
|
/// </summary>
|
|
public class TaskManager : IDisposable
|
|
{
|
|
private readonly PassiveStateMachine<TaskManagerState, TaskManagerTrigger> _stateMachine;
|
|
private readonly ConcurrentDictionary<string, ScriptTask> _tasks = new();
|
|
private readonly Lock _lockObject = new();
|
|
private readonly Lock _stateLockObject = new();
|
|
private readonly VariableManager _variableManager;
|
|
private readonly IScriptEngineResource _scriptResource;
|
|
private readonly ILogger<ScriptEngineGlobals> _logger;
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
private readonly ConsoleHubContext _consoleHubContext;
|
|
private readonly IConfiguration _configuration;
|
|
private bool _disposed;
|
|
private TaskManagerState _currentState = TaskManagerState.Idle;
|
|
|
|
/// <summary>
|
|
/// Gets the current state of the TaskManager.
|
|
/// </summary>
|
|
public TaskManagerState State => _currentState;
|
|
|
|
/// <summary>
|
|
/// Gets all tasks.
|
|
/// </summary>
|
|
public IReadOnlyDictionary<string, ScriptTask> Tasks => _tasks;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of TaskManager.
|
|
/// </summary>
|
|
public TaskManager(
|
|
VariableManager variableManager,
|
|
IScriptEngineResource scriptResource,
|
|
ILogger<ScriptEngineGlobals> logger,
|
|
IServiceScopeFactory scopeFactory,
|
|
ConsoleHubContext consoleHubContext,
|
|
IConfiguration configuration)
|
|
{
|
|
_variableManager = variableManager ?? throw new ArgumentNullException(nameof(variableManager));
|
|
_scriptResource = scriptResource ?? throw new ArgumentNullException(nameof(scriptResource));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
|
_consoleHubContext = consoleHubContext ?? throw new ArgumentNullException(nameof(consoleHubContext));
|
|
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
|
var builder = new StateMachineDefinitionBuilder<TaskManagerState, TaskManagerTrigger>();
|
|
|
|
// Idle state - can add/remove tasks
|
|
builder.In(TaskManagerState.Idle)
|
|
.ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = TaskManagerState.Idle; } })
|
|
.On(TaskManagerTrigger.Start)
|
|
.Goto(TaskManagerState.Running)
|
|
.Execute(() => { lock (_stateLockObject) { _currentState = TaskManagerState.Running; } OnEnterRunning(); });
|
|
|
|
// Running state - cannot add/remove tasks
|
|
builder.In(TaskManagerState.Running)
|
|
.ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = TaskManagerState.Running; } })
|
|
.On(TaskManagerTrigger.Stop)
|
|
.Goto(TaskManagerState.Stopping)
|
|
.Execute(() => { lock (_stateLockObject) { _currentState = TaskManagerState.Stopping; } OnEnterStopping(); });
|
|
|
|
// Stopping state - cannot add/remove tasks, waiting for all tasks to stop
|
|
builder.In(TaskManagerState.Stopping)
|
|
.ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = TaskManagerState.Stopping; } })
|
|
.On(TaskManagerTrigger.StoppingCompleted)
|
|
.Goto(TaskManagerState.Idle)
|
|
.Execute(() => { lock (_stateLockObject) { _currentState = TaskManagerState.Idle; } });
|
|
|
|
_stateMachine = builder
|
|
.WithInitialState(TaskManagerState.Idle)
|
|
.Build()
|
|
.CreatePassiveStateMachine();
|
|
|
|
_stateMachine.Start();
|
|
}
|
|
|
|
private void OnEnterRunning()
|
|
{
|
|
// Start all tasks with AutoStart == true
|
|
lock (_lockObject)
|
|
{
|
|
foreach (var task in _tasks.Values)
|
|
{
|
|
if (task.AutoStart)
|
|
{
|
|
try
|
|
{
|
|
task.Start();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log error but continue starting other tasks
|
|
_logger.LogError(ex, $"Failed to start task '{task.Name}'");
|
|
_consoleHubContext.LogErrorToTask(task.Name, $"Failed to start task: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnEnterStopping()
|
|
{
|
|
// Fire and forget async operation with proper error handling
|
|
_ = Task.Run(async () =>
|
|
{
|
|
// Stop all tasks and wait for them to be stopped
|
|
var stopTasks = new List<Task>();
|
|
|
|
lock (_lockObject)
|
|
{
|
|
foreach (var task in _tasks.Values)
|
|
{
|
|
stopTasks.Add(Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
// Try to stop the task
|
|
task.Stop();
|
|
|
|
// Wait for task to stop, but only timeout if actually executing
|
|
// This will also wait for state machine transition if task is not executing
|
|
WaitForTaskStopped(task);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log error but continue stopping other tasks
|
|
_logger.LogError(ex, $"Failed to stop task '{task.Name}'");
|
|
_consoleHubContext.LogErrorToTask(task.Name, $"Failed to stop task: {ex.Message}");
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
|
|
// Wait for all tasks to stop (with timeout)
|
|
try
|
|
{
|
|
await Task.WhenAll(stopTasks).WaitAsync(TimeSpan.FromSeconds(35));
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
// Check if any tasks are still executing
|
|
var stillExecuting = _tasks.Values.Where(t => t.IsExecuting).ToList();
|
|
if (stillExecuting.Count > 0)
|
|
{
|
|
_logger.LogWarning($"Timeout waiting for {stillExecuting.Count} task(s) to stop. ScriptRunner may be blocking.");
|
|
_consoleHubContext.LogWarning($"Timeout waiting for {stillExecuting.Count} task(s) to stop. ScriptRunner may be blocking.");
|
|
}
|
|
else
|
|
{
|
|
_logger.LogInformation("All tasks stopped. Proceeding with state transition.");
|
|
_consoleHubContext.LogInfo("All tasks stopped. Proceeding with state transition.");
|
|
}
|
|
}
|
|
|
|
// Fire StoppingCompleted trigger (always proceed, even if some tasks didn't stop)
|
|
_stateMachine.Fire(TaskManagerTrigger.StoppingCompleted);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a task by name.
|
|
/// </summary>
|
|
public ScriptTask? GetTask(string name)
|
|
{
|
|
_tasks.TryGetValue(name, out var task);
|
|
return task;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets all tasks. Clears all tasks and disposes them.
|
|
/// Only allowed when state is Idle.
|
|
/// </summary>
|
|
public MessageResult Reset()
|
|
{
|
|
try
|
|
{
|
|
lock (_stateLockObject)
|
|
{
|
|
if (_currentState != TaskManagerState.Idle)
|
|
return new MessageResult(false, $"Cannot reset tasks when TaskManager is in state: {_currentState}");
|
|
}
|
|
|
|
lock (_lockObject)
|
|
{
|
|
var count = _tasks.Count;
|
|
foreach (var task in _tasks.Values)
|
|
{
|
|
try
|
|
{
|
|
task.Dispose();
|
|
}
|
|
catch
|
|
{
|
|
// Ignore disposal errors
|
|
}
|
|
}
|
|
_tasks.Clear();
|
|
return new MessageResult(true, $"Reset {count} task(s) successfully");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new MessageResult(false, $"Failed to reset tasks: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads all tasks from a collection of ScriptTaskModel. This clears existing tasks first.
|
|
/// Only allowed when state is Idle.
|
|
/// </summary>
|
|
/// <param name="taskModels">The collection of task models to load.</param>
|
|
public MessageResult Load(IEnumerable<ScriptTaskModel> taskModels)
|
|
{
|
|
try
|
|
{
|
|
ArgumentNullException.ThrowIfNull(taskModels);
|
|
|
|
lock (_stateLockObject)
|
|
{
|
|
if (_currentState != TaskManagerState.Idle)
|
|
return new MessageResult(false, $"Cannot load tasks when TaskManager is in state: {_currentState}");
|
|
}
|
|
|
|
// Reset existing tasks first
|
|
var resetResult = Reset();
|
|
if (!resetResult.IsSuccess)
|
|
return resetResult;
|
|
|
|
// Load all tasks
|
|
lock (_lockObject)
|
|
{
|
|
var loadedCount = 0;
|
|
var errorCount = 0;
|
|
var errors = new List<string>();
|
|
|
|
foreach (var model in taskModels)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(model.Name))
|
|
continue;
|
|
|
|
try
|
|
{
|
|
// Create LoggerTask with task name
|
|
var loggerTask = new LoggerTask(model.Name, _consoleHubContext);
|
|
|
|
// Create ScriptEngineGlobals with LoggerTask
|
|
var scriptEngineGlobals = new ScriptEngineGlobals(loggerTask, _scopeFactory);
|
|
var robotNetDict = ScriptHelper.ConvertGlobalsToDictionary(scriptEngineGlobals, typeof(IScriptGlobals));
|
|
var appApisDict = _scriptResource.GetTaskGlobals();
|
|
var globalVariablesDict = _variableManager.Globals;
|
|
var missionParametersDict = new Dictionary<string, object?>();
|
|
|
|
var globals = new ScriptGlobals(
|
|
robotNetDict,
|
|
appApisDict,
|
|
globalVariablesDict,
|
|
missionParametersDict
|
|
);
|
|
|
|
var task = new ScriptTask(model, globals);
|
|
_tasks.TryAdd(model.Name, task);
|
|
loadedCount++;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errorCount++;
|
|
errors.Add($"Failed to load task '{model.Name}': {ex.Message}");
|
|
}
|
|
}
|
|
|
|
if (errorCount > 0)
|
|
{
|
|
return new MessageResult(false,
|
|
$"Loaded {loadedCount} task(s) successfully, {errorCount} failed. Errors: {string.Join("; ", errors)}");
|
|
}
|
|
|
|
return new MessageResult(true, $"Loaded {loadedCount} task(s) successfully");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new MessageResult(false, $"Failed to load tasks: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the TaskManager. This will start all tasks with AutoStart == true.
|
|
/// </summary>
|
|
public MessageResult Start()
|
|
{
|
|
try
|
|
{
|
|
_stateMachine.Fire(TaskManagerTrigger.Start);
|
|
return new MessageResult(true, "TaskManager started successfully");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new MessageResult(false, $"Failed to start TaskManager: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops the TaskManager. This will ensure all tasks are stopped.
|
|
/// </summary>
|
|
public MessageResult Stop()
|
|
{
|
|
try
|
|
{
|
|
_stateMachine.Fire(TaskManagerTrigger.Stop);
|
|
return new MessageResult(true, "TaskManager stop initiated");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new MessageResult(false, $"Failed to stop TaskManager: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Waits for a task to reach Stopped state.
|
|
/// </summary>
|
|
private void WaitForTaskStopped(ScriptTask task, int timeoutMs = 5000)
|
|
{
|
|
var startTime = DateTime.UtcNow;
|
|
// Only wait if task is actually executing - if not executing, wait for state machine transition
|
|
while (task.IsExecuting && (DateTime.UtcNow - startTime).TotalMilliseconds < timeoutMs)
|
|
{
|
|
// Check if state changed to stopped/error/idle
|
|
if (task.State == ScriptTaskState.Stopped ||
|
|
task.State == ScriptTaskState.Idle ||
|
|
task.State == ScriptTaskState.Error)
|
|
{
|
|
// Task stopped, but wait a bit more to ensure IsExecuting is reset
|
|
Thread.Sleep(100);
|
|
if (!task.IsExecuting)
|
|
{
|
|
return; // Task fully stopped
|
|
}
|
|
}
|
|
Thread.Sleep(50); // Check every 50ms
|
|
}
|
|
|
|
// If task is not executing, wait a bit for state machine to transition
|
|
if (!task.IsExecuting)
|
|
{
|
|
// Wait up to 500ms for state machine transition to complete
|
|
var stateTransitionTimeout = 500;
|
|
var stateStartTime = DateTime.UtcNow;
|
|
while ((task.State == ScriptTaskState.Stopping || task.State == ScriptTaskState.Pausing) &&
|
|
(DateTime.UtcNow - stateStartTime).TotalMilliseconds < stateTransitionTimeout)
|
|
{
|
|
Thread.Sleep(50);
|
|
// Check if state transitioned
|
|
if (task.State == ScriptTaskState.Stopped ||
|
|
task.State == ScriptTaskState.Idle ||
|
|
task.State == ScriptTaskState.Error)
|
|
{
|
|
return; // State machine transitioned successfully
|
|
}
|
|
}
|
|
|
|
// If still in Stopping/Pausing state after waiting, it's a state machine issue
|
|
// But task is not executing, so it's effectively stopped - proceed anyway
|
|
if (task.State == ScriptTaskState.Stopping || task.State == ScriptTaskState.Pausing)
|
|
{
|
|
// Don't log warning - this is expected if state machine is slow
|
|
// The task is effectively stopped (not executing)
|
|
return;
|
|
}
|
|
|
|
return; // Task is not executing, proceed
|
|
}
|
|
|
|
// Timeout - task is still executing (ScriptRunner blocking)
|
|
_logger.LogWarning($"Task '{task.Name}' is still executing after {timeoutMs}ms timeout. ScriptRunner may be blocking.");
|
|
_consoleHubContext.LogWarningToTask(task.Name, $"Task is still executing after {timeoutMs}ms timeout. ScriptRunner may be blocking.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pauses a specific task by name.
|
|
/// </summary>
|
|
public MessageResult PauseTask(string name)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
return new MessageResult(false, "Task name cannot be null or empty");
|
|
|
|
if (!_tasks.TryGetValue(name, out var task))
|
|
return new MessageResult(false, $"Task '{name}' not found");
|
|
|
|
if (task.State != ScriptTaskState.Running)
|
|
return new MessageResult(false, $"Task '{name}' is not in Running state (current state: {task.State})");
|
|
|
|
task.Pause();
|
|
return new MessageResult(true, $"Task '{name}' paused successfully");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new MessageResult(false, $"Failed to pause task '{name}': {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all tasks as ScriptTaskDto array.
|
|
/// </summary>
|
|
public ScriptTaskDto[] GetScriptTasks()
|
|
{
|
|
lock (_lockObject)
|
|
{
|
|
return [.. _tasks.Values.Select(t => new ScriptTaskDto(
|
|
t.Name,
|
|
t.Interval,
|
|
t.State == ScriptTaskState.Running,
|
|
t.ExecutionCount))];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds specific tasks by names as ScriptTaskDto array.
|
|
/// </summary>
|
|
public ScriptTaskDto[] FindScriptTasks(string[] names)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(names);
|
|
|
|
lock (_lockObject)
|
|
{
|
|
return [.. _tasks.Values
|
|
.Where(t => names.Contains(t.Name))
|
|
.Select(t => new ScriptTaskDto(
|
|
t.Name,
|
|
t.Interval,
|
|
t.State == ScriptTaskState.Running,
|
|
t.ExecutionCount))];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enables a task (resumes if paused, starts if stopped).
|
|
/// </summary>
|
|
public MessageResult EnableTask(string name)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
return new MessageResult(false, "Task name cannot be null or empty");
|
|
|
|
if (!_tasks.TryGetValue(name, out var task))
|
|
return new MessageResult(false, $"Task '{name}' not found");
|
|
|
|
if (task.State == ScriptTaskState.Paused)
|
|
{
|
|
task.Resume();
|
|
return new MessageResult(true, $"Task '{name}' resumed successfully");
|
|
}
|
|
else if (task.State == ScriptTaskState.Stopped || task.State == ScriptTaskState.Idle)
|
|
{
|
|
task.Start();
|
|
return new MessageResult(true, $"Task '{name}' started successfully");
|
|
}
|
|
else if (task.State == ScriptTaskState.Running)
|
|
{
|
|
return new MessageResult(true, $"Task '{name}' is already running");
|
|
}
|
|
else
|
|
{
|
|
return new MessageResult(false, $"Task '{name}' cannot be enabled from state: {task.State}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new MessageResult(false, $"Failed to enable task '{name}': {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disables a task (pauses if running).
|
|
/// </summary>
|
|
public MessageResult DisableTask(string name)
|
|
{
|
|
return PauseTask(name);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resumes a specific task by name. Only works for tasks in Paused or Idle state.
|
|
/// </summary>
|
|
public MessageResult ResumeTask(string name)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
return new MessageResult(false, "Task name cannot be null or empty");
|
|
|
|
if (!_tasks.TryGetValue(name, out var task))
|
|
return new MessageResult(false, $"Task '{name}' not found");
|
|
|
|
if (task.State == ScriptTaskState.Paused)
|
|
{
|
|
// Resume paused task
|
|
task.Resume();
|
|
return new MessageResult(true, $"Task '{name}' resumed successfully");
|
|
}
|
|
else if (task.State == ScriptTaskState.Idle)
|
|
{
|
|
// Start idle task
|
|
task.Start();
|
|
return new MessageResult(true, $"Task '{name}' started successfully");
|
|
}
|
|
else
|
|
{
|
|
return new MessageResult(false, $"Task '{name}' cannot be resumed from state: {task.State}. Only Paused or Idle states are allowed.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new MessageResult(false, $"Failed to resume task '{name}': {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if all tasks are stopped.
|
|
/// </summary>
|
|
public bool AreAllTasksStopped()
|
|
{
|
|
lock (_lockObject)
|
|
{
|
|
return _tasks.Values.All(t => t.State == ScriptTaskState.Stopped || t.State == ScriptTaskState.Error);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the count of tasks.
|
|
/// </summary>
|
|
public int Count => _tasks.Count;
|
|
|
|
/// <summary>
|
|
/// Disposes the TaskManager and all tasks.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
// Stop state machine first
|
|
try
|
|
{
|
|
_stateMachine.Stop();
|
|
}
|
|
catch
|
|
{
|
|
// Ignore errors when stopping state machine
|
|
}
|
|
|
|
lock (_lockObject)
|
|
{
|
|
foreach (var task in _tasks.Values)
|
|
{
|
|
try
|
|
{
|
|
task.Dispose();
|
|
}
|
|
catch
|
|
{
|
|
// Ignore disposal errors
|
|
}
|
|
}
|
|
_tasks.Clear();
|
|
}
|
|
|
|
_disposed = true;
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|