Files
I150/srcs/RobotNet10/Commons/RobotNet10.ScriptEngine/ScriptEngine.cs
2026-07-03 16:37:12 +07:00

646 lines
26 KiB
C#

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;
/// <summary>
/// Core ScriptEngine class that manages script compilation, execution, and state machine.
/// </summary>
public class ScriptEngine : IDisposable
{
private readonly PassiveStateMachine<ScriptEngineState, ScriptEngineTrigger> _stateMachine;
private readonly ScriptBuilder _scriptBuilder;
private readonly FileManager _fileManager;
private readonly VariableManager _variableManager;
private readonly TaskManager _taskManager;
private readonly MissionManager _missionManager;
private readonly ILogger<ScriptEngine> _logger;
private readonly IHubContext<ScriptManagerHub> _hubContext;
private readonly ConsoleHubContext _consoleHubContext;
private readonly Lock _stateLockObject = new();
private ScriptEngineState _currentState = ScriptEngineState.Initializing;
private bool _disposed;
/// <summary>
/// Gets the current state of the ScriptEngine.
/// </summary>
public ScriptEngineState State => _currentState;
/// <summary>
/// Gets the FileManager instance.
/// </summary>
public FileManager FileManager => _fileManager;
/// <summary>
/// Gets the VariableManager instance.
/// </summary>
public VariableManager VariableManager => _variableManager;
/// <summary>
/// Gets the TaskManager instance.
/// </summary>
public TaskManager TaskManager => _taskManager;
/// <summary>
/// Gets the MissionManager instance.
/// </summary>
public MissionManager MissionManager => _missionManager;
/// <summary>
/// Initializes a new instance of ScriptEngine.
/// </summary>
public ScriptEngine(
IScriptEngineResource scriptResource,
IConfiguration configuration,
FileManager fileManager,
VariableManager variableManager,
TaskManager taskManager,
MissionManager missionManager,
ILogger<ScriptEngine> logger,
IHubContext<ScriptManagerHub> 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<ScriptEngineState, ScriptEngineTrigger>();
// 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<ScriptEngineState, ScriptEngineTrigger> 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);
}
/// <summary>
/// Notifies all connected clients about state change via SignalR.
/// </summary>
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
/// <summary>
/// Builds scripts from all files. Only allowed when state is Idle or BuildError.
/// </summary>
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);
}
}
/// <summary>
/// Starts the ScriptEngine. Only allowed when state is Ready.
/// </summary>
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);
}
}
/// <summary>
/// Stops the ScriptEngine. Only allowed when state is Running.
/// </summary>
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);
}
}
/// <summary>
/// Resets the ScriptEngine. Allowed from Idle, Ready, BuildError, Running, or Fault.
/// </summary>
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
}