using Microsoft.CodeAnalysis.Scripting; using RobotNet.Script; using RobotNet.Script.Shares; using System.Collections.Concurrent; namespace RobotNet.ScriptManager.Models; public class ScriptMissionGlobals(IDictionary _globals, IDictionary _robotnet, ConcurrentDictionary _parameters) { public readonly IDictionary globals = _globals; public readonly IDictionary robotnet = _robotnet; public readonly IDictionary parameters = _parameters; } public class ScriptMission : IDisposable { public Guid Id { get; } public string Name { get; } public MissionStatus Status { get; private set; } = MissionStatus.Idle; public Exception? Exception { get; private set; } = null; public CancellationToken CancellationToken => internalCts?.Token ?? CancellationToken.None; public WaitHandle WaitHandle => waitHandle; private readonly ScriptRunner> Runner; private readonly ScriptMissionGlobals Globals; private readonly CancellationTokenSource internalCts; private readonly Thread thread; private bool disposed; private readonly Mutex mutex = new(); private readonly ScriptMissionLogger? Logger; private readonly ManualResetEvent waitHandle = new(false); public ScriptMission(Guid id, string name, ScriptRunner> runner, ScriptMissionGlobals globals, CancellationTokenSource cts) { Id = id; Name = name; Runner = runner; Globals = globals; internalCts = cts; thread = new Thread(() => Run(internalCts.Token)) { IsBackground = true, Priority = ThreadPriority.Highest }; if (Globals.robotnet.TryGetValue($"get_{nameof(IRobotNetGlobals.Logger)}", out object? get_logger) && get_logger is Func get_logger_func) { Logger = get_logger_func.Invoke() as ScriptMissionLogger; } } public string GetLog() => Logger?.GetLog() ?? string.Empty; public void Start() { if (!mutex.WaitOne(1000)) return; try { ObjectDisposedException.ThrowIf(disposed, nameof(ScriptMission)); if (Status != MissionStatus.Idle) throw new InvalidOperationException("Mission can only be started after preparation."); if (internalCts.IsCancellationRequested) throw new InvalidOperationException("Mission is not prepared or has been canceled."); Status = MissionStatus.Running; thread.Start(); } finally { mutex.ReleaseMutex(); } } public bool Pause() { if (!mutex.WaitOne(1000)) return false; try { ObjectDisposedException.ThrowIf(disposed, nameof(ScriptMission)); if (Status == MissionStatus.Running) { Status = MissionStatus.Pausing; return true; } else { return false; // Cannot pause if not running } } catch (Exception) { return false; } finally { mutex.ReleaseMutex(); } } public bool Resume() { if (!mutex.WaitOne(1000)) return false; try { ObjectDisposedException.ThrowIf(disposed, nameof(ScriptMission)); if (Status == MissionStatus.Paused) { Status = MissionStatus.Resuming; return true; } else { return false; // Cannot resume if not paused } } catch (Exception) { return false; } finally { mutex.ReleaseMutex(); } } public bool Cancel(string reason) { if (!mutex.WaitOne(1000)) { return false; } try { ObjectDisposedException.ThrowIf(disposed, nameof(ScriptMission)); if (!internalCts.IsCancellationRequested) { internalCts.Cancel(); } if (Status == MissionStatus.Canceling || Status == MissionStatus.Canceled) { return true; } if (Status == MissionStatus.Idle) { Logger?.LogInfo($"{DateTime.UtcNow} Idle Mission '{Name}' cancel with reason: {reason}"); Status = MissionStatus.Canceled; return true; } else if (Status == MissionStatus.Running || Status == MissionStatus.Pausing || Status == MissionStatus.Paused || Status == MissionStatus.Resuming) { Status = MissionStatus.Canceling; Logger?.LogInfo($"{DateTime.UtcNow} Mission '{Name}' canceling with reason: {reason}"); return true; } else { return true; } } catch (Exception) { return false; } finally { mutex.ReleaseMutex(); } } private void Run(CancellationToken cancellationToken) { Task.Factory.StartNew(async Task () => { try { var cts = new CancellationTokenSource(); cancellationToken.Register(() => { cts.CancelAfter(TimeSpan.FromSeconds(5)); }); await foreach (var state in await Runner.Invoke(Globals, cts.Token)) { Logger?.LogInfo($"{DateTime.UtcNow} Mission {Name}-{Id} with step: {state.Step}; message: {state.Message}"); if (Status == MissionStatus.Pausing) { Status = MissionStatus.Paused; while (Status == MissionStatus.Paused && !cancellationToken.IsCancellationRequested) { try { await Task.Delay(1000, cancellationToken); } catch (TaskCanceledException) { } catch (OperationCanceledException) { } } } if (cancellationToken.IsCancellationRequested) { if (Status != MissionStatus.Canceling) { Exception = new OperationCanceledException($"{DateTime.UtcNow} Mission {Name}-{Id} was canceled externally."); Logger?.LogError(Exception); Status = MissionStatus.Error; } break; } if (Status == MissionStatus.Resuming) { Status = MissionStatus.Running; continue; } else if (Status == MissionStatus.Canceling) { if (internalCts == null || internalCts.IsCancellationRequested) { Exception = new OperationCanceledException($"{DateTime.UtcNow} Mission {Name}-{Id} was canceled externally."); Logger?.LogError(Exception); Status = MissionStatus.Error; break; } else { Logger?.LogError($"{DateTime.UtcNow} Mission {Name}-{Id} is canceling"); continue; } } else if (Status == MissionStatus.Running) { continue; } else if (Status != MissionStatus.Error) { Status = MissionStatus.Error; Exception = new InvalidOperationException($"{DateTime.UtcNow} Mission {Name}-{Id} Unexpected status change: {Status}"); Logger?.LogError(Exception); break; } } if (Status == MissionStatus.Running) { Logger?.LogInfo($"{DateTime.UtcNow} Mission {Name}-{Id} is completed"); Status = MissionStatus.Completed; } else if (Status == MissionStatus.Canceling) { Logger?.LogError($"{DateTime.UtcNow} Mission {Name}-{Id} is canceled"); Status = MissionStatus.Canceled; } else if (Status != MissionStatus.Error) { Exception = new OperationCanceledException($"{DateTime.UtcNow} Mission {Name}-{Id} Cancel from status {Status}"); Logger?.LogError(Exception); Status = MissionStatus.Error; } } catch (OperationCanceledException oce) { if (Status != MissionStatus.Canceling) { Status = MissionStatus.Error; Exception = oce; Logger?.LogError(oce.ToString()); } else { Logger?.LogInfo($"{DateTime.UtcNow} Mission {Name}-{Id} was canceled successfully."); Status = MissionStatus.Canceled; } } catch (Exception ex) { Status = MissionStatus.Error; Exception = ex; Logger?.LogError(Exception); } finally { waitHandle.Set(); Logger?.LogInfo($"{DateTime.UtcNow} End Mission wtih status: {Status}"); } }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Current).GetAwaiter().GetResult(); } public void Dispose() { if (disposed) return; disposed = true; if (!internalCts.IsCancellationRequested) { internalCts.Cancel(); } thread.Join(); internalCts.Dispose(); GC.SuppressFinalize(this); } }