first commit -push
This commit is contained in:
36
RobotNet.ScriptManager/Services/DashboardConfig.cs
Normal file
36
RobotNet.ScriptManager/Services/DashboardConfig.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class DashboardConfig : BackgroundService
|
||||
{
|
||||
public string[] MissionNames => [.. Config.MissionNames ?? []];
|
||||
private ConfigData Config;
|
||||
private const string DataPath = "dashboardConfig.json";
|
||||
private struct ConfigData
|
||||
{
|
||||
public List<string> MissionNames { get; set; }
|
||||
}
|
||||
|
||||
public async Task UpdateMissionNames(string[] names)
|
||||
{
|
||||
Config.MissionNames = [..names];
|
||||
await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config));
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (File.Exists(DataPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
Config = JsonSerializer.Deserialize<ConfigData>(await File.ReadAllTextAsync(DataPath, CancellationToken.None));
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
else await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
115
RobotNet.ScriptManager/Services/DashboardPublisher.cs
Normal file
115
RobotNet.ScriptManager/Services/DashboardPublisher.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.Script.Shares.Dashboard;
|
||||
using RobotNet.ScriptManager.Data;
|
||||
using RobotNet.ScriptManager.Hubs;
|
||||
using System;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class DashboardPublisher(DashboardConfig Config, IServiceProvider ServiceProvider, IConfiguration Configuration, IHubContext<DashboardHub> DashboardHub, ILogger<DashboardPublisher> Logger) : BackgroundService
|
||||
{
|
||||
private readonly int UpdateTime = Configuration.GetValue<int>("Dashboard:UpdateTimeMilliSeconds", 5000);
|
||||
private readonly int CycleDate = Configuration.GetValue<int>("Dashboard:CycleDate", 7);
|
||||
|
||||
private static TaktTimeMissionDto GetTaktTime(InstanceMission[] missions, DateTime date)
|
||||
{
|
||||
TaktTimeMissionDto taktTime = new() { Label = date.ToString("dd/MM/yyyy") };
|
||||
if (missions.Length > 0)
|
||||
{
|
||||
double TaktTimeAll = 0;
|
||||
foreach (var mission in missions)
|
||||
{
|
||||
var time = mission.StopedAt - mission.CreatedAt;
|
||||
if (time.TotalMinutes > 0)
|
||||
{
|
||||
TaktTimeAll += time.TotalMinutes;
|
||||
if (time.TotalMinutes < taktTime.Min || taktTime.Min == 0) taktTime.Min = time.TotalMinutes;
|
||||
if (time.TotalMinutes > taktTime.Max || taktTime.Max == 0) taktTime.Max = time.TotalMinutes;
|
||||
}
|
||||
}
|
||||
taktTime.Average = TaktTimeAll / missions.Length;
|
||||
}
|
||||
return taktTime;
|
||||
}
|
||||
|
||||
private bool IsMissionInConfig(string missionName)
|
||||
{
|
||||
return Config.MissionNames.Length == 0 || Config.MissionNames.Contains(missionName);
|
||||
}
|
||||
|
||||
public DashboardDto GetData()
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
var MissionDb = scope.ServiceProvider.GetRequiredService<ScriptManagerDbContext>();
|
||||
List<DailyPerformanceDto> TotalMissionPerformance = [];
|
||||
List<TaktTimeMissionDto> TaktTimeMissions = [];
|
||||
var startDate = DateTime.Today.Date.AddDays(-CycleDate);
|
||||
for (var i = startDate; i <= DateTime.Today.Date; i = i.AddDays(1))
|
||||
{
|
||||
var missions = MissionDb.InstanceMissions.Where(im => im.CreatedAt.Date == i.Date).ToList();
|
||||
missions = [.. missions.Where(im => IsMissionInConfig(im.MissionName))];
|
||||
var completedMissions = missions.Where(im => im.Status == Script.Shares.MissionStatus.Completed).ToList();
|
||||
var errorMissions = missions.Where(im => im.Status == Script.Shares.MissionStatus.Error).ToList();
|
||||
TotalMissionPerformance.Add(new DailyPerformanceDto
|
||||
{
|
||||
Label = i.ToString("dd/MM/yyyy"),
|
||||
Completed = completedMissions.Count,
|
||||
Error = errorMissions.Count,
|
||||
Other = missions.Count - completedMissions.Count - errorMissions.Count,
|
||||
});
|
||||
TaktTimeMissions.Add(GetTaktTime([.. completedMissions], i));
|
||||
}
|
||||
DailyPerformanceDto TodayPerformance = TotalMissionPerformance[^1];
|
||||
DailyPerformanceDto ThisWeekPerformance = new()
|
||||
{
|
||||
Completed = TotalMissionPerformance.Sum(dp => dp.Completed),
|
||||
Error = TotalMissionPerformance.Sum(dp => dp.Error),
|
||||
Other = TotalMissionPerformance.Sum(dp => dp.Other),
|
||||
};
|
||||
|
||||
var toDayMissions = MissionDb.InstanceMissions.Where(im => im.CreatedAt.Date == DateTime.Today.Date).ToList();
|
||||
toDayMissions = [.. toDayMissions.Where(im => IsMissionInConfig(im.MissionName))];
|
||||
var toDaycompletedMissions = toDayMissions.Where(im => im.Status == Script.Shares.MissionStatus.Completed).ToList();
|
||||
var toDayerrorMissions = toDayMissions.Where(im => im.Status == Script.Shares.MissionStatus.Error).ToList();
|
||||
var toDayTaktTime = GetTaktTime([.. toDaycompletedMissions], DateTime.Today);
|
||||
|
||||
DailyMissionDto DailyMission = new()
|
||||
{
|
||||
Completed = toDaycompletedMissions.Count,
|
||||
Error = toDayerrorMissions.Count,
|
||||
Total = toDayMissions.Count,
|
||||
CompletedRate = toDayMissions.Count > 0 ? (int)((toDaycompletedMissions.Count * 100.0 / toDayMissions.Count) + 0.5) : 0,
|
||||
TaktTimeMin = toDayTaktTime.Min,
|
||||
TaktTimeAverage = toDayTaktTime.Average,
|
||||
TaktTimeMax = toDayTaktTime.Max,
|
||||
RobotOnline = 1,
|
||||
};
|
||||
|
||||
|
||||
return new()
|
||||
{
|
||||
TaktTimeMissions = [.. TaktTimeMissions],
|
||||
ThisWeekPerformance = ThisWeekPerformance,
|
||||
TodayPerformance = TodayPerformance,
|
||||
TotalMissionPerformance = [.. TotalMissionPerformance],
|
||||
DailyMission = DailyMission,
|
||||
};
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await Task.Yield();
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(DashboardHub.Clients.All.SendAsync("DashboardDataUpdated", GetData(), cancellationToken: stoppingToken), Task.Delay(UpdateTime, stoppingToken));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning("Dashboard Publisher xảy ra lỗi: {ex}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
RobotNet.ScriptManager/Services/LoopService.cs
Normal file
45
RobotNet.ScriptManager/Services/LoopService.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public abstract class LoopService(int interval) : IHostedService
|
||||
{
|
||||
private readonly TimeSpan Interval = TimeSpan.FromMilliseconds(interval);
|
||||
private readonly CancellationTokenSource cancellationTokenSource = new();
|
||||
private readonly Stopwatch stopwatch = new();
|
||||
private Timer? timer;
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await BeforExecuteAsync(cancellationToken);
|
||||
stopwatch.Start();
|
||||
timer = new Timer(Callback, cancellationTokenSource, 0, Timeout.Infinite);
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationTokenSource.Cancel();
|
||||
timer?.Dispose();
|
||||
await AfterExecuteAsync(cancellationToken);
|
||||
}
|
||||
protected virtual Task BeforExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
protected abstract void Execute(CancellationToken stoppingToken);
|
||||
protected virtual Task AfterExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
private void Callback(object? state)
|
||||
{
|
||||
if (state is not CancellationTokenSource cts || cts.IsCancellationRequested) return;
|
||||
|
||||
Execute(cts.Token);
|
||||
|
||||
if (!cts.IsCancellationRequested)
|
||||
{
|
||||
long nextTrigger = Interval.Ticks - (stopwatch.ElapsedTicks % Interval.Ticks);
|
||||
if (nextTrigger <= 0) nextTrigger = Interval.Ticks;
|
||||
|
||||
int nextIntervalMs = (int)(nextTrigger / TimeSpan.TicksPerMillisecond);
|
||||
|
||||
timer?.Change(nextIntervalMs, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
}
|
||||
80
RobotNet.ScriptManager/Services/ScriptConnectionManager.cs
Normal file
80
RobotNet.ScriptManager/Services/ScriptConnectionManager.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using RobotNet.Script;
|
||||
using RobotNet.ScriptManager.Connections;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class ScriptConnectionManager : IConnectionManager
|
||||
{
|
||||
private readonly SemaphoreSlim _connectLock = new(1, 1);
|
||||
private readonly Dictionary<string, IModbusTcpClient> ModbusTcpConnections = [];
|
||||
private readonly Dictionary<string, ICcLinkIeBasicClient> CcLinkConnections = [];
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
foreach (var client in ModbusTcpConnections.Values)
|
||||
{
|
||||
client.Dispose();
|
||||
}
|
||||
ModbusTcpConnections.Clear();
|
||||
|
||||
foreach (var client in CcLinkConnections.Values)
|
||||
{
|
||||
client.Dispose();
|
||||
}
|
||||
CcLinkConnections.Clear();
|
||||
}
|
||||
|
||||
public async Task<IModbusTcpClient> ConnectModbusTcp(string ipAddress, int port, byte unitId)
|
||||
{
|
||||
string key = $"{ipAddress}:{port}:{unitId}";
|
||||
await _connectLock.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (ModbusTcpConnections.TryGetValue(key, out var existingClient))
|
||||
{
|
||||
return existingClient;
|
||||
}
|
||||
else
|
||||
{
|
||||
var client = new ModbusTcpClient(ipAddress, port, unitId);
|
||||
|
||||
await client.ConnectAsync();
|
||||
|
||||
ModbusTcpConnections.Add(key, client);
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICcLinkIeBasicClient> ConnectCcLink(IeBasicClientOptions option)
|
||||
{
|
||||
string key = $"{option.Host}:{option.Port}:{option.NetworkNo}:{option.ModuleIoNo}:{option.MultidropNo}";
|
||||
await _connectLock.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (CcLinkConnections.TryGetValue(key, out var existingClient))
|
||||
{
|
||||
return existingClient;
|
||||
}
|
||||
else
|
||||
{
|
||||
var client = new CcLinkIeBasicClient(option);
|
||||
await client.ConnectAsync();
|
||||
|
||||
CcLinkConnections.Add(key, client);
|
||||
return client;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
231
RobotNet.ScriptManager/Services/ScriptGlobalsManager.cs
Normal file
231
RobotNet.ScriptManager/Services/ScriptGlobalsManager.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using OpenIddict.Client;
|
||||
using RobotNet.OpenIddictClient;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Hubs;
|
||||
using RobotNet.ScriptManager.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class ScriptGlobalsManager
|
||||
{
|
||||
public IDictionary<string, object?> Globals => _globals;
|
||||
private readonly ConcurrentDictionary<string, object?> _globals = new();
|
||||
private readonly Dictionary<string, Type> _globalsTypes = [];
|
||||
private readonly Dictionary<string, ScriptVariableSyntax> variables = [];
|
||||
private readonly IHubContext<ConsoleHub> consoleHubContext;
|
||||
private readonly ScriptRobotManager robotManager;
|
||||
private readonly ScriptMapManager mapManager;
|
||||
private readonly ScriptConnectionManager ConnectionManager;
|
||||
private readonly IServiceScopeFactory scopeFactory;
|
||||
|
||||
public ScriptGlobalsManager(IConfiguration configuration, OpenIddictClientService openIddictClient, IHubContext<ConsoleHub> _consoleHubContext, ScriptConnectionManager connectionManager, IServiceScopeFactory _scopeFactory)
|
||||
{
|
||||
consoleHubContext = _consoleHubContext;
|
||||
this.scopeFactory = _scopeFactory;
|
||||
ConnectionManager = connectionManager;
|
||||
|
||||
var robotManagerOptions = configuration.GetSection("RobotManager").Get<OpenIddictResourceOptions>() ?? throw new InvalidOperationException("OpenID configuration not found or invalid format.");
|
||||
robotManager = new ScriptRobotManager(openIddictClient, robotManagerOptions.Url, robotManagerOptions.Scopes);
|
||||
|
||||
var mapManagerOptions = configuration.GetSection("MapManager").Get<OpenIddictResourceOptions>() ?? throw new InvalidOperationException("OpenID configuration not found or invalid format.");
|
||||
mapManager = new ScriptMapManager(openIddictClient, mapManagerOptions.Url, mapManagerOptions.Scopes);
|
||||
}
|
||||
|
||||
public void LoadVariables(IEnumerable<ScriptVariableSyntax> variableSynctaxs)
|
||||
{
|
||||
_globals.Clear();
|
||||
_globalsTypes.Clear();
|
||||
variables.Clear();
|
||||
foreach (var v in variableSynctaxs)
|
||||
{
|
||||
variables.Add(v.Name, new(v.Name, v.Type, v.DefaultValue, v.PublicRead, v.PublicWrite));
|
||||
_globals.TryAdd(v.Name, v.DefaultValue);
|
||||
_globalsTypes.TryAdd(v.Name, v.Type);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ScriptVariableDto> GetVariablesData()
|
||||
{
|
||||
var vars = new List<ScriptVariableDto>();
|
||||
foreach (var v in variables.Values)
|
||||
{
|
||||
if (v.PublicRead)
|
||||
{
|
||||
vars.Add(new ScriptVariableDto(v.Name, v.TypeName, _globals[v.Name]?.ToString() ?? "null"));
|
||||
}
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
public Type? GetVariableType(string name)
|
||||
{
|
||||
if (_globalsTypes.TryGetValue(name, out var type))
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> GetRobotNetTask(string name)
|
||||
{
|
||||
var globalRobotNet = new ScriptGlobalsRobotNetTask(new ScriptTaskLogger(consoleHubContext, name), robotManager, mapManager, ConnectionManager, scopeFactory);
|
||||
|
||||
return ConvertGlobalsToDictionary(globalRobotNet);
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> GetRobotNetMission(Guid missionId)
|
||||
{
|
||||
var globalRobotNet = new ScriptGlobalsRobotNetMission(missionId, new ScriptMissionLogger(consoleHubContext, missionId), robotManager, mapManager, ConnectionManager, scopeFactory);
|
||||
|
||||
return ConvertGlobalsToDictionary(globalRobotNet);
|
||||
}
|
||||
|
||||
public void SetValue(string name, string value)
|
||||
{
|
||||
if (variables.TryGetValue(name, out var variable))
|
||||
{
|
||||
if (variable.PublicWrite)
|
||||
{
|
||||
if (_globalsTypes.TryGetValue(name, out var type))
|
||||
{
|
||||
try
|
||||
{
|
||||
var convertedValue = Convert.ChangeType(value, type);
|
||||
_globals[name] = convertedValue;
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot convert value '{value}' to type '{type.FullName}' for variable '{name}'.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new KeyNotFoundException($"Variable type for '{name}' not found.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Variable '{name}' is not writable.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new KeyNotFoundException($"Variable '{name}' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetValue(string name)
|
||||
{
|
||||
if (variables.TryGetValue(name, out var variable))
|
||||
{
|
||||
if (_globalsTypes.TryGetValue(name, out var type))
|
||||
{
|
||||
_globals[name] = Convert.ChangeType(variable.DefaultValue, type);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new KeyNotFoundException($"Variable type for '{name}' not found.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new KeyNotFoundException($"Variable '{name}' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> ConvertGlobalsToDictionary(IRobotNetGlobals globals)
|
||||
{
|
||||
var robotnet = new Dictionary<string, object?>();
|
||||
var type = typeof(IRobotNetGlobals);
|
||||
|
||||
|
||||
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
robotnet.TryAdd(field.Name, field.GetValue(globals));
|
||||
}
|
||||
|
||||
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
if (prop.GetIndexParameters().Length == 0)
|
||||
{
|
||||
// Forward getter if available
|
||||
if (prop.GetGetMethod() != null)
|
||||
{
|
||||
var getter = Delegate.CreateDelegate(
|
||||
Expression.GetDelegateType([prop.PropertyType]),
|
||||
globals,
|
||||
prop.GetGetMethod()!
|
||||
);
|
||||
robotnet.TryAdd($"get_{prop.Name}", getter);
|
||||
}
|
||||
// Forward setter if available
|
||||
if (prop.GetSetMethod() != null)
|
||||
{
|
||||
var setter = Delegate.CreateDelegate(
|
||||
Expression.GetDelegateType([prop.PropertyType, typeof(void)]),
|
||||
globals,
|
||||
prop.GetSetMethod()!
|
||||
);
|
||||
robotnet.TryAdd($"set_{prop.Name}", setter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle indexers (properties with parameters)
|
||||
var indexParams = prop.GetIndexParameters().Select(p => p.ParameterType).ToArray();
|
||||
|
||||
// Forward indexer getter if available
|
||||
if (prop.GetGetMethod() != null)
|
||||
{
|
||||
var getterParamTypes = indexParams.Concat([prop.PropertyType]).ToArray();
|
||||
var getterDelegate = Delegate.CreateDelegate(
|
||||
Expression.GetDelegateType(getterParamTypes),
|
||||
globals,
|
||||
prop.GetGetMethod()!
|
||||
);
|
||||
robotnet.TryAdd($"get_{prop.Name}_indexer", getterDelegate);
|
||||
}
|
||||
|
||||
// Forward indexer setter if available
|
||||
if (prop.GetSetMethod() != null)
|
||||
{
|
||||
var setterParamTypes = indexParams.Concat([prop.PropertyType, typeof(void)]).ToArray();
|
||||
var setterDelegate = Delegate.CreateDelegate(
|
||||
Expression.GetDelegateType(setterParamTypes),
|
||||
globals,
|
||||
prop.GetSetMethod()!
|
||||
);
|
||||
robotnet.TryAdd($"set_{prop.Name}_indexer", setterDelegate);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
||||
{
|
||||
if (!method.IsSpecialName)
|
||||
{
|
||||
var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.FullName} {p.Name}"));
|
||||
var args = string.Join(", ", method.GetParameters().Select(p => p.Name));
|
||||
var returnType = method.ReturnType == typeof(void) ? "void" : method.ReturnType.FullName;
|
||||
|
||||
var paramTypes = string.Join(",", method.GetParameters().Select(p => p.ParameterType.FullName));
|
||||
var methodKey = $"{method.Name}({paramTypes})";
|
||||
|
||||
var del = Delegate.CreateDelegate(
|
||||
Expression.GetDelegateType([.. method.GetParameters().Select(p => p.ParameterType), method.ReturnType]),
|
||||
globals,
|
||||
method
|
||||
);
|
||||
// TODO: tôi muốn thay vì sử dụng tên phương thức, tôi muốn sử dụng tên phương thức với danh sánh kiểu dữ liệu của các tham số trong dấu ngoặc tròn
|
||||
robotnet.TryAdd(methodKey, del);
|
||||
}
|
||||
}
|
||||
return robotnet;
|
||||
}
|
||||
}
|
||||
63
RobotNet.ScriptManager/Services/ScriptMissionCreator.cs
Normal file
63
RobotNet.ScriptManager/Services/ScriptMissionCreator.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Data;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class ScriptMissionCreator(ScriptMissionManager missionManager, ScriptManagerDbContext scriptManagerDb)
|
||||
{
|
||||
public async Task<Guid> CreateMissionAsync(string name, IDictionary<string, string> parameters)
|
||||
{
|
||||
if (!missionManager.ContainsMissionName(name))
|
||||
throw new Exception($"Mission {name} không tồn tại");
|
||||
|
||||
var entry = scriptManagerDb.InstanceMissions.Add(new InstanceMission
|
||||
{
|
||||
MissionName = name,
|
||||
Parameters = System.Text.Json.JsonSerializer.Serialize(parameters),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Status = MissionStatus.Idle,
|
||||
});
|
||||
|
||||
await scriptManagerDb.SaveChangesAsync();
|
||||
|
||||
try
|
||||
{
|
||||
missionManager.Create(entry.Entity.Id, name, parameters);
|
||||
return entry.Entity.Id;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scriptManagerDb.InstanceMissions.Remove(entry.Entity);
|
||||
await scriptManagerDb.SaveChangesAsync();
|
||||
throw new Exception($"Failed to create mission: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateMissionAsync(string name, object[] parameters)
|
||||
{
|
||||
if (!missionManager.ContainsMissionName(name))
|
||||
throw new Exception($"Mission {name} không tồn tại");
|
||||
|
||||
var entry = scriptManagerDb.InstanceMissions.Add(new InstanceMission
|
||||
{
|
||||
MissionName = name,
|
||||
Parameters = System.Text.Json.JsonSerializer.Serialize(parameters),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Status = MissionStatus.Idle,
|
||||
});
|
||||
|
||||
await scriptManagerDb.SaveChangesAsync();
|
||||
|
||||
try
|
||||
{
|
||||
missionManager.Create(entry.Entity.Id, name, parameters);
|
||||
return entry.Entity.Id;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scriptManagerDb.InstanceMissions.Remove(entry.Entity);
|
||||
await scriptManagerDb.SaveChangesAsync();
|
||||
throw new Exception($"Failed to create mission: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
410
RobotNet.ScriptManager/Services/ScriptMissionManager.cs
Normal file
410
RobotNet.ScriptManager/Services/ScriptMissionManager.cs
Normal file
@@ -0,0 +1,410 @@
|
||||
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<string, ScriptMissionData> MissionDatas = [];
|
||||
private readonly Dictionary<string, ScriptRunner<IAsyncEnumerable<MissionState>>> Runners = [];
|
||||
private readonly ConcurrentDictionary<Guid, ScriptMission> Missions = [];
|
||||
private readonly ConcurrentQueue<ScriptMission> idleMissions = [];
|
||||
private readonly ConcurrentQueue<ScriptMission> 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<ScriptMissionData> 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<IAsyncEnumerable<MissionState>>(mission.Script, ScriptConfiguration.ScriptOptions, globalsType: typeof(ScriptMissionGlobals));
|
||||
Runners.Add(mission.Name, script.CreateDelegate());
|
||||
}
|
||||
State = ProcessorState.Ready;
|
||||
}
|
||||
|
||||
public IEnumerable<ScriptMissionDto> 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<string, string> 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<string, object?>();
|
||||
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<string, object?>();
|
||||
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<string, object?> 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<WaitHandle>();
|
||||
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<ScriptManagerDbContext>();
|
||||
|
||||
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<ScriptManagerDbContext>();
|
||||
|
||||
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<ScriptManagerDbContext>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
298
RobotNet.ScriptManager/Services/ScriptStateManager.cs
Normal file
298
RobotNet.ScriptManager/Services/ScriptStateManager.cs
Normal file
@@ -0,0 +1,298 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Helpers;
|
||||
using RobotNet.ScriptManager.Hubs;
|
||||
using RobotNet.ScriptManager.Models;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class ScriptStateManager(ScriptTaskManager taskManager,
|
||||
ScriptMissionManager missionManager,
|
||||
ScriptGlobalsManager globalsManager,
|
||||
ScriptConnectionManager connectionManager,
|
||||
IHubContext<ProcessorHub> processorHubContext,
|
||||
IHubContext<HMIHub> hmiHubContext,
|
||||
IHubContext<ConsoleHub> consoleHubContext,
|
||||
ILogger<ScriptStateManager> logger) : LoopService(500)
|
||||
{
|
||||
public string StateMesssage { get; private set; } = "";
|
||||
public ProcessorState State { get; private set; } = ProcessorState.Idle;
|
||||
public ProcessorRequest Request { get; private set; } = ProcessorRequest.None;
|
||||
private readonly ConsoleLog consoleLog = new(consoleHubContext, logger);
|
||||
|
||||
private readonly Mutex mutex = new();
|
||||
private CancellationTokenSource? runningCancellation;
|
||||
|
||||
public bool Build(ref string message)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if (mutex.WaitOne(1000))
|
||||
{
|
||||
if (Request != ProcessorRequest.None)
|
||||
{
|
||||
message = $"Không thể thực hiện build vì Processor đang thực hiện {Request}";
|
||||
}
|
||||
else if (State == ProcessorState.Running)
|
||||
{
|
||||
message = "Không thể thực hiện build vì Processor đang Running}";
|
||||
}
|
||||
else
|
||||
{
|
||||
result = true;
|
||||
SetRequest(ProcessorRequest.Build);
|
||||
}
|
||||
|
||||
mutex.ReleaseMutex();
|
||||
}
|
||||
else
|
||||
{
|
||||
message = "Không thể thực hiện build vì request timeout";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool Run(ref string message)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if (mutex.WaitOne(1000))
|
||||
{
|
||||
if (Request != ProcessorRequest.None)
|
||||
{
|
||||
message = $"Không thể thực hiện run vì Processor đang thực hiện {Request}";
|
||||
}
|
||||
else if (State != ProcessorState.Ready)
|
||||
{
|
||||
message = $"Không thể thực hiện run vì Processor đang ở trạng thái {State}, không phải Ready";
|
||||
}
|
||||
else
|
||||
{
|
||||
result = true;
|
||||
SetRequest(ProcessorRequest.Run);
|
||||
}
|
||||
|
||||
mutex.ReleaseMutex();
|
||||
}
|
||||
else
|
||||
{
|
||||
message = "Không thể thực hiện run vì request timeout";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool Stop(ref string message)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if (mutex.WaitOne(1000))
|
||||
{
|
||||
if (Request != ProcessorRequest.None)
|
||||
{
|
||||
message = $"Không thể thực hiện stop vì Processor đang thực hiện {Request}";
|
||||
}
|
||||
else if (State != ProcessorState.Running)
|
||||
{
|
||||
message = $"Không thể thực hiện stop vì Processor đang ở trạng thái {State}, không phải Running";
|
||||
}
|
||||
else
|
||||
{
|
||||
result = true;
|
||||
SetRequest(ProcessorRequest.Stop);
|
||||
}
|
||||
|
||||
mutex.ReleaseMutex();
|
||||
}
|
||||
else
|
||||
{
|
||||
message = "Không thể thực hiện stop vì request timeout";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool Reset(ref string message)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if (mutex.WaitOne(1000))
|
||||
{
|
||||
if (Request != ProcessorRequest.None)
|
||||
{
|
||||
message = $"Không thể thực hiện reset vì Processor đang thực hiện {Request}";
|
||||
}
|
||||
else if (State != ProcessorState.Ready && State != ProcessorState.Error && State != ProcessorState.BuildError && State != ProcessorState.Idle)
|
||||
{
|
||||
message = $"Không thể thực hiện reset vì Processor đang ở trạng thái {State}, không phải Ready hoặc Error hoặc BuildError";
|
||||
}
|
||||
else
|
||||
{
|
||||
result = true;
|
||||
SetRequest(ProcessorRequest.Reset);
|
||||
}
|
||||
|
||||
mutex.ReleaseMutex();
|
||||
}
|
||||
else
|
||||
{
|
||||
message = "Không thể thực hiện reset vì request timeout";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void SetRequest(ProcessorRequest request)
|
||||
{
|
||||
if (Request == request) return;
|
||||
|
||||
Request = request;
|
||||
_ = Task.Factory.StartNew(async () =>
|
||||
{
|
||||
await processorHubContext.Clients.All.SendAsync("RequestChanged", Request);
|
||||
await hmiHubContext.Clients.All.SendAsync("RequestChanged", Request);
|
||||
});
|
||||
}
|
||||
|
||||
private void SetState(ProcessorState state)
|
||||
{
|
||||
if (State == state) return;
|
||||
|
||||
State = state;
|
||||
_ = Task.Factory.StartNew(async () =>
|
||||
{
|
||||
await processorHubContext.Clients.All.SendAsync("StateChanged", State);
|
||||
await hmiHubContext.Clients.All.SendAsync("StateChanged", State);
|
||||
});
|
||||
}
|
||||
|
||||
protected override async Task BeforExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
missionManager.ResetMissionDb();
|
||||
SetRequest(ProcessorRequest.Build);
|
||||
await base.BeforExecuteAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override void Execute(CancellationToken stoppingToken)
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
case ProcessorState.Idle:
|
||||
case ProcessorState.BuildError:
|
||||
case ProcessorState.Error:
|
||||
if (Request == ProcessorRequest.Build)
|
||||
{
|
||||
SetState(ProcessorState.Building);
|
||||
SetRequest(ProcessorRequest.None);
|
||||
BuildHandler();
|
||||
}
|
||||
else if (Request == ProcessorRequest.Reset)
|
||||
{
|
||||
missionManager.Reset();
|
||||
taskManager.Reset();
|
||||
connectionManager.Reset();
|
||||
SetState(ProcessorState.Idle);
|
||||
SetRequest(ProcessorRequest.None);
|
||||
}
|
||||
break;
|
||||
case ProcessorState.Ready:
|
||||
if (Request == ProcessorRequest.Build)
|
||||
{
|
||||
SetState(ProcessorState.Building);
|
||||
SetRequest(ProcessorRequest.None);
|
||||
BuildHandler();
|
||||
}
|
||||
else if (Request == ProcessorRequest.Run)
|
||||
{
|
||||
SetState(ProcessorState.Running);
|
||||
SetRequest(ProcessorRequest.None);
|
||||
RunHandler();
|
||||
}
|
||||
else if (Request == ProcessorRequest.Reset)
|
||||
{
|
||||
missionManager.Reset();
|
||||
taskManager.Reset();
|
||||
connectionManager.Reset();
|
||||
SetState(ProcessorState.Idle);
|
||||
SetRequest(ProcessorRequest.None);
|
||||
}
|
||||
break;
|
||||
case ProcessorState.Running:
|
||||
if (Request == ProcessorRequest.Stop)
|
||||
{
|
||||
connectionManager.Reset();
|
||||
SetState(ProcessorState.Stopping);
|
||||
SetRequest(ProcessorRequest.None);
|
||||
StopHandler();
|
||||
}
|
||||
break;
|
||||
case ProcessorState.Building:
|
||||
case ProcessorState.Stopping:
|
||||
case ProcessorState.Starting:
|
||||
default:
|
||||
SetRequest(ProcessorRequest.None);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildHandler()
|
||||
{
|
||||
_ = Task.Factory.StartNew(() =>
|
||||
{
|
||||
consoleLog.LogInfo($"Start build all scripts");
|
||||
var watch = new Stopwatch();
|
||||
watch.Start();
|
||||
var code = ScriptConfiguration.GetScriptCode();
|
||||
|
||||
string error = string.Empty;
|
||||
try
|
||||
{
|
||||
if (CSharpSyntaxHelper.VerifyScript(code, out error, out var variables, out var tasks, out var missions))
|
||||
{
|
||||
globalsManager.LoadVariables(variables);
|
||||
taskManager.LoadTasks(tasks);
|
||||
missionManager.LoadMissions(missions);
|
||||
watch.Stop();
|
||||
consoleLog.LogInfo($"Build all scripts successfully in {watch.ElapsedMilliseconds} ms");
|
||||
SetState(ProcessorState.Ready);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.ToString();
|
||||
}
|
||||
|
||||
watch.Stop();
|
||||
SetState(ProcessorState.BuildError);
|
||||
consoleLog.LogError(error);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private void RunHandler()
|
||||
{
|
||||
_ = Task.Factory.StartNew(() =>
|
||||
{
|
||||
runningCancellation = new();
|
||||
taskManager.StartAll(runningCancellation.Token);
|
||||
missionManager.Start(runningCancellation.Token);
|
||||
SetState(ProcessorState.Running);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private void StopHandler()
|
||||
{
|
||||
_ = Task.Factory.StartNew(() =>
|
||||
{
|
||||
runningCancellation?.Cancel();
|
||||
taskManager.StopAll();
|
||||
missionManager.Stop();
|
||||
SetState(ProcessorState.Ready);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
91
RobotNet.ScriptManager/Services/ScriptTaskManager.cs
Normal file
91
RobotNet.ScriptManager/Services/ScriptTaskManager.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet.Script.Shares;
|
||||
using RobotNet.ScriptManager.Helpers;
|
||||
using RobotNet.ScriptManager.Hubs;
|
||||
using RobotNet.ScriptManager.Models;
|
||||
|
||||
namespace RobotNet.ScriptManager.Services;
|
||||
|
||||
public class ScriptTaskManager(ScriptGlobalsManager globalsManager, IHubContext<ScriptTaskHub> scriptHub)
|
||||
{
|
||||
private readonly List<ScriptTaskData> ScriptTaskDatas = [];
|
||||
private readonly Dictionary<string, ScriptTask> ScriptTasks = [];
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
ScriptTaskDatas.Clear();
|
||||
foreach (var task in ScriptTasks.Values)
|
||||
{
|
||||
task.Dispose();
|
||||
}
|
||||
ScriptTasks.Clear();
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
public void LoadTasks(IEnumerable<ScriptTaskData> tasks)
|
||||
{
|
||||
Reset();
|
||||
ScriptTaskDatas.AddRange(tasks);
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
ScriptTasks.Add(task.Name, new ScriptTask(task, ScriptConfiguration.ScriptOptions, globalsManager.Globals, globalsManager.GetRobotNetTask(task.Name)));
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ScriptTaskDto> GetTaskDatas() => [.. ScriptTaskDatas.Select(t => new ScriptTaskDto(t.Name, t.Interval, t.Code))];
|
||||
|
||||
public void StartAll(CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (var task in ScriptTasks.Values)
|
||||
{
|
||||
task.Start(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public void StopAll()
|
||||
{
|
||||
foreach (var task in ScriptTasks.Values)
|
||||
{
|
||||
task.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
public IDictionary<string, bool> GetTaskStates()
|
||||
{
|
||||
return ScriptTasks.ToDictionary(task => task.Key, task => !task.Value.Paused);
|
||||
}
|
||||
|
||||
public bool Pause(string name)
|
||||
{
|
||||
if (ScriptTasks.TryGetValue(name, out var task))
|
||||
{
|
||||
task.Pause();
|
||||
_ = Task.Factory.StartNew(async Task () =>
|
||||
{
|
||||
await scriptHub.Clients.All.SendAsync("TaskPaused", name);
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Resume(string name)
|
||||
{
|
||||
if(ScriptTasks.TryGetValue(name, out var task))
|
||||
{
|
||||
task.Resume();
|
||||
_ = Task.Factory.StartNew(async Task () =>
|
||||
{
|
||||
await scriptHub.Clients.All.SendAsync("TaskResumed", name);
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user