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; /// /// State enum for TaskManager. /// public enum TaskManagerState { Idle = 0, Running, Stopping, } /// /// Triggers for TaskManager state machine. /// public enum TaskManagerTrigger { Start, Stop, StoppingCompleted, } /// /// Manages script tasks with state machine support. /// public class TaskManager : IDisposable { private readonly PassiveStateMachine _stateMachine; private readonly ConcurrentDictionary _tasks = new(); private readonly Lock _lockObject = new(); private readonly Lock _stateLockObject = new(); private readonly VariableManager _variableManager; private readonly IScriptEngineResource _scriptResource; private readonly ILogger _logger; private readonly IServiceScopeFactory _scopeFactory; private readonly ConsoleHubContext _consoleHubContext; private readonly IConfiguration _configuration; private bool _disposed; private TaskManagerState _currentState = TaskManagerState.Idle; /// /// Gets the current state of the TaskManager. /// public TaskManagerState State => _currentState; /// /// Gets all tasks. /// public IReadOnlyDictionary Tasks => _tasks; /// /// Initializes a new instance of TaskManager. /// public TaskManager( VariableManager variableManager, IScriptEngineResource scriptResource, ILogger 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(); // 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(); 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); }); } /// /// Gets a task by name. /// public ScriptTask? GetTask(string name) { _tasks.TryGetValue(name, out var task); return task; } /// /// Resets all tasks. Clears all tasks and disposes them. /// Only allowed when state is Idle. /// 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}"); } } /// /// Loads all tasks from a collection of ScriptTaskModel. This clears existing tasks first. /// Only allowed when state is Idle. /// /// The collection of task models to load. public MessageResult Load(IEnumerable 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(); 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(); 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}"); } } /// /// Starts the TaskManager. This will start all tasks with AutoStart == true. /// 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}"); } } /// /// Stops the TaskManager. This will ensure all tasks are stopped. /// 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}"); } } /// /// Waits for a task to reach Stopped state. /// 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."); } /// /// Pauses a specific task by name. /// 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}"); } } /// /// Gets all tasks as ScriptTaskDto array. /// public ScriptTaskDto[] GetScriptTasks() { lock (_lockObject) { return [.. _tasks.Values.Select(t => new ScriptTaskDto( t.Name, t.Interval, t.State == ScriptTaskState.Running, t.ExecutionCount))]; } } /// /// Finds specific tasks by names as ScriptTaskDto array. /// 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))]; } } /// /// Enables a task (resumes if paused, starts if stopped). /// 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}"); } } /// /// Disables a task (pauses if running). /// public MessageResult DisableTask(string name) { return PauseTask(name); } /// /// Resumes a specific task by name. Only works for tasks in Paused or Idle state. /// 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}"); } } /// /// Checks if all tasks are stopped. /// public bool AreAllTasksStopped() { lock (_lockObject) { return _tasks.Values.All(t => t.State == ScriptTaskState.Stopped || t.State == ScriptTaskState.Error); } } /// /// Gets the count of tasks. /// public int Count => _tasks.Count; /// /// Disposes the TaskManager and all tasks. /// 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); } }