DataCollector.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. using System.Text;
  2. using LibreHardwareMonitor.Hardware;
  3. using MinecraftStatus;
  4. using SteamQueryNet;
  5. using SteamQueryNet.Interfaces;
  6. using System.Text.Json;
  7. using Microsoft.Extensions.Configuration;
  8. using VeloeMonitorDataCollector.DatabaseConnectors;
  9. using VeloeMonitorDataCollector.Models;
  10. using Serilog;
  11. using VeloeMonitorDataCollector.Dependencies;
  12. namespace VeloeMonitorDataCollector
  13. {
  14. public class DataCollector
  15. {
  16. private Dictionary<string, Task> _updaterTasks; //TODO i can use an array here
  17. CancellationToken _token;
  18. CancellationTokenSource _cancellationTokenSource;
  19. Serilog.ILogger _logger;
  20. Computer? _computerHardware;
  21. float prevIdle = 0f;
  22. float prevTotal = 0f;
  23. List<DriveInfo> _drives;
  24. Dictionary<string, int> _deviceLoadSensorIndex = new()
  25. {
  26. {"cpuload", -1},
  27. {"ramavailable", -1},
  28. {"ramused", -1},
  29. {"ramload", -1}
  30. };
  31. private List<IDataSendable> _sendToDb; //TODO and here
  32. //Exception thrown on checking values
  33. /// <exception cref="ArgumentNullException">when there is no value in INI file or this value is not declared.</exception>
  34. /// <exception cref="ArgumentOutOfRangeException">when value in INI file does not match in range. Example: Port value range is 1..65565]</exception>
  35. /// <exception cref="FormatException">when value can not be parsed to needed type. Expample: Port value can't be NaN</exception>
  36. /// <exception cref="OverflowException">when provided value caused overflowed return variable in parse method.</exception>
  37. public DataCollector(in IConfiguration data,in Serilog.ILogger logger)
  38. {
  39. _updaterTasks = new Dictionary<string, Task>();
  40. _cancellationTokenSource = new CancellationTokenSource();
  41. _token = _cancellationTokenSource.Token;
  42. _logger = logger;
  43. //ini file check
  44. //is it needed?
  45. if (!File.Exists("config.ini"))
  46. {
  47. File.WriteAllText("config.ini",
  48. "#[Hardware]\n#hardware = true\n#hardwareUpdateInterval = true\n\n#[MySQL]\n#server = 127.0.0.1\n#port = 8806\n#uid = User\n#pwd = Password\n#database = values\n\n#[WebSoket]\n#url = http://192.168.1.2:5000\n\n#[MinecraftServer]\n#Ip = 127.0.0.1\n#Port = 25565\n#Type = Minecraft\n#updateInterval = 30\n\n#[SteamAPIServer]\n#Ip = 127.0.0.1\n#Port = 27015\n#Type = Steam\n#updateInterval = 30\n\n#[Gamespy3Server]\n#Ip = 127.0.0.1\n#Port = 5446\n#Type = Gamespy3\n#updateInterval = 30\n\n#[Gamespy2Server]\n#Ip = 127.0.0.1\n#Port = 10480\n#Type = Gamespy2\n#updateInterval = 30");
  49. throw new FileNotFoundException("config.ini does not exist! Default config was created as an example.");
  50. }
  51. //check read/write
  52. //is it needed
  53. FileStream config = File.Open("config.ini", FileMode.Open, FileAccess.ReadWrite);
  54. config.Close();
  55. config.Dispose();
  56. var hardware = data.GetSection("Hardware");
  57. //ini hardvare section validation
  58. if (hardware["hardware"] != "true" && hardware["hardware"] != "false" || hardware["hardware"] is null)
  59. {
  60. hardware["hardware"] = "false";
  61. }
  62. //configuring database connections
  63. var dbSections = data
  64. .GetChildren()
  65. .Where(section =>
  66. section.Path is ("MySQL" or "InfluxDB" or "TimescaleDB" or "WebSoket"))
  67. .ToArray();
  68. _sendToDb = new List<IDataSendable>();
  69. if (!dbSections.Any())
  70. {
  71. _logger.Information("No databases detected.");
  72. }
  73. else
  74. {
  75. foreach (var dbSection in dbSections)
  76. {
  77. //init db connections
  78. switch (dbSection.Path)
  79. {
  80. case "MySQL":
  81. try
  82. {
  83. var mySqlDb = new VeloeMonitorDataCollector.DatabaseConnectors.MySqlConnector(dbSection, logger);
  84. _sendToDb.Add(mySqlDb);
  85. }
  86. catch (Exception ex)
  87. {
  88. logger.Warning("Database connector not confugured properly. It won't be added in working configuration.");
  89. logger.Error(ex.Message);
  90. }
  91. break;
  92. case "WebSoket":
  93. try
  94. {
  95. SignalRConnector signalRWebApp = new(dbSection, logger);
  96. _sendToDb.Add(signalRWebApp);
  97. }
  98. catch (Exception ex)
  99. {
  100. logger.Warning("SignalR connector not confugured properly. It won't be added in working configuration.");
  101. logger.Error(ex.Message);
  102. }
  103. break;
  104. case "TimescaleDB":
  105. throw new NotImplementedException();
  106. case "InfluxDB":
  107. throw new NotImplementedException();
  108. }
  109. }
  110. }
  111. // checking params in game servers sections
  112. // configuring game servers
  113. var gameServersSections = data
  114. .GetChildren()
  115. .Where (section =>
  116. section.Key is not ("MySQL" or "InfluxDB" or"TimescaleDB" or "WebSoket" or "Hardware"))
  117. .ToArray();
  118. if (!gameServersSections.Any())
  119. {
  120. //add commented block with default example
  121. _logger.Information("No game servers detected.");
  122. }
  123. else
  124. foreach (var gameServer in gameServersSections)
  125. {
  126. //Console.WriteLine(gameServer.Key);
  127. if (gameServer["Ip"] == null ||
  128. gameServer["Port"] == null ||
  129. gameServer["Type"] == null)
  130. {
  131. throw new ArgumentNullException($"Some parameters for {gameServer.Key} are missing.");
  132. }
  133. if (!(Int32.Parse(gameServer["Port"]) is >= 1 and <= 65565))
  134. throw new ArgumentOutOfRangeException(gameServer["Port"]);
  135. foreach (char c in gameServer.Key)
  136. {
  137. if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')))
  138. {
  139. throw new ArgumentException($"Not allowed symbols in {gameServer.Key}. Only letters and numbers are allowed.");
  140. }
  141. }
  142. }
  143. // create task for hardware update
  144. if (bool.Parse(hardware["hardware"]))
  145. {
  146. _computerHardware = new Computer()
  147. {
  148. IsCpuEnabled = true,
  149. IsMemoryEnabled = true,
  150. };
  151. _drives = new List<DriveInfo>();
  152. DriveInfo[] allDrives = DriveInfo.GetDrives();
  153. foreach (DriveInfo d in allDrives)
  154. {
  155. if (d.IsReady == true && (d.DriveType is DriveType.Fixed or DriveType.Network) && d.TotalSize != 0 && !(d.Name.StartsWith("/boot") || d.Name.StartsWith("/dev")))
  156. {
  157. _drives.Add(d);
  158. }
  159. }
  160. _computerHardware.Open();
  161. SetValuesTimeZero(); //no history
  162. CreateHardwareInfoFile();
  163. //check database table configuration
  164. foreach (var database in _sendToDb)
  165. {
  166. var hardwareConfiguration = UpdateHardware();
  167. if (database.CheckHardware(hardwareConfiguration) is false)
  168. {
  169. throw new Exception(
  170. "Table configuration for hardware is invalid. Repair it manually or drop table and restart program.");
  171. }
  172. }
  173. int updateIntervalHardware;
  174. if (!Int32.TryParse(hardware["updateIntervalHardware"], out updateIntervalHardware))
  175. {
  176. _logger.Warning("Unable to parse updateIntervalHardware. Used default value.");
  177. updateIntervalHardware = 1;
  178. }
  179. _updaterTasks.Add("hardware", new Task(async () =>
  180. {
  181. while (!_token.IsCancellationRequested)
  182. {
  183. //Console.WriteLine(_token.IsCancellationRequested);
  184. var data = UpdateHardware();
  185. if (_sendToDb != null)
  186. foreach (var dbController in _sendToDb)
  187. {
  188. dbController.SendHardware(data);
  189. }
  190. await Task.Delay(TimeSpan.FromSeconds(updateIntervalHardware));
  191. }
  192. }, _token));
  193. }
  194. //create tasks for game servers updates
  195. foreach (var section in gameServersSections)
  196. {
  197. //check database table configuration
  198. foreach (var database in _sendToDb)
  199. { //Gamespy3 servers ignores it
  200. //Tablecheck in BuildUpdater method
  201. if (database.CheckGameServer(section.Key, section["Type"]) is false)
  202. {
  203. throw new Exception(
  204. $"Table {section.Key} configuration is invalid. Repair it manually or drop table and restart program.");
  205. }
  206. }
  207. int interval;
  208. if (!Int32.TryParse(section["updateInterval"], out interval))
  209. {
  210. _logger.Information("Unable to parse updateInterval. Used default value.");
  211. interval = 1;
  212. }
  213. BuildUpdater(section.Path,
  214. section["Ip"],
  215. Int32.Parse(section["Port"]),
  216. section["Type"], interval);
  217. }
  218. }
  219. public void Start()
  220. {
  221. foreach (var task in _updaterTasks)
  222. {
  223. _logger.Information("{0} started!",task.Key);
  224. task.Value.Start();
  225. }
  226. }
  227. public void Stop()
  228. {
  229. _cancellationTokenSource.Cancel();
  230. bool tasksStillActive = true;
  231. while (tasksStillActive)
  232. {
  233. tasksStillActive = false;
  234. foreach (var task in _updaterTasks)
  235. if (task.Value.Status == TaskStatus.Running) tasksStillActive = true;
  236. Task.Delay(TimeSpan.FromMilliseconds(250));
  237. }
  238. if (_computerHardware is not null)
  239. _computerHardware.Close();
  240. //wait to Tasks to end their execution
  241. //close all db connections
  242. foreach (var database in _sendToDb)
  243. {
  244. database.Close();
  245. }
  246. }
  247. private void SetValuesTimeZero()
  248. {
  249. if (_computerHardware is null)
  250. return;
  251. foreach (var hardware in _computerHardware.Hardware)
  252. {
  253. foreach (var sensor in hardware.Sensors)
  254. {
  255. sensor.ValuesTimeWindow = TimeSpan.Zero;
  256. }
  257. }
  258. }
  259. private void CreateHardwareInfoFile()
  260. {
  261. File.Create("sysinfo.txt").Close();
  262. StreamWriter sw = File.AppendText("sysinfo.txt");
  263. if (OperatingSystem.IsLinux())
  264. {
  265. var cpuLine = File
  266. .ReadAllLines("/proc/stat")
  267. .First()
  268. .Split(' ', StringSplitOptions.RemoveEmptyEntries)
  269. .Skip(1)
  270. .Select(float.Parse)
  271. .ToArray();
  272. var idle = cpuLine[3];
  273. var total = cpuLine.Sum();
  274. var percent = 100 * (1.0f - (idle - prevIdle) / (total - prevTotal));
  275. sw.WriteLine("CPU Load: {0}", percent);
  276. prevIdle = idle;
  277. prevTotal = total;
  278. }
  279. foreach (var hardware in _computerHardware.Hardware)
  280. {
  281. hardware.Update();
  282. //foreach (var sensor in hardware.Sensors)
  283. // _logger.Warning("{0}: {1}", sensor.Name, sensor.Value);
  284. sw.WriteLine(hardware.Name);
  285. sw.WriteLine(" Type: {0}", hardware.HardwareType);
  286. sw.WriteLine(" Type: {0}", hardware.Identifier);
  287. sw.WriteLine(" Sensors:");
  288. foreach (var sensor in hardware.Sensors)
  289. {
  290. sw.WriteLine(" {0,-25} {1,-15} {2,-15}",sensor.Name, sensor.SensorType, sensor.Value);
  291. }
  292. }
  293. foreach (DriveInfo d in DriveInfo.GetDrives())
  294. {
  295. if (d.IsReady == true && (d.DriveType is DriveType.Fixed or DriveType.Network) && d.TotalSize != 0 && !(d.Name.StartsWith("/boot") || d.Name.StartsWith("/dev")))
  296. {
  297. sw.WriteLine("Drive {0}", d.Name);
  298. sw.WriteLine(" File type: {0}", d.DriveType);
  299. sw.WriteLine(" Volume label: {0}", d.VolumeLabel);
  300. sw.WriteLine(" File system: {0}", d.DriveFormat);
  301. sw.WriteLine(
  302. " Available space to current user:{0, 15} bytes",
  303. d.AvailableFreeSpace);
  304. sw.WriteLine(
  305. " Total available space: {0, 15} bytes",
  306. d.TotalFreeSpace);
  307. sw.WriteLine(
  308. " Total size of drive: {0, 15} bytes ",
  309. d.TotalSize);
  310. }
  311. }
  312. sw.Close();
  313. }
  314. private Dictionary<string, float> UpdateHardware()
  315. {
  316. Dictionary<string, float> output = new Dictionary<string, float>();
  317. if (_computerHardware is null)
  318. return output;
  319. if (OperatingSystem.IsLinux())
  320. {
  321. var cpuLine = File
  322. .ReadAllLines("/proc/stat")
  323. .First()
  324. .Split(' ', StringSplitOptions.RemoveEmptyEntries)
  325. .Skip(1)
  326. .Select(float.Parse)
  327. .ToArray();
  328. var idle = cpuLine[3];
  329. var total = cpuLine.Sum();
  330. var percent = 100 * (1.0f - (idle - prevIdle) / (total - prevTotal));
  331. //for the first calc number can be inf or nan
  332. if (float.IsFinite(percent))
  333. output.Add("cpuload", percent);
  334. prevIdle = idle;
  335. prevTotal = total;
  336. }
  337. foreach (var hardware in _computerHardware.Hardware)
  338. {
  339. hardware.Update();
  340. //foreach (var sensor in hardware.Sensors)
  341. // _logger.Warning("{0}: {1}", sensor.Name, sensor.Value);
  342. if (hardware.HardwareType is HardwareType.Cpu && OperatingSystem.IsWindows())
  343. if (_deviceLoadSensorIndex["cpuload"] is not -1)
  344. output.Add("cpuload", hardware.Sensors[_deviceLoadSensorIndex["cpuload"]].Value.GetValueOrDefault());
  345. else
  346. {
  347. for(int i = 0; i < hardware.Sensors.Length; i++)
  348. if (hardware.Sensors[i].SensorType is SensorType.Load && hardware.Sensors[i].Name == "CPU Total")
  349. {
  350. output.Add("cpuload", hardware.Sensors[i].Value.GetValueOrDefault());
  351. _deviceLoadSensorIndex["cpuload"] = i;
  352. }
  353. }
  354. if (hardware.HardwareType is HardwareType.Memory)
  355. {
  356. //output.Add("ramavailable", hardware.Sensors[1].Value.GetValueOrDefault());
  357. if (_deviceLoadSensorIndex["ramavailable"] is not -1)
  358. output.Add("ramavailable", hardware.Sensors[_deviceLoadSensorIndex["ramavailable"]].Value.GetValueOrDefault());
  359. else
  360. {
  361. for (int i = 0; i < hardware.Sensors.Length; i++)
  362. if (hardware.Sensors[i].SensorType is SensorType.Data && hardware.Sensors[i].Name == "Memory Available")
  363. {
  364. output.Add("ramavailable", hardware.Sensors[i].Value.GetValueOrDefault());
  365. _deviceLoadSensorIndex["ramavailable"] = i;
  366. }
  367. }
  368. //output.Add("ramused", hardware.Sensors[0].Value.GetValueOrDefault());
  369. if (_deviceLoadSensorIndex["ramused"] is not -1)
  370. output.Add("ramused", hardware.Sensors[_deviceLoadSensorIndex["ramused"]].Value.GetValueOrDefault());
  371. else
  372. {
  373. for (int i = 0; i < hardware.Sensors.Length; i++)
  374. if (hardware.Sensors[i].SensorType is SensorType.Data && hardware.Sensors[i].Name == "Memory Used")
  375. {
  376. output.Add("ramused", hardware.Sensors[i].Value.GetValueOrDefault());
  377. _deviceLoadSensorIndex["ramused"] = i;
  378. }
  379. }
  380. //output.Add("ramload", hardware.Sensors[2].Value.GetValueOrDefault());
  381. if (_deviceLoadSensorIndex["ramload"] is not -1)
  382. output.Add("ramload", hardware.Sensors[_deviceLoadSensorIndex["ramload"]].Value.GetValueOrDefault());
  383. else
  384. {
  385. for (int i = 0; i < hardware.Sensors.Length; i++)
  386. if (hardware.Sensors[i].SensorType is SensorType.Load && hardware.Sensors[i].Name == "Memory")
  387. {
  388. output.Add("ramload", hardware.Sensors[i].Value.GetValueOrDefault());
  389. _deviceLoadSensorIndex["ramload"] = i;
  390. }
  391. }
  392. }
  393. }
  394. for (int i = 0; i < _drives.Count; i++)
  395. {
  396. _drives[i] = new DriveInfo(_drives[i].Name);
  397. var name = "";
  398. if (OperatingSystem.IsWindows())
  399. name = _drives[i].Name.ToLower().Replace(":\\", "").Replace('\\','_');
  400. else
  401. if (_drives[i].Name == "/")
  402. name = "root_directory";
  403. else
  404. name = _drives[i].Name.ToLower().Replace('/','_');
  405. output.Add(name , (float)_drives[i].TotalFreeSpace / _drives[i].TotalSize);
  406. }
  407. return output;
  408. }
  409. private void BuildUpdater(string name ,string ip, int port, string type, int interval)
  410. {
  411. switch (type)
  412. {
  413. case "Minecraft":
  414. _updaterTasks.Add(name, new Task(async () => {
  415. while (!_token.IsCancellationRequested)
  416. {
  417. await Task.Delay(TimeSpan.FromSeconds(interval));
  418. McStatus? data = null;
  419. data = UpdateMinecraft(ip, port);
  420. if (data is null)
  421. continue;
  422. if (_sendToDb is null)
  423. continue;
  424. foreach (var dbController in _sendToDb)
  425. {
  426. //_logger.Debug("Sending {0}", name);
  427. dbController.SendMinecraft(data,name);
  428. }
  429. }
  430. }, _token));
  431. break;
  432. case "Steam":
  433. _updaterTasks.Add(name, new Task(async () => {
  434. while (!_token.IsCancellationRequested)
  435. {
  436. await Task.Delay(TimeSpan.FromSeconds(interval));
  437. SteamData? data = null;
  438. data = await UpdateSteam(ip, port, interval);
  439. if (data is null)
  440. continue;
  441. if (_sendToDb is null)
  442. continue;
  443. foreach (var dbController in _sendToDb)
  444. {
  445. dbController.SendSteam(data.Value,name);
  446. }
  447. }
  448. }, _token));
  449. break;
  450. case "Gamespy3":
  451. _updaterTasks.Add(name, new Task(async () => {
  452. try
  453. {
  454. Gs3Status server = new Gs3Status(ip, port);
  455. /*
  456. bool dataIsNull = true;
  457. while (dataIsNull && !_token.IsCancellationRequested)
  458. {
  459. var data = server.GetStatus();
  460. if (data is null)
  461. continue;
  462. if (_sendToDb is null)
  463. dataIsNull = false;
  464. foreach(var dbController in _sendToDb)
  465. {
  466. dbController.CheckGamespy3(data, name);
  467. }
  468. }
  469. */
  470. while (!_token.IsCancellationRequested)
  471. {
  472. try
  473. {
  474. await Task.Delay(TimeSpan.FromSeconds(interval));
  475. var data = server.GetStatus();
  476. if (data is null)
  477. continue;
  478. if (_sendToDb is null)
  479. continue;
  480. foreach (var dbController in _sendToDb)
  481. {
  482. dbController.SendGamespy3(data, name);
  483. }
  484. }
  485. catch (System.Net.Sockets.SocketException ex)
  486. {
  487. _logger.Debug("{0}:{1} {2}", ip, port, ex.Message);
  488. }
  489. }
  490. }
  491. catch(System.Net.Sockets.SocketException ex)
  492. {
  493. _logger.Debug("{0}:{1} {2}", ip, port, ex.Message);
  494. }
  495. }, _token));
  496. break;
  497. case "Gamespy2":
  498. _updaterTasks.Add(name, new Task(async () => {
  499. try
  500. {
  501. Gs2Status server = new Gs2Status(ip, port);
  502. while (!_token.IsCancellationRequested)
  503. {
  504. try
  505. {
  506. await Task.Delay(TimeSpan.FromSeconds(interval));
  507. var data = server.GetStatus();
  508. if (data is null)
  509. continue;
  510. if (_sendToDb is null)
  511. continue;
  512. foreach (var dbController in _sendToDb)
  513. {
  514. dbController.SendGamespy2(data, name);
  515. }
  516. }
  517. catch (System.Net.Sockets.SocketException ex)
  518. {
  519. _logger.Debug("{0}:{1} {2}", ip, port, ex.Message);
  520. }
  521. }
  522. }
  523. catch (System.Net.Sockets.SocketException ex)
  524. {
  525. _logger.Debug("{0}:{1} {2}", ip, port, ex.Message);
  526. }
  527. }, _token));
  528. break;
  529. }
  530. }
  531. private McStatus? UpdateMinecraft(string ip, int port)
  532. {
  533. McStatus? status = null;
  534. try
  535. {
  536. status = McStatus.GetStatus(ip, port);
  537. }
  538. catch (Exception ex)
  539. {
  540. _logger.Debug("{0}:{1} {2}", ip, port, ex.Message);
  541. }
  542. if (status != null)
  543. return status;
  544. return null;
  545. }
  546. private async Task<SteamData?> UpdateSteam(string ip, int port, int interval)
  547. {
  548. ServerQuery serverQuery = new ServerQuery();
  549. try
  550. {
  551. serverQuery.SendTimeout = 2000;
  552. serverQuery.ReceiveTimeout = 2000;
  553. SteamData? output = null;
  554. serverQuery.Connect(ip, (ushort)port);
  555. var serverInfo = await serverQuery.GetServerInfoAsync();
  556. var serverPlayers = await serverQuery.GetPlayersAsync();
  557. output = new SteamData
  558. {
  559. ServerInfo = serverInfo,
  560. Players = serverPlayers
  561. };
  562. if (output is null)
  563. return null;
  564. return output;
  565. }
  566. catch (TimeoutException ex)
  567. {
  568. _logger.Debug("{0}:{1} {2}", ip, port, ex.Message);
  569. return null;
  570. }
  571. catch (Exception ex)
  572. {
  573. _logger.Debug("{0}:{1} {2}", ip, port, ex.Message);
  574. return null;
  575. }
  576. }
  577. }
  578. }