diff --git a/MP.TaskMan/MP.TaskMan.csproj b/MP.TaskMan/MP.TaskMan.csproj index 6f2a7fe4..3125651f 100644 --- a/MP.TaskMan/MP.TaskMan.csproj +++ b/MP.TaskMan/MP.TaskMan.csproj @@ -7,12 +7,10 @@ - - @@ -23,6 +21,7 @@ + @@ -30,8 +29,6 @@ - - diff --git a/MP.TaskMan/Models/TaskExecModel.cs b/MP.TaskMan/Models/TaskExecModel.cs new file mode 100644 index 00000000..8a1c9332 --- /dev/null +++ b/MP.TaskMan/Models/TaskExecModel.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +#nullable disable +// +// This is here so CodeMaid doesn't reorganize this document +// +namespace MP.TaskMan.Models +{ + [Table("TaskExec")] + public partial class TaskExecModel + { + #region Public Properties + + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int TaskExecId { get; set; } = 0; + + /// + /// task Id di riferimento + /// + public int TaskId { get; set; } = 0; + + /// + /// DataOra inizio + /// + public DateTime DtStart { get; set; } = DateTime.Now; + + /// + /// DataOra fine + /// + public DateTime DtEnd { get; set; } = DateTime.Now.AddDays(-1); + + /// + /// Durata ultima esecuzione in secondi + /// + [NotMapped] + public double Duration + { + get => DtEnd.Subtract(DtStart).TotalSeconds; + } + + /// + /// Esito in Errore + /// + public bool IsError { get; set; } = false; + + /// + /// Ultimo risultato registrato + /// + public string Result { get; set; } = ""; + + /// + /// Navigazione oggetto TaskList + /// + [ForeignKey("TaskId")] + public virtual TaskListModel TaskListNav { get; set; } = null!; + + + #endregion Public Properties + } +} \ No newline at end of file diff --git a/MP.TaskMan/Models/TaskListModel.cs b/MP.TaskMan/Models/TaskListModel.cs new file mode 100644 index 00000000..950ef5ad --- /dev/null +++ b/MP.TaskMan/Models/TaskListModel.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using static MP.TaskMan.Objects.Enums; + +#nullable disable +// +// This is here so CodeMaid doesn't reorganize this document +// +namespace MP.TaskMan.Models +{ + [Table("TaskList")] + public partial class TaskListModel + { + #region Public Properties + + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int TaskId { get; set; } = 0; + + /// + /// Ordinale x esecuzione + /// + public int Ordinal { get; set; } = 0; + + /// + /// Nome Task + /// + public string Name { get; set; } = ""; + + /// + /// Descrizione Task + /// + public string Descript { get; set; } = ""; + + /// + /// Tipo Task + /// + public Task2ExeType TType { get; set; } = Task2ExeType.ND; + + /// + /// Comando da invocare + /// + public string Command { get; set; } = ""; + + /// + /// Elenco argomenti (json) + /// + public string Args { get; set; } = ""; + + /// + /// Frequenza esecuzione da enum + /// + public TaskFreqType Freq { get; set; } = TaskFreqType.ND; + + /// + /// Cadenza esecuzione + /// + public int Cad { get; set; } = 1; + + /// + /// DataOra ultima esecuzione + /// + public DateTime DtLastExec { get; set; } = DateTime.Today.AddYears(-10); + /// + /// DataOra ultima esecuzione + /// + public DateTime DtNextExec { get; set; } = DateTime.Today.AddYears(-9); + + /// + /// Durata ultima esecuzione in secondi + /// + public double LastDuration { get; set; } = 0; + + /// + /// Esito ultima esecuzione in Errore + /// + public bool LastIsError { get; set; } = false; + + /// + /// Ultimo risultato registrato + /// + public string LastResult { get; set; } = ""; + + #endregion Public Properties + } +} \ No newline at end of file diff --git a/MP.TaskMan/Models/TaskResultModel.cs b/MP.TaskMan/Models/TaskResultModel.cs new file mode 100644 index 00000000..e1b929c7 --- /dev/null +++ b/MP.TaskMan/Models/TaskResultModel.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using static MP.TaskMan.Objects.Enums; + +#nullable disable +// +// This is here so CodeMaid doesn't reorganize this document +// +namespace MP.TaskMan.Models +{ + [NotMapped] + public partial class TaskResultModel + { + #region Public Properties + + /// + /// Tipo di task eseguito + /// + public string Task { get; set; } = ""; + + /// + /// Risultato: >0 = successo, <0 = errore + /// + public int ExecResult { get; set; } = 0; + + /// + /// Risultato esecuzione testuale + /// + public string TextResult { get; set; } = ""; + + + #endregion Public Properties + } +} \ No newline at end of file diff --git a/MP.TaskMan/Objects/Enums.cs b/MP.TaskMan/Objects/Enums.cs new file mode 100644 index 00000000..6173e302 --- /dev/null +++ b/MP.TaskMan/Objects/Enums.cs @@ -0,0 +1,216 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace MP.TaskMan.Objects +{ + public class Enums + { + #region Public Enums + + /// + /// Intervallo dati (es per definizione quanti dati FluxLog tenere x intervallo + /// + public enum DataInterval + { + minute, + hour, + day + } + + public enum DataItemCategory + { + CONDITION = 0, + EVENT = 1, + SAMPLE = 2 + } + + /// + /// Tipo di esito (generico) + /// + public enum esitoExec + { + undone, + ok, + error + } + + //[JsonConverter(typeof(StringEnumConverter))] + public enum Task2ExeType + { + /// + /// Tipo indefinito / ALL + /// + ND, + + /// + /// Chiamata exe esterno + /// + Exe, + + /// + /// Chiamata a SQL Command + /// + SqlCommand, + + /// + /// Chiamata a SQL Stored Procedure + /// + SqlStored, + + /// + /// Chiamata REST tipo Get + /// + RestCallGet, + + ///// + ///// Chiamata REST tipo Post + ///// + //RestCallPost + } + + //[JsonConverter(typeof(StringEnumConverter))] + public enum TaskFreqType + { + /// + /// Tipo indefinito / ALL + /// + ND, + + /// + /// Secondi + /// + Sec, + + /// + /// Minuti + /// + Min, + + /// + /// Ore + /// + Hour, + + /// + /// Giorni + /// + Day, + + /// + /// Settimane + /// + Week, + + /// + /// Mesi + /// + Month, + + /// + /// Anni + /// + Year + } + + /// + /// Elenco task ammessi (x IOB-WIN da eseguire...) + /// + public enum taskType + { + /// + /// Task nullo / fake + /// + nihil, + + /// + /// Rimanda a PLC eventuale segnale NON in setup (MA NON RESETTA) + /// + fixStopSetup, + + /// + /// Indica al PLC di forzare il reset del contapezzi + /// + forceResetPzCount, + + /// + /// Indica al PLC di forzare il NUOVO valore di contapezzi (impostato come value) + /// + forceSetPzCount, + + /// + /// Imposta Articolo su PLC + /// + setArt, + + /// + /// Imposta Commessa su PLC + /// + setComm, + + /// + /// Set di un PARAMETRO su PLC (in value avremo un JSON object) + /// + setParameter, + + /// + /// Set Programma CNC su PLC + /// + setProg, + + /// + /// Indica al PLC di impostare il numero di pezzi da produrre per la commessa (impostato + /// come value) + /// + setPzComm, + + /// + /// Indica al PLC iniziato setup (e secondo casi ferma contapezzi /resetta) + /// + startSetup, + + /// + /// Indica al PLC finito setup (e secondo casi ferma contapezzi /resetta) + /// + stopSetup, + + /// + /// Richiesta invio watchdog a PLC + /// + sendWatchDogMes2Plc, + + /// + /// Indica che è FINITA la produzione (e quindi cancello dati backup) + /// + endProd, + + /// + /// Richiesta esecuzione di un sync dei dati DB di frontiera + /// + syncDbData, + + /// + /// Imposta Fornitore (es grower x ICOEL) + /// + setSupplier, + + /// + /// Effettua processing other info (es ritorno consumi x ricette FIMAT) + /// + processOtherInfo + } + + /// + /// Finestra temporale di aggregazione dati VC + /// + public enum timeWindow + { + free, + hour, + day, + week, + month + } + + #endregion Public Enums + } +} \ No newline at end of file diff --git a/MP.TaskMan/Services/BaseServ.cs b/MP.TaskMan/Services/BaseServ.cs new file mode 100644 index 00000000..f4a2660f --- /dev/null +++ b/MP.TaskMan/Services/BaseServ.cs @@ -0,0 +1,70 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MP.TaskMan.Services +{ + /// + /// Classe di partenza x costruzione servizi di accesso dati + cache + /// + public class BaseServ + { + #region Protected Properties + + /// + /// Durata cache breve (1 min circa + perturbazione percentuale +/-10%) + /// + protected TimeSpan FastCache + { + get => TimeSpan.FromSeconds(cacheTtlShort * rnd.Next(900, 1100) / 1000); + } + + /// + /// Durata cache lunga (+ perturbazione percentuale +/-10%) + /// + protected TimeSpan LongCache + { + get => TimeSpan.FromSeconds(cacheTtlLong * rnd.Next(900, 1100) / 1000); + } + + /// + /// Durata cache MOLTO breve (10 sec circa + perturbazione percentuale +/-10%) + /// + protected TimeSpan UltraFastCache + { + get => TimeSpan.FromSeconds(cacheTtlShort / 6 * rnd.Next(900, 1100) / 1000); + } + + /// + /// Durata cache MOLTO lunga (+ perturbazione percentuale +/-10%) + /// + protected TimeSpan UltraLongCache + { + get => TimeSpan.FromSeconds(cacheTtlLong * 10 * rnd.Next(900, 1100) / 1000); + } + + #endregion Protected Properties + + + protected static IConfiguration _configuration = null!; + + #region Private Fields + + /// + /// Durata cache lunga IN SECONDI + /// + private int cacheTtlLong = 60 * 5; + + /// + /// Durata cache breve IN SECONDI + /// + private int cacheTtlShort = 60 * 1; + + private Random rnd = new Random(); + + #endregion Private Fields + } +} \ No newline at end of file diff --git a/MP.TaskMan/Services/TaskService.cs b/MP.TaskMan/Services/TaskService.cs new file mode 100644 index 00000000..0d252495 --- /dev/null +++ b/MP.TaskMan/Services/TaskService.cs @@ -0,0 +1,370 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using MP.AppAuth.Services; +using MP.TaskMan.Controllers; +using MP.TaskMan.DatabaseModels; +using Newtonsoft.Json; +using NLog; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static MP.TaskMan.Objects.Enums; + +namespace MP.TaskMan.Services +{ + public class TaskService : BaseServ, IDisposable + { + #region Public Constructors + + /// + /// Init servizio TAB + /// + /// + public TaskService(IConfiguration configuration) + { + _configuration = configuration; + + // setup compoenti REDIS + redisConn = ConnectionMultiplexer.Connect(_configuration.GetConnectionString("Redis")); + redisDb = redisConn.GetDatabase(); + + // conf DB + ConnStr = _configuration.GetConnectionString("MP.All"); + if (string.IsNullOrEmpty(ConnStr)) + { + Log.Error("ConnString empty!"); + } + else + { + StringBuilder sb = new StringBuilder(); + MLController = new MpLandController(configuration); + sb.AppendLine($"TaskService | MpLandController OK"); + Log.Info(sb.ToString()); + // sistemo i parametri x redHas... + CodModulo = _configuration.GetValue("ServerConf:CodModulo"); + var cstringArray = ConnStr.Split(";"); + foreach (var item in cstringArray) + { + var cData = item.Trim().Split("="); + if (cData.Length == 2) + { + if (!connStrParams.ContainsKey(cData[0])) + { + connStrParams.Add(cData[0], cData[1]); + } + } + } + // sistemo + DataSource = connStrParams["Server"]; + DataBase = connStrParams["Database"]; + } + + // conf rest call service + RCallService = new RestCallService(_configuration); + } + + #endregion Public Constructors + + #region Public Events + + /// + /// Evento richiesta rilettura dati pagina (x refresh pagine aperte) + /// + public event EventHandler ReloadRequest = delegate { }; + + #endregion Public Events + + #region Public Methods + + public void Dispose() + { + // Clear database controller + MLController.Dispose(); + // redis dispose + redisConn = null; + redisDb = null; + } + + /// + /// Chiamata esecuzione di un singolo task programmato + /// + /// Task richiesto + /// Se true rischedula successiva chiamata + /// + public async Task ExecuteTask(TaskListModel TaskRec, bool SchedNext) + { + TaskResultModel answ = new TaskResultModel() + { + Task = $"TaskId: {TaskRec.TaskId} | {TaskRec.TType}", + ExecResult = -1, + TextResult = "Task Not recognized" + }; + // verifico tipo di task ed eseguo di conseguenza... + switch (TaskRec.TType) + { + //case Task2ExeType.ND: + // break; + //case Task2ExeType.Exe: + // break; + //case Task2ExeType.SqlCommand: + // break; + case Task2ExeType.SqlStored: + answ = MLController.ExecuteSqlTask(TaskRec.TaskId, SchedNext); + break; + case Task2ExeType.RestCallGet: + DateTime dtStart = DateTime.Now; + // in primis testo la chiamata al servizio Health + string rAnsw = await RCallService.CheckServer(); + // se ok effettuo vera chiamata... + if (rAnsw.ToUpper() == "OK") + { + var callResp = await RCallService.CallRestGet(TaskRec.Command, TaskRec.Args); + DateTime dtEnd = DateTime.Now; + TaskExecModel tExeMod = new TaskExecModel() + { + DtEnd = dtEnd, + DtStart = dtStart, + IsError = callResp.StatusCode != System.Net.HttpStatusCode.OK, + TaskId = TaskRec.TaskId, + Result = $"{callResp.Content}".Replace("\"", ""), + }; + // salvo su DB + answ = MLController.TaskExecSaveExecuted(TaskRec.TaskId, SchedNext, tExeMod); + } + break; + default: + break; + } + // svuoto cache! + await FlushCache("Task"); + return answ; + } + + /// + /// Pulizia cache Redis (tutta) + /// + /// + public async Task FlushCache() + { + RedisValue pattern = new RedisValue($"{redisBaseKey}:*"); + bool answ = await ExecFlushRedisPattern(pattern); + return answ; + } + + /// + /// Pulizia cache Redis per chiave specifica (da redisBaseKey...) + /// + /// + public async Task FlushCache(string KeyReq) + { + RedisValue pattern = new RedisValue($"{redisBaseKey}:{KeyReq}:*"); + bool answ = await ExecFlushRedisPattern(pattern); + return answ; + } + + /// + /// Invio notifica rilettura (con parametro) + /// + /// + public void NotifyReloadRequest(string message) + { + if (ReloadRequest != null) + { + // messaggio + ReloadEventArgs rea = new ReloadEventArgs(message); + ReloadRequest.Invoke(this, rea); + } + } + + public void rollBackEdit(object item) + { + MLController.RollBackEntity(item); + } + + /// + /// Ricerca task dato tipo + num max (desc) + /// + /// TaskId da cui deriva + /// + public async Task> TaskExecGetFilt(int TaskId, int maxRec, string searchVal) + { + // setup parametri costanti + string source = "DB"; + Stopwatch sw = new Stopwatch(); + sw.Start(); + List result = new List(); + // cerco in redis... + DateTime adesso = DateTime.Now; + string currKey = $"{redisBaseKey}:Task:ExecList:{TaskId}:{adesso:yyMMdd}:{adesso:HHmm}:{maxRec}"; + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + source = "REDIS"; + } + else + { + result = MLController.TaskExecGetFilt(TaskId, maxRec); + // serializzp e salvo... + rawData = JsonConvert.SerializeObject(result); + await redisDb.StringSetAsync(currKey, rawData, FastCache); + } + if (result == null) + { + result = new List(); + } + sw.Stop(); + Log.Debug($"TaskExecGetFilt | {source} | {sw.Elapsed.TotalMilliseconds}ms"); + return result; + } + + /// + /// Elenco TaskList gestiti + /// + /// + /// + /// + public async Task> TaskListAll(Task2ExeType TType, string searchVal = "") + { + // setup parametri costanti + string source = "DB"; + Stopwatch sw = new Stopwatch(); + sw.Start(); + List result = new List(); + // cerco in redis... + DateTime adesso = DateTime.Now; + string currKey = $"{redisBaseKey}:Task:List:{TType}"; + RedisValue rawData = await redisDb.StringGetAsync(currKey); + if (rawData.HasValue) + { + result = JsonConvert.DeserializeObject>($"{rawData}"); + source = "REDIS"; + } + else + { + result = MLController.TaskListGetAll(TType); + // serializzp e salvo... + rawData = JsonConvert.SerializeObject(result); + await redisDb.StringSetAsync(currKey, rawData, FastCache); + } + if (result == null) + { + result = new List(); + } + // se necessario filtro.. + if (!string.IsNullOrEmpty(searchVal)) + { + result = result + .Where(x => x.Name.Contains(searchVal, StringComparison.InvariantCultureIgnoreCase) + || x.Descript.Contains(searchVal, StringComparison.InvariantCultureIgnoreCase)) + .ToList(); + } + sw.Stop(); + Log.Debug($"TaskListAll | {source} | {sw.Elapsed.TotalMilliseconds}ms"); + return result; + } + + /// + /// Update ordinamento task + /// + /// Record da spostare x priorità + /// + public async Task TaskListMove(TaskListModel rec2upd, bool moveUp) + { + bool dbResult = MLController.TaskListMove(rec2upd, moveUp); + // svuoto cache! + await FlushCache("Task"); + return await Task.FromResult(dbResult); + } + + /// + /// Update/Insert record TaskList + /// + /// + /// + public async Task TaskListUpsert(TaskListModel rec2upd) + { + bool dbResult = MLController.TaskListUpsert(rec2upd); + // svuoto cache! + await FlushCache("Task"); + return dbResult; + } + + #endregion Public Methods + + #region Protected Fields + + /// + /// Oggetto per connessione a REDIS + /// + protected ConnectionMultiplexer redisConn = null!; + + /// + /// Oggetto DB redis da impiegare x chiamate R/W + /// + protected IDatabase redisDb = null!; + + #endregion Protected Fields + + #region Private Fields + + private static Logger Log = LogManager.GetCurrentClassLogger(); + + private string CodModulo = ""; + + private string ConnStr = ""; + + private Dictionary connStrParams = new Dictionary(); + + private string DataBase = ""; + + private string DataSource = ""; + + private string redisBaseKey = "MP:TASK"; + + #endregion Private Fields + + #region Private Properties + + private static MpLandController MLController { get; set; } = null!; + private RestCallService RCallService { get; set; } = null!; + + #endregion Private Properties + + #region Private Methods + + /// + /// Esegue flush memoria redis dato pattern + /// + /// + /// + private async Task ExecFlushRedisPattern(RedisValue pattern) + { + bool answ = false; + var listEndpoints = redisConn.GetEndPoints(); + foreach (var endPoint in listEndpoints) + { + //var server = redisConnAdmin.GetServer(listEndpoints[0]); + var server = redisConn.GetServer(endPoint); + if (server != null) + { + var keyList = server.Keys(redisDb.Database, pattern); + foreach (var item in keyList) + { + await redisDb.KeyDeleteAsync(item); + } + answ = true; + } + } + // notifico update ai client in ascolto x reset cache + NotifyReloadRequest($"FlushRedisCache | {pattern}"); + return answ; + } + + #endregion Private Methods + } +} \ No newline at end of file diff --git a/MP.TaskMan/TaskEdit.razor b/MP.TaskMan/TaskEdit.razor index 69d6674a..566a5e22 100644 --- a/MP.TaskMan/TaskEdit.razor +++ b/MP.TaskMan/TaskEdit.razor @@ -11,7 +11,7 @@
- @foreach (var option in Enum.GetValues(typeof(MP.Data.Objects.Enums.TaskFreqType))) + @foreach (var option in Enum.GetValues(typeof(MP.TaskMan.Objects.Enums.TaskFreqType))) {