using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; using RobotNet.Script; using RobotNet.Script.Shares; using RobotNet.ScriptManager.Data; using RobotNet.ScriptManager.Helpers; using RobotNet.ScriptManager.Models; using System.Collections.Concurrent; using System.Diagnostics; namespace RobotNet.ScriptManager.Services; public class ScriptMissionManager(ScriptGlobalsManager globalsManager, IServiceScopeFactory scopeFactory) { public ProcessorState State { get; private set; } = ProcessorState.Idle; private readonly Dictionary MissionDatas = []; private readonly Dictionary>> Runners = []; private readonly ConcurrentDictionary Missions = []; private readonly ConcurrentQueue idleMissions = []; private readonly ConcurrentQueue runningMissions = []; public void Reset() { if (State != ProcessorState.Idle && State != ProcessorState.Ready) { throw new InvalidOperationException("Cannot reset missions while the processor is running."); } MissionDatas.Clear(); Runners.Clear(); foreach (var mission in Missions.Values) { mission.Dispose(); } Missions.Clear(); foreach (var mission in idleMissions) { mission.Dispose(); } idleMissions.Clear(); foreach (var mission in runningMissions) { mission.Dispose(); } runningMissions.Clear(); GC.Collect(); State = ProcessorState.Idle; } public void LoadMissions(IEnumerable missionDatas) { if (State != ProcessorState.Idle && State != ProcessorState.Ready) { throw new InvalidOperationException("Cannot load missions while the processor is running."); } MissionDatas.Clear(); Runners.Clear(); runningMissions.Clear(); idleMissions.Clear(); runningMissions.Clear(); foreach (var mission in missionDatas) { MissionDatas.Add(mission.Name, mission); var script = CSharpScript.Create>(mission.Script, ScriptConfiguration.ScriptOptions, globalsType: typeof(ScriptMissionGlobals)); Runners.Add(mission.Name, script.CreateDelegate()); } State = ProcessorState.Ready; } public IEnumerable GetMissionDatas() => [.. MissionDatas.Values.Select(m => new ScriptMissionDto(m.Name, m.Parameters.Select(p => new ScriptMissionParameterDto(p.Name, p.Type.FullName ?? p.Type.Name, p.DefaultValue?.ToString())), m.Code))]; public bool ContainsMissionName(string name) => Runners.ContainsKey(name); public void Create(Guid id, string name, IDictionary parameterStrings) { if (!MissionDatas.TryGetValue(name, out var missionData)) throw new ArgumentException($"Mission data for '{name}' not found."); var cts = CancellationTokenSource.CreateLinkedTokenSource(internalCts.Token); var parameters = new ConcurrentDictionary(); bool hasCancellationToken = false; foreach (var param in missionData.Parameters) { if (param.Type == typeof(CancellationToken)) { if (hasCancellationToken) { throw new ArgumentException($"Mission '{name}' already has a CancellationToken parameter defined."); } hasCancellationToken = true; parameters.TryAdd(param.Name, cts.Token); // Use the internal CancellationTokenSource for the mission continue; } if (!parameterStrings.TryGetValue(param.Name, out string? valueStr)) throw new ArgumentException($"Parameter '{param.Name}' not found in provided parameters."); if (CSharpSyntaxHelper.ResolveValueFromString(valueStr, param.Type, out var value) && value != null) { parameters.TryAdd(param.Name, value); } else { throw new ArgumentException($"Invalid value for parameter '{param.Name}': {valueStr}"); } } Create(id, name, parameters, cts); } public void Create(Guid id, string name, object[] parameters) { if (!MissionDatas.TryGetValue(name, out var missionData)) throw new ArgumentException($"Mission data for '{name}' not found."); if (parameters.Length != missionData.Parameters.Count()) { var count = missionData.Parameters.Count(p => p.Type == typeof(CancellationToken)); if (count == 1) { if (parameters.Length != missionData.Parameters.Count() - count) { throw new ArgumentException($"Mission '{name}' expects {missionData.Parameters.Count()} parameters, but received {parameters.Length} without CancellationToken."); } } else if (count != 0) { throw new ArgumentException($"Mission '{name}' just have one CancellationToken, but received {parameters.Length}."); } } var inputParameters = new ConcurrentDictionary(); bool hasCancellationToken = false; var cts = CancellationTokenSource.CreateLinkedTokenSource(internalCts.Token); int index = 0; foreach (var param in missionData.Parameters) { if (param.Type == typeof(CancellationToken)) { if (hasCancellationToken) { throw new ArgumentException($"Mission '{name}' already has a CancellationToken parameter defined."); } hasCancellationToken = true; inputParameters.TryAdd(param.Name, cts.Token); // Use the internal CancellationTokenSource for the mission continue; } inputParameters.TryAdd(param.Name, parameters[index]); index++; } Create(id, name, inputParameters, cts); } public void Create(Guid id, string name, ConcurrentDictionary parameters, CancellationTokenSource cts) { if (!Runners.TryGetValue(name, out var runner)) throw new ArgumentException($"Mission '{name}' not found."); var robotnet = globalsManager.GetRobotNetMission(id); var mission = new ScriptMission(id, name, runner, new ScriptMissionGlobals(globalsManager.Globals, robotnet, parameters), cts); Missions.TryAdd(id, mission); idleMissions.Enqueue(mission); } private CancellationTokenSource internalCts = new(); private Thread? thread; public void Start(CancellationToken cancellationToken = default) { Stop(); // Ensure previous thread is stopped before starting a new one ResetMissionDb(); internalCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var token = internalCts.Token; thread = new Thread(() => { State = ProcessorState.Running; while (!token.IsCancellationRequested || !runningMissions.IsEmpty) { var stopwatch = Stopwatch.StartNew(); MiningIdleMissionHandle(); MiningRunningMissionHandle(); stopwatch.Stop(); int elapsed = (int)stopwatch.ElapsedMilliseconds; int remaining = 1000 - elapsed; // If execution time exceeds ProcessTime, add another cycle if (elapsed > 900) { remaining += 1000; } if (remaining > 0) { try { Thread.Sleep(remaining); } catch (ThreadInterruptedException) { break; } } } State = ProcessorState.Ready; }) { IsBackground = true, Priority = ThreadPriority.Highest, }; thread.Start(); } public bool Pause(Guid id) { if (Missions.TryGetValue(id, out var mission)) { return mission.Pause(); } return false; } public bool Resume(Guid id) { if (Missions.TryGetValue(id, out var mission)) { return mission.Resume(); } return false; } public bool Cancel(Guid id, string reason) { if (Missions.TryGetValue(id, out var mission)) { return mission.Cancel(reason); } return false; // Mission not found or not running } public void Stop() { if (!idleMissions.IsEmpty || !runningMissions.IsEmpty) { var listWaitHandles = new List(); while (idleMissions.TryDequeue(out var mission)) { mission.Cancel("Cancel by script mission manager is stoped"); listWaitHandles.Add(mission.WaitHandle); } while (runningMissions.TryDequeue(out var mission)) { mission.Cancel("Cancel by script mission manager is stoped"); listWaitHandles.Add(mission.WaitHandle); } WaitHandle.WaitAll([.. listWaitHandles]); } if (!internalCts.IsCancellationRequested) { internalCts.Cancel(); } if (thread != null && thread.IsAlive) { thread.Interrupt(); thread.Join(); } internalCts.Dispose(); thread = null; } private void RemoveMission(ScriptMission mission) { mission.Dispose(); Missions.TryRemove(mission.Id, out _); } public void ResetMissionDb() { using var scope = scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var missions = dbContext.InstanceMissions.Where(m => m.Status == MissionStatus.Running || m.Status == MissionStatus.Pausing || m.Status == MissionStatus.Paused || m.Status == MissionStatus.Canceling || m.Status == MissionStatus.Resuming).ToList(); foreach (var mission in missions) { mission.Log += $"{Environment.NewLine}{DateTime.UtcNow}: Mission Manager start, but instance mission has state {mission.Status}"; mission.Status = MissionStatus.Error; } dbContext.SaveChanges(); } private void MiningIdleMissionHandle() { using var scope = scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); int count = idleMissions.Count; bool hasChanges = false; for (int i = 0; i < count; i++) { if (!idleMissions.TryDequeue(out var mission)) break; var dbMission = dbContext.InstanceMissions.Find(mission.Id); if (dbMission == null) { RemoveMission(mission); continue; // Skip if mission not found in database } if (mission.Status == MissionStatus.Idle) { mission.Start(); runningMissions.Enqueue(mission); dbMission.Status = mission.Status; } else { RemoveMission(mission); if (mission.Status == MissionStatus.Canceled) { dbMission.Status = MissionStatus.Canceled; dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}"; } else { dbMission.Status = MissionStatus.Error; dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}{Environment.NewLine}{DateTime.UtcNow}: Mission is not in idle state. [{mission.Status}]"; } hasChanges = true; } } if (hasChanges) { dbContext.SaveChanges(); } } private void MiningRunningMissionHandle() { using var scope = scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); bool hasChanges = false; int count = runningMissions.Count; for (int i = 0; i < count; i++) { if (!runningMissions.TryDequeue(out var mission)) break; var dbMission = dbContext.InstanceMissions.Find(mission.Id); if (dbMission == null) { RemoveMission(mission); continue; // Skip if mission not found in database } switch (mission.Status) { case MissionStatus.Running: case MissionStatus.Paused: case MissionStatus.Canceling: case MissionStatus.Resuming: case MissionStatus.Pausing: if (dbMission.Status != mission.Status) { dbMission.Status = mission.Status; hasChanges = true; } runningMissions.Enqueue(mission); break; case MissionStatus.Completed: case MissionStatus.Canceled: case MissionStatus.Error: dbMission.Status = mission.Status; dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}"; dbMission.StopedAt = DateTime.UtcNow; hasChanges = true; RemoveMission(mission); break; // Handle these statuses in their respective methods default: dbMission.Status = MissionStatus.Error; dbMission.Log += $"{Environment.NewLine} Wrong mission status on running: {mission.Status}"; dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}"; hasChanges = true; RemoveMission(mission); continue; // Skip unknown statuses } } if (hasChanges) { dbContext.SaveChanges(); } } }