using AutoMapper; using Edge.Core.Configuration; using Edge.Core.Database; using Edge.Core.Database.Models; using Edge.Core.Processor; using Edge.Core.Processor.Dispatcher.Attributes; using Edge.Core.UniversalApi.Auditing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace Edge.Core.UniversalApi { public class UniversalApiHub { private IEnumerable processors; private IEnumerable auditingStores; private IServiceProvider services; private bool config_DisableApiAuditing = false; private bool config_EnableApiInputAndOutputAuditing = false; public event EventHandler OnUniversalGenericAlarmEventFired; public async Task InitAsync(IEnumerable processors) { this.processors = processors; foreach (var p in this.CommunicationProviders) { await p.SetupAsync(this.processors); } } public IEnumerable CommunicationProviders { get; } public UniversalApiHub(IEnumerable communicationProviders, IServiceProvider services) { this.CommunicationProviders = communicationProviders; this.services = services; var configurator = services.GetService(); this.config_DisableApiAuditing = bool.Parse(configurator.MetaConfiguration.Parameter ?.FirstOrDefault(p => p.Name.Equals("disableUniversalApiAuditing", StringComparison.OrdinalIgnoreCase))?.Value ?? "false"); this.config_EnableApiInputAndOutputAuditing = bool.Parse(configurator.MetaConfiguration.Parameter ?.FirstOrDefault(p => p.Name.Equals("enableUniversalApiInputAndOutputAuditing", StringComparison.OrdinalIgnoreCase))?.Value ?? "false"); this.auditingStores = services.GetRequiredService>(); configurator.OnConfigFileChanged += (_, __) => { this.config_DisableApiAuditing = bool.Parse(configurator.MetaConfiguration.Parameter ?.FirstOrDefault(p => p.Name.Equals("disableUniversalApiAuditing", StringComparison.OrdinalIgnoreCase))?.Value ?? "false"); this.config_EnableApiInputAndOutputAuditing = bool.Parse(configurator.MetaConfiguration.Parameter ?.FirstOrDefault(p => p.Name.Equals("enableUniversalApiInputAndOutputAuditing", StringComparison.OrdinalIgnoreCase))?.Value ?? "false"); }; } /// /// Fire an event to all underlying CommunicationProviders. /// Make sure class Attribute of `Event UniversalApi` declared in App or DeviceHandler. /// /// /// /// /// public async Task FireEvent(IProcessor source, string eventName, object eventData) { #region AuditLogInfo auditLogInfo = null; if (!this.config_DisableApiAuditing) { auditLogInfo = new AuditLogInfo() { ClientIdentity = "", DeviceHandlerOrAppName = source.SelectHandlerOrAppThenCast().GetType().FullName, ExecutionTime = DateTime.Now, }; auditLogInfo.Actions.Add(new AuditLogActionInfo() { ApiName = "event : " + eventName, InputParameters = this.config_EnableApiInputAndOutputAuditing ? JsonSerializer.Serialize(eventData) : null, }); auditLogInfo.Prepare(); } #endregion try { if (this.CommunicationProviders == null) return; foreach (var provider in this.CommunicationProviders) { try { var executeResult = await provider.RouteEventAsync(source, new EventDescriptor() { Name = eventName, Data = eventData }); } catch (Exception exxx) { #region //if (!this.config_DisableApiAuditing) //{ auditLogInfo.CommitWithExceptions(new Exception[] { exxx }); } #endregion } } } finally { if (!this.config_DisableApiAuditing) { auditLogInfo.CommitWithExeResult(true); Task.WaitAll(this.auditingStores.Select(s => s.SaveAsync(auditLogInfo)).ToArray()); } } } /// /// Fire generic event without persist it(like fire and forget). /// the UI component would receive it at the moment and only for onetime, to showing a message to user. /// /// /// /// public Task FireGenericAlarm(IProcessor source, GenericAlarm genericAlarm) { return this.FireGenericAlarm(source, genericAlarm, false, null); } /// /// Fire generic event and persist it. /// the UI component would receive it at the moment and can retrieve it anytime later, to showing a message to user. /// /// /// /// any custom data to persist together with this alarm, used for searching. /// the persist record id, use it for retrieve back the record. public Task FirePersistGenericAlarm(IProcessor source, GenericAlarm genericAlarm, Func hiddenDataSelector) { return this.FireGenericAlarm(source, genericAlarm, true, hiddenDataSelector(genericAlarm)); } /// /// /// /// /// /// if true, the alarm will be persist /// /// not firing the event, used for scenario that refresh the persist alarm without re-firing it. /// private async Task FireGenericAlarm(IProcessor source, GenericAlarm genericAlarm, bool persist, string hiddenData, bool muteFiring = false) { int? persistAlarmDbRecordKey = null; if (!persist) { } else { var mapper = this.services.GetRequiredService(); using (var scope = this.services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); var pDesc = source.ProcessorDescriptor(); var originatorDisplayName = pDesc?.DeviceHandlerOrApp?.GetType() ?.GetCustomAttributes()?.FirstOrDefault()?.DisplayName; if (string.IsNullOrEmpty(originatorDisplayName)) originatorDisplayName = pDesc?.DeviceHandlerOrApp?.GetType().FullName; var dbAlarm = mapper.Map(genericAlarm); dbAlarm.ProcessorEndpointFullTypeStr = pDesc?.DeviceHandlerOrApp?.GetType().FullName; dbAlarm.OriginatorDisplayName = originatorDisplayName; dbAlarm.OpeningTimestamp = DateTime.Now; dbAlarm.HiddenData = hiddenData; dbContext.Add(dbAlarm); if (DateTime.Now.Second % 5 == 0) { //start purging old datas DateTime due = DateTime.Now.Subtract(new TimeSpan(60, 0, 0, 0)); var removing = await dbContext.GenericAlarms.Where(ga => ga.OpeningTimestamp <= due).ToArrayAsync(); dbContext.RemoveRange(removing); } await dbContext.SaveChangesAsync(); persistAlarmDbRecordKey = dbAlarm.Id; } } if (!muteFiring) this.OnUniversalGenericAlarmEventFired?.Invoke(this, new UniversalGenericAlarmEventFiredEventArg(source, genericAlarm, persistAlarmDbRecordKey)); //await this.FireEvent(source, GenericAlarm.UniversalApiEventName, genericAlarm); return persistAlarmDbRecordKey; } /// /// Mark several Opening state alarms to Closed state, and fill with close reason for each. /// /// list of opening state alarm id and close reason. /// public async Task ClosePersistGenericAlarms(IEnumerable> alarmIdAndCloseReasons) { using (var scope = this.services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); foreach (var idAndCloseReason in alarmIdAndCloseReasons) { var target = dbContext.GenericAlarms.Find(idAndCloseReason.Item1); if (target == null) continue; target.ClosedTimestamp = DateTime.Now; target.ClosedReason = idAndCloseReason.Item2; dbContext.GenericAlarms.Update(target); } await dbContext.SaveChangesAsync(); } } public async Task AckPersistGenericAlarms(IEnumerable> alarmIdAndAckReasons, bool isForUnAck = false) { using (var scope = this.services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); foreach (var idAndAckReason in alarmIdAndAckReasons) { var target = dbContext.GenericAlarms.Find(idAndAckReason.Item1); if (isForUnAck) { target.AckedTimestamp = null; target.AckedReason = null; } else { target.AckedTimestamp = DateTime.Now; target.AckedReason = idAndAckReason.Item2; } dbContext.GenericAlarms.Update(target); } await dbContext.SaveChangesAsync(); } } /// /// Mark all Opening state alarms to Closed state, and fill with the same close reason, for a specific processor. /// /// all alarms for this processor will be closed. /// all target alarms will set with this close reason /// public Task ClosePersistGenericAlarms(IProcessor source, string closeReason) { return this.ClosePersistGenericAlarms(source, null, closeReason); } /// /// Mark all Opening state alarms to Closed state, and fill with the same close reason. /// /// /// the searching criteria, the hidden data fields contains this value will be matched. /// all target alarms will set with this close reason /// public async Task ClosePersistGenericAlarms(IProcessor source, string hiddenDataHint, string closeReason) { var originator = source.ProcessorDescriptor()?.DeviceHandlerOrApp?.GetType() ?.GetCustomAttributes()?.FirstOrDefault()?.DisplayName; if (string.IsNullOrEmpty(originator)) originator = source.ProcessorDescriptor()?.DeviceHandlerOrApp?.GetType().FullName; using (var scope = this.services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); IQueryable ownedOpenAlarms; if (string.IsNullOrEmpty(hiddenDataHint)) ownedOpenAlarms = dbContext.GenericAlarms.Where(ga => ga.OriginatorDisplayName == originator && ga.ClosedTimestamp == null); else ownedOpenAlarms = dbContext.GenericAlarms.Where(ga => ga.OriginatorDisplayName == originator && ga.HiddenData.Contains(hiddenDataHint) && ga.ClosedTimestamp == null); foreach (var al in ownedOpenAlarms) { al.ClosedTimestamp = DateTime.Now; al.ClosedReason = closeReason; dbContext.GenericAlarms.Update(al); } var effectRowCount = await dbContext.SaveChangesAsync(); return effectRowCount; } } /// /// Query the persist generic alarms by search conditions. /// /// only alarms from this processor are returned, leave null to ignore this constrait. /// null to ignore this constrait. /// null to ignore this constrait. /// null to ignore this constrait. /// null to ignore this constrait. /// null to ignore this constrait. /// null to ignore this constrait. /// null to ignore this constrait. /// /// /// /// /// public async Task> GetPersistGenericAlarms( IProcessor source, string[] originators, string[] alarmCategories, string[] alarmSubCategories, string alarmHiddenData, DateTime? alarmOpeningDateTimeFrom, DateTime? alarmOpeningDateTimeTo, GenericAlarmSeverity? severity, bool includeAckedStateAlarm, bool includeClosedStateAlarm, int pageIndex, int pageRowCount) { using (var scope = this.services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); IQueryable data = dbContext.GenericAlarms; if (alarmOpeningDateTimeFrom != null) data = data.Where(ga => ga.OpeningTimestamp >= alarmOpeningDateTimeFrom); if (alarmOpeningDateTimeTo != null) data = data.Where(ga => ga.OpeningTimestamp <= alarmOpeningDateTimeTo); if (source != null) { var typeFullName = source.ProcessorDescriptor()?.DeviceHandlerOrApp?.GetType().FullName; data = data.Where(ga => ga.ProcessorEndpointFullTypeStr == typeFullName); } if (originators != null && originators.Any()) { var sqlRaw = $"SELECT * FROM GenericAlarms where {originators.Select(input => "OriginatorDisplayName like '%" + input + "%' ").Aggregate((acc, n) => acc + " or " + n)}"; data = ((DbSet)data).FromSqlRaw(sqlRaw); } if (alarmCategories != null) { var sqlRaw = $"SELECT * FROM GenericAlarms where {alarmCategories.Select(input => "Category like '%" + input + "%' ").Aggregate((acc, n) => acc + " or " + n)}"; data = ((DbSet)data).FromSqlRaw(sqlRaw); } if (alarmSubCategories != null) { var sqlRaw = $"SELECT * FROM GenericAlarms where {alarmSubCategories.Select(input => "SubCategory like '%" + input + "%' ").Aggregate((acc, n) => acc + " or " + n)}"; data = ((DbSet)data).FromSqlRaw(sqlRaw); } if (alarmHiddenData != null) data = data.Where(ga => ga.HiddenData == alarmHiddenData); if (severity != null) data = data.Where(ga => ga.Severity == severity); if (!includeClosedStateAlarm) data = data.Where(ga => ga.ClosedTimestamp == null); if (!includeAckedStateAlarm) data = data.Where(ga => ga.AckedTimestamp == null); var r = await data.OrderByDescending(d => d.OpeningTimestamp).Skip(pageIndex * pageRowCount).Take(pageRowCount).ToListAsync(); return r; //return r.Select(d => new //{ // d.Id, // d.Originator, // d.Severity, // d.Category, // d.SubCategory, // d.Title, // d.Detail, // d.ClosedTimestamp, // d.ClosedReason, // d.Action, //}); } } /// /// Close the alarms by match its hidden data, and create a new persist generic alarm. /// /// /// predictor for hidden data match for test with all exist alarms, the matched ones will be all closed. /// /// /// generator for hidden data that will be used for create a new alarm. /// if true, then new alarm event will not be fired, used for scenario that create an alarm but not firing event to gain user attention. /// public async Task CloseAndFirePersistGenericAlarm(IProcessor source, GenericAlarm newAlarm, Func closingAlarmsHiddenDataPredicate, string closeReason, Func newAlarmHiddenDataSelector, bool muteNewAlarmFiring = false) { if (source == null) throw new ArgumentNullException(nameof(source)); bool lockTaken = false; try { this.spinLock_GuardTwoStageOperation.Enter(ref lockTaken); var alarmsForClosing = await GetPersistGenericAlarms(source, null, null, null, closingAlarmsHiddenDataPredicate(newAlarm), null, null, null, true, false, 0, 100); if (alarmsForClosing.Any()) await this.ClosePersistGenericAlarms(alarmsForClosing.Select(c => new Tuple(c.Id, closeReason))); var r = await this.FireGenericAlarm(source, newAlarm, true, newAlarmHiddenDataSelector(newAlarm), muteNewAlarmFiring); return r; } finally { if (lockTaken) this.spinLock_GuardTwoStageOperation.Exit(false); } } private SpinLock spinLock_GuardTwoStageOperation = new SpinLock(); /// /// Clear(remove from db) the alarms by match its hidden data, and create a new persist generic alarm. /// /// /// /// predictor for hidden data match for test with all exist alarms, the matched ones will be all cleared. /// /// if true, then new alarm event will not be fired, used for scenario that create an alarm but not firing event to gain user attention. /// public async Task ClearAndFirePersistGenericAlarm(IProcessor source, GenericAlarm newAlarm, Func clearingAlarmsHiddenDataPredicate, Func newAlarmHiddenDataSelector, bool muteNewAlarmFiring = false) { if (source == null) throw new ArgumentNullException(nameof(source)); bool lockTaken = false; try { this.spinLock_GuardTwoStageOperation.Enter(ref lockTaken); using (var scope = this.services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); var hd = clearingAlarmsHiddenDataPredicate(newAlarm); var typeFullName = source.ProcessorDescriptor()?.DeviceHandlerOrApp?.GetType().FullName; var deleting = dbContext.GenericAlarms.Where(ga => ga.ProcessorEndpointFullTypeStr == typeFullName && ga.HiddenData == hd); dbContext.GenericAlarms.RemoveRange(deleting); var effectRowCount = await dbContext.SaveChangesAsync(); } var r = await this.FireGenericAlarm(source, newAlarm, true, newAlarmHiddenDataSelector(newAlarm), muteNewAlarmFiring); return r; } finally { if (lockTaken) this.spinLock_GuardTwoStageOperation.Exit(false); } } /// /// Try firing an alarm by only there's no matched opened alarm in db. /// /// /// /// predictor for hidden data match for test with all exist alarms, any matched ones will be considered as EXISTS, and then will not firing anymore. /// /// public async Task FirePersistGenericAlarmIfNotExists(IProcessor source, GenericAlarm newAlarm, Func existsAlarmsHiddenDataPredicate, Func newAlarmHiddenDataSelector) { if (source == null) throw new ArgumentNullException(nameof(source)); bool lockTaken = false; try { this.spinLock_GuardTwoStageOperation.Enter(ref lockTaken); using (var scope = this.services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); var hd = existsAlarmsHiddenDataPredicate(newAlarm); var exist = await this.GetPersistGenericAlarms(source, null, null, null, hd, null, null, null, true, false, 0, 1); if (exist.Any()) return exist.First().Id; var r = await this.FireGenericAlarm(source, newAlarm, true, newAlarmHiddenDataSelector(newAlarm)); return r; } } finally { if (lockTaken) this.spinLock_GuardTwoStageOperation.Exit(false); } } } public class UniversalGenericAlarmEventFiredEventArg : System.EventArgs { public IProcessor Originator { get; } public GenericAlarm GenericAlarm { get; } public int? PersistId { get; } public UniversalGenericAlarmEventFiredEventArg(IProcessor originator, GenericAlarm genericAlarm, int? persistId) { this.Originator = originator; this.GenericAlarm = genericAlarm; this.PersistId = persistId; } } }