Files
Denso/srcs/RobotNet10/Commons/RobotNet10.ScriptEngine/MissionManager.cs
2026-07-03 16:31:37 +07:00

924 lines
37 KiB
C#

using Appccelerate.StateMachine;
using Appccelerate.StateMachine.Machine;
using Microsoft.EntityFrameworkCore;
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.Data;
using RobotNet10.ScriptEngine.Models;
using RobotNet10.ScriptEngine.Shared;
using RobotNet10.Shared;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.Json;
namespace RobotNet10.ScriptEngine;
/// <summary>
/// State enum for MissionManager.
/// </summary>
public enum MissionManagerState
{
Idle = 0,
Running,
Stopping,
}
/// <summary>
/// Triggers for MissionManager state machine.
/// </summary>
public enum MissionManagerTrigger
{
Start,
Stop,
StoppingCompleted,
}
/// <summary>
/// Manages script missions with state machine support.
/// </summary>
public class MissionManager : IDisposable
{
private readonly PassiveStateMachine<MissionManagerState, MissionManagerTrigger> _stateMachine;
private readonly ConcurrentDictionary<string, ScriptMissionModel> _missionModels = new();
private readonly ConcurrentQueue<ScriptMission> _idleMissions = new();
private readonly ConcurrentQueue<ScriptMission> _runningMissions = new();
private readonly ConcurrentDictionary<Guid, ScriptMission> _allMissions = new();
private readonly Lock _lockObject = new();
private readonly Lock _stateLockObject = new();
private readonly Lock _queueLockObject = new();
private readonly IServiceScopeFactory _scopeFactory;
private readonly VariableManager _variableManager;
private readonly IScriptEngineResource _scriptResource;
private readonly ILogger<ScriptEngineGlobals> _logger;
private readonly ConsoleHubContext _consoleHubContext;
private readonly IConfiguration _configuration;
private Task? _runningHandlerTask;
private CancellationTokenSource? _runningHandlerCts;
private readonly ManualResetEventSlim _stoppedWaitHandle = new(false);
private bool _disposed;
private MissionManagerState _currentState = MissionManagerState.Idle;
/// <summary>
/// Gets the current state of the MissionManager.
/// </summary>
public MissionManagerState State => _currentState;
/// <summary>
/// Gets all mission models.
/// </summary>
public IReadOnlyDictionary<string, ScriptMissionModel> MissionModels => _missionModels;
/// <summary>
/// Initializes a new instance of MissionManager.
/// </summary>
public MissionManager(
IServiceScopeFactory scopeFactory,
VariableManager variableManager,
IScriptEngineResource scriptResource,
ILogger<ScriptEngineGlobals> logger,
ConsoleHubContext consoleHubContext,
IConfiguration configuration)
{
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
_variableManager = variableManager ?? throw new ArgumentNullException(nameof(variableManager));
_scriptResource = scriptResource ?? throw new ArgumentNullException(nameof(scriptResource));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_consoleHubContext = consoleHubContext ?? throw new ArgumentNullException(nameof(consoleHubContext));
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
var builder = new StateMachineDefinitionBuilder<MissionManagerState, MissionManagerTrigger>();
// Idle state - can add/remove mission models
builder.In(MissionManagerState.Idle)
.ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = MissionManagerState.Idle; } })
.On(MissionManagerTrigger.Start)
.Goto(MissionManagerState.Running)
.Execute(() => { lock (_stateLockObject) { _currentState = MissionManagerState.Running; } OnEnterRunning(); });
// Running state - can create missions
builder.In(MissionManagerState.Running)
.ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = MissionManagerState.Running; } })
.On(MissionManagerTrigger.Stop)
.Goto(MissionManagerState.Stopping)
.Execute(() => { lock (_stateLockObject) { _currentState = MissionManagerState.Stopping; } OnEnterStopping(); });
// Stopping state - waiting for all missions to complete
builder.In(MissionManagerState.Stopping)
.ExecuteOnEntry(() => { lock (_stateLockObject) { _currentState = MissionManagerState.Stopping; } })
.On(MissionManagerTrigger.StoppingCompleted)
.Goto(MissionManagerState.Idle)
.Execute(() => { lock (_stateLockObject) { _currentState = MissionManagerState.Idle; } });
_stateMachine = builder
.WithInitialState(MissionManagerState.Idle)
.Build()
.CreatePassiveStateMachine();
_stateMachine.Start();
}
private void OnEnterRunning()
{
// Start running handler thread
_runningHandlerCts = new CancellationTokenSource();
_stoppedWaitHandle.Reset();
// Use standard thread pool
_runningHandlerTask = Task.Run(() => RunningHandlerAsync(_runningHandlerCts.Token));
// Create missions for models with AutoStart == true
lock (_lockObject)
{
foreach (var model in _missionModels.Values)
{
if (model.AutoStart)
{
try
{
// Validate auto-start mission parameters: must be empty or have exactly one CancellationToken parameter
var paramList = model.Parameters.ToList();
if (paramList.Count > 1)
{
var message = $"Mission '{model.Name}' has AutoStart=true but has {paramList.Count} parameters. Auto-start missions must have 0 or 1 parameter (CancellationToken). Skipping auto-start.";
_logger.LogWarning(message);
_consoleHubContext.LogWarning(message);
continue;
}
if (paramList.Count == 1)
{
var param = paramList[0];
if (param.Type != typeof(CancellationToken))
{
var message = $"Mission '{model.Name}' has AutoStart=true but parameter '{param.Name}' is not CancellationToken. Auto-start missions must have 0 or 1 CancellationToken parameter. Skipping auto-start.";
_logger.LogWarning(message);
_consoleHubContext.LogWarning(message);
continue;
}
}
// Generate mission ID first
var missionId = Guid.NewGuid();
// Create mission instance with default parameters
var parameters = model.Parameters.Select(p =>
{
object? value = p.Type == typeof(CancellationToken)
? CancellationToken.None
: p.DefaultValue;
return new ScriptMissionParameterModel(p.Name, p.Type, value);
});
var result = CreateMissionInternal(model.Name, parameters, missionId);
if (!result.IsSuccess)
{
// Log error but continue creating other missions
_logger.LogError($"Failed to auto-start mission '{model.Name}': {result.Message}");
_consoleHubContext.LogError($"Failed to auto-start mission '{model.Name}': {result.Message}");
}
}
catch (Exception ex)
{
// Log error but continue creating other missions
_logger.LogError(ex, $"Failed to auto-start mission '{model.Name}'");
_consoleHubContext.LogError($"Failed to auto-start mission '{model.Name}': {ex.Message}");
}
}
}
}
}
private void OnEnterStopping()
{
// Fire and forget async operation with proper error handling
_ = Task.Run(async () =>
{
try
{
// Stop running handler
_runningHandlerCts?.Cancel();
// Wait for running handler to complete (with timeout)
if (_runningHandlerTask != null)
{
try
{
await _runningHandlerTask.WaitAsync(TimeSpan.FromSeconds(10));
}
catch (TimeoutException)
{
_logger.LogWarning("MissionManager running handler task did not complete within timeout. Proceeding anyway.");
_consoleHubContext.LogWarning("MissionManager running handler task did not complete within timeout. Proceeding anyway.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error waiting for running handler task to complete");
}
}
// Wait for stopped signal (with timeout)
// If timeout, check if missions are actually executing
if (!_stoppedWaitHandle.Wait(TimeSpan.FromSeconds(30)))
{
// Check if there are actually running missions
var runningMissions = GetAllMissions().Where(m => m.IsExecuting).ToList();
if (runningMissions.Count > 0)
{
_logger.LogWarning($"Timeout waiting for {runningMissions.Count} mission(s) to stop. ScriptRunner may be blocking. Proceeding anyway.");
_consoleHubContext.LogWarning($"Timeout waiting for {runningMissions.Count} mission(s) to stop. ScriptRunner may be blocking. Proceeding anyway.");
}
else
{
_logger.LogInformation("All missions stopped. Proceeding with state transition.");
_consoleHubContext.LogInfo("All missions stopped. Proceeding with state transition.");
}
}
// Fire StoppingCompleted trigger (always proceed)
_stateMachine.Fire(MissionManagerTrigger.StoppingCompleted);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error during MissionManager stopping");
_consoleHubContext.LogError($"MissionManager stopping error: {ex.Message}");
// Ensure WaitHandle is set even on error to prevent deadlock
try
{
_stoppedWaitHandle.Set();
}
catch { /* Ignore */ }
// Fire trigger to proceed
_stateMachine.Fire(MissionManagerTrigger.StoppingCompleted);
}
});
}
private async Task RunningHandlerAsync(CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
int elapsed;
int remaining;
int interval = 1000;
int processTime = (int)(interval * 0.8);
int count;
_stoppedWaitHandle.Reset();
while (!cancellationToken.IsCancellationRequested)
{
stopwatch.Restart();
using (var scope = _scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ScriptEngineDbContext>();
// Process idle queue: transition to running or complete immediately
count = _idleMissions.Count;
for (int i = 0; i < count; i++)
{
if (!_idleMissions.TryDequeue(out var mission)) break;
// Find corresponding database record
var dbMission = await dbContext.InstanceMissions.FindAsync([mission.Id], cancellationToken);
if (dbMission == null)
{
mission.Dispose();
_allMissions.TryRemove(mission.Id, out _);
continue; // Skip if mission not found in database
}
if (mission.State == ScriptMissionState.Idle)
{
// Start Mission and move to running queue
mission.Start();
_runningMissions.Enqueue(mission);
dbMission.State = mission.State;
}
else if (mission.State == ScriptMissionState.Completed
|| mission.State == ScriptMissionState.Canceled
|| mission.State == ScriptMissionState.Error)
{
// Mission completed/canceled/errored before running
dbMission.State = mission.State;
dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}";
dbMission.Score = mission.CurrentScore;
dbMission.StoppedAt = DateTime.UtcNow;
mission.Dispose();
_allMissions.TryRemove(mission.Id, out _);
}
else
{
// Invalid state
dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}{Environment.NewLine}{DateTime.UtcNow:O}: Mission is not in idle state. [{mission.State}]";
dbMission.State = ScriptMissionState.Error;
mission.Dispose();
_allMissions.TryRemove(mission.Id, out _);
}
}
// Process running queue: check completion or keep
count = _runningMissions.Count;
for (int i = 0; i < count; i++)
{
if (!_runningMissions.TryDequeue(out var mission)) break;
// Find corresponding database record
var dbMission = await dbContext.InstanceMissions.FindAsync([mission.Id], cancellationToken);
if (dbMission == null)
{
mission.Dispose();
_allMissions.TryRemove(mission.Id, out _);
continue; // Skip if mission not found in database
}
if (mission.State == ScriptMissionState.Completed
|| mission.State == ScriptMissionState.Canceled
|| mission.State == ScriptMissionState.Error)
{
// Mission completed - save results to database
dbMission.State = mission.State;
dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}";
dbMission.Score = mission.CurrentScore;
dbMission.StoppedAt = DateTime.UtcNow;
mission.Dispose();
_allMissions.TryRemove(mission.Id, out _);
}
else
{
// Mission still running - update log and state, keep in queue
dbMission.State = mission.State;
var newLog = mission.GetLog();
if (!string.IsNullOrEmpty(newLog))
{
dbMission.Log += $"{Environment.NewLine}{newLog}";
}
dbMission.Score = mission.CurrentScore;
_runningMissions.Enqueue(mission);
}
}
// Save all changes to database
await dbContext.SaveChangesAsync(cancellationToken);
}
// Collect memory after each cycle
GC.Collect();
stopwatch.Stop();
elapsed = (int)stopwatch.ElapsedMilliseconds;
remaining = interval - elapsed;
// If execution time exceeds 80% of interval, add another cycle
if (elapsed > processTime)
{
remaining += interval;
}
if (remaining > 0)
{
try
{
await Task.Delay(remaining, cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
}
}
// Cleanup on stop: dispose all unstarted Missions
while (_idleMissions.TryDequeue(out var mission))
{
mission.Dispose();
_allMissions.TryRemove(mission.Id, out _);
}
// Cancel and save state of all running Missions
using (var scope = _scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ScriptEngineDbContext>();
while (_runningMissions.TryDequeue(out var mission))
{
try
{
mission.Cancel("engin is stopping");
// Wait for stop, but only timeout if actually executing
mission.WaitForStop();
}
catch (Exception ex)
{
// Log error but continue disposing
_logger.LogError(ex, $"Error stopping mission '{mission.Name}'");
}
finally
{
// Update final state to database
try
{
var dbMission = await dbContext.InstanceMissions.FindAsync([mission.Id], CancellationToken.None);
if (dbMission != null)
{
dbMission.State = mission.State;
dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}";
dbMission.Score = mission.CurrentScore;
dbMission.StoppedAt = DateTime.UtcNow;
}
}
catch (Exception ex)
{
// Log error but continue
_logger.LogError(ex, $"Error updating mission '{mission.Name}' state in database");
}
mission.Dispose();
_allMissions.TryRemove(mission.Id, out _);
}
}
await dbContext.SaveChangesAsync(CancellationToken.None);
}
// Signal that stopping is complete
_stoppedWaitHandle.Set();
}
/// <summary>
/// Resets all mission models. Clears all mission models.
/// Only allowed when state is Idle.
/// </summary>
public MessageResult Reset()
{
try
{
lock (_stateLockObject)
{
if (_currentState != MissionManagerState.Idle)
return new MessageResult(false, $"Cannot reset mission models when MissionManager is in state: {_currentState}");
}
lock (_lockObject)
{
var count = _missionModels.Count;
_missionModels.Clear();
return new MessageResult(true, $"Reset {count} mission model(s) successfully");
}
}
catch (Exception ex)
{
return new MessageResult(false, $"Failed to reset mission models: {ex.Message}");
}
}
/// <summary>
/// Loads all mission models from a collection of ScriptMissionModel. This clears existing mission models first.
/// Only allowed when state is Idle.
/// </summary>
/// <param name="missionModels">The collection of mission models to load.</param>
public MessageResult Load(IEnumerable<ScriptMissionModel> missionModels)
{
try
{
if (missionModels == null)
throw new ArgumentNullException(nameof(missionModels));
lock (_stateLockObject)
{
if (_currentState != MissionManagerState.Idle)
return new MessageResult(false, $"Cannot load mission models when MissionManager is in state: {_currentState}");
}
// Reset existing mission models first
var resetResult = Reset();
if (!resetResult.IsSuccess)
return resetResult;
// Load all mission models
lock (_lockObject)
{
var loadedCount = 0;
var errorCount = 0;
var errors = new List<string>();
foreach (var model in missionModels)
{
if (string.IsNullOrWhiteSpace(model.Name))
continue;
try
{
_missionModels.TryAdd(model.Name, model);
loadedCount++;
}
catch (Exception ex)
{
errorCount++;
errors.Add($"Failed to load mission model '{model.Name}': {ex.Message}");
}
}
if (errorCount > 0)
{
return new MessageResult(false,
$"Loaded {loadedCount} mission model(s) successfully, {errorCount} failed. Errors: {string.Join("; ", errors)}");
}
return new MessageResult(true, $"Loaded {loadedCount} mission model(s) successfully");
}
}
catch (Exception ex)
{
return new MessageResult(false, $"Failed to load mission models: {ex.Message}");
}
}
/// <summary>
/// Creates a mission instance with parameters provided as a dictionary (matched by parameter name).
/// Only allowed when state is Running.
/// </summary>
/// <param name="missionName">The name of the mission model.</param>
/// <param name="parameters">Dictionary of parameter values keyed by parameter name.</param>
/// <returns>MessageResult containing the mission ID if successful.</returns>
public MessageResult<Guid> CreateMission(string missionName, Dictionary<string, object?> parameters)
{
try
{
if (string.IsNullOrWhiteSpace(missionName))
return new MessageResult<Guid>(false, default, "Mission name cannot be null or empty");
ArgumentNullException.ThrowIfNull(parameters);
lock (_stateLockObject)
{
if (_currentState != MissionManagerState.Running)
return new MessageResult<Guid>(false, default, $"Cannot create mission when MissionManager is in state: {_currentState}");
}
if (!_missionModels.TryGetValue(missionName, out var model))
return new MessageResult<Guid>(false, default, $"Mission model '{missionName}' not found");
// Convert dictionary to ScriptMissionParameterModel list
var parameterModels = new List<ScriptMissionParameterModel>();
foreach (var paramModel in model.Parameters)
{
// Skip CancellationToken parameters
if (paramModel.Type == typeof(CancellationToken))
continue;
// Get value from dictionary or use default
object? value = parameters.TryGetValue(paramModel.Name, out var paramValue)
? paramValue
: paramModel.DefaultValue;
parameterModels.Add(new ScriptMissionParameterModel(paramModel.Name, paramModel.Type, value));
}
var missionId = Guid.NewGuid();
return CreateMissionInternal(missionName, parameterModels, missionId);
}
catch (Exception ex)
{
return new MessageResult<Guid>(false, default, $"Failed to create mission: {ex.Message}");
}
}
/// <summary>
/// Creates a mission instance with parameters provided as an object array (matched by order, skipping CancellationToken).
/// Only allowed when state is Running.
/// </summary>
/// <param name="missionName">The name of the mission model.</param>
/// <param name="parameters">Array of parameter values in order (CancellationToken parameters in model are skipped).</param>
/// <returns>MessageResult containing the mission ID if successful.</returns>
public MessageResult<Guid> CreateMission(string missionName, object[] parameters)
{
try
{
if (string.IsNullOrWhiteSpace(missionName))
return new MessageResult<Guid>(false, default, "Mission name cannot be null or empty");
ArgumentNullException.ThrowIfNull(parameters);
lock (_stateLockObject)
{
if (_currentState != MissionManagerState.Running)
return new MessageResult<Guid>(false, default, $"Cannot create mission when MissionManager is in state: {_currentState}");
}
if (!_missionModels.TryGetValue(missionName, out var model))
return new MessageResult<Guid>(false, default, $"Mission model '{missionName}' not found");
// Convert object array to ScriptMissionParameterModel list
// Skip CancellationToken parameters when mapping
var parameterModels = new List<ScriptMissionParameterModel>();
var paramIndex = 0;
foreach (var paramModel in model.Parameters)
{
// Skip CancellationToken parameters
if (paramModel.Type == typeof(CancellationToken))
continue;
// Get value from array or use default
object? value = paramIndex < parameters.Length
? parameters[paramIndex]
: paramModel.DefaultValue;
parameterModels.Add(new ScriptMissionParameterModel(paramModel.Name, paramModel.Type, value));
paramIndex++;
}
// Validate that all provided parameters were used
if (paramIndex < parameters.Length)
{
return new MessageResult<Guid>(false, default, $"Too many parameters provided. Expected {paramIndex} parameters (excluding CancellationToken), but got {parameters.Length}.");
}
var missionId = Guid.NewGuid();
return CreateMissionInternal(missionName, parameterModels, missionId);
}
catch (Exception ex)
{
return new MessageResult<Guid>(false, default, $"Failed to create mission: {ex.Message}");
}
}
private MessageResult<Guid> CreateMissionInternal(string missionName, IEnumerable<ScriptMissionParameterModel> parameters, Guid missionId)
{
try
{
if (!_missionModels.TryGetValue(missionName, out var model))
return new MessageResult<Guid>(false, default, $"Mission model '{missionName}' not found");
// Populate MissionParameters dictionary with provided parameters
var missionParameters = new Dictionary<string, object?>();
foreach (var param in parameters)
{
// Use DefaultValue from parameter (which may have been set to actual value by caller)
missionParameters[param.Name] = param.DefaultValue;
}
// Add CancellationToken parameters from model if they exist
// (These are skipped in CreateMission overloads but need to be in MissionParameters for script execution)
foreach (var paramModel in model.Parameters)
{
if (paramModel.Type == typeof(CancellationToken))
{
// CancellationToken will be provided by mission's internal token source
missionParameters[paramModel.Name] = CancellationToken.None;
}
}
// Serialize parameters to JSON (include all parameters from model, including CancellationToken)
var allParametersForJson = model.Parameters.Select(p => new ScriptMissionParameterDto(p.Name, p.Type.FullName ?? "", p.DefaultValue?.ToString() ?? "null"));
var parametersJson = JsonSerializer.Serialize(allParametersForJson);
// Create InstanceMission in database FIRST with initial values
using (var scope = _scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ScriptEngineDbContext>();
var dbMission = new InstanceMission
{
Id = missionId,
MissionName = missionName,
CreatedAt = DateTime.UtcNow,
Parameters = parametersJson,
TotalScore = model.TotalScore,
State = ScriptMissionState.Idle,
Score = 0,
StoppedAt = DateTime.UtcNow,
Log = string.Empty
};
dbContext.InstanceMissions.Add(dbMission);
dbContext.SaveChanges();
}
// Create LoggerMission with mission ID
var loggerMission = new LoggerMission(missionId, _consoleHubContext);
// Create ScriptEngineGlobals with LoggerMission
var scriptEngineGlobals = new ScriptEngineGlobals(loggerMission, _scopeFactory);
var robotNetDict = ScriptHelper.ConvertGlobalsToDictionary(scriptEngineGlobals, typeof(IScriptGlobals));
// Get AppApis with mission ID
var appApisDict = _scriptResource.GetMissionGlobals(missionId, CancellationToken.None);
// Get GlobalVariables from VariableManager
var globalVariablesDict = _variableManager.Globals;
// Create ScriptGlobals with LoggerMission, mission ID, GlobalVariables, and MissionParameters
var populatedGlobals = new ScriptGlobals(
robotNetDict,
appApisDict,
globalVariablesDict,
missionParameters
);
// Create ScriptMission instance
var mission = new ScriptMission(missionId, model, populatedGlobals);
// Add to idle queue
_idleMissions.Enqueue(mission);
_allMissions.TryAdd(missionId, mission);
return new MessageResult<Guid>(true, missionId, $"Mission '{missionName}' created successfully with ID: {missionId}");
}
catch (Exception ex)
{
return new MessageResult<Guid>(false, default, $"Failed to create mission '{missionName}': {ex.Message}");
}
}
/// <summary>
/// Gets a mission instance by ID.
/// </summary>
public ScriptMission? GetMission(Guid missionId)
{
_allMissions.TryGetValue(missionId, out var mission);
return mission;
}
/// <summary>
/// Gets a mission model by name.
/// </summary>
public ScriptMissionModel? GetMissionModel(string name)
{
_missionModels.TryGetValue(name, out var model);
return model;
}
/// <summary>
/// Gets all mission instances.
/// </summary>
public IEnumerable<ScriptMission> GetAllMissions()
{
return _allMissions.Values;
}
/// <summary>
/// Gets all mission models as ScriptMissionDto array.
/// </summary>
public ScriptMissionDto[] GetScriptMissions()
{
lock (_lockObject)
{
return _missionModels.Values.Select(m => new ScriptMissionDto(
m.Name,
m.Parameters.Select(p => new ScriptMissionParameterDto(
p.Name,
p.Type.FullName ?? p.Type.Name,
p.DefaultValue?.ToString())).ToArray())).ToArray();
}
}
/// <summary>
/// Finds specific mission models by names as ScriptMissionDto array.
/// </summary>
public ScriptMissionDto[] FindScriptMissions(string[] names)
{
if (names == null)
throw new ArgumentNullException(nameof(names));
lock (_lockObject)
{
return _missionModels.Values
.Where(m => names.Contains(m.Name))
.Select(m => new ScriptMissionDto(
m.Name,
m.Parameters.Select(p => new ScriptMissionParameterDto(
p.Name,
p.Type.FullName ?? p.Type.Name,
p.DefaultValue?.ToString())).ToArray())).ToArray();
}
}
/// <summary>
/// Starts the MissionManager. This will start missions with AutoStart == true.
/// </summary>
public MessageResult Start()
{
try
{
_stateMachine.Fire(MissionManagerTrigger.Start);
return new MessageResult(true, "MissionManager started successfully");
}
catch (Exception ex)
{
return new MessageResult(false, $"Failed to start MissionManager: {ex.Message}");
}
}
/// <summary>
/// Stops the MissionManager. This will ensure all missions are stopped.
/// </summary>
public MessageResult Stop()
{
try
{
_stateMachine.Fire(MissionManagerTrigger.Stop);
return new MessageResult(true, "MissionManager stop initiated");
}
catch (Exception ex)
{
return new MessageResult(false, $"Failed to stop MissionManager: {ex.Message}");
}
}
/// <summary>
/// Checks if all missions are not running.
/// </summary>
public bool AreAllMissionsNotRunning()
{
lock (_queueLockObject)
{
return _idleMissions.IsEmpty && _runningMissions.IsEmpty;
}
}
/// <summary>
/// Gets the count of mission models.
/// </summary>
public int MissionModelCount => _missionModels.Count;
/// <summary>
/// Gets the count of active missions.
/// </summary>
public int ActiveMissionCount => _allMissions.Count;
/// <summary>
/// Disposes the MissionManager and all missions.
/// </summary>
public void Dispose()
{
if (_disposed)
return;
// Stop state machine first
try
{
_stateMachine.Stop();
}
catch
{
// Ignore errors when stopping state machine
}
// Stop running handler
_runningHandlerCts?.Cancel();
if (_runningHandlerTask != null)
{
try
{
_runningHandlerTask.Wait(TimeSpan.FromSeconds(5));
}
catch
{
// Ignore errors
}
}
// Dispose all missions
lock (_queueLockObject)
{
while (_idleMissions.TryDequeue(out var mission))
{
try
{
mission.Dispose();
}
catch
{
// Ignore disposal errors
}
}
while (_runningMissions.TryDequeue(out var mission))
{
try
{
mission.Dispose();
}
catch
{
// Ignore disposal errors
}
}
}
_allMissions.Clear();
_missionModels.Clear();
_runningHandlerCts?.Dispose();
_stoppedWaitHandle.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
}
}