first commit -push

This commit is contained in:
dungtt
2025-10-15 15:15:53 +07:00
parent 674ae395be
commit a9577c5756
885 changed files with 74595 additions and 0 deletions

View 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);
}
}

View 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);
}
}
}
}

View 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);
}
}
}

View 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();
}
}
}

View 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;
}
}

View 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);
}
}
}

View 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();
}
}
}

View 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);
});
}
}

View 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;
}
}
}