using System.Text; using LibreHardwareMonitor.Hardware; using MinecraftStatus; using SteamQueryNet; using SteamQueryNet.Interfaces; using System.Text.Json; using Microsoft.Extensions.Configuration; using VeloeMonitorDataCollector.DatabaseConnectors; using VeloeMonitorDataCollector.Models; using Serilog; using VeloeMonitorDataCollector.Dependencies; namespace VeloeMonitorDataCollector { public class DataCollector { private Dictionary _updaterTasks; //TODO i can use an array here CancellationToken _token; CancellationTokenSource _cancellationTokenSource; Serilog.ILogger _logger; Computer? _computerHardware; private List _sendToDb; //TODO and here //Exception thrown on checking values /// when there is no value in INI file or this value is not declared. /// when value in INI file does not match in range. Example: Port value range is 1..65565] /// when value can not be parsed to needed type. Expample: Port value can't be NaN /// when provided value caused overflowed return variable in parse method. public DataCollector(in IConfiguration data,in Serilog.ILogger logger) { _updaterTasks = new Dictionary(); _cancellationTokenSource = new CancellationTokenSource(); _token = _cancellationTokenSource.Token; _logger = logger; //ini file check //is it needed? if (!File.Exists("config.ini")) { File.WriteAllText("config.ini", "#[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"); throw new FileNotFoundException("config.ini does not exist! Default config was created as an example."); } //check read/write //is it needed FileStream config = File.Open("config.ini", FileMode.Open, FileAccess.ReadWrite); config.Close(); config.Dispose(); var hardware = data.GetSection("Hardware"); //ini hardvare section validation if (hardware["hardware"] != "true" && hardware["hardware"] != "false" || hardware["hardware"] is null) { hardware["hardware"] = "false"; } //configuring database connections var dbSections = data .GetChildren() .Where(section => section.Path is ("MySQL" or "InfluxDB" or "TimescaleDB" or "WebSoket")) .ToArray(); _sendToDb = new List(); if (!dbSections.Any()) { _logger.Information("No databases detected."); } else { foreach (var dbSection in dbSections) { //init db connections switch (dbSection.Path) { case "MySQL": try { MySqlConnector mySqlDb = new MySqlConnector(dbSection, logger); _sendToDb.Add(mySqlDb); } catch (Exception ex) { logger.Warning("Database connector not confugured properly. It won't be added in working configuration."); } break; case "WebSoket": try { SignalRConnector signalRWebApp = new(dbSection, logger); _sendToDb.Add(signalRWebApp); } catch (Exception ex) { logger.Warning("SignalR connector not confugured properly. It won't be added in working configuration."); } break; case "TimescaleDB": throw new NotImplementedException(); case "InfluxDB": throw new NotImplementedException(); } } } // checking params in game servers sections // configuring game servers var gameServersSections = data .GetChildren() .Where (section => section.Key is not ("MySQL" or "InfluxDB" or"TimescaleDB" or "WebSoket" or "Hardware")) .ToArray(); if (!gameServersSections.Any()) { //add commented block with default example _logger.Information("No game servers detected."); } else foreach (var gameServer in gameServersSections) { //Console.WriteLine(gameServer.Key); if (gameServer["Ip"] == null || gameServer["Port"] == null || gameServer["Type"] == null) { throw new ArgumentNullException($"Some parameters for {gameServer.Key} are missing."); } if (!(Int32.Parse(gameServer["Port"]) is >= 1 and <= 65565)) throw new ArgumentOutOfRangeException(gameServer["Port"]); foreach (char c in gameServer.Key) { if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) { throw new ArgumentException($"Not allowed symbols in {gameServer.Key}. Only letters and numbers are allowed."); } } } // create task for hardware update if (bool.Parse(hardware["hardware"])) { _computerHardware = new Computer() { IsCpuEnabled = true, IsMemoryEnabled = true, IsStorageEnabled = true, }; _computerHardware.Open(); SetValuesTimeZero(); //why //check database table configuration foreach (var database in _sendToDb) { var hardwareConfiguration = UpdateHardware(); if (database.CheckHardware(hardwareConfiguration) is false) { throw new Exception( "Table configuration for hardware is invalid. Repair it manually or drop table and restart program."); } } int updateIntervalHardware; if (!Int32.TryParse(hardware["updateIntervalHardware"], out updateIntervalHardware)) { _logger.Warning("Unable to parse updateIntervalHardware. Used default value."); updateIntervalHardware = 1; } _updaterTasks.Add("hardware", new Task(async () => { while (!_token.IsCancellationRequested) { //Console.WriteLine(_token.IsCancellationRequested); var data = UpdateHardware(); if (_sendToDb != null) foreach (var dbController in _sendToDb) { dbController.SendHardware(data); } await Task.Delay(TimeSpan.FromSeconds(updateIntervalHardware)); } }, _token)); } //create tasks for game servers updates foreach (var section in gameServersSections) { //check database table configuration foreach (var database in _sendToDb) { //Gamespy3 servers ignores it //Tablecheck in BuildUpdater method if (database.CheckGameServer(section.Key, section["Type"]) is false) { throw new Exception( $"Table {section.Key} configuration is invalid. Repair it manually or drop table and restart program."); } } int interval; if (!Int32.TryParse(section["updateInterval"], out interval)) { _logger.Information("Unable to parse updateInterval. Used default value."); interval = 1; } BuildUpdater(section.Path, section["Ip"], Int32.Parse(section["Port"]), section["Type"], interval); } } public void Start() { foreach (var task in _updaterTasks) { _logger.Information("{0} started!",task.Key); task.Value.Start(); } } public void Stop() { _cancellationTokenSource.Cancel(); bool tasksStillActive = true; while (tasksStillActive) { tasksStillActive = false; foreach (var task in _updaterTasks) if (task.Value.Status == TaskStatus.Running) tasksStillActive = true; Task.Delay(TimeSpan.FromMilliseconds(250)); } if (_computerHardware is not null) _computerHardware.Close(); //wait to Tasks to end their execution //close all db connections foreach (var database in _sendToDb) { database.Close(); } } private void SetValuesTimeZero() { if (_computerHardware is null) return; foreach (var hardware in _computerHardware.Hardware) { foreach (var sensor in hardware.Sensors) { sensor.ValuesTimeWindow = TimeSpan.Zero; } } } private Dictionary UpdateHardware() { Dictionary output = new Dictionary(); if (_computerHardware is null) return output; foreach (var hardware in _computerHardware.Hardware) { hardware.Update(); if (hardware.HardwareType is HardwareType.Cpu) output.Add("cpuload", hardware.Sensors[8].Value.GetValueOrDefault()); if (hardware.HardwareType is HardwareType.Memory) { output.Add("ramavailable", hardware.Sensors[1].Value.GetValueOrDefault()); output.Add("ramused", hardware.Sensors[0].Value.GetValueOrDefault()); output.Add("ramload", hardware.Sensors[2].Value.GetValueOrDefault()); } if (hardware.HardwareType is HardwareType.Storage) { StringBuilder sb = new StringBuilder(); foreach (char c in hardware.Name.ToLower()) { if (c is not (' ' or '/' or '-')) { sb.Append(c); } } output.Add(sb.ToString(), hardware.Sensors[1].Value.GetValueOrDefault()); } } return output; } private void BuildUpdater(string name ,string ip, int port, string type, int interval) { switch (type) { case "Minecraft": _updaterTasks.Add(name, new Task(async () => { while (!_token.IsCancellationRequested) { await Task.Delay(TimeSpan.FromSeconds(interval)); McStatus? data = null; data = UpdateMinecraft(ip, port); if (data is null) continue; if (_sendToDb is null) continue; foreach (var dbController in _sendToDb) { //_logger.Debug("Sending {0}", name); dbController.SendMinecraft(data,name); } } }, _token)); break; case "Steam": _updaterTasks.Add(name, new Task(async () => { while (!_token.IsCancellationRequested) { await Task.Delay(TimeSpan.FromSeconds(interval)); SteamData? data = null; data = await UpdateSteam(ip, port, interval); if (data is null) continue; if (_sendToDb is null) continue; foreach (var dbController in _sendToDb) { dbController.SendSteam(data.Value,name); } } }, _token)); break; case "Gamespy3": _updaterTasks.Add(name, new Task(async () => { try { Gs3Status server = new Gs3Status(ip, port); /* bool dataIsNull = true; while (dataIsNull && !_token.IsCancellationRequested) { var data = server.GetStatus(); if (data is null) continue; if (_sendToDb is null) dataIsNull = false; foreach(var dbController in _sendToDb) { dbController.CheckGamespy3(data, name); } } */ while (!_token.IsCancellationRequested) { try { await Task.Delay(TimeSpan.FromSeconds(interval)); var data = server.GetStatus(); if (data is null) continue; if (_sendToDb is null) continue; foreach (var dbController in _sendToDb) { dbController.SendGamespy3(data, name); } } catch (System.Net.Sockets.SocketException ex) { _logger.Debug(ex.Message); } } } catch(System.Net.Sockets.SocketException ex) { _logger.Debug(ex.Message); } }, _token)); break; case "Gamespy2": _updaterTasks.Add(name, new Task(async () => { try { Gs2Status server = new Gs2Status(ip, port); while (!_token.IsCancellationRequested) { try { await Task.Delay(TimeSpan.FromSeconds(interval)); var data = server.GetStatus(); if (data is null) continue; if (_sendToDb is null) continue; foreach (var dbController in _sendToDb) { dbController.SendGamespy2(data, name); } } catch (System.Net.Sockets.SocketException ex) { _logger.Debug(ex.Message); } } } catch (System.Net.Sockets.SocketException ex) { _logger.Debug(ex.Message); } }, _token)); break; } } private McStatus? UpdateMinecraft(string ip, int port) { McStatus? status = null; try { status = McStatus.GetStatus(ip, port); } catch (Exception ex) { _logger.Debug(ex.Message); } if (status != null) return status; return null; } private async Task UpdateSteam(string ip, int port, int interval) { ServerQuery serverQuery = new ServerQuery(); try { serverQuery.SendTimeout = 2000; serverQuery.ReceiveTimeout = 2000; SteamData? output = null; serverQuery.Connect(ip, (ushort)port); var serverInfo = await serverQuery.GetServerInfoAsync(); var serverPlayers = await serverQuery.GetPlayersAsync(); output = new SteamData { ServerInfo = serverInfo, Players = serverPlayers }; if (output is null) return null; return output; } catch (TimeoutException ex) { _logger.Debug($"{{0}}:{{1}} {ex.Message}", ip, port); return null; } catch (Exception ex) { _logger.Debug(ex.Message); return null; } } } }