Selaa lähdekoodia

finalize QMonReceiverHandler

Dalibor Votruba 2 vuotta sitten
vanhempi
commit
4c86838587

+ 12 - 6
Workbench/QMonitor/qmonlib.console/Commands/BaseCommand.cs

@@ -18,9 +18,9 @@ namespace Quadarax.Foundation.Core.QMonitor.Console.Commands
         private const string CS_ARG_HINT_DUMP = "<is_dump_reponses>";
         private const string CS_ARG_DESC_DUMP = "Flag if set dumps incomming json messages.";
 
-        protected const string CS_ARG_NAME_DEBUG = "dbg";
-        private const string CS_ARG_HINT_DEBUG = "<is_debug_mode>";
-        private const string CS_ARG_DESC_DEBUG = "Flag if set writes debug messages.";
+        //protected const string CS_ARG_NAME_DEBUG = "dbg";
+        //private const string CS_ARG_HINT_DEBUG = "<is_debug_mode>";
+        //private const string CS_ARG_DESC_DEBUG = "Flag if set writes debug messages.";
 
         protected const string CS_ARG_NAME_URIGENERAL = "uri-gen";
         private const string CS_ARG_HINT_URIGENERAL = "<general_uri>";
@@ -30,6 +30,11 @@ namespace Quadarax.Foundation.Core.QMonitor.Console.Commands
         private const string CS_ARG_HINT_URIDATA = "";
         private const string CS_ARG_DESC_URIDATA = "Uri listener specification for receiving data. If not set use default.";
 
+        protected const string CS_ARG_NAME_PURGE = "purge";
+        private const string CS_ARG_HINT_PURGE = "<purge_cached_data>";
+        private const string CS_ARG_DESC_PURGE = "Purges chached data before start monitoring.";
+
+
         #endregion
 
         #region *** Properties ***
@@ -88,7 +93,7 @@ namespace Quadarax.Foundation.Core.QMonitor.Console.Commands
         {
             base.OnValidateArguments();
 
-            IsDebug = WasArgumentSpecified(CS_ARG_NAME_DEBUG);
+            IsDebug = Engine.Configuration.IsConsoleDebug;
 
             if (IsDebug)
                 DebugConsoleWriter.IsWriteDebugEnabled = true;
@@ -103,9 +108,10 @@ namespace Quadarax.Foundation.Core.QMonitor.Console.Commands
             FileSystem = new FileSystem();
             var args = base.OnSetupArguments().ToList();
             args.Add(new FlagArgument(CS_ARG_NAME_DUMP, CS_ARG_DESC_DUMP, CS_ARG_HINT_DUMP, false));
-            args.Add(new FlagArgument(CS_ARG_NAME_DEBUG, CS_ARG_DESC_DEBUG, CS_ARG_HINT_DEBUG, false));
+            //args.Add(new FlagArgument(CS_ARG_NAME_DEBUG, CS_ARG_DESC_DEBUG, CS_ARG_HINT_DEBUG, false));
             args.Add(new NamedArgument(CS_ARG_NAME_URIGENERAL, CS_ARG_DESC_URIGENERAL, CS_ARG_HINT_URIGENERAL, TypeValuesEnum.String,string.Empty, false));
             args.Add(new NamedArgument(CS_ARG_NAME_URIDATA, CS_ARG_DESC_URIDATA, CS_ARG_HINT_URIDATA, TypeValuesEnum.String,string.Empty, false));
+            args.Add(new FlagArgument(CS_ARG_NAME_PURGE, CS_ARG_DESC_PURGE, CS_ARG_HINT_PURGE, false));
             return args;
         }
 
@@ -135,7 +141,7 @@ namespace Quadarax.Foundation.Core.QMonitor.Console.Commands
             rcvCfg.SourceUriGeneral = _uriGeneral ?? Configuration.GeneralUri;
             rcvCfg.SourceUriData = _uriData ?? Configuration.DataUri;
             WriteInfo($"Starting listener for general ({rcvCfg.SourceUriGeneral}) and data ({rcvCfg.SourceUriData}) ...");
-            Receiver = new QMonReceiver(this.FileSystem, rcvCfg,null, OnGeneralReceived, OnDataReceived);
+            Receiver = new QMonReceiver(this.FileSystem, rcvCfg,GetArgumentValueOrDefault<bool>(CS_ARG_NAME_PURGE), (IsDebug ? this.Engine.Configuration.DefaultLoggerFactory : null)!, OnGeneralReceived, OnDataReceived);
         }
 
         protected override Result EndExecute(Result incommingResult)

+ 127 - 4
Workbench/QMonitor/qmonlib.console/Commands/MonitorCommand.cs

@@ -1,8 +1,15 @@
-using Quadarax.Foundation.Core.QConsole;
+using System.Globalization;
+using System.Runtime.InteropServices.JavaScript;
+using System.Text;
+using System.Text.Json;
+using Quadarax.Foundation.Core.Data;
+using Quadarax.Foundation.Core.QConsole;
 using Quadarax.Foundation.Core.QConsole.Argument;
 using Quadarax.Foundation.Core.QConsole.Attributes;
 using Quadarax.Foundation.Core.QConsole.Value;
