first commit -push
This commit is contained in:
25
RobotNet.ScriptManager/Models/ConsoleLog.cs
Normal file
25
RobotNet.ScriptManager/Models/ConsoleLog.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.ScriptManager.Hubs;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ConsoleLog(IHubContext<ConsoleHub> consoleHub, ILogger? logger = null) : Script.ILogger
|
||||
{
|
||||
public void LogError(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.All.SendAsync("MessageError", message));
|
||||
logger?.LogError(message);
|
||||
}
|
||||
|
||||
public void LogInfo(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.All.SendAsync("MessageInfo", message));
|
||||
logger?.LogInformation(message);
|
||||
}
|
||||
|
||||
public void LogWarning(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.All.SendAsync("MessageWarning", message));
|
||||
logger?.LogWarning(message);
|
||||
}
|
||||
}
|
||||
63
RobotNet.ScriptManager/Models/ScriptGlobalsRobotNet.cs
Normal file
63
RobotNet.ScriptManager/Models/ScriptGlobalsRobotNet.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using RobotNet.Script;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Services;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public abstract class ScriptGlobalsRobotNet(Script.ILogger logger, IRobotManager robotManager, IMapManager mapManager, ScriptConnectionManager connectionManager, IServiceScopeFactory scopeFactory) : IRobotNetGlobals
|
||||
{
|
||||
public Script.ILogger Logger { get; } = logger;
|
||||
|
||||
public IRobotManager RobotManager { get; } = robotManager;
|
||||
|
||||
public IMapManager MapManager { get; } = mapManager;
|
||||
|
||||
public virtual Guid CurrentMissionId => throw new NotImplementedException();
|
||||
|
||||
public IUnixDevice UnixDevice { get; } = Connections.UnixDevice.Instance;
|
||||
|
||||
public IConnectionManager ConnectionManager { get; } = connectionManager;
|
||||
|
||||
public async Task<Guid> CreateMission(string name, params object[] parameters)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var missionCreator = scope.ServiceProvider.GetRequiredService<ScriptMissionCreator>();
|
||||
return await missionCreator.CreateMissionAsync(name, []);
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateMission(string name) => await CreateMission(name, []);
|
||||
|
||||
public bool CancelMission(Guid missionId, string reason)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var missionManager = scope.ServiceProvider.GetRequiredService<ScriptMissionManager>();
|
||||
return missionManager.Cancel(missionId, reason);
|
||||
}
|
||||
|
||||
public bool DisableTask(string name)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var taskManager = scope.ServiceProvider.GetRequiredService<ScriptTaskManager>();
|
||||
return taskManager.Pause(name);
|
||||
}
|
||||
|
||||
public bool EnableTask(string name)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var taskManager = scope.ServiceProvider.GetRequiredService<ScriptTaskManager>();
|
||||
return taskManager.Resume(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class ScriptGlobalsRobotNetTask(Script.ILogger logger, IRobotManager robotManager, IMapManager mapManager, ScriptConnectionManager connectionManager, IServiceScopeFactory scopeFactory)
|
||||
: ScriptGlobalsRobotNet(logger, robotManager, mapManager, connectionManager, scopeFactory)
|
||||
{
|
||||
public override Guid CurrentMissionId => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public class ScriptGlobalsRobotNetMission(Guid id, Script.ILogger logger, IRobotManager robotManager, IMapManager mapManager, ScriptConnectionManager connectionManager, IServiceScopeFactory scopeFactory)
|
||||
: ScriptGlobalsRobotNet(logger, robotManager, mapManager, connectionManager, scopeFactory)
|
||||
{
|
||||
public override Guid CurrentMissionId { get; } = id;
|
||||
}
|
||||
109
RobotNet.ScriptManager/Models/ScriptMapElement.cs
Normal file
109
RobotNet.ScriptManager/Models/ScriptMapElement.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using RobotNet.MapShares;
|
||||
using RobotNet.MapShares.Dtos;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptMapElement(string mapName,
|
||||
ElementDto element,
|
||||
Func<string, string, bool, Task> UpdateIsOpenFunc,
|
||||
Func<string, string, ElementPropertyUpdateModel, Task>? UpdatePropertiesFunc) : Script.IElement
|
||||
{
|
||||
public Guid Id { get; } = element.Id;
|
||||
|
||||
public Guid ModelId { get; } = element.ModelId;
|
||||
|
||||
public string ModelName { get; } = element.ModelName ?? throw new ArgumentNullException(nameof(element), "Model name cannot be null");
|
||||
|
||||
public Guid NodeId { get; } = element.NodeId;
|
||||
|
||||
public string MapName { get; } = mapName ?? throw new ArgumentNullException(nameof(mapName), "Map name cannot be null");
|
||||
|
||||
public string Name { get; } = element.Name ?? throw new ArgumentNullException(nameof(element), "Element name cannot be null");
|
||||
|
||||
public double OffsetX { get; } = element.OffsetX;
|
||||
|
||||
public double OffsetY { get; } = element.OffsetY;
|
||||
|
||||
public string NodeName { get; } = element.NodeName ?? throw new ArgumentNullException(nameof(element), "Node name cannot be null");
|
||||
|
||||
public double X { get; } = element.X;
|
||||
|
||||
public double Y { get; } = element.Y;
|
||||
|
||||
public double Theta { get; } = element.Theta;
|
||||
|
||||
public Script.Expressions.ElementProperties Properties { get; } = MapManagerExtensions.GetElementProperties(element.IsOpen, element.Content);
|
||||
|
||||
private Script.Expressions.ElementProperties StoredProperties = MapManagerExtensions.GetElementProperties(element.IsOpen, element.Content);
|
||||
|
||||
public async Task SaveChangesAsync()
|
||||
{
|
||||
if (Properties.IsOpen != StoredProperties.IsOpen && UpdateIsOpenFunc != null)
|
||||
{
|
||||
await UpdateIsOpenFunc.Invoke(MapName, Name, Properties.IsOpen);
|
||||
StoredProperties.IsOpen = Properties.IsOpen;
|
||||
}
|
||||
|
||||
if (UpdatePropertiesFunc != null)
|
||||
{
|
||||
var changedProperties = new List<MapShares.Property.ElementProperty>();
|
||||
foreach (var prop in StoredProperties.Bool)
|
||||
{
|
||||
if (Properties.Bool.TryGetValue(prop.Key, out var value) && value != prop.Value)
|
||||
{
|
||||
changedProperties.Add(new MapShares.Property.ElementProperty()
|
||||
{
|
||||
Name = prop.Key,
|
||||
DefaultValue = value.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in StoredProperties.Double)
|
||||
{
|
||||
if (Properties.Double.TryGetValue(prop.Key, out var value) && value != prop.Value)
|
||||
{
|
||||
changedProperties.Add(new MapShares.Property.ElementProperty()
|
||||
{
|
||||
Name = prop.Key,
|
||||
DefaultValue = value.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in StoredProperties.Int)
|
||||
{
|
||||
if (Properties.Int.TryGetValue(prop.Key, out var value) && value != prop.Value)
|
||||
{
|
||||
changedProperties.Add(new MapShares.Property.ElementProperty()
|
||||
{
|
||||
Name = prop.Key,
|
||||
DefaultValue = value.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in StoredProperties.String)
|
||||
{
|
||||
if (Properties.String.TryGetValue(prop.Key, out var value) && value != prop.Value)
|
||||
{
|
||||
changedProperties.Add(new MapShares.Property.ElementProperty()
|
||||
{
|
||||
Name = prop.Key,
|
||||
DefaultValue = value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.Count != 0)
|
||||
{
|
||||
var updateModel = new ElementPropertyUpdateModel
|
||||
{
|
||||
Properties = [.. changedProperties]
|
||||
};
|
||||
await UpdatePropertiesFunc.Invoke(MapName, Name, updateModel);
|
||||
StoredProperties = Properties; // Update stored properties after saving
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
319
RobotNet.ScriptManager/Models/ScriptMapManager.cs
Normal file
319
RobotNet.ScriptManager/Models/ScriptMapManager.cs
Normal file
@@ -0,0 +1,319 @@
|
||||
using OpenIddict.Client;
|
||||
using RobotNet.MapShares.Dtos;
|
||||
using RobotNet.MapShares.Models;
|
||||
using RobotNet.Script;
|
||||
using RobotNet.Shares;
|
||||
using Serialize.Linq.Serializers;
|
||||
using System.Linq.Expressions;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptMapManager(OpenIddictClientService openIddictClient, string MapManagerUrl, string[] MapManagerScopes) : IMapManager
|
||||
{
|
||||
private string? CachedToken;
|
||||
private DateTime TokenExpiry;
|
||||
private static readonly ExpressionSerializer expressionSerializer = new(new Serialize.Linq.Serializers.JsonSerializer());
|
||||
|
||||
public Task<IElement[]> FindElements(string map, string model)
|
||||
=> FindElements(map, model, element => true);
|
||||
|
||||
public Task<IElement[]> FindElements(string map, string model, Expression<Func<Script.Expressions.ElementProperties, bool>> expr)
|
||||
=> FindElements(map, model, expr, true);
|
||||
|
||||
private async Task<IElement[]> FindElements(string map, string model, Expression<Func<Script.Expressions.ElementProperties, bool>> expr, bool retry)
|
||||
{
|
||||
var accessToken = await RequestAccessToken();
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(MapManagerUrl)
|
||||
};
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, $"api/ScriptElements");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
// Đưa modelExpression vào body của request
|
||||
request.Content = JsonContent.Create(new ElementExpressionModel
|
||||
{
|
||||
MapName = map,
|
||||
ModelName = model,
|
||||
Expression = expressionSerializer.SerializeText(expr),
|
||||
});
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
using var status = response.EnsureSuccessStatusCode();
|
||||
if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
CachedToken = null; // Clear cached token to force re-authentication
|
||||
return await FindElements(map, model, expr, false); // Retry without recall
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
|
||||
}
|
||||
}
|
||||
else if (status.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to get elements: {status.ReasonPhrase}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResult<IEnumerable<ElementDto>>>();
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize response from server");
|
||||
}
|
||||
else if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Error from server: {result.Message}");
|
||||
}
|
||||
else if (result.Data == null || !result.Data.Any())
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return [.. result.Data.Select(e => new ScriptMapElement(map, e, UpdateIsOpenAsync, UpdatePropertiesAsync))];
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IElement> GetElement(string map, string name) => GetElement(map, name, true);
|
||||
|
||||
private async Task<IElement> GetElement(string map, string name, bool retry)
|
||||
{
|
||||
if (string.IsNullOrEmpty(map) || string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ArgumentException("Map name and element name cannot be null or empty");
|
||||
}
|
||||
|
||||
var accessToken = await RequestAccessToken();
|
||||
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(MapManagerUrl)
|
||||
};
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"api/ScriptElements/{map}/element/{name}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
using var status = response.EnsureSuccessStatusCode();
|
||||
if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
CachedToken = null; // Clear cached token to force re-authentication
|
||||
return await GetElement(map, name, false); // Retry without recall
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
|
||||
}
|
||||
}
|
||||
else if (status.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to get element: {status.ReasonPhrase}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResult<ElementDto>>();
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize response from server");
|
||||
}
|
||||
else if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Error from server: {result.Message}");
|
||||
}
|
||||
else if (result.Data == null)
|
||||
{
|
||||
throw new KeyNotFoundException("Element not found in response");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ScriptMapElement(map, result.Data, UpdateIsOpenAsync, UpdatePropertiesAsync);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Task UpdateIsOpenAsync(string mapName, string elementName, bool isOpen) => UpdateIsOpenAsync(mapName, elementName, isOpen, true);
|
||||
|
||||
private async Task UpdateIsOpenAsync(string mapName, string elementName, bool isOpen, bool retry)
|
||||
{
|
||||
var accessToken = await RequestAccessToken();
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(MapManagerUrl)
|
||||
};
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Patch, $"api/ScriptElements/{mapName}/element/{elementName}/IsOpen?isOpen={isOpen}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
//request.Content = JsonContent.Create(new { isOpen });
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
using var status = response.EnsureSuccessStatusCode();
|
||||
if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
CachedToken = null; // Clear cached token to force re-authentication
|
||||
await UpdateIsOpenAsync(mapName, elementName, isOpen, false); // Retry without recall
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
|
||||
}
|
||||
}
|
||||
else if (status.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to update IsOpen: {status.ReasonPhrase}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResult>();
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize response from server");
|
||||
}
|
||||
else if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Error from server: {result.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
private Task UpdatePropertiesAsync(string mapName, string elementName, ElementPropertyUpdateModel model)
|
||||
=> UpdatePropertiesAsync(mapName, elementName, model, true);
|
||||
|
||||
private async Task UpdatePropertiesAsync(string mapName, string elementName, ElementPropertyUpdateModel model, bool retry)
|
||||
{
|
||||
var accessToken = await RequestAccessToken();
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(MapManagerUrl)
|
||||
};
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Patch, $"api/ScriptElements/{mapName}/element/{elementName}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
// Đưa modelExpression vào body của request
|
||||
request.Content = JsonContent.Create(model);
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
using var status = response.EnsureSuccessStatusCode();
|
||||
if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
CachedToken = null; // Clear cached token to force re-authentication
|
||||
await UpdatePropertiesAsync(mapName, elementName, model, false); // Retry without recall
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
|
||||
}
|
||||
}
|
||||
else if (status.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to get elements: {status.ReasonPhrase}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResult>();
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize response from server");
|
||||
}
|
||||
else if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Error from server: {result.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> RequestAccessToken()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(CachedToken) && DateTime.UtcNow < TokenExpiry)
|
||||
{
|
||||
return CachedToken;
|
||||
}
|
||||
|
||||
var result = await openIddictClient.AuthenticateWithClientCredentialsAsync(new()
|
||||
{
|
||||
Scopes = [.. MapManagerScopes],
|
||||
});
|
||||
|
||||
if (result == null || result.AccessToken == null || result.AccessTokenExpirationDate == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
TokenExpiry = result.AccessTokenExpirationDate.Value.UtcDateTime;
|
||||
CachedToken = result.AccessToken;
|
||||
return CachedToken;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<INode> GetNode(string map, string name) => GetNode(map, name, true);
|
||||
|
||||
private async Task<INode> GetNode(string map, string name, bool retry)
|
||||
{
|
||||
if (string.IsNullOrEmpty(map) || string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ArgumentException("Map name and element name cannot be null or empty");
|
||||
}
|
||||
|
||||
var accessToken = await RequestAccessToken();
|
||||
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(MapManagerUrl)
|
||||
};
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"api/ScriptElements/{map}/node/{name}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
using var status = response.EnsureSuccessStatusCode();
|
||||
if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
CachedToken = null; // Clear cached token to force re-authentication
|
||||
return await GetNode(map, name, false); // Retry without recall
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate.");
|
||||
}
|
||||
}
|
||||
else if (status.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to get element: {status.ReasonPhrase}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResult<NodeDto>>();
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize response from server");
|
||||
}
|
||||
else if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Error from server: {result.Message}");
|
||||
}
|
||||
else if (result.Data == null)
|
||||
{
|
||||
throw new KeyNotFoundException("Element not found in response");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ScriptMapNode(result.Data.Id, result.Data.MapId, result.Data.Name, result.Data.X, result.Data.Y, result.Data.Theta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
RobotNet.ScriptManager/Models/ScriptMapNode.cs
Normal file
3
RobotNet.ScriptManager/Models/ScriptMapNode.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public record ScriptMapNode(Guid Id, Guid MapId, string Name, double X, double Y, double Theta) : Script.INode;
|
||||
298
RobotNet.ScriptManager/Models/ScriptMission.cs
Normal file
298
RobotNet.ScriptManager/Models/ScriptMission.cs
Normal file
@@ -0,0 +1,298 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
4
RobotNet.ScriptManager/Models/ScriptMissionData.cs
Normal file
4
RobotNet.ScriptManager/Models/ScriptMissionData.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public record ScriptMissionParameter(string Name, Type Type, object? DefaultValue = null);
|
||||
public record ScriptMissionData(string Name, IEnumerable<ScriptMissionParameter> Parameters, string Code, string Script, bool IsMultipleRun);
|
||||
47
RobotNet.ScriptManager/Models/ScriptMissionLogger.cs
Normal file
47
RobotNet.ScriptManager/Models/ScriptMissionLogger.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.ScriptManager.Hubs;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptMissionLogger(IHubContext<ConsoleHub> consoleHub, Guid missionId) : Script.ILogger
|
||||
{
|
||||
private string log = "";
|
||||
private readonly Mutex mutexLog = new();
|
||||
public void LogError(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "missions", $"mission-{missionId}").SendAsync("MessageError", message));
|
||||
|
||||
mutexLog.WaitOne();
|
||||
log += $"E {DateTime.UtcNow} {message}{Environment.NewLine}";
|
||||
mutexLog.ReleaseMutex();
|
||||
}
|
||||
|
||||
public void LogError(Exception ex) => LogError($"{ex.GetType().FullName} {ex.Message}");
|
||||
|
||||
public void LogInfo(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "missions", $"mission-{missionId}").SendAsync("MessageInfo", message));
|
||||
|
||||
mutexLog.WaitOne();
|
||||
log += $"I {DateTime.UtcNow} {message}{Environment.NewLine}";
|
||||
mutexLog.ReleaseMutex();
|
||||
}
|
||||
|
||||
public void LogWarning(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "missions", $"mission-{missionId}").SendAsync("MessageWarning", message));
|
||||
|
||||
mutexLog.WaitOne();
|
||||
log += $"W {DateTime.UtcNow} {message}{Environment.NewLine}";
|
||||
mutexLog.ReleaseMutex();
|
||||
}
|
||||
|
||||
public string GetLog()
|
||||
{
|
||||
mutexLog.WaitOne();
|
||||
var result = log;
|
||||
log = ""; // Clear log after reading
|
||||
mutexLog.ReleaseMutex();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
185
RobotNet.ScriptManager/Models/ScriptRobotManager.cs
Normal file
185
RobotNet.ScriptManager/Models/ScriptRobotManager.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using OpenIddict.Client;
|
||||
using RobotNet.MapShares.Models;
|
||||
using RobotNet.RobotShares.Models;
|
||||
using RobotNet.Script;
|
||||
using RobotNet.Script.Expressions;
|
||||
using RobotNet.ScriptManager.Clients;
|
||||
using RobotNet.ScriptManager.Helpers;
|
||||
using RobotNet.Shares;
|
||||
using Serialize.Linq.Serializers;
|
||||
using System.Linq.Expressions;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptRobotManager(OpenIddictClientService openIddictClient, string RobotManagerUrl, string[] RobotManagerScopes) : IRobotManager
|
||||
{
|
||||
private string? CachedToken;
|
||||
private DateTime TokenExpiry;
|
||||
private static readonly ExpressionSerializer expressionSerializer = new(new Serialize.Linq.Serializers.JsonSerializer());
|
||||
|
||||
public async Task<IRobot?> GetRobotById(string robotId)
|
||||
{
|
||||
var robot = new RobotManagerHubClient(robotId, $"{RobotManagerUrl}/hubs/robot-manager", async () =>
|
||||
{
|
||||
var result = await openIddictClient.AuthenticateWithClientCredentialsAsync(new()
|
||||
{
|
||||
Scopes = [.. RobotManagerScopes],
|
||||
});
|
||||
if (result == null || result.AccessToken == null || result.AccessTokenExpirationDate == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return result.AccessToken;
|
||||
}
|
||||
});
|
||||
await robot.StartAsync();
|
||||
return robot;
|
||||
}
|
||||
|
||||
public Task<RobotState> GetRobotState(string robotId) => GetRobotState(robotId, true);
|
||||
public async Task<RobotState> GetRobotState(string robotId, bool retry)
|
||||
{
|
||||
var accessToken = await RequestAccessToken();
|
||||
if (string.IsNullOrEmpty(accessToken))
|
||||
{
|
||||
throw new ArgumentException("Failed to get access token");
|
||||
}
|
||||
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(RobotManagerUrl)
|
||||
};
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"api/RobotManager/State/{robotId}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
using var status = response.EnsureSuccessStatusCode();
|
||||
if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
// Retry with a new access token
|
||||
CachedToken = null; // Clear cached token to force a new request
|
||||
return await GetRobotState(robotId, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnauthorizedAccessException("Access token is invalid or expired.");
|
||||
}
|
||||
}
|
||||
else if (status.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to get robot state: {status.ReasonPhrase}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var state = await response.Content.ReadFromJsonAsync<MessageResult<RobotStateModel>>();
|
||||
if (state == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize robot state response");
|
||||
}
|
||||
else if (!state.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Robot Manager error: {state.Message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (state.Data == null)
|
||||
{
|
||||
throw new InvalidOperationException("Robot state data is null.");
|
||||
}
|
||||
return VDA5050ScriptHelper.ConvertToRobotState(state.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IEnumerable<string>> SearchRobots(string map, string model)
|
||||
=> SearchRobots(map, model, robot => true, true);
|
||||
|
||||
public Task<IEnumerable<string>> SearchRobots(string map, string model, Expression<Func<RobotState, bool>> expr)
|
||||
=> SearchRobots(map, model, expr, true);
|
||||
|
||||
public async Task<IEnumerable<string>> SearchRobots(string map, string model, Expression<Func<RobotState, bool>> expr, bool retry)
|
||||
{
|
||||
var accessToken = await RequestAccessToken();
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(RobotManagerUrl)
|
||||
};
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, $"api/RobotManager/Search");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
request.Content = JsonContent.Create(new ElementExpressionModel
|
||||
{
|
||||
MapName = map,
|
||||
ModelName = model,
|
||||
Expression = expressionSerializer.SerializeText(expr),
|
||||
});
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
using var status = response.EnsureSuccessStatusCode();
|
||||
if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
// Retry with a new access token
|
||||
CachedToken = null; // Clear cached token to force a new request
|
||||
return await SearchRobots(map, model, expr, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnauthorizedAccessException("Access token is invalid or expired.");
|
||||
}
|
||||
}
|
||||
else if (response.EnsureSuccessStatusCode().StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to search robots: {response.ReasonPhrase}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResult<string[]>>() ?? throw new Exception("Failed to convert result from Robot Manager");
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return result.Data ?? [];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Robot Manager error: {result.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> RequestAccessToken()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(CachedToken) && DateTime.UtcNow < TokenExpiry)
|
||||
{
|
||||
return CachedToken;
|
||||
}
|
||||
|
||||
var result = await openIddictClient.AuthenticateWithClientCredentialsAsync(new()
|
||||
{
|
||||
Scopes = [.. RobotManagerScopes],
|
||||
});
|
||||
|
||||
if (result == null || result.AccessToken == null || result.AccessTokenExpirationDate == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
TokenExpiry = result.AccessTokenExpirationDate.Value.UtcDateTime;
|
||||
CachedToken = result.AccessToken;
|
||||
return CachedToken;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
155
RobotNet.ScriptManager/Models/ScriptTask.cs
Normal file
155
RobotNet.ScriptManager/Models/ScriptTask.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using RobotNet.Script.Shares;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptTask : IDisposable
|
||||
{
|
||||
public bool Paused { get; private set; } = false;
|
||||
private readonly ScriptRunner<object> scriptRunner;
|
||||
private readonly ScriptTaskGlobals globals;
|
||||
private readonly int Interval;
|
||||
private readonly double ProcessTime;
|
||||
private CancellationTokenSource? internalCts;
|
||||
private Thread? thread;
|
||||
private bool disposed;
|
||||
private readonly string Name;
|
||||
private readonly RobotNet.Script.ILogger? Logger;
|
||||
private readonly bool AutoStart;
|
||||
|
||||
public class ScriptTaskGlobals(IDictionary<string, object?> _globals, IDictionary<string, object?> _robotnet)
|
||||
{
|
||||
public IDictionary<string, object?> globals = _globals;
|
||||
public IDictionary<string, object?> robotnet = _robotnet;
|
||||
}
|
||||
|
||||
public ScriptTask(ScriptTaskData task, ScriptOptions options, IDictionary<string, object?> _globals, IDictionary<string, object?> _robotnet)
|
||||
{
|
||||
var script = CSharpScript.Create(task.Script, options, globalsType: typeof(ScriptTaskGlobals));
|
||||
scriptRunner = script.CreateDelegate();
|
||||
|
||||
Name = task.Name;
|
||||
globals = new ScriptTaskGlobals(_globals, _robotnet);
|
||||
Interval = task.Interval;
|
||||
ProcessTime = Interval * 0.8;
|
||||
AutoStart = task.AutoStart;
|
||||
Paused = !task.AutoStart;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
public void Start(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Stop(); // Ensure previous thread is stopped before starting a new one
|
||||
|
||||
internalCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
Paused = !AutoStart;
|
||||
thread = new Thread(RunningHandler)
|
||||
{
|
||||
IsBackground = true,
|
||||
Priority = ThreadPriority.Highest,
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
Paused = true;
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
Paused = false;
|
||||
}
|
||||
|
||||
private void RunningHandler()
|
||||
{
|
||||
if (internalCts == null || internalCts.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var cts = new CancellationTokenSource();
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
int elapsed;
|
||||
int remaining;
|
||||
|
||||
internalCts.Token.Register(() =>
|
||||
{
|
||||
cts.CancelAfter(TimeSpan.FromMilliseconds(Interval * 3));
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
while (!internalCts.IsCancellationRequested)
|
||||
{
|
||||
if (Paused)
|
||||
{
|
||||
Thread.Sleep(Interval); // Sleep briefly to avoid busy waiting
|
||||
continue;
|
||||
}
|
||||
stopwatch.Restart();
|
||||
scriptRunner.Invoke(globals, cts.Token).GetAwaiter().GetResult();
|
||||
|
||||
stopwatch.Stop();
|
||||
elapsed = (int)stopwatch.ElapsedMilliseconds;
|
||||
remaining = Interval - elapsed;
|
||||
|
||||
// If execution time exceeds ProcessTime, add another cycle
|
||||
if (elapsed > ProcessTime)
|
||||
{
|
||||
remaining += Interval;
|
||||
}
|
||||
|
||||
if (remaining > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.Sleep(remaining);
|
||||
}
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
GC.Collect();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError($"Task \"{Name}\" execution failed: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (internalCts != null && !internalCts.IsCancellationRequested)
|
||||
{
|
||||
internalCts.Cancel();
|
||||
}
|
||||
|
||||
if (thread != null && thread.IsAlive)
|
||||
{
|
||||
//thread.Interrupt();
|
||||
thread.Join();
|
||||
}
|
||||
|
||||
internalCts?.Dispose();
|
||||
internalCts = null;
|
||||
thread = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed) return;
|
||||
Stop();
|
||||
disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
3
RobotNet.ScriptManager/Models/ScriptTaskData.cs
Normal file
3
RobotNet.ScriptManager/Models/ScriptTaskData.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public record ScriptTaskData(string Name, int Interval, bool AutoStart, string Code, string Script);
|
||||
22
RobotNet.ScriptManager/Models/ScriptTaskLogger.cs
Normal file
22
RobotNet.ScriptManager/Models/ScriptTaskLogger.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.ScriptManager.Hubs;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptTaskLogger(IHubContext<ConsoleHub> consoleHub, string name) : Script.ILogger
|
||||
{
|
||||
public void LogError(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "tasks", $"task-{name}").SendAsync("MessageError", message));
|
||||
}
|
||||
|
||||
public void LogInfo(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "tasks", $"task-{name}").SendAsync("MessageInfo", message));
|
||||
}
|
||||
|
||||
public void LogWarning(string message)
|
||||
{
|
||||
_ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "tasks", $"task-{name}").SendAsync("MessageWarning", message));
|
||||
}
|
||||
}
|
||||
23
RobotNet.ScriptManager/Models/ScriptVariable.cs
Normal file
23
RobotNet.ScriptManager/Models/ScriptVariable.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using RobotNet.ScriptManager.Helpers;
|
||||
|
||||
namespace RobotNet.ScriptManager.Models;
|
||||
|
||||
public class ScriptVariableSyntax(string name, Type type, object? defaultValue, bool publicRead, bool publicWrite)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public Type Type { get; } = type;
|
||||
public string TypeName { get; } = CSharpSyntaxHelper.ToTypeString(type);
|
||||
public object? DefaultValue { get; } = defaultValue;
|
||||
public bool PublicRead { get; } = publicRead;
|
||||
public bool PublicWrite { get; } = publicWrite;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj == null) return false;
|
||||
if(obj is not ScriptVariableSyntax variable) return false;
|
||||
if (variable.Name != Name) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => Name.GetHashCode();
|
||||
}
|
||||
Reference in New Issue
Block a user