using Appccelerate.StateMachine; using Appccelerate.StateMachine.Machine; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using RobotNet10.ScriptEngine.Enums; using RobotNet10.ScriptEngine.Helpers; using RobotNet10.ScriptEngine.HubContexts; using RobotNet10.ScriptEngine.Hubs; using RobotNet10.ScriptEngine.Models; using RobotNet10.ScriptEngine.Shared; using RobotNet10.Shared; using System.Text; namespace RobotNet10.ScriptEngine; /// /// Core ScriptEngine class that manages script compilation, execution, and state machine. /// public class ScriptEngine : IDisposable { private readonly PassiveStateMachine _stateMachine; private readonly ScriptBuilder _scriptBuilder; private readonly FileManager _fileManager; private readonly VariableManager _variableManager; private readonly TaskManager _taskManager; private readonly MissionManager _missionManager; private readonly ILogger _logger; private readonly IHubContext _hubContext; private readonly ConsoleHubContext _consoleHubContext; private readonly Lock _stateLockObject = new(); private ScriptEngineState _currentState = ScriptEngineState.Initializing; private bool _disposed; /// /// Gets the current state of the ScriptEngine. /// public ScriptEngineState State => _currentState; /// /// Gets the FileManager instance. /// public FileManager FileManager => _fileManager; /// /// Gets the VariableManager instance. /// public VariableManager VariableManager => _variableManager; /// /// Gets the TaskManager instance. /// public TaskManager TaskManager => _taskManager; /// /// Gets the MissionManager instance. /// public MissionManager MissionManager => _missionManager; /// /// Initializes a new instance of ScriptEngine. /// public ScriptEngine( IScriptEngineResource scriptResource, IConfiguration configuration, FileManager fileManager, VariableManager variableManager, TaskManager taskManager, MissionManager missionManager, ILogger logger, IHubContext hubContext, ConsoleHubContext consoleHubContext) { _fileManager = fileManager ?? throw new ArgumentNullException(nameof(fileManager)); _variableManager = variableManager ?? throw new ArgumentNullException(nameof(variableManager)); _taskManager = taskManager ?? throw new ArgumentNullException(nameof(taskManager)); _missionManager = missionManager ?? throw new ArgumentNullException(nameof(missionManager)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext)); _consoleHubContext = consoleHubContext ?? throw new ArgumentNullException(nameof(consoleHubContext)); _scriptBuilder = new ScriptBuilder(scriptResource, configuration["ScriptEngine:RuntimeDllFolder"] ?? "dlls"); var builder = new StateMachineDefinitionBuilder(); // Configure state machine transitions according to StateMachine_Design.md ConfigureStateMachine(builder); _stateMachine = builder .WithInitialState(ScriptEngineState.Initializing) .Build() .CreatePassiveStateMachine(); _currentState = ScriptEngineState.Initializing; _stateMachine.Start(); // Auto-transition from Initializing to Idle _stateMachine.Fire(ScriptEngineTrigger.InitializationCompleted); } private void ConfigureStateMachine(StateMachineDefinitionBuilder builder) { // Initializing state builder.In(ScriptEngineState.Initializing) .ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = ScriptEngineState.Initializing; } }) .On(ScriptEngineTrigger.InitializationCompleted) .Goto(ScriptEngineState.Idle) .On(ScriptEngineTrigger.Reset) .Goto(ScriptEngineState.Resetting); // Resetting state builder.In(ScriptEngineState.Resetting) .ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = ScriptEngineState.Resetting; } OnEnterResetting(); }) .On(ScriptEngineTrigger.ResettingCompleted) .Goto(ScriptEngineState.Idle) .On(ScriptEngineTrigger.FaultOccurred) .Goto(ScriptEngineState.Fault); // Idle state - scripts can be edited builder.In(ScriptEngineState.Idle) .ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = ScriptEngineState.Idle; } OnEnterIdle(); }) .On(ScriptEngineTrigger.Build) .Goto(ScriptEngineState.Building) .On(ScriptEngineTrigger.Reset) .Goto(ScriptEngineState.Resetting); // Building state builder.In(ScriptEngineState.Building) .ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = ScriptEngineState.Building; } OnEnterBuilding(); }) .On(ScriptEngineTrigger.BuildingCompleted) .Goto(ScriptEngineState.Ready) .On(ScriptEngineTrigger.BuildErrorOccurred) .Goto(ScriptEngineState.BuildError) .On(ScriptEngineTrigger.FaultOccurred) .Goto(ScriptEngineState.Fault); // BuildError state builder.In(ScriptEngineState.BuildError) .ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = ScriptEngineState.BuildError; } OnEnterBuildError(); }) .On(ScriptEngineTrigger.Reset) .Goto(ScriptEngineState.Idle) .On(ScriptEngineTrigger.Build) .Goto(ScriptEngineState.Building); // Ready state - scripts compiled successfully, cannot edit builder.In(ScriptEngineState.Ready) .ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = ScriptEngineState.Ready; } OnEnterReady(); }) .On(ScriptEngineTrigger.Start) .Goto(ScriptEngineState.Starting) .On(ScriptEngineTrigger.Reset) .Goto(ScriptEngineState.Idle) .On(ScriptEngineTrigger.Build) .Goto(ScriptEngineState.Building); // Starting state builder.In(ScriptEngineState.Starting) .ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = ScriptEngineState.Starting; } OnEnterStarting(); }) .On(ScriptEngineTrigger.StartingCompleted) .Goto(ScriptEngineState.Running) .On(ScriptEngineTrigger.FaultOccurred) .Goto(ScriptEngineState.Fault); // Running state - tasks and missions can execute builder.In(ScriptEngineState.Running) .ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = ScriptEngineState.Running; } OnEnterRunning(); }) .On(ScriptEngineTrigger.Stop) .Goto(ScriptEngineState.Stopping) .On(ScriptEngineTrigger.Reset) .Goto(ScriptEngineState.Resetting) .On(ScriptEngineTrigger.FaultOccurred) .Goto(ScriptEngineState.Fault); // Stopping state builder.In(ScriptEngineState.Stopping) .ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = ScriptEngineState.Stopping; } OnEnterStopping(); }) .On(ScriptEngineTrigger.StoppingCompleted) .Goto(ScriptEngineState.Ready) .On(ScriptEngineTrigger.FaultOccurred) .Goto(ScriptEngineState.Fault); // Fault state builder.In(ScriptEngineState.Fault) .ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = ScriptEngineState.Fault; } OnEnterFault(); }) .On(ScriptEngineTrigger.Reset) .Goto(ScriptEngineState.Resetting); } #region State Machine Event Handlers private void OnEnterIdle() { _fileManager.SetState(ScriptEngineState.Idle); var message = "ScriptEngine entered Idle state. Scripts can be edited."; _logger.LogInformation(message); _consoleHubContext.LogInfo(message); _ = NotifyStateChanged(ScriptEngineState.Idle); } private void OnEnterBuilding() { _fileManager.SetState(ScriptEngineState.Building); var message = "ScriptEngine entered Building state. Compiling scripts..."; _logger.LogInformation(message); _consoleHubContext.LogInfo(message); _ = NotifyStateChanged(ScriptEngineState.Building); // Fire and forget async operation with proper error handling _ = Task.Run(async () => { try { // Aggregate all script files code var aggregatedCode = await _fileManager.AggregateAllCodeAsync(); if (string.IsNullOrWhiteSpace(aggregatedCode)) { var warningMessage = "No script files found to compile."; _logger.LogWarning(warningMessage); _consoleHubContext.LogWarning(warningMessage); _stateMachine.Fire(ScriptEngineTrigger.BuildingCompleted); return; } // Build using ScriptBuilder _scriptBuilder.Build(aggregatedCode, out var variables, out var tasks, out var missions); // Load into managers _variableManager.Load(variables); var taskLoadResult = _taskManager.Load(tasks); var missionLoadResult = _missionManager.Load(missions); if (!taskLoadResult.IsSuccess) { var errorMessage = $"Failed to load tasks: {taskLoadResult.Message}"; _logger.LogError(errorMessage); _consoleHubContext.LogError(errorMessage); _stateMachine.Fire(ScriptEngineTrigger.BuildErrorOccurred); return; } if (!missionLoadResult.IsSuccess) { var errorMessage = $"Failed to load missions: {missionLoadResult.Message}"; _logger.LogError(errorMessage); _consoleHubContext.LogError(errorMessage); _stateMachine.Fire(ScriptEngineTrigger.BuildErrorOccurred); return; } var successMessage = $"Build completed successfully. Loaded {variables.Count()} variables, {tasks.Count()} tasks, {missions.Count()} missions."; _logger.LogInformation(successMessage); _consoleHubContext.LogInfo(successMessage); _stateMachine.Fire(ScriptEngineTrigger.BuildingCompleted); } catch (ScriptCompilationException ex) { var errorMessage = $"Script compilation failed: {ex.Message}"; _logger.LogError(ex, "Script compilation failed."); _consoleHubContext.LogError(errorMessage); _stateMachine.Fire(ScriptEngineTrigger.BuildErrorOccurred); } catch (Exception ex) { var errorMessage = $"Unexpected error during script building: {ex.Message}"; _logger.LogError(ex, "Unexpected error during script building."); _consoleHubContext.LogError(errorMessage); _stateMachine.Fire(ScriptEngineTrigger.FaultOccurred); } }); } private void OnEnterBuildError() { _fileManager.SetState(ScriptEngineState.BuildError); var errorMessage = "ScriptEngine entered BuildError state. Script compilation failed."; _logger.LogWarning(errorMessage); _consoleHubContext.LogError(errorMessage); _ = NotifyStateChanged(ScriptEngineState.BuildError); } private void OnEnterReady() { _fileManager.SetState(ScriptEngineState.Ready); var message = "ScriptEngine entered Ready state. Scripts compiled successfully, ready to start."; _logger.LogInformation(message); _consoleHubContext.LogInfo(message); _ = NotifyStateChanged(ScriptEngineState.Ready); } private void OnEnterStarting() { _fileManager.SetState(ScriptEngineState.Starting); var message = "ScriptEngine entered Starting state. Starting TaskManager and MissionManager..."; _logger.LogInformation(message); _consoleHubContext.LogInfo(message); _ = NotifyStateChanged(ScriptEngineState.Starting); // Fire and forget async operation with proper error handling _ = Task.Run(async () => { try { // Start TaskManager and MissionManager var taskStartResult = _taskManager.Start(); var missionStartResult = _missionManager.Start(); if (!taskStartResult.IsSuccess) { var errorMessage = $"Failed to start TaskManager: {taskStartResult.Message}"; _logger.LogError(errorMessage); _consoleHubContext.LogError(errorMessage); _stateMachine.Fire(ScriptEngineTrigger.FaultOccurred); return; } if (!missionStartResult.IsSuccess) { var errorMessage = $"Failed to start MissionManager: {missionStartResult.Message}"; _logger.LogError(errorMessage); _consoleHubContext.LogError(errorMessage); _stateMachine.Fire(ScriptEngineTrigger.FaultOccurred); return; } // Wait a bit to ensure managers are started await Task.Delay(100); var successMessage = "TaskManager and MissionManager started successfully."; _logger.LogInformation(successMessage); _consoleHubContext.LogInfo(successMessage); _stateMachine.Fire(ScriptEngineTrigger.StartingCompleted); } catch (Exception ex) { var errorMessage = $"Unexpected error during starting: {ex.Message}"; _logger.LogError(ex, "Unexpected error during starting."); _consoleHubContext.LogError(errorMessage); _stateMachine.Fire(ScriptEngineTrigger.FaultOccurred); } }); } private void OnEnterRunning() { _fileManager.SetState(ScriptEngineState.Running); var message = "ScriptEngine entered Running state. Tasks and missions can execute."; _logger.LogInformation(message); _consoleHubContext.LogInfo(message); _ = NotifyStateChanged(ScriptEngineState.Running); } private void OnEnterStopping() { _fileManager.SetState(ScriptEngineState.Stopping); var message = "ScriptEngine entered Stopping state. Stopping TaskManager and MissionManager..."; _logger.LogInformation(message); _consoleHubContext.LogInfo(message); _ = NotifyStateChanged(ScriptEngineState.Stopping); // Fire and forget async operation with proper error handling _ = Task.Run(async () => { try { // Stop TaskManager and MissionManager var taskStopResult = _taskManager.Stop(); var missionStopResult = _missionManager.Stop(); if (!taskStopResult.IsSuccess) { _logger.LogWarning($"Failed to stop TaskManager: {taskStopResult.Message}"); } if (!missionStopResult.IsSuccess) { _logger.LogWarning($"Failed to stop MissionManager: {missionStopResult.Message}"); } // Wait for all tasks to stop and all missions to not be running // Only timeout when tasks/missions are actually executing (ScriptRunner running) var maxWaitTime = TimeSpan.FromSeconds(30); var checkInterval = TimeSpan.FromMilliseconds(100); var elapsed = TimeSpan.Zero; while (elapsed < maxWaitTime) { // Check if all tasks are stopped (snapshot collection to avoid race condition) var tasksSnapshot = _taskManager.Tasks.Values.ToList(); // Check both state and IsExecuting - only consider stopped if not executing var allTasksStopped = tasksSnapshot.All(t => (t.State == ScriptTaskState.Stopped || t.State == ScriptTaskState.Idle || t.State == ScriptTaskState.Error) && !t.IsExecuting); // Check if all missions are not running (snapshot collection to avoid race condition) var missionsSnapshot = _missionManager.GetAllMissions().ToList(); // Only consider stopped if not in Running state and not executing var allMissionsNotRunning = missionsSnapshot.All(m => m.State != ScriptMissionState.Running && !m.IsExecuting); if (allTasksStopped && allMissionsNotRunning) { var successMessage = "All tasks stopped and all missions are not running."; _logger.LogInformation(successMessage); _consoleHubContext.LogInfo(successMessage); _stateMachine.Fire(ScriptEngineTrigger.StoppingCompleted); return; } await Task.Delay(checkInterval); elapsed = elapsed.Add(checkInterval); } // Only log timeout if there are actually running tasks/missions var runningTasks = _taskManager.Tasks.Values.Where(t => t.IsExecuting).ToList(); var runningMissions = _missionManager.GetAllMissions().Where(m => m.IsExecuting).ToList(); if (runningTasks.Count > 0 || runningMissions.Count > 0) { var warningMessage = $"Timeout waiting for {runningTasks.Count} task(s) and {runningMissions.Count} mission(s) to stop. ScriptRunner may be blocking. Proceeding anyway."; _logger.LogWarning(warningMessage); _consoleHubContext.LogWarning(warningMessage); } else { // All stopped but state machine may not have transitioned - proceed anyway var infoMessage = "All tasks and missions stopped. Proceeding with state transition."; _logger.LogInformation(infoMessage); _consoleHubContext.LogInfo(infoMessage); } _stateMachine.Fire(ScriptEngineTrigger.StoppingCompleted); } catch (Exception ex) { var errorMessage = $"Unexpected error during stopping: {ex.Message}"; _logger.LogError(ex, "Unexpected error during stopping."); _consoleHubContext.LogError(errorMessage); _stateMachine.Fire(ScriptEngineTrigger.FaultOccurred); } }); } private void OnEnterResetting() { _fileManager.SetState(ScriptEngineState.Resetting); var message = "ScriptEngine entered Resetting state. Resetting managers..."; _logger.LogInformation(message); _consoleHubContext.LogInfo(message); try { // Reset all managers _variableManager.Reset(); var taskResetResult = _taskManager.Reset(); var missionResetResult = _missionManager.Reset(); if (!taskResetResult.IsSuccess) { _logger.LogWarning($"Failed to reset TaskManager: {taskResetResult.Message}"); } if (!missionResetResult.IsSuccess) { _logger.LogWarning($"Failed to reset MissionManager: {missionResetResult.Message}"); } var successMessage = "All managers reset successfully."; _logger.LogInformation(successMessage); _consoleHubContext.LogInfo(successMessage); _stateMachine.Fire(ScriptEngineTrigger.ResettingCompleted); } catch (Exception ex) { var errorMessage = $"Unexpected error during resetting: {ex.Message}"; _logger.LogError(ex, "Unexpected error during resetting."); _consoleHubContext.LogError(errorMessage); _stateMachine.Fire(ScriptEngineTrigger.FaultOccurred); } } private void OnEnterFault() { _fileManager.SetState(ScriptEngineState.Fault); var errorMessage = "ScriptEngine entered Fault state. System error occurred."; _logger.LogError(errorMessage); _consoleHubContext.LogError(errorMessage); _ = NotifyStateChanged(ScriptEngineState.Fault); } /// /// Notifies all connected clients about state change via SignalR. /// private async Task NotifyStateChanged(ScriptEngineState newState) { try { await _hubContext.Clients.All.SendAsync("StateChanged", newState); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to notify state change via SignalR."); } } #endregion #region Public Methods /// /// Builds scripts from all files. Only allowed when state is Idle or BuildError. /// public MessageResult Build() { try { lock (_stateLockObject) { if (_currentState != ScriptEngineState.Idle && _currentState != ScriptEngineState.BuildError) { return new MessageResult(false, $"Cannot build when ScriptEngine is in state: {_currentState}"); } } _stateMachine.Fire(ScriptEngineTrigger.Build); return new MessageResult(true, "Build initiated successfully"); } catch (Exception ex) { var errorMessage = $"Failed to initiate build: {ex.Message}"; _logger.LogError(ex, "Failed to initiate build."); _consoleHubContext.LogError(errorMessage); return new MessageResult(false, errorMessage); } } /// /// Starts the ScriptEngine. Only allowed when state is Ready. /// public MessageResult Start() { try { lock (_stateLockObject) { if (_currentState != ScriptEngineState.Ready) { return new MessageResult(false, $"Cannot start when ScriptEngine is in state: {_currentState}"); } } _stateMachine.Fire(ScriptEngineTrigger.Start); return new MessageResult(true, "Start initiated successfully"); } catch (Exception ex) { var errorMessage = $"Failed to initiate start: {ex.Message}"; _logger.LogError(ex, "Failed to initiate start."); _consoleHubContext.LogError(errorMessage); return new MessageResult(false, errorMessage); } } /// /// Stops the ScriptEngine. Only allowed when state is Running. /// public MessageResult Stop() { try { lock (_stateLockObject) { if (_currentState != ScriptEngineState.Running) { return new MessageResult(false, $"Cannot stop when ScriptEngine is in state: {_currentState}"); } } _stateMachine.Fire(ScriptEngineTrigger.Stop); return new MessageResult(true, "Stop initiated successfully"); } catch (Exception ex) { var errorMessage = $"Failed to initiate stop: {ex.Message}"; _logger.LogError(ex, "Failed to initiate stop."); _consoleHubContext.LogError(errorMessage); return new MessageResult(false, errorMessage); } } /// /// Resets the ScriptEngine. Allowed from Idle, Ready, BuildError, Running, or Fault. /// public MessageResult Reset() { try { lock (_stateLockObject) { if (_currentState != ScriptEngineState.Idle && _currentState != ScriptEngineState.Ready && _currentState != ScriptEngineState.BuildError && _currentState != ScriptEngineState.Running && _currentState != ScriptEngineState.Fault) { return new MessageResult(false, $"Cannot reset when ScriptEngine is in state: {_currentState}"); } } _stateMachine.Fire(ScriptEngineTrigger.Reset); return new MessageResult(true, "Reset initiated successfully"); } catch (Exception ex) { var errorMessage = $"Failed to initiate reset: {ex.Message}"; _logger.LogError(ex, "Failed to initiate reset."); _consoleHubContext.LogError(errorMessage); return new MessageResult(false, errorMessage); } } #endregion #region IDisposable public void Dispose() { if (_disposed) return; try { _stateMachine?.Stop(); _taskManager?.Dispose(); _missionManager?.Dispose(); } catch (Exception ex) { _logger.LogError(ex, "Error during ScriptEngine disposal."); } _disposed = true; GC.SuppressFinalize(this); } #endregion }