123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502 |
- 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<IProcessor> processors;
- private IEnumerable<IAuditingStore> auditingStores;
- private IServiceProvider services;
- private bool config_DisableApiAuditing = false;
- private bool config_EnableApiInputAndOutputAuditing = false;
- public event EventHandler<UniversalGenericAlarmEventFiredEventArg> OnUniversalGenericAlarmEventFired;
- public async Task InitAsync(IEnumerable<IProcessor> processors)
- {
- this.processors = processors;
- foreach (var p in this.CommunicationProviders)
- {
- await p.SetupAsync(this.processors);
- }
- }
- public IEnumerable<ICommunicationProvider> CommunicationProviders { get; }
- public UniversalApiHub(IEnumerable<ICommunicationProvider> communicationProviders, IServiceProvider services)
- {
- this.CommunicationProviders = communicationProviders;
- this.services = services;
- var configurator = services.GetService<Configurator>();
- 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<IEnumerable<IAuditingStore>>();
- 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");
- };
- }
- /// <summary>
- /// Fire an event to all underlying CommunicationProviders.
- /// Make sure class Attribute of `Event UniversalApi` declared in App or DeviceHandler.
- /// </summary>
- /// <param name="source"></param>
- /// <param name="eventName"></param>
- /// <param name="eventData"></param>
- /// <returns></returns>
- 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<object>().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());
- }
- }
- }
- /// <summary>
- /// 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.
- /// </summary>
- /// <param name="source"></param>
- /// <param name="genericAlarm"></param>
- /// <returns></returns>
- public Task FireGenericAlarm(IProcessor source, GenericAlarm genericAlarm)
- {
- return this.FireGenericAlarm(source, genericAlarm, false, null);
- }
- /// <summary>
- /// 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.
- /// </summary>
- /// <param name="source"></param>
- /// <param name="genericAlarm"></param>
- /// <param name="hiddenDataSelector">any custom data to persist together with this alarm, used for searching.</param>
- /// <returns>the persist record id, use it for retrieve back the record.</returns>
- public Task<int?> FirePersistGenericAlarm(IProcessor source, GenericAlarm genericAlarm, Func<GenericAlarm, string> hiddenDataSelector)
- {
- return this.FireGenericAlarm(source, genericAlarm, true, hiddenDataSelector(genericAlarm));
- }
- /// <summary>
- ///
- /// </summary>
- /// <param name="source"></param>
- /// <param name="genericAlarm"></param>
- /// <param name="persist">if true, the alarm will be persist</param>
- /// <param name="hiddenData"></param>
- /// <param name="muteFiring">not firing the event, used for scenario that refresh the persist alarm without re-firing it.</param>
- /// <returns></returns>
- private async Task<int?> FireGenericAlarm(IProcessor source, GenericAlarm genericAlarm, bool persist, string hiddenData, bool muteFiring = false)
- {
- int? persistAlarmDbRecordKey = null;
- if (!persist)
- {
- }
- else
- {
- var mapper = this.services.GetRequiredService<IMapper>();
- using (var scope = this.services.CreateScope())
- {
- var dbContext = scope.ServiceProvider.GetRequiredService<SqliteDbContext>();
- var pDesc = source.ProcessorDescriptor();
- var originatorDisplayName = pDesc?.DeviceHandlerOrApp?.GetType()
- ?.GetCustomAttributes<MetaPartsDescriptor>()?.FirstOrDefault()?.DisplayName;
- if (string.IsNullOrEmpty(originatorDisplayName))
- originatorDisplayName = pDesc?.DeviceHandlerOrApp?.GetType().FullName;
- var dbAlarm = mapper.Map<GenericAlarmDbModel>(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;
- }
- /// <summary>
- /// Mark several Opening state alarms to Closed state, and fill with close reason for each.
- /// </summary>
- /// <param name="alarmIdAndCloseReasons">list of opening state alarm id and close reason.</param>
- /// <returns></returns>
- public async Task ClosePersistGenericAlarms(IEnumerable<Tuple<int, string>> alarmIdAndCloseReasons)
- {
- using (var scope = this.services.CreateScope())
- {
- var dbContext = scope.ServiceProvider.GetRequiredService<SqliteDbContext>();
- 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<Tuple<int, string>> alarmIdAndAckReasons, bool isForUnAck = false)
- {
- using (var scope = this.services.CreateScope())
- {
- var dbContext = scope.ServiceProvider.GetRequiredService<SqliteDbContext>();
- 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();
- }
- }
- /// <summary>
- /// Mark all Opening state alarms to Closed state, and fill with the same close reason, for a specific processor.
- /// </summary>
- /// <param name="source">all alarms for this processor will be closed.</param>
- /// <param name="closeReason">all target alarms will set with this close reason</param>
- /// <returns></returns>
- public Task<int> ClosePersistGenericAlarms(IProcessor source, string closeReason)
- {
- return this.ClosePersistGenericAlarms(source, null, closeReason);
- }
- /// <summary>
- /// Mark all Opening state alarms to Closed state, and fill with the same close reason.
- /// </summary>
- /// <param name="source"></param>
- /// <param name="hiddenDataHint">the searching criteria, the hidden data fields contains this value will be matched.</param>
- /// <param name="closeReason">all target alarms will set with this close reason</param>
- /// <returns></returns>
- public async Task<int> ClosePersistGenericAlarms(IProcessor source, string hiddenDataHint, string closeReason)
- {
- var originator = source.ProcessorDescriptor()?.DeviceHandlerOrApp?.GetType()
- ?.GetCustomAttributes<MetaPartsDescriptor>()?.FirstOrDefault()?.DisplayName;
- if (string.IsNullOrEmpty(originator))
- originator = source.ProcessorDescriptor()?.DeviceHandlerOrApp?.GetType().FullName;
- using (var scope = this.services.CreateScope())
- {
- var dbContext = scope.ServiceProvider.GetRequiredService<SqliteDbContext>();
- IQueryable<GenericAlarmDbModel> 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;
- }
- }
- /// <summary>
- /// Query the persist generic alarms by search conditions.
- /// </summary>
- /// <param name="source">only alarms from this processor are returned, leave null to ignore this constrait.</param>
- /// <param name="originators">null to ignore this constrait.</param>
- /// <param name="alarmCategories">null to ignore this constrait.</param>
- /// <param name="alarmSubCategories">null to ignore this constrait.</param>
- /// <param name="alarmHiddenData">null to ignore this constrait.</param>
- /// <param name="alarmOpeningDateTimeFrom">null to ignore this constrait.</param>
- /// <param name="alarmOpeningDateTimeTo">null to ignore this constrait.</param>
- /// <param name="severity">null to ignore this constrait.</param>
- /// <param name="includeAckedStateAlarm"></param>
- /// <param name="includeClosedStateAlarm"></param>
- /// <param name="pageIndex"></param>
- /// <param name="pageRowCount"></param>
- /// <returns></returns>
- public async Task<IEnumerable<GenericAlarmDbModel>> 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<SqliteDbContext>();
- IQueryable<GenericAlarmDbModel> 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<GenericAlarmDbModel>)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<GenericAlarmDbModel>)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<GenericAlarmDbModel>)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,
- //});
- }
- }
- /// <summary>
- /// Close the alarms by match its hidden data, and create a new persist generic alarm.
- /// </summary>
- /// <param name="source"></param>
- /// <param name="closingAlarmsHiddenDataPredicate">predictor for hidden data match for test with all exist alarms, the matched ones will be all closed.</param>
- /// <param name="closeReason"></param>
- /// <param name="newAlarm"></param>
- /// <param name="newAlarmHiddenDataSelector">generator for hidden data that will be used for create a new alarm.</param>
- /// <param name="muteNewAlarmFiring">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.</param>
- /// <returns></returns>
- public async Task<int?> CloseAndFirePersistGenericAlarm(IProcessor source, GenericAlarm newAlarm,
- Func<GenericAlarm, string> closingAlarmsHiddenDataPredicate, string closeReason,
- Func<GenericAlarm, string> 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<int, string>(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();
- /// <summary>
- /// Clear(remove from db) the alarms by match its hidden data, and create a new persist generic alarm.
- /// </summary>
- /// <param name="source"></param>
- /// <param name="newAlarm"></param>
- /// <param name="clearingAlarmsHiddenDataPredicate">predictor for hidden data match for test with all exist alarms, the matched ones will be all cleared.</param>
- /// <param name="newAlarmHiddenDataSelector"></param>
- /// <param name="muteNewAlarmFiring">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.</param>
- /// <returns></returns>
- public async Task<int?> ClearAndFirePersistGenericAlarm(IProcessor source, GenericAlarm newAlarm,
- Func<GenericAlarm, string> clearingAlarmsHiddenDataPredicate,
- Func<GenericAlarm, string> 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<SqliteDbContext>();
- 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);
- }
- }
- /// <summary>
- /// Try firing an alarm by only there's no matched opened alarm in db.
- /// </summary>
- /// <param name="source"></param>
- /// <param name="newAlarm"></param>
- /// <param name="existsAlarmsHiddenDataPredicate">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.</param>
- /// <param name="newAlarmHiddenDataSelector"></param>
- /// <returns></returns>
- public async Task<int?> FirePersistGenericAlarmIfNotExists(IProcessor source, GenericAlarm newAlarm,
- Func<GenericAlarm, string> existsAlarmsHiddenDataPredicate,
- Func<GenericAlarm, string> 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<SqliteDbContext>();
- 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;
- }
- }
- }
|