Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View 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);
}
}