Initial commit
This commit is contained in:
607
srcs/RobotNet10/Commons/RobotNet10.ScriptEngine/TaskManager.cs
Normal file
607
srcs/RobotNet10/Commons/RobotNet10.ScriptEngine/TaskManager.cs
Normal file
@@ -0,0 +1,607 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user