-using Quadarax.Foundation.Core.Value;
+using Quadarax.Foundation.Core.QMonitor.Interfaces;
+using Quadarax.Foundation.Core.Value.Extensions;
+using Result = Quadarax.Foundation.Core.Value.Result;
 
 namespace Quadarax.Foundation.Core.QMonitor.Console.Commands
 {
@@ -22,12 +29,32 @@ namespace Quadarax.Foundation.Core.QMonitor.Console.Commands
         private const string CS_ARG_HINT_CHANNEL = "<chennel>";
         private const string CS_ARG_DESC_CHANNEL = "Specify full channel name to monitor.";
 
+        protected const string CS_ARG_NAME_REFRESH = "refresh";
+        private const string CS_ARG_HINT_REFRESH = "<refresh_interval>";
+        private const string CS_ARG_DESC_REFRESH = "Specify receiver refresh interval to detect changes. Default is 00:00:05.";
+
+        protected const string CS_ARG_NAME_GENFILE = "genfile";
+        private const string CS_ARG_HINT_GENFILE = "<general_file>";
+        private const string CS_ARG_DESC_GENFILE = "Use general metadata from json file during initialization (no needs to wait to receive).";
+
         
         #endregion
 
         #region *** Properties ***
         public override string Name => CS_CMD_MONITOR_NAME;
         public override string Description => CS_CMD_MONITOR_DESC;
+        
+        private string InstanceIdentifier { get; set; }
+        private string ChannelName { get; set; }
+        private TimeSpan RefreshInterval { get; set; }
+        private string GeneralFile { get; set; }
+        #endregion
+
+        #region *** Private fields ***
+
+        private long _dumpCnt;
+        private MonData? _lastData;
+        private IDictionary<string, MonData> _lastDataCollection = new Dictionary<string, MonData>();
         #endregion
 
         #region *** Constructor ***
@@ -39,20 +66,116 @@ namespace Quadarax.Foundation.Core.QMonitor.Console.Commands
         #region *** Execute ***
         protected override Result OnExecute()
         {
-            WriteInfo($"Waiting for channel definition ''");
+            WriteInfo($"Refresh interval is set to {RefreshInterval}");
+            WriteInfo($"Waiting for channel definition '{InstanceIdentifier}' and channel '{ChannelName}'...");
+            using (var hnd = Receiver.CreateHandler(InstanceIdentifier, ChannelName, RefreshInterval,GeneralFile, true))
+            {
+                hnd.ReceiverHandlerStateChanged += OnReceiverHandlerStateChanged;
+                hnd.ReceiverHandlerDataChanged += OnReceiverHandlerDataChanged;
+                hnd.Play(false);
+                WriteCaption("To close receiver press any key ...");
+                System.Console.ReadKey();
+            }
             return new Result();
         }
+
         #endregion
         #region *** Private Operations ***
-        
+        protected override void OnInitialize()
+        {
+            base.OnInitialize();
+
+            InstanceIdentifier = GetArgumentValueOrDefault<string>(CS_ARG_NAME_IID);
+            ChannelName = GetArgumentValueOrDefault<string>(CS_ARG_NAME_CHANNEL);
+            RefreshInterval = GetArgumentValueOrDefault<TimeSpan>(CS_ARG_NAME_REFRESH);
+            GeneralFile = GetArgumentValueOrDefault<string>(CS_ARG_NAME_GENFILE);
+        }
+
+
         protected override IEnumerable<AbstractArgument> OnSetupArguments()
         {
             var list = base.OnSetupArguments().ToList();
             list.Add(new NamedArgument(CS_ARG_NAME_IID, CS_ARG_HINT_IID, CS_ARG_DESC_IID, TypeValuesEnum.String,string.Empty,true));
             list.Add(new NamedArgument(CS_ARG_NAME_CHANNEL, CS_ARG_HINT_CHANNEL, CS_ARG_DESC_CHANNEL, TypeValuesEnum.String,string.Empty,true));
+            list.Add(new NamedArgument(CS_ARG_NAME_REFRESH, CS_ARG_HINT_REFRESH, CS_ARG_DESC_REFRESH, TypeValuesEnum.TimeSpan,TimeSpan.FromSeconds(5).ToString(),false));
+            list.Add(new NamedArgument(CS_ARG_NAME_GENFILE, CS_ARG_HINT_GENFILE, CS_ARG_DESC_GENFILE, TypeValuesEnum.String, string.Empty, false));
             return list;
         }
 
+        private void OnReceiverHandlerDataChanged(IReceiverHandler sender, MonData newData)
+        {
+            RenderData(sender.Descriptor, newData);
+        }
+
+        private void OnReceiverHandlerStateChanged(IReceiverHandler sender, ReceiverHandlerStateEnum state)
+        {
+            WriteInfo($"! Receiver state change to {sender.State.ToString().ToUpper()}");
+        }
+
+        private void RenderData(MonItemGeneral descriptor, MonData data)
+        {
+            var firstLine = $"*** Dump [{_dumpCnt}] {descriptor.ViewType} * {descriptor.Caption} ***";
+            WriteInfo(firstLine);
+            WriteCaption($"Timestamp: {data.Timestamp} Name: {data.Name}");
+            WriteInfo(new('-', firstLine.Length));
+            var diff = _lastData?.Diff(data);
+            if (descriptor.ViewType == MonItemGeneral.MonViewType.Item)
+            {
+
+                //item view
+                foreach (var item in descriptor.Items.OrderBy(x=>x.Order))
+                {
+                    var sb = new StringBuilder();
+                    if (diff != null)
+                    {
+                        if (diff.ContainsKey(item.Name)) sb.Append("[*] ");
+                    }
+                    sb.Append(string.IsNullOrEmpty(item.Label) ? item.Name : item.Label).Append(" : ");
+                    var properties = ParseProperties(JsonDocument.Parse(data.Data).RootElement);
+                    var arg = properties.FirstOrDefault(x => string.Equals(x.Name, item.Name, StringComparison.InvariantCultureIgnoreCase));
+                    sb.Append(arg != null ? arg.Value : "N/A");
+                    WriteInfo(sb.ToString());
+                }
+            }
+
+            WriteInfo(new('-', firstLine.Length));
+            _lastData = data;
+            _dumpCnt++;
+        }
+
+        private TypedArgument[] ParseProperties(JsonElement element)
+        {
+            var result = new List<TypedArgument>();
+            foreach (var property in element.EnumerateObject())
+            {
+                if (property.Value.ValueKind.In(JsonValueKind.False, JsonValueKind.True, JsonValueKind.Number,
+                        JsonValueKind.String))
+                {
+                    TypedArgument? arg = null;
+                    if (property.Value.ValueKind.In(JsonValueKind.False, JsonValueKind.True))
+                    {
+                        arg = new TypedArgument(property.Name, property.Value.GetBoolean());
+                    }
+                    else if (property.Value.ValueKind == JsonValueKind.Null)
+                    {
+                        result.Add(new TypedArgument(property.Name,null, typeof(string)));
+                    }
+                    else if (property.Value.ValueKind == JsonValueKind.Number)
+                    {
+                        result.Add(new TypedArgument(property.Name, property.Value.GetDecimal()));
+                    }
+                    else if (property.Value.ValueKind == JsonValueKind.String)
+                    {
+                        result.Add(new TypedArgument(property.Name, property.Value.GetString()));
+                    }
+
+                    if (arg != null)
+                        result.Add(arg);
+                }
+                
+            }
+            return result.ToArray();
+        }
         #endregion
         
     }

