299 lines
10 KiB
C#
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);
|
|
}
|
|
|
|
}
|