RobotNet/RobotNet.ScriptManager/Models/ScriptMission.cs
2025-10-15 15:15:53 +07:00

299 lines
10 KiB
C#

using Microsoft.CodeAnalysis.Scripting;
using RobotNet.Script;
using RobotNet.Script.Shares;
using System.Collections.Concurrent;
namespace RobotNet.ScriptManager.Models;
public class ScriptMissionGlobals(IDictionary<string, object?> _globals, IDictionary<string, object?> _robotnet, ConcurrentDictionary<string, object?> _parameters)
{
public readonly IDictionary<string, object?> globals = _globals;
public readonly IDictionary<string, object?> robotnet = _robotnet;
public readonly IDictionary<string, object?> 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<IAsyncEnumerable<MissionState>> 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<IAsyncEnumerable<MissionState>> 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<RobotNet.Script.ILogger> 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);
}
}