+ 18 - 1
Workbench/QMonitor/qmonlib.console/Commands/ScanCommand.cs

@@ -1,6 +1,9 @@
-using Quadarax.Foundation.Core.QConsole;
+using System.Text.Json;
+using Quadarax.Foundation.Core.Json;
+using Quadarax.Foundation.Core.QConsole;
 using Quadarax.Foundation.Core.QConsole.Argument;
 using Quadarax.Foundation.Core.QConsole.Attributes;
+using Quadarax.Foundation.Core.QConsole.Value;
 using Quadarax.Foundation.Core.Value;
 
 namespace Quadarax.Foundation.Core.QMonitor.Console.Commands
@@ -18,6 +21,10 @@ namespace Quadarax.Foundation.Core.QMonitor.Console.Commands
         protected const string CS_ARG_NAME_DATA = "data";
         private const string CS_ARG_HINT_DATA = "<is_data_included>";
         private const string CS_ARG_DESC_DATA = "Flag if set shows receiving data entry.";
+
+        protected const string CS_ARG_NAME_GENFILE = "genfile";
+        private const string CS_ARG_HINT_GENFILE = "<general_file>";
+        private const string CS_ARG_DESC_GENFILE = "Saves received general metadata to json file.";
         #endregion
 
         
@@ -26,6 +33,7 @@ namespace Quadarax.Foundation.Core.QMonitor.Console.Commands
         public override string Description => CS_CMD_SCAN_DESC;
 
         private bool IsDataDump { get; set; }
+        private string GeneralFile { get; set; }
         #endregion
 
         #region *** Private fields ***
@@ -45,6 +53,13 @@ namespace Quadarax.Foundation.Core.QMonitor.Console.Commands
             WriteInfo($"Waiting for collecting general metadata {waitToData.TotalSeconds} seconds...");
             Task.Delay(waitToData).Wait();
             WriteInfo($"Collected {_cachedGenerals.Count} items above.");
+            if (!string.IsNullOrEmpty(GeneralFile))
+            {
+                var list = _cachedGenerals.Values.OrderBy(x=>x.Name).ToArray();
+                var binder = new Binder(FileSystem, true, true, 5);
+                binder.Save(GeneralFile, list);
+                WriteInfo($"Collected general metadata saved to '{GeneralFile}'");
+            }
             return new Result();
         }
         #endregion
@@ -55,11 +70,13 @@ namespace Quadarax.Foundation.Core.QMonitor.Console.Commands
         {
             base.OnInitialize();
             IsDataDump = GetArgumentValueOrDefault<bool>(CS_ARG_NAME_DATA);
+            GeneralFile = GetArgumentValueOrDefault<string>(CS_ARG_NAME_GENFILE);
         }
         protected override IEnumerable<AbstractArgument> OnSetupArguments()
         {
             var result = new List<AbstractArgument>(base.OnSetupArguments())
             {
+                new NamedArgument(CS_ARG_NAME_GENFILE, CS_ARG_HINT_GENFILE, CS_ARG_DESC_GENFILE, TypeValuesEnum.String, string.Empty, false),
                 new FlagArgument(CS_ARG_NAME_DATA, CS_ARG_HINT_DATA, CS_ARG_DESC_DATA, false)
             };
             return result;

+ 3 - 1
Workbench/QMonitor/qmonlib.console/Program.cs

@@ -1,6 +1,7 @@
 // See https://aka.ms/new-console-template for more information
 
 using System.Reflection;
+using Quadarax.Foundation.Core.Logging;
 using Quadarax.Foundation.Core.QConsole;
 using Quadarax.Foundation.Core.QConsole.Configuration;
 using Quadarax.Foundation.Core.QConsole.Context;
@@ -13,7 +14,8 @@ config.WaitOnKeyInNonInteractiveMode = false;
 config.CommandDefinitionAssebmly = new[]{Assembly.GetEntryAssembly()};
 config.DisableDefaultCommands();
 config.ShowStatusOnEveryCommand = false;
-config.IsConsoleDebug = false;
+config.IsConsoleDebug = true;
+config.DefaultLoggerFactory = new ConsoleLogger(LogSeverityEnum.Trace);
 
 var console = new Engine(config, new DefualtEngineContext(), args);
 console.Start();

+ 10 - 2
Workbench/QMonitor/qmonlib.console/Properties/launchSettings.json

@@ -5,11 +5,19 @@
     },
     "Scan": {
       "commandName": "Project",
-      "commandLineArgs": "scan -dbg"
+      "commandLineArgs": "scan -genfile:general.json -dbg"
     },
     "Scan with data": {
       "commandName": "Project",
-      "commandLineArgs": "scan -dbg -data"
+      "commandLineArgs": "scan -dbg -data -purge"
+    },
+    "Monitor": {
+      "commandName": "Project",
+      "commandLineArgs": "monitor -iid:test -channel:Quadarax.Foundation.Core.QMonitor.Emit.TestStructures.ServiceStatus -purge -dbg"
+    },
+    "Monitor w/o purge": {
+      "commandName": "Project",
+      "commandLineArgs": "monitor -iid:test -channel:Quadarax.Foundation.Core.QMonitor.Emit.TestStructures.ServiceStatus -dbg -genfile:general.json"
     }
   }
 }

+ 1 - 1
Workbench/QMonitor/qmonlib.console/qmonlib.console.csproj

@@ -10,7 +10,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="qdr.fnd.core.qconsole" Version="0.0.1-alpha" />
+    <PackageReference Include="qdr.fnd.core.qconsole" Version="0.0.2-alpha" />
   </ItemGroup>
 
   <ItemGroup>

+ 1 - 0
Workbench/QMonitor/qmonlib.emit/Program.cs

