QMonReceiverHandler.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. using System.Collections.Concurrent;
  2. using System.IO.Abstractions;
  3. using System.Text.Json;
  4. using Quadarax.Foundation.Core.Collections;
  5. using Quadarax.Foundation.Core.Json.Extensions;
  6. using Quadarax.Foundation.Core.Logging;
  7. using Quadarax.Foundation.Core.QMonitor.Interfaces;
  8. using Quadarax.Foundation.Core.Object;
  9. using Quadarax.Foundation.Core.Value.Extensions;
  10. namespace Quadarax.Foundation.Core.QMonitor
  11. {
  12. public class QMonReceiverHandler : DisposableObject, IReceiverHandler
  13. {
  14. #region *** Properties ***
  15. public string InstanceIdentifier { get; }
  16. public MonItemGeneral Descriptor { get; }
  17. public ReceiverHandlerStateEnum State
  18. {
  19. get => _state;
  20. private set
  21. {
  22. _state = value;
  23. ReceiverHandlerStateChanged?.Invoke(this, _state);
  24. }
  25. }
  26. public bool IsLiveBroadcast {get; private set; }
  27. public DateTime? Timestamp { get; private set; }
  28. public TimeSpan RefreshInterval { get => _refreshInterval; set => SetRefreshInterval(value); }
  29. public MonData[]? CurrentData { get; private set; }
  30. public event ReceiverHandlerStateChanged? ReceiverHandlerStateChanged;
  31. public event ReceiverHandlerDataChanged? ReceiverHandlerDataChanged;
  32. #endregion
  33. #region *** Private fields ***
  34. private ReceiverHandlerStateEnum _state;
  35. private TimeSpan _refreshInterval;
  36. private IFileSystem _fileSystem;
  37. private string _cachePath;
  38. private ILog? _log;
  39. private IFileSystemWatcher? _watcher;
  40. private Timer _refreshTimer;
  41. private readonly LockedList<DataCacheItem> _dataCache = new();
  42. private bool _isOverlapOnRefreshTimer;
  43. #endregion
  44. #region *** Constructor ***
  45. internal QMonReceiverHandler(IFileSystem fileSystem, string cachePath, string instanceIdentifier, MonItemGeneral descriptor,TimeSpan refreshInterval, ILogger? logger)
  46. {
  47. _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
  48. if (string.IsNullOrEmpty(cachePath)) throw new ArgumentNullException(nameof(cachePath));
  49. _cachePath = cachePath;
  50. _log = logger?.GetLogger(nameof(QMonReceiverHandler));
  51. if(string.IsNullOrEmpty(instanceIdentifier)) throw new ArgumentNullException(nameof(instanceIdentifier));
  52. InstanceIdentifier = instanceIdentifier;
  53. Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor));
  54. RefreshFiles();
  55. _watcher = _fileSystem.FileSystemWatcher.New(_fileSystem.Path.Combine(_cachePath, InstanceIdentifier),
  56. GetFileSearchMask());
  57. _watcher.Created += Watcher_Created;
  58. _watcher.Deleted += Watcher_Deleted;
  59. RefreshInterval = refreshInterval;
  60. Timestamp = GetLastAvailableDataTimestamp();
  61. _state = ReceiverHandlerStateEnum.Stop;
  62. }
  63. #endregion
  64. #region *** Public Operations ***
  65. public DateTime? GetFirstAvailableDataTimestamp()
  66. {
  67. if (_dataCache.Count == 0) return null;
  68. var first = _dataCache.Select(x => x.Timestamp).MinBy(x => x);
  69. return first;
  70. }
  71. public DateTime? GetLastAvailableDataTimestamp()
  72. {
  73. if (_dataCache.Count == 0) return null;
  74. var last = _dataCache.Select(x => x.Timestamp).MinBy(x => x);
  75. return last;
  76. }
  77. public void Play(bool isLive)
  78. {
  79. State = ReceiverHandlerStateEnum.Play;
  80. if (isLive)
  81. {
  82. if (_dataCache.Count == 0) return;
  83. SetAsCurrent(_dataCache.Last());
  84. }
  85. }
  86. public void RewindToPreviousChange()
  87. {
  88. RewindToChange(false);
  89. }
  90. public void RewindToNextChange()
  91. {
  92. RewindToChange(true);
  93. }
  94. public void Record()
  95. {
  96. throw new NotImplementedException();
  97. }
  98. public void Rewind(TimeSpan? duration = null, bool isBackward = false)
  99. {
  100. State = ReceiverHandlerStateEnum.Rewind;
  101. if (_dataCache.Count == 0 || _dataCache.Count == 1)
  102. {
  103. State = ReceiverHandlerStateEnum.Stop;
  104. return;
  105. }
  106. if (duration == null || duration == TimeSpan.Zero)
  107. {
  108. if (isBackward)
  109. SetAsCurrent(_dataCache.First());
  110. else
  111. SetAsCurrent(_dataCache.Last());
  112. State = ReceiverHandlerStateEnum.Stop;
  113. return;
  114. }
  115. var findTimestamp = (Timestamp ?? DateTime.Now);
  116. findTimestamp = isBackward ? findTimestamp.Subtract(duration.Value) : findTimestamp.Add(duration.Value);
  117. int newIndex = -1;
  118. if (isBackward)
  119. {
  120. for (var i = 0; i < _dataCache.Count; i++)
  121. {
  122. if (_dataCache[i].Timestamp >= findTimestamp)
  123. {
  124. newIndex = i;
  125. break;
  126. }
  127. }
  128. }
  129. else
  130. {
  131. for (var i = _dataCache.Count - 1; i >= 0; i--)
  132. {
  133. if (_dataCache[i].Timestamp <= findTimestamp)
  134. {
  135. newIndex = i;
  136. break;
  137. }
  138. }
  139. }
  140. if (newIndex >= 0)
  141. {
  142. SetAsCurrent(_dataCache[newIndex]);
  143. }
  144. State = ReceiverHandlerStateEnum.Stop;
  145. }
  146. public void Stop()
  147. {
  148. State = ReceiverHandlerStateEnum.Stop;
  149. }
  150. #endregion
  151. #region *** Private Operations ***
  152. public int FindNextChangeIndex(int currentIndex, bool isForward)
  153. {
  154. if (currentIndex < 0 || currentIndex >= _dataCache.Count) return -1;
  155. var originalIndex = currentIndex;
  156. if (isForward)
  157. {
  158. if (currentIndex >= _dataCache.Count - 1) return -1;
  159. currentIndex++;
  160. }
  161. else
  162. {
  163. if (currentIndex <= 0) return -1;
  164. currentIndex--;
  165. }
  166. if (_dataCache[originalIndex].Data.Diff(_dataCache[currentIndex].Data).Count > 0)
  167. return currentIndex;
  168. return FindNextChangeIndex(currentIndex, isForward);
  169. }
  170. public void RewindToChange(bool isForward)
  171. {
  172. if (Timestamp == null) return;
  173. var timestamp = Timestamp;
  174. int currentIndex = -1;
  175. var currentItem = _dataCache.FirstOrDefault(x => x.Timestamp == timestamp);
  176. if (currentItem == null) return;
  177. currentIndex = _dataCache.IndexOf(currentItem);
  178. var nextChangeIndex = FindNextChangeIndex(currentIndex, isForward);
  179. if (nextChangeIndex >= 0)
  180. SetAsCurrent(_dataCache[nextChangeIndex]);
  181. }
  182. private bool SetAsCurrent(DataCacheItem item)
  183. {
  184. var result = false;
  185. using (var fReader = _fileSystem.File.OpenText(item.FileName))
  186. {
  187. var data = JsonSerializer.Deserialize<MonData>(fReader.BaseStream);
  188. if (data != null)
  189. {
  190. ReceiverHandlerDataChanged?.Invoke(this, data);
  191. Timestamp = item.Timestamp;
  192. CurrentData = new[] { data };
  193. result = true;
  194. }
  195. }
  196. return result;
  197. }
  198. private void SetRefreshInterval(TimeSpan refreshInterval)
  199. {
  200. _refreshInterval = refreshInterval;
  201. _refreshTimer?.Dispose();
  202. _log?.Log(LogSeverityEnum.Debug, $"[{InstanceIdentifier}/{Descriptor.Name}] Refresh interval was set to {RefreshInterval.ToReadableString()}");
  203. _refreshTimer = new Timer(OnRefreshTimer, this, TimeSpan.Zero, refreshInterval);
  204. }
  205. private void OnRefreshTimer(object? state)
  206. {
  207. //TODO: add overlap checking here
  208. if (!_isOverlapOnRefreshTimer)
  209. {
  210. _isOverlapOnRefreshTimer = true;
  211. try
  212. {
  213. if (State == ReceiverHandlerStateEnum.Play)
  214. {
  215. if (_dataCache.Count == 0) return;
  216. if (Timestamp < _dataCache.Last().Timestamp)
  217. {
  218. var currentItem = _dataCache.FirstOrDefault(x => x.Timestamp == Timestamp);
  219. if (currentItem == null) return;
  220. var currentIndex = _dataCache.IndexOf(currentItem);
  221. var newChangeIndex = FindNextChangeIndex(currentIndex, true);
  222. if (newChangeIndex >= 0)
  223. SetAsCurrent(_dataCache[newChangeIndex]);
  224. }
  225. }
  226. }
  227. catch (Exception ex)
  228. {
  229. _log?.Log(LogSeverityEnum.Error,
  230. $"[{InstanceIdentifier}/{Descriptor.Name}] Error on refresh timer: {ex.Message}");
  231. }
  232. finally
  233. {
  234. _isOverlapOnRefreshTimer = false;
  235. }
  236. }
  237. }
  238. private void RefreshFiles()
  239. {
  240. var result = new List<Tuple<string, DateTime>>();
  241. var dirName = _fileSystem.Path.Combine(_cachePath, InstanceIdentifier);
  242. if (!_fileSystem.Directory.Exists(dirName))
  243. {
  244. _fileSystem.Directory.CreateDirectory(dirName);
  245. _log?.Log(LogSeverityEnum.Debug, $"Data receiver created data cache directory '{dirName}'");
  246. }
  247. var files = _fileSystem.Directory.GetFiles(dirName, GetFileSearchMask());
  248. var start = DateTime.Now;
  249. //remove missing files (was deleted by retention policy)
  250. var cacheItemsToRemove = _dataCache.Where(x => !files.Contains(x.FileName)).ToList();
  251. foreach (var item in cacheItemsToRemove)
  252. {
  253. _dataCache.Remove(item);
  254. item.Data.Dispose();
  255. }
  256. var addedCnt = 0;
  257. foreach (var file in files)
  258. if (MayAddFileToCache(file)) addedCnt++;
  259. _log?.Log(LogSeverityEnum.Debug,$"[{InstanceIdentifier}/{Descriptor.Name}] Read {addedCnt} files and add to cache @ {DateTime.Now.Subtract(start).TotalMilliseconds} ms.");
  260. }
  261. private bool MayAddFileToCache(string fileName)
  262. {
  263. if (_dataCache.Any(x=>x.FileName == fileName)) return false;
  264. using (var fReader = _fileSystem.File.OpenText(fileName))
  265. {
  266. var data = JsonSerializer.Deserialize<MonData>(fReader.BaseStream);
  267. if (data == null) return false;
  268. _dataCache.Add(new DataCacheItem(fileName, data));
  269. }
  270. return true;
  271. }
  272. private string GetFileSearchMask()
  273. {
  274. return $"{Descriptor.Name}*.json";
  275. }
  276. protected override void OnDisposing()
  277. {
  278. ReceiverHandlerStateChanged = null;
  279. ReceiverHandlerStateChanged = null;
  280. _watcher?.Dispose();
  281. _watcher = null;
  282. _refreshTimer?.Dispose();
  283. }
  284. private void Watcher_Deleted(object sender, FileSystemEventArgs e)
  285. {
  286. if (_dataCache.Any(x => x.FileName == e.FullPath))
  287. {
  288. var item = _dataCache.First(x => x.FileName == e.FullPath);
  289. _dataCache.Remove(item);
  290. item.Data.Dispose();
  291. }
  292. }
  293. private void Watcher_Created(object sender, FileSystemEventArgs e)
  294. {
  295. if (_dataCache.All(x => x.FileName != e.FullPath))
  296. {
  297. MayAddFileToCache(e.FullPath);
  298. }
  299. }
  300. #endregion
  301. #region *** Nested Classes ***
  302. private class DataCacheItem
  303. {
  304. public DateTime Timestamp { get; set; }
  305. public JsonDocument Data { get; set; }
  306. public string FileName { get; set; }
  307. public DataCacheItem(string fileName, MonData data)
  308. {
  309. Timestamp = data.Timestamp;
  310. Data = JsonDocument.Parse(data.Data);
  311. FileName = fileName;
  312. }
  313. }
  314. #endregion
  315. }
  316. }