@@ -91,6 +91,7 @@ namespace Quadarax.Foundation.Core.QMonitor.Emit // Note: actual namespace depen
             {
                 ctx.Status.Status = ServiceStatus.StatusEnum.Offline;
             }
+            System.Console.WriteLine($"# OnServiceStatusTimer-> status: {ctx.Status.Status}");
             ctx.Client.Notify(ctx.Status);
         }
 

+ 10 - 3
Workbench/QMonitor/qmonlib/Interfaces/IReceiverHandler.cs

@@ -1,20 +1,27 @@
 namespace Quadarax.Foundation.Core.QMonitor.Interfaces
 {
-    public delegate void ReceiverHandlerDataChanged(IReceiverHandler sender, ReceiverHandlerStateEnum state);
+    public delegate void ReceiverHandlerStateChanged(IReceiverHandler sender, ReceiverHandlerStateEnum state);
+    public delegate void ReceiverHandlerDataChanged(IReceiverHandler sender, MonData newData);
     public interface IReceiverHandler : IDisposable
     {
         string InstanceIdentifier { get;  }
         MonItemGeneral Descriptor { get; }
         ReceiverHandlerStateEnum State { get; }
         bool IsLiveBroadcast { get; }
-        DateTime Timestamp { get; }
+        DateTime? Timestamp { get; }
+        TimeSpan RefreshInterval { get; set; }
+        MonData[]? CurrentData { get; }
 
-        event ReceiverHandlerDataChanged? DataChanged;
+        event ReceiverHandlerStateChanged? ReceiverHandlerStateChanged;
+        event ReceiverHandlerDataChanged? ReceiverHandlerDataChanged;
 
         void Play(bool isLive);
         void Stop();
         void Rewind(TimeSpan? duration = null, bool isBackward = false);
+        void RewindToNextChange();
+        void RewindToPreviousChange();
         void Record();
+        
 
         DateTime? GetFirstAvailableDataTimestamp();
     }

+ 39 - 3
Workbench/QMonitor/qmonlib/MonData.cs

@@ -1,8 +1,11 @@
-using System.Text.Json.Serialization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Quadarax.Foundation.Core.Data;
+using Quadarax.Foundation.Core.Json.Extensions;
 
 namespace Quadarax.Foundation.Core.QMonitor
 {
-    public class MonData
+    public class MonData : IEquatable<MonData>
     {
         [JsonPropertyName("iid")]
         public string InstanceIdentifier { get; set; }
@@ -12,7 +15,40 @@ namespace Quadarax.Foundation.Core.QMonitor
         [JsonPropertyName("tms")]
         public DateTime Timestamp { get; set; }
         [JsonPropertyName("dta")]
-        public string Data { get; set; }
+        public string Data { get; set; } = string.Empty;
 
+        public IDictionary<string, JsonDocumentExt.DiffOccurrence>? Diff(MonData other)
+        {
+            if (other == null) throw new ArgumentNullException(nameof(other));
+
+            var thisDoc = JsonDocument.Parse(Data);
+            var otherDoc = JsonDocument.Parse(other.Data);
+            thisDoc.Diff(otherDoc);
+            return thisDoc.Diff(otherDoc);
+        }
+
+        public JsonDocument DataToJsonDocument()
+        {
+            return JsonDocument.Parse(Data);
+        } 
+        public bool Equals(MonData? other)
+        {
+            if (ReferenceEquals(null, other)) return false;
+            if (ReferenceEquals(this, other)) return true;
+            return InstanceIdentifier == other.InstanceIdentifier && Name == other.Name && Data == other.Data;
+        }
+
+        public override bool Equals(object? obj)
+        {
+            if (ReferenceEquals(null, obj)) return false;
+            if (ReferenceEquals(this, obj)) return true;
+            if (obj.GetType() != this.GetType()) return false;
+            return Equals((MonData)obj);
+        }
+
+        public override int GetHashCode()
+        {
+            return HashCode.Combine(InstanceIdentifier, Name, Data);
+        }
     }
 }

+ 24 - 0
Workbench/QMonitor/qmonlib/MonItemGeneral.cs

@@ -1,5 +1,7 @@
 using System.Reflection;
+using System.Text;
 using System.Text.Json.Serialization;
+using Quadarax.Foundation.Core.Data;
 
 namespace Quadarax.Foundation.Core.QMonitor
 {
@@ -19,6 +21,28 @@ namespace Quadarax.Foundation.Core.QMonitor
 
         public List<ItemDescriptor> Items { get; set; } = new List<ItemDescriptor>();
 
+
+        public string GetKeyAsString(TypedArgument[] values)
+        {
+            if (values== null) throw new ArgumentNullException(nameof(values));
+            var sb = new StringBuilder();
+            foreach (var key in KeyOrdinals)
+            {
+                var item = Items[key];
+                var val = values.FirstOrDefault(x =>
+                    string.Equals(x.Name, item.Name, StringComparison.InvariantCultureIgnoreCase));
+                if (val == null) throw new InvalidOperationException($"Key {item.Name} not found in provided values.");
+                sb.Append(val.Value).Append("_");
+            }
+
+            var result = sb.ToString();
+
+            if (result.Equals("_"))
+                result = result.Remove(result.Length - 1, 1);
+            return result;
+        }
+
+
         public class ItemDescriptor
         {
             [JsonPropertyName("nme")]

+ 115 - 21
Workbench/QMonitor/qmonlib/QMonReceiver.cs

@@ -1,9 +1,13 @@
 using System.Collections.Concurrent;
+using System.Collections.Immutable;
+using System.Diagnostics;
 using System.IO.Abstractions;
+using System.Net;
 using System.Net.Sockets;
 using System.Text;
 using System.Text.Json;
 using System.Text.Json.Serialization;
+using Quadarax.Foundation.Core.Json;
 using Quadarax.Foundation.Core.Logging;
 using Quadarax.Foundation.Core.QMonitor.Interfaces;
 using Quadarax.Foundation.Core.Thread;
@@ -30,10 +34,12 @@ namespace Quadarax.Foundation.Core.QMonitor
         private DateTime _lastSavedTimestamp;
         private readonly IFileSystem _fileSystem;
         private ContiguousWorker? _dataReceiverWorker;
+        private ILogger? _logger;
         #endregion
         #region *** Constructors ***
-        public QMonReceiver(IFileSystem fileSystem, QMonReceiverConfiguration configuration, ILogger logger, GeneralDataReceivedDelegate? generalDataReceivedCallback = null, DataReceivedDelegate? liveDataReceivedCallback = null) : base(configuration, logger)
+        public QMonReceiver(IFileSystem fileSystem, QMonReceiverConfiguration configuration,bool purgeOnInit, ILogger logger, GeneralDataReceivedDelegate? generalDataReceivedCallback = null, DataReceivedDelegate? liveDataReceivedCallback = null) : base(configuration, logger)
         {
+            _logger = logger;
             _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
             _lastSavedTimestamp = DateTime.Now;
             _generalReceivedCallback = generalDataReceivedCallback;
@@ -55,13 +61,85 @@ namespace Quadarax.Foundation.Core.QMonitor
                 Log(LogSeverityEnum.Debug, $"Data receiver created data cache directory '{Configuration.DataCache.Path}'");
 
             }
+
+            if (purgeOnInit)
+            {
+                var files = _fileSystem.Directory.GetFiles(Configuration.DataCache.Path, "*.json", SearchOption.AllDirectories);
+                var deletedCnt = 0;
+                foreach (var file in files)
+                {
+                    _fileSystem.File.Delete(file);
+                    deletedCnt++;
+                }
+                Log(LogSeverityEnum.Info, $"Data receiver purge data cache on init, removes {deletedCnt} files");
+            }
         }
         #endregion
 
         #region *** Public Operations ***
-        public IReceiverHandler CreateHandler(string instanceIdentifier, string name)
+        public IReceiverHandler CreateHandler(string instanceIdentifier, string name,TimeSpan refreshInterval,string? alternativeGeneralFile = null,  bool waitForGeneralData = false, TimeSpan? timeout = null)
+        {
+
+            if (alternativeGeneralFile != null && !string.IsNullOrEmpty(alternativeGeneralFile))
+            {
+                if (_fileSystem.File.Exists(alternativeGeneralFile))
+                {
+                    Log(LogSeverityEnum.Info, $"Using general file '{alternativeGeneralFile}' before initialization.");
+                    var binder = new Binder(_fileSystem, true, true, 5);
+                    var list = binder.Load<IEnumerable<MonItemGeneral>>(alternativeGeneralFile)?.ToList();
+                    if (list == null) throw new InvalidOperationException($"Cannot load general file '{alternativeGeneralFile}'");
+
+                    var ep = new IPEndPoint(IPAddress.Parse(UriGeneral.DnsSafeHost), UriGeneral.Port);
+                    var monGeneral = new MonGeneral() { InstanceIdentifier = instanceIdentifier, Items = list.ToList() };
+                        if (!_generalCache.ContainsKey(instanceIdentifier))
+                            _generalCache.Add(instanceIdentifier, new MonReceiverGeneralCahedItem(DateTime.Now,ep,monGeneral));
+                        else
+                        {
+                            foreach (var genitem in monGeneral.Items)
+                            {
+                                if (_generalCache[instanceIdentifier].Data.Items.All(x =>
+                                        string.Equals(x.Name, genitem.Name,
+                                            StringComparison.InvariantCultureIgnoreCase)))
+                                {
+                                    _generalCache[instanceIdentifier].Data.Items.Add(genitem);
+                                }
+                            }
+                        }
+                }
+            }
+
+            if (waitForGeneralData)
+                WaitForGeneralData(instanceIdentifier, name, timeout.GetValueOrDefault(Configuration.GeneralReceiverInterval));
+
+            if (_generalCache.TryGetValue(instanceIdentifier, out var item))
+            {
+                var generalItem = item.Data.Items.FirstOrDefault(x =>
+                    string.Equals(x.Name, name, StringComparison.InvariantCultureIgnoreCase));
+                if (generalItem == null) throw new InvalidOperationException($"Cannot find descriptor '{name}' in general data cache for instance identifier '{instanceIdentifier}'. Specification is not available yet.");
+                return new QMonReceiverHandler(_fileSystem, Configuration.DataCache.Path,instanceIdentifier, generalItem, refreshInterval, _logger);
+            }
+            throw new InvalidOperationException($"Instance identifier '{instanceIdentifier}' has not been available in general data cache yet.");
+        }
+
+        public bool WaitForGeneralData(string instanceIdentifier, string name, TimeSpan timeout)
         {
-            return null;
+            Log(LogSeverityEnum.Debug, $"Waiting for general descriptor for instance identifier '{instanceIdentifier}' and channel '{name}' for {timeout.Seconds} sec...");
+            var sw = Stopwatch.StartNew();
+            while (sw.Elapsed < timeout)
+            {
+                if (_generalCache.TryGetValue(instanceIdentifier, out var item))
+                {
+                    var generalItem = item.Data.Items.FirstOrDefault(x =>
+                                               string.Equals(x.Name, name, StringComparison.InvariantCultureIgnoreCase));
+                    if (generalItem != null)
+                    {
+                        Log(LogSeverityEnum.Debug, "Descriptor received.");
+                        return true;
+                    }
+                }
+                System.Threading.Thread.CurrentThread.Join(100);
+            }
+            return false;
         }
         #endregion
 
@@ -89,9 +167,9 @@ namespace Quadarax.Foundation.Core.QMonitor
         protected override async Task OnTimerData(QMonWorkerContext context)
         {
             _lastSavedTimestamp = DateTime.Now;
-            var itemsToSave = _dataCache.Where(x => x.Timestamp < _lastSavedTimestamp).ToArray();
-            var instances = itemsToSave.Select(x => x.InstanceIdentifier).Distinct();
-
+            IEnumerable<MonData> itemsToSave = _dataCache.Where(x => x.Timestamp < _lastSavedTimestamp).ToImmutableArray();
+            IEnumerable<string> instances = itemsToSave.Select(x => x.InstanceIdentifier).Distinct();
+       
             foreach (var instance in instances)
             {
                 var dirName = _fileSystem.Path.Combine(Configuration.DataCache.Path, instance);
@@ -104,17 +182,19 @@ namespace Quadarax.Foundation.Core.QMonitor
                 var batchData = itemsToSave.Where(x => x.InstanceIdentifier == instance).OrderBy(x => x.Timestamp).ToArray();
                 _dataReceivedCallback?.Invoke(batchData);
 
-                var dtFrom = batchData.Min(x => x.Timestamp).ToFileTimeUtc();
-                var dtTo = batchData.Max(x => x.Timestamp).ToFileTimeUtc();
-                var sbFileName = new StringBuilder();
-                sbFileName.Append("mondata_").Append(dtFrom).Append("-").Append(dtTo).Append(".json");
-                var fileName = _fileSystem.Path.Combine(dirName, sbFileName.ToString());
-                await using (var streamWriter = File.CreateText(fileName))
+                foreach (var data in batchData)
                 {
-                    await JsonSerializer.SerializeAsync(streamWriter.BaseStream, batchData, _jsonSerializerOptions,context.Cancel);
-                    await streamWriter.FlushAsync();
-                }
-                Log(LogSeverityEnum.Trace, $"Data receiver created dump data file '{fileName}'");
+                    var sbFileName = new StringBuilder();
+                    sbFileName.Append(data.Name).Append("_").Append(data.Timestamp.ToFileTimeUtc()).Append(".json");
+                    var fileName = _fileSystem.Path.Combine(dirName, sbFileName.ToString());
+                    await using (var streamWriter = File.CreateText(fileName))
+                    {
+                        await JsonSerializer.SerializeAsync(streamWriter.BaseStream, data, _jsonSerializerOptions,context.Cancel);
+                        await streamWriter.FlushAsync();
+                    }
+                    Log(LogSeverityEnum.Trace, $"Data receiver created dump data file '{fileName}'");
+                }               
+                
             }
 
             foreach (var item in itemsToSave)
@@ -129,11 +209,25 @@ namespace Quadarax.Foundation.Core.QMonitor
                 //    new IPEndPoint(IPAddress.Parse(UriGeneral.DnsSafeHost),UriGeneral.Port);
                 var data = await context.Client.ReceiveAsync(context.Cancel);
                 var general = (MonGeneral)Binder.LoadFromString(Encoding.UTF8.GetString(data.Buffer), typeof(MonGeneral));
-                Log(LogSeverityEnum.Debug,
+                Log(LogSeverityEnum.Trace,
                     $"[ReceiverGeneral] receive general data from '{data.RemoteEndPoint}' [{general.InstanceIdentifier}] size={data.Buffer.Length} bytes.");
 
-                _generalCache.TryAdd(general.InstanceIdentifier,
-                    new MonReceiverGeneralCahedItem(DateTime.Now, data.RemoteEndPoint, general));
+                if (_generalCache.TryGetValue(general.InstanceIdentifier, out var value))
+                {
+                    foreach (var item in general.Items)
+                    {
+                        if (!value.Data.Items.Any(x=>string.Equals(x.Name, item.Name, StringComparison.InvariantCultureIgnoreCase)))
+                        {
+                            value.Data.Items.Add(item);
+                            value.Received = DateTime.Now;
+                        }
+                    }
+                }
+                else
+                {
+                    _generalCache.TryAdd(general.InstanceIdentifier,
+                        new MonReceiverGeneralCahedItem(DateTime.Now, data.RemoteEndPoint, general));   
+                }
                 _generalReceivedCallback?.Invoke(general);
             }
             catch (SocketException e)
@@ -153,7 +247,7 @@ namespace Quadarax.Foundation.Core.QMonitor
                 try
                 {
                     System.Threading.Thread.CurrentThread.Join(100);
-                    Log(LogSeverityEnum.Debug, $"[ReceiverData] Waiting for data on '{UriData}'...");
+                    Log(LogSeverityEnum.Trace, $"[ReceiverData] Waiting for data on '{UriData}'...");
                     var data = await context.Client.ReceiveAsync(context.Cancel);
                     var monData = JsonSerializer.Deserialize<MonData>(data.Buffer, _jsonSerializerOptions);
                     if (monData == null)
@@ -162,7 +256,7 @@ namespace Quadarax.Foundation.Core.QMonitor
                         continue;
                     }
                     _dataCache.Add(monData);
-                    Log(LogSeverityEnum.Debug, $"[ReceiverData] receive monitor data from '{data.RemoteEndPoint}' size={data.Buffer.Length} bytes.");
+                    Log(LogSeverityEnum.Trace, $"[ReceiverData] receive monitor data from '{data.RemoteEndPoint}' size={data.Buffer.Length} bytes.");
                 }
                 catch (Exception e)
                 {

+ 327 - 12
Workbench/QMonitor/qmonlib/QMonReceiverHandler.cs

@@ -1,42 +1,119 @@
-using Quadarax.Foundation.Core.QMonitor.Interfaces;
+using System.Collections.Concurrent;
+using System.IO.Abstractions;
+using System.Text.Json;
+using Quadarax.Foundation.Core.Collections;
+using Quadarax.Foundation.Core.Json.Extensions;
+using Quadarax.Foundation.Core.Logging;
+using Quadarax.Foundation.Core.QMonitor.Interfaces;
 using Quadarax.Foundation.Core.Object;
+using Quadarax.Foundation.Core.Value.Extensions;
 
 namespace Quadarax.Foundation.Core.QMonitor
 {
     public class QMonReceiverHandler : DisposableObject, IReceiverHandler
     {
-        #region *** PRoperties ***
+        #region *** Properties ***
 
         public string InstanceIdentifier { get; }
         public MonItemGeneral Descriptor { get; }
-        public ReceiverHandlerStateEnum State => throw new NotImplementedException();
-        public bool IsLiveBroadcast => throw new NotImplementedException();
-        public DateTime Timestamp => throw new NotImplementedException();
 
-        public event ReceiverHandlerDataChanged? DataChanged;
+        public ReceiverHandlerStateEnum State
+        {
+            get => _state;
+            private set
+            {
+                _state = value;
+                ReceiverHandlerStateChanged?.Invoke(this, _state);
+            }
+        }
+
+        public bool IsLiveBroadcast {get; private set; }
+        public DateTime? Timestamp { get; private set; }
+        public TimeSpan RefreshInterval { get => _refreshInterval; set => SetRefreshInterval(value); }
+        public MonData[]? CurrentData { get; private set; }
+        public event ReceiverHandlerStateChanged? ReceiverHandlerStateChanged;
+        public event ReceiverHandlerDataChanged? ReceiverHandlerDataChanged;
+
+        #endregion
+
+        #region *** Private fields ***
+
+        private ReceiverHandlerStateEnum _state;
+        private TimeSpan _refreshInterval;
+        private IFileSystem _fileSystem;
+        private string _cachePath;
+        private ILog? _log;
+        private IFileSystemWatcher? _watcher;
+        private Timer _refreshTimer;
+        private readonly LockedList<DataCacheItem> _dataCache = new();
+
+        private bool _isOverlapOnRefreshTimer;
         #endregion
 
         #region *** Constructor ***
-        internal QMonReceiverHandler(string instanceIdentifier, MonItemGeneral descriptor)
+        internal QMonReceiverHandler(IFileSystem fileSystem, string cachePath, string instanceIdentifier, MonItemGeneral descriptor,TimeSpan refreshInterval, ILogger? logger)
         {
+            _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
+            if (string.IsNullOrEmpty(cachePath)) throw new ArgumentNullException(nameof(cachePath));
+            _cachePath = cachePath;
+            _log = logger?.GetLogger(nameof(QMonReceiverHandler));
+            
             if(string.IsNullOrEmpty(instanceIdentifier)) throw new ArgumentNullException(nameof(instanceIdentifier));
 
             InstanceIdentifier = instanceIdentifier;
             Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor));
+
+            RefreshFiles();
+            _watcher = _fileSystem.FileSystemWatcher.New(_fileSystem.Path.Combine(_cachePath, InstanceIdentifier),
+                GetFileSearchMask());
+            _watcher.Created += Watcher_Created;
+            _watcher.Deleted += Watcher_Deleted;
+            RefreshInterval = refreshInterval;
+            Timestamp = GetLastAvailableDataTimestamp();
+            _state = ReceiverHandlerStateEnum.Stop;
         }
+
         #endregion
 
         #region *** Public Operations ***
         public DateTime? GetFirstAvailableDataTimestamp()
         {
-            throw new NotImplementedException();
+            
+                if (_dataCache.Count == 0) return null;
+                var first = _dataCache.Select(x => x.Timestamp).MinBy(x => x);
+                return first;
+            
+        }
+        public DateTime? GetLastAvailableDataTimestamp()
+        {
+            
+                if (_dataCache.Count == 0) return null;
+                var last = _dataCache.Select(x => x.Timestamp).MinBy(x => x);
+                return last;
+            
         }
 
         public void Play(bool isLive)
         {
-            throw new NotImplementedException();
+            State = ReceiverHandlerStateEnum.Play;
+            if (isLive)
+            {
+                if (_dataCache.Count == 0) return;
+                SetAsCurrent(_dataCache.Last());
+            }
+            
+        }
+
+        public void RewindToPreviousChange()
+        {
+            RewindToChange(false);
+        }
+        public void RewindToNextChange()
+        {
+            RewindToChange(true);
         }
 
+
         public void Record()
         {
             throw new NotImplementedException();
@@ -44,19 +121,257 @@ namespace Quadarax.Foundation.Core.QMonitor
 
         public void Rewind(TimeSpan? duration = null, bool isBackward = false)
         {
-            throw new NotImplementedException();
+            State = ReceiverHandlerStateEnum.Rewind;
+            if (_dataCache.Count == 0 || _dataCache.Count == 1)
+            {
+                State = ReceiverHandlerStateEnum.Stop;
+                return;
+            }
+
+            if (duration == null || duration == TimeSpan.Zero)
+            {
+                if (isBackward)
+                    SetAsCurrent(_dataCache.First());
+                else
+                    SetAsCurrent(_dataCache.Last());
+
+                State = ReceiverHandlerStateEnum.Stop;
+                return;
+            }
+
+            var findTimestamp = (Timestamp ?? DateTime.Now);
+            findTimestamp = isBackward ? findTimestamp.Subtract(duration.Value) : findTimestamp.Add(duration.Value);
+            int newIndex = -1;
+            if (isBackward)
+            {
+                    for (var i = 0; i < _dataCache.Count; i++)
+                    {
+                        if (_dataCache[i].Timestamp >= findTimestamp)
+                        {
+                            newIndex = i;
+                            break;
+                        }
+                    }
+            
+            }
+            else
+            {
+            
+                    for (var i = _dataCache.Count - 1; i >= 0; i--)
+                    {
+                        if (_dataCache[i].Timestamp <= findTimestamp)
+                        {
+                            newIndex = i;
+                            break;
+                        }
+                    }
+
+            }
+
+            if (newIndex >= 0)
+            {
+                SetAsCurrent(_dataCache[newIndex]);
+            }
+            State = ReceiverHandlerStateEnum.Stop;
         }
 
+        
         public void Stop()
         {
-            throw new NotImplementedException();
+            State = ReceiverHandlerStateEnum.Stop;
         }
         #endregion
 
         #region *** Private Operations ***
+
+        public int FindNextChangeIndex(int currentIndex, bool isForward)
+        {
+
+            if (currentIndex < 0 || currentIndex >= _dataCache.Count) return -1;
+            var originalIndex = currentIndex;
+            if (isForward)
+            {
+                if (currentIndex >= _dataCache.Count - 1) return -1;
+                currentIndex++;
+            }
+            else
+            {
+                if (currentIndex <= 0) return -1;
+                currentIndex--;
+            }
+
+            if (_dataCache[originalIndex].Data.Diff(_dataCache[currentIndex].Data).Count > 0)
+                return currentIndex;
+
+            return FindNextChangeIndex(currentIndex, isForward);
+        }
+
+        public void RewindToChange(bool isForward)
+        {
+            if (Timestamp == null) return;
+            var timestamp = Timestamp;
+            int currentIndex = -1;
+            
+            var currentItem = _dataCache.FirstOrDefault(x => x.Timestamp == timestamp);
+            if (currentItem == null) return;
+            currentIndex = _dataCache.IndexOf(currentItem);
+            
+            var nextChangeIndex = FindNextChangeIndex(currentIndex, isForward);
+            if (nextChangeIndex >= 0)
+               SetAsCurrent(_dataCache[nextChangeIndex]);
+        }
+
+
+        private bool SetAsCurrent(DataCacheItem item)
+        {
+            var result = false;
+            using (var fReader = _fileSystem.File.OpenText(item.FileName))
+            {
+
+                var data = JsonSerializer.Deserialize<MonData>(fReader.BaseStream);
+                if (data != null)
+                {
+                    ReceiverHandlerDataChanged?.Invoke(this, data);
+                    Timestamp = item.Timestamp;
+                    CurrentData = new[] { data };
+                    result = true;
+                }
+            }
+            return result;
+        }
+        
+        private void SetRefreshInterval(TimeSpan refreshInterval)
+        {
+            _refreshInterval = refreshInterval;
+            _refreshTimer?.Dispose();
+            _log?.Log(LogSeverityEnum.Debug, $"[{InstanceIdentifier}/{Descriptor.Name}] Refresh interval was set to {RefreshInterval.ToReadableString()}");
+            _refreshTimer = new Timer(OnRefreshTimer, this, TimeSpan.Zero, refreshInterval);
+        }
+
+
+        private void OnRefreshTimer(object? state)
+        {
+            //TODO: add overlap checking here
+            if (!_isOverlapOnRefreshTimer)
+            {
+                _isOverlapOnRefreshTimer = true;
+                try
+                {
+                    if (State == ReceiverHandlerStateEnum.Play)
+                    {
+                        if (_dataCache.Count == 0) return;
+                        if (Timestamp < _dataCache.Last().Timestamp)
+                        {
+                            var currentItem = _dataCache.FirstOrDefault(x => x.Timestamp == Timestamp);
+                            if (currentItem == null) return;
+                            var currentIndex = _dataCache.IndexOf(currentItem);
+                            var newChangeIndex = FindNextChangeIndex(currentIndex, true);
+                            if (newChangeIndex >= 0)
+                                SetAsCurrent(_dataCache[newChangeIndex]);
+                        }
+                    }
+                }
+                catch (Exception ex)
+                {
+                    _log?.Log(LogSeverityEnum.Error,
+                        $"[{InstanceIdentifier}/{Descriptor.Name}] Error on refresh timer: {ex.Message}");
+                }
+                finally
+                {
+                    _isOverlapOnRefreshTimer = false;
+                }
+            }
+        }
+
+
+        private void RefreshFiles()
+        {
+            var result = new List<Tuple<string, DateTime>>();
+            var dirName = _fileSystem.Path.Combine(_cachePath, InstanceIdentifier);
+            if (!_fileSystem.Directory.Exists(dirName))
+            {
+                _fileSystem.Directory.CreateDirectory(dirName);
+                _log?.Log(LogSeverityEnum.Debug, $"Data receiver created data cache directory '{dirName}'");
+            }
+
+            var files = _fileSystem.Directory.GetFiles(dirName, GetFileSearchMask());
+            
+                var start = DateTime.Now;
+                
+                //remove missing files (was deleted by retention policy)
+                var cacheItemsToRemove = _dataCache.Where(x => !files.Contains(x.FileName)).ToList();
+                foreach (var item in cacheItemsToRemove)
+                {
+                    _dataCache.Remove(item);
+                    item.Data.Dispose();
+                }
+
+                var addedCnt = 0;
+                foreach (var file in files)
+                    if (MayAddFileToCache(file)) addedCnt++;
+
+                _log?.Log(LogSeverityEnum.Debug,$"[{InstanceIdentifier}/{Descriptor.Name}] Read {addedCnt} files and add to cache @ {DateTime.Now.Subtract(start).TotalMilliseconds} ms.");
+            
+        }
+
+        private bool MayAddFileToCache(string fileName)
+        {
+            if (_dataCache.Any(x=>x.FileName == fileName)) return false;
+            using (var fReader = _fileSystem.File.OpenText(fileName))
+            {
+                var data = JsonSerializer.Deserialize<MonData>(fReader.BaseStream);
+                if (data == null) return false;
+                _dataCache.Add(new DataCacheItem(fileName, data));
+            }
+
+            return true;
+        }
+        private string GetFileSearchMask()
+        {
+            return $"{Descriptor.Name}*.json";
+        }
         protected override void OnDisposing()
         {
-            DataChanged = null;
+            ReceiverHandlerStateChanged = null;
+            ReceiverHandlerStateChanged = null;
+            _watcher?.Dispose();
+            _watcher = null;
+            _refreshTimer?.Dispose();
+        }
+        private void Watcher_Deleted(object sender, FileSystemEventArgs e)
+        {
+            if (_dataCache.Any(x => x.FileName == e.FullPath))
+            {
+                var item = _dataCache.First(x => x.FileName == e.FullPath);
+                
+                    _dataCache.Remove(item);
+                    item.Data.Dispose();
+            }   
+        }
+
+        private void Watcher_Created(object sender, FileSystemEventArgs e)
+        {
+            if (_dataCache.All(x => x.FileName != e.FullPath))
+            {
+                MayAddFileToCache(e.FullPath);
+            }
+        }
+        #endregion
+
+        #region *** Nested Classes ***
+
+        private class DataCacheItem
+        {
+            public DateTime Timestamp { get; set; }
+            public JsonDocument Data { get; set; }
+            public string FileName { get; set; }
+
+            public DataCacheItem(string fileName, MonData data)
+            {
+                Timestamp = data.Timestamp;
+                Data = JsonDocument.Parse(data.Data);
+                FileName = fileName;
+            }
         }
         #endregion
     }

+ 2 - 0
Workbench/QMonitor/run_emit.cmd

@@ -0,0 +1,2 @@
+@echo off
+qmonlib.emit\bin\Debug\net7.0\qdr.fnd.core.qmonlib.emit.exe