Pārlūkot izejas kodu

Add project files.

Veloe 3 gadi atpakaļ
vecāks
revīzija
06c32f9f53

+ 13 - 0
.idea/.idea.VeloeMonitorDataCollector/.idea/.gitignore

@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/.idea.VeloeMonitorDataCollector.iml
+/contentModel.xml
+/projectSettingsUpdater.xml
+/modules.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 12 - 0
.idea/.idea.VeloeMonitorDataCollector/.idea/dataSources.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
+    <data-source source="LOCAL" name="valuesdb@192.168.10.1" uuid="0d78db40-6828-46dc-aed8-3db94822240f">
+      <driver-ref>mysql.8</driver-ref>
+      <synchronize>true</synchronize>
+      <jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
+      <jdbc-url>jdbc:mysql://192.168.10.1:3306/valuesdb</jdbc-url>
+      <working-dir>$ProjectFileDir$</working-dir>
+    </data-source>
+  </component>
+</project>

+ 4 - 0
.idea/.idea.VeloeMonitorDataCollector/.idea/encodings.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
+</project>

+ 8 - 0
.idea/.idea.VeloeMonitorDataCollector/.idea/indexLayout.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="UserContentModel">
+    <attachedFolders />
+    <explicitIncludes />
+    <explicitExcludes />
+  </component>
+</project>

+ 6 - 0
.idea/.idea.VeloeMonitorDataCollector/.idea/sqldialects.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="SqlDialectMappings">
+    <file url="file://$PROJECT_DIR$/VeloeMonitorDataCollector/DatabaseConnectors/MySqlConnector.cs" dialect="GenericSQL" />
+  </component>
+</project>

+ 42 - 0
.vscode/launch.json

@@ -0,0 +1,42 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": ".NET Core Launch (console)",
+            "type": "coreclr",
+            "request": "launch",
+            //"WARNING01": "*********************************************************************************",
+            //"WARNING02": "The C# extension was unable to automatically decode projects in the current",
+            //"WARNING03": "workspace to create a runnable launch.json file. A template launch.json file has",
+            //"WARNING04": "been created as a placeholder.",
+            //"WARNING05": "",
+            //"WARNING06": "If OmniSharp is currently unable to load your project, you can attempt to resolve",
+            //"WARNING07": "this by restoring any missing project dependencies (example: run 'dotnet restore')",
+            //"WARNING08": "and by fixing any reported errors from building the projects in your workspace.",
+            //"WARNING09": "If this allows OmniSharp to now load your project then --",
+            //"WARNING10": "  * Delete this file",
+            ///"WARNING11": "  * Open the Visual Studio Code command palette (View->Command Palette)",
+            //"WARNING12": "  * run the command: '.NET: Generate Assets for Build and Debug'.",
+            //"WARNING13": "",
+            //"WARNING14": "If your project requires a more complex launch configuration, you may wish to delete",
+            //"WARNING15": "this configuration and pick a different template using the 'Add Configuration...'",
+            //"WARNING16": "button at the bottom of this file.",
+            //"WARNING17": "*********************************************************************************",
+            //"preLaunchTask": "build",
+            
+            "program": "C:/Users/Veloe/repos/VeloeMonitorDataCollector/VeloeMonitorDataCollector/bin/Debug/net6.0/VeloeMonitorDataCollector.dll",
+            "args": [],
+            "cwd": "${workspaceFolder}",
+            "console": "internalConsole",
+            "stopAtEntry": false
+        },
+        {
+            "name": ".NET Core Attach",
+            "type": "coreclr",
+            "request": "attach"
+        }
+    ]
+}

+ 24 - 0
.vscode/tasks.json

@@ -0,0 +1,24 @@
+{
+    // See https://go.microsoft.com/fwlink/?LinkId=733558
+    // for the documentation about the tasks.json format
+    "version": "2.0.0",
+    "tasks": [
+        {
+            "label": "build",
+            "command": "dotnet",
+            "type": "shell",
+            "args": [
+                "build",
+                // Ask dotnet build to generate full paths for file names.
+                "/property:GenerateFullPaths=true",
+                // Do not generate summary otherwise it leads to duplicate errors in Problems panel
+                "/consoleloggerparameters:NoSummary"
+            ],
+            "group": "build",
+            "presentation": {
+                "reveal": "silent"
+            },
+            "problemMatcher": "$msCompile"
+        }
+    ]
+}

+ 25 - 0
VeloeMonitorDataCollector.sln

@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.1.32228.430
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VeloeMonitorDataCollector", "VeloeMonitorDataCollector\VeloeMonitorDataCollector.csproj", "{63864AC3-310A-448D-A740-E6A9F7105BFD}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{63864AC3-310A-448D-A740-E6A9F7105BFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{63864AC3-310A-448D-A740-E6A9F7105BFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{63864AC3-310A-448D-A740-E6A9F7105BFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{63864AC3-310A-448D-A740-E6A9F7105BFD}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {E5DDD9BE-6825-423A-9201-6FEA630D96A8}
+	EndGlobalSection
+EndGlobal

+ 465 - 0
VeloeMonitorDataCollector/DataCollector.cs

@@ -0,0 +1,465 @@
+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<string, Task> _updaterTasks; //TODO i can use an array here
+
+        CancellationToken _token;
+        CancellationTokenSource _cancellationTokenSource;
+
+        Serilog.ILogger _logger;
+        
+        Computer? _computerHardware;
+        
+        private List<IDataSendable> _sendToDb; //TODO and here
+        
+        
+        //Exception thrown on checking values 
+        /// <exception cref="ArgumentNullException">when there is no value in INI file or this value is not declared.</exception>
+        /// <exception cref="ArgumentOutOfRangeException">when value in INI file does not match in range. Example: Port value range is 1..65565]</exception>
+        /// <exception cref="FormatException">when value can not be parsed to needed type. Expample: Port value can't be NaN</exception>
+        /// <exception cref="OverflowException">when provided value caused overflowed return variable in parse method.</exception>
+
+        public DataCollector(in IConfiguration data,in Serilog.ILogger logger)
+        {
+            _updaterTasks = new Dictionary<string, Task>(); 
+            _cancellationTokenSource = new CancellationTokenSource();
+            _token = _cancellationTokenSource.Token;
+
+            _logger = logger;
+         
+            //ini file check
+            //is it needed?
+            if (!File.Exists("config.ini"))
+            {
+                File.WriteAllText("config.ini",
+                    "#hardware = true\n\n#[MySQL]\n#Ip = 127.0.0.1\n#Port = 8806\n#Username = User\n#Password = Password\n#Scheme = 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");
+                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 data 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<IDataSendable>();
+            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);
+                        if (_sendToDb != null)
+                            foreach (var dbController in _sendToDb)
+                            {
+                                dbController.SendHardware(UpdateHardware());
+                            }
+                        
+                        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<string, float> UpdateHardware()
+        {
+            Dictionary<string, float> output = new Dictionary<string, float>();
+
+            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));
+                            var data = UpdateMinecraft(ip, port);
+                            //_logger.Debug("Updating {0}", name);
+                            
+                            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));
+                            var data = UpdateSteam(ip, port);
+
+                            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)
+                            {
+                                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);
+                        }
+                    }, _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 SteamData? UpdateSteam(string ip, int port)
+        {   
+            IServerQuery serverQuery = new ServerQuery(ip, (ushort)port);
+            try
+            {
+                SteamData output = new SteamData
+                {
+                    ServerInfo = serverQuery.GetServerInfo(),
+
+                    Players = serverQuery.GetPlayers()
+                };
+
+                if (output.ServerInfo is null)
+                    return null;
+                
+                return output;
+            }
+            catch (Exception ex)
+            {
+                _logger.Debug(ex.Message);
+                return null;
+            }
+        }
+        
+    }
+}

+ 576 - 0
VeloeMonitorDataCollector/DatabaseConnectors/MySqlConnector.cs

@@ -0,0 +1,576 @@
+using System.Globalization;
+using System.Text;
+using System.Text.Json;
+using Microsoft.Extensions.Configuration;
+using MinecraftStatus;
+using MySql.Data.MySqlClient;
+using Serilog;
+using VeloeMonitorDataCollector.Dependencies;
+using VeloeMonitorDataCollector.Models;
+
+namespace VeloeMonitorDataCollector.DatabaseConnectors
+{
+    class MySqlConnector : IDataSendable
+    {
+        MySqlConnection _connection;
+        private string _database;
+        Serilog.ILogger _logger;
+
+        Dictionary<string, bool?> _gamespyTableCheck = new();
+
+        /// <summary>
+        /// Creates connection to MySQL database using config.ini
+        /// Checks if server is available and schema is created
+        /// </summary>
+        /// <param name="section">Configuration section with tag [MySQL]</param>
+        /// <exception cref="ArgumentNullException"></exception>
+        public MySqlConnector(in IConfigurationSection section, in Serilog.ILogger logger)
+        {
+            _logger = logger;
+
+            section["port"] ??= "3306";
+ 
+            _database = section["database"][..(section["database"].IndexOfAny(new[] { ' ', ';' }) >= 0 ?
+                                                            section["database"].IndexOfAny(new[] { ' ', ';' }) :
+                                                            section["database"].Length)];
+             
+            if (_database is null)
+                throw new ArgumentNullException(_database);
+
+            MySqlConnectionStringBuilder connectionString = new MySqlConnectionStringBuilder();
+            //connectionString.Database = section["database"];
+            connectionString.Server = section["server"];
+            connectionString.Port = UInt32.Parse(section["port"]);
+            connectionString.UserID = section["uid"];
+            connectionString.Password = section["pwd"];
+            connectionString.ConnectionReset = true;
+
+            _logger.Information("Connecting to {0} (MySQL)", section["server"]);
+            _connection = new MySqlConnection(connectionString.ToString());
+            _connection.Open();
+
+            try
+            {
+                _connection.ChangeDatabase(_database);
+            }
+            catch (MySqlException ex)
+            {
+                switch (ex.Number)
+                {
+                    case 1049:
+                        _logger.Warning(ex.Message);
+                        _logger.Information("Create database? Y/N");
+                        var key = Console.ReadKey();
+                        if (key.KeyChar.Equals('Y'))
+                        {
+                            CreateDatabase(_database);
+                        }
+                        break;
+                    default:
+                        throw;
+                }
+            }            
+        }
+
+        public void Close()
+        {
+            _connection.Close();
+            _connection.Dispose();
+        }
+
+        /// <summary>
+        /// Check if table in database is correct for sending data from game servers
+        /// Creates table if schema creates for the first time or empty
+        /// </summary>
+        /// <param name="name">name of table in database</param>
+        /// <param name="type">type of server</param>
+        /// <exception cref="ArgumentException">when entered type is not supported or invalid string accepted</exception>
+        public bool CheckGameServer(in string name, in string type)
+        {
+            MySqlCommand command;
+            string[] cols;
+            string[] types;
+
+            //TODO store cols and types strings somewhere else
+            switch (type)
+            {
+                case "Minecraft":
+                    cols = new string[]
+                        {
+                            "messageofaday",
+                            "gametype",
+                            "gameid",
+                            "version",
+                            "plugins",
+                            "map",
+                            "numplayers",
+                            "maxplayers",
+                            "players"
+                        };
+
+                    types = new string[]
+                    {
+                            "varchar(255)",
+                            "varchar(16)",
+                            "varchar(16)",
+                            "varchar(16)",
+                            "varchar(255)",
+                            "varchar(48)",
+                            "int",
+                            "int",
+                            "varchar(255)"
+                    };
+                    break;
+
+                case "Steam":
+                    cols = new string[]
+                        {
+                            "bots",
+                            "environment",
+                            "folder",
+                            "game",
+                            "gameid",
+                            "keywords",
+                            "map",
+                            "maxplayers",
+                            "name",
+                            "players",
+                            "servertype",
+                            "vac",
+                            "version",
+                            "visibility",
+                            "playerslist"
+                        };
+
+                    types = new string[]
+                    {
+                            "tinyint unsigned",
+                            "varchar(16)",
+                            "varchar(48)",
+                            "varchar(48)",
+                            "bigint",
+                            "varchar(255)",
+                            "varchar(48)",
+                            "tinyint unsigned",
+                            "varchar(128)",
+                            "tinyint unsigned",
+                            "varchar(16)",
+                            "varchar(16)",
+                            "varchar(16)",
+                            "varchar(16)",
+                            "varchar(255)"
+                        };
+                    break;
+
+                case "Gamespy3":
+                    _logger.Warning("Table {0} can't be checked before game server quering service init because server uses {1} protocol. Table will be checked after first success connection to the server.", name, "Gamespy3");
+                    return true;
+
+                default:
+                    throw new ArgumentException(type);
+            }
+
+            try
+            {
+                bool valid = true;
+                command = new MySqlCommand($"SHOW COLUMNS FROM {name};", _connection);
+                using var result = command.ExecuteReader();
+
+                _logger.Information(
+                    "Checking table {0} in {1} for {2} (MySQL)", name, _connection.Database, _connection.DataSource);
+                if (result.Read() is false)
+                {
+                    CreateTableGameServer(name, type);
+                    return true;
+                }
+
+                if (result.GetString(0) is not "date" &&
+                    result.GetString(1) is not "bigint")
+                {
+                    _logger.Error("Error at column {0}. Correct is {1} {2}",0,"date","bigint");
+                    valid = false;
+                }
+             
+                //check here other columns
+
+                for (int i = 0; i < cols.Length; i++)
+                {
+                    if (!result.Read())
+                    {
+                        _logger.Error("End of table on column {0}/{1}",i+1,cols.Length);
+                        return false;
+                    }
+                    if (result.GetString(0) != cols[i] &&
+                        result.GetString(1) != types[i])
+                    {
+                        _logger.Error("Error at colunm {0}. Correct is {1} {2}",i+1,cols[i],types[i]);
+                        valid = false;
+                    }
+                }
+
+                if (valid) _logger.Information("Ok");
+                return valid;
+            }
+            catch (MySqlException e)
+            {
+                if (e.Number == 1146)
+                {
+                    CreateTableGameServer(name, type);
+                    return true;
+                }
+
+                throw;
+            }
+
+        }
+
+        public bool CheckHardware(in Dictionary<string, float> input)
+        {
+            var command = new MySqlCommand("SHOW COLUMNS FROM hardware;", _connection);
+
+            try
+            {
+                bool valid = true;
+                using var result = command.ExecuteReader();
+                _logger.Information("Checking table {0} in {1} for {2} (MySQL)", "hardware",_connection.Database, _connection.DataSource);
+                if (result.Read() is false)
+                {
+                    CreateTableHardware(input);
+                    return true;
+                }
+
+                if (result.GetString(0) is not "date" && result.GetString(1) is not "bigint")
+                {
+                    _logger.Error("Error at column {0}. Correct is {1} {2}",0,"date","bigint");
+                    valid = false;
+                }
+
+                int i = 1;
+                foreach (var parameter in input)
+                {
+                    if (result.Read() is false)
+                    {
+                        _logger.Error("End of table on column {0}/{1}",i,input.Count);
+                        return false;
+                    }
+
+                    if (result.GetString(0) != parameter.Key || result.GetString(1) != "float")
+                    {
+                        _logger.Error("Error at colunm {0}. Correct is {1} {2}",i,parameter.Key,"float");
+                        valid = false;
+                    }
+                    i++;
+                }
+
+                if (valid) _logger.Information("Ok");
+                return valid;
+            }
+            catch (MySqlException e)
+            {
+                if (e.Number == 1146)
+                {
+                    CreateTableHardware(input);
+                    return true;
+                }
+                throw;
+            }
+        }
+
+        public void CreateTableGameServer(in string name,in string type)
+        {
+            MySqlCommand command;
+            StringBuilder query;
+            switch (type)
+            {
+                case "Minecraft":
+                    //TODO change it when C#11 releases
+                    //TODO query builder
+                    query = new StringBuilder();
+                    query.AppendFormat("CREATE TABLE {0} (", name);
+                    query.Append("date BIGINT NOT NULL,");
+                    query.Append("messageofaday VARCHAR(255),");
+                    query.Append("gametype VARCHAR(16),");
+                    query.Append("gameid VARCHAR(16),");
+                    query.Append("version VARCHAR(16),");
+                    query.Append("plugins VARCHAR(255),");
+                    query.Append("map VARCHAR(48),");
+                    query.Append("numplayers INT,");
+                    query.Append("maxplayers INT,");
+                    query.Append("players VARCHAR(255)");
+                    query.Append(",PRIMARY KEY (date))");
+                    command = new MySqlCommand(query.ToString(), _connection);
+
+                    break;
+                case "Steam":
+                    query = new StringBuilder();
+                    query.AppendFormat("CREATE TABLE {0} (", name);
+                    query.Append("date BIGINT NOT NULL,");
+                    query.Append("bots tinyint unsigned,");
+                    query.Append("environment VARCHAR(16),");
+                    query.Append("folder VARCHAR(48),");
+                    query.Append("game VARCHAR(48),");
+                    query.Append("gameid bigint,");
+                    query.Append("keywords VARCHAR(255),");
+                    query.Append("map varchar(48),");
+                    query.Append("maxplayers tinyint unsigned,");
+                    query.Append("name VARCHAR(128),");
+                    query.Append("players tinyint unsigned,");
+                    query.Append("servertype VARCHAR(16),");
+                    query.Append("vac VARCHAR(16),");
+                    query.Append("version VARCHAR(16),");
+                    query.Append("visibility VARCHAR(16),");
+                    query.Append("playerslist VARCHAR(255)");
+                    query.Append(",PRIMARY KEY (date))");
+                    command = new MySqlCommand(query.ToString(), _connection);
+
+                    
+                    break;
+
+                case "Gamespy3":
+                    throw new NotImplementedException("Gamespy3");
+                    return;
+
+                default:    
+                    throw new ArgumentException(type);
+            }
+            command.ExecuteNonQuery();
+
+            _logger.Information("Table {0} created in {1} for {2} (MySQL)", name, _connection.Database, _connection.DataSource);
+        }
+
+        public void CreateTableHardware(in Dictionary<string, float> input)
+        {           
+            StringBuilder columns = new StringBuilder();
+            foreach (var parameter in input)
+            {
+                columns.Append($",{parameter.Key} FLOAT ");
+            }
+            MySqlCommand command = new MySqlCommand(
+                $"CREATE TABLE hardware (date BIGINT NOT NULL {columns} ,PRIMARY KEY (date))", _connection);
+
+            command.ExecuteNonQuery();
+
+            _logger.Information("Table hardware created in {0} for {1} (MySQL)", _connection.Database,_connection.DataSource);
+        }
+
+        public void SendHardware(in Dictionary<string,float> data)
+        {
+            MySqlCommand command;
+            CultureInfo oldCultureInfo = CultureInfo.CurrentCulture;
+            CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
+            var cols = string.Join(',',data.Keys);
+            var values = string.Join(", @",data.Values);
+            CultureInfo.CurrentCulture = oldCultureInfo;
+
+            var query = $"INSERT INTO hardware (date ,{cols}) VALUES (@date ,{values})";
+
+            //Console.WriteLine(query);
+            command = new MySqlCommand(query, _connection);
+
+            command.Parameters.Add("date", MySqlDbType.Int64).Value = DateTimeOffset.Now.ToUnixTimeSeconds();
+            foreach (var parameter in data)
+            {
+                command.Parameters.Add(parameter.Key, MySqlDbType.Float).Value = parameter.Value;
+            }
+
+            command.ExecuteNonQueryAsync();
+        }
+
+        public void SendMinecraft(in McStatus data, in string name)
+        {
+            MySqlCommand command;
+
+            command = new MySqlCommand($"INSERT INTO {name} (date ,messageofaday, gametype, gameid, version, plugins, map, numplayers, maxplayers, players) VALUES (@date, @messageofaday, @gametype, @gameid, @version, @plugins, @map, @numplayers, @maxplayers, @players)", _connection);
+
+            command.Parameters.Clear();
+            command.Parameters.Add("date", MySqlDbType.UInt64).Value = DateTimeOffset.Now.ToUnixTimeSeconds();
+            command.Parameters.Add("messageofaday", MySqlDbType.VarString).Value = data.MessageOfTheDay;
+            command.Parameters.Add("gametype", MySqlDbType.VarString).Value = data.Gametype;
+            command.Parameters.Add("gameid", MySqlDbType.VarString).Value = data.GameId;
+            command.Parameters.Add("version", MySqlDbType.VarString).Value = data.Version;
+            command.Parameters.Add("plugins", MySqlDbType.VarString).Value = data.Plugins;
+            command.Parameters.Add("map", MySqlDbType.VarString).Value = data.Map;
+            command.Parameters.Add("numplayers", MySqlDbType.Int32).Value = data.NumPlayers;
+            command.Parameters.Add("maxplayers", MySqlDbType.Int32).Value = data.MaxPlayers;
+            command.Parameters.Add("players", MySqlDbType.VarString).Value = JsonSerializer.Serialize(data.Players);
+            
+            command.ExecuteNonQueryAsync();
+        }
+
+        public void SendSteam(in SteamData data, in string name)
+        {
+            MySqlCommand command;
+
+            command = new MySqlCommand($"INSERT INTO {name} (date, bots, environment, folder, game, gameid, keywords, map, maxplayers, name, players, servertype, vac, version, visibility, playerslist) VALUES  (@date, @bots, @environment, @folder, @game, @gameid, @keywords, @map, @maxplayers, @name, @players, @servertype, @vac, @version, @visibility, @playerslist)", _connection);
+
+            command.Parameters.Clear();
+            command.Parameters.Add("date", MySqlDbType.UInt64).Value = DateTimeOffset.Now.ToUnixTimeSeconds();
+            command.Parameters.Add("bots", MySqlDbType.UByte).Value = data.ServerInfo.Bots;
+            command.Parameters.Add("environment", MySqlDbType.VarString).Value = data.ServerInfo.Environment;
+            command.Parameters.Add("folder", MySqlDbType.VarString).Value = data.ServerInfo.Folder;
+            command.Parameters.Add("game", MySqlDbType.VarString).Value = data.ServerInfo.Game;
+            command.Parameters.Add("gameid", MySqlDbType.Int64).Value = data.ServerInfo.GameID;
+            command.Parameters.Add("keywords", MySqlDbType.VarString).Value = data.ServerInfo.Keywords;
+            command.Parameters.Add("map", MySqlDbType.VarString).Value = data.ServerInfo.Map;
+            command.Parameters.Add("maxplayers", MySqlDbType.VarString).Value = data.ServerInfo.MaxPlayers;
+            command.Parameters.Add("name", MySqlDbType.VarString).Value = data.ServerInfo.Name;
+            command.Parameters.Add("players", MySqlDbType.VarString).Value = data.ServerInfo.Players;
+            command.Parameters.Add("servertype", MySqlDbType.VarString).Value = data.ServerInfo.ServerType;
+            command.Parameters.Add("vac", MySqlDbType.VarString).Value = data.ServerInfo.VAC;
+            command.Parameters.Add("version", MySqlDbType.VarString).Value = data.ServerInfo.Version;
+            command.Parameters.Add("visibility", MySqlDbType.VarString).Value = data.ServerInfo.Visibility;
+            command.Parameters.Add("playerslist", MySqlDbType.VarString).Value = JsonSerializer.Serialize(data.Players);
+
+            command.ExecuteNonQueryAsync();
+        }
+
+        public void SendGamespy3(in Gs3Status.Status data,in string name)
+        {
+            if (_gamespyTableCheck.GetValueOrDefault(name, null) is null)
+            {
+                _gamespyTableCheck[name] = CheckGamespy3(data, name);
+            }
+
+            if (_gamespyTableCheck[name] is false)
+                return;
+
+            MySqlCommand command;
+
+            StringBuilder queryBuilder = new StringBuilder();
+            queryBuilder.Append($"INSERT INTO {name} (date");
+
+            foreach (var parameter in data.Info)
+            {
+                if (parameter.Value != String.Empty && parameter.Value is not null)
+                {
+                    queryBuilder.Append(", ");
+                    queryBuilder.Append(parameter.Key);
+                }
+            }
+
+            queryBuilder.Append(", playerslist) VALUES  ( @date ");
+
+            foreach (var parameter in data.Info)
+            {
+                if (parameter.Value != String.Empty && parameter.Value is not null)
+                {
+                    queryBuilder.Append(", @");
+                    queryBuilder.Append(parameter.Key);
+
+                    //_logger.Debug("'{0}' = '{1}'", parameter.Key, parameter.Value);
+                }
+            }
+
+            queryBuilder.Append(", @playerslist )");
+
+            command = new MySqlCommand(queryBuilder.ToString(), _connection);
+
+            command.Parameters.Clear();
+            command.Parameters.Add("date", MySqlDbType.UInt64).Value = DateTimeOffset.Now.ToUnixTimeSeconds();
+            
+            foreach(var parameter in data.Info)
+            {
+                if (parameter.Value != String.Empty && parameter.Value is not null)
+                
+                    command.Parameters.Add(parameter.Key, MySqlDbType.VarString).Value = parameter.Value;
+            }
+
+            command.Parameters.Add("playerslist", MySqlDbType.VarString).Value = JsonSerializer.Serialize(data.Players);
+            command.ExecuteNonQueryAsync();
+        }
+
+        private bool CheckGamespy3(Gs3Status.Status input, string name)
+        {
+            var command = new MySqlCommand($"SHOW COLUMNS FROM {name};", _connection);
+
+            try
+            {
+                bool valid = true;
+                using var result = command.ExecuteReaderAsync().Result;
+                _logger.Information("Checking table {0} ({1}) in {2} for {3} (MySQL)", name, "Gamespy3" , _connection.Database, _connection.DataSource);
+                if (result.Read() is false)
+                {
+                    CreateTableGamespy3(input, name);
+                    return true;
+                }
+
+                int colsCount = 1;
+
+                foreach(var parameter in input.Info)
+                    if (parameter.Value is not null && parameter.Value != String.Empty)
+                        colsCount++;
+
+                if (result.GetString(0) is not "date" && result.GetString(1) is not "bigint")
+                {
+                    _logger.Error("Error at column {0}. Correct is {1} {2}", 0, "date", "bigint");
+                    valid = false;
+                }
+
+                int i = 1;
+                foreach (var parameter in input.Info)
+                {
+                    if (result.Read() is false)
+                    {
+                        _logger.Error("End of table on column {0}/{1}", i, colsCount);
+                        return false;
+                    }
+
+                    if (result.GetString(0) != parameter.Key || result.GetString(1) != "varchar(127)")
+                    {
+                        _logger.Error("Error at colunm {0}. Correct is {1} {2}", i, parameter.Key, "varchar(127)");
+                        valid = false;
+                    }
+                    i++;
+                }
+
+                if (result.Read() is false)
+                {
+                    _logger.Error("End of table on column {0}/{0}", colsCount);
+                    return false;
+                }
+
+                if (result.GetString(0) != "playerslist" || result.GetString(1) != "varchar(255)")
+                {
+                    _logger.Error("Error at colunm {0}. Correct is {1} {2}", i, "playerslist", "varchar(255)");
+                    valid = false;
+                }
+
+
+                if (valid) _logger.Information("Ok");
+                //else _logger.Warning("Table {0} isn't receiving any updates to {1} (MySQL)", name, _connection.DataSource);
+                return valid;
+            }
+            catch (MySqlException e)
+            {
+                if (e.Number == 1146)
+                {
+                    CreateTableGamespy3(input, name);
+                    return true;
+                }
+                throw;
+            }
+        }
+
+        private void CreateTableGamespy3(Gs3Status.Status input, string name)
+        {
+            StringBuilder columns = new StringBuilder();
+            foreach (var parameter in input.Info)
+            {
+                if (parameter.Value != String.Empty && parameter.Value is not null)
+                    columns.Append($",{parameter.Key} VARCHAR(127) ");
+            }
+            MySqlCommand command = new MySqlCommand(
+                $"CREATE TABLE {name} (date BIGINT NOT NULL {columns}, playerslist VARCHAR(255) ,PRIMARY KEY (date))", _connection);
+
+            command.ExecuteNonQuery();
+
+            _logger.Information("Table {0} created in {1} for {2} (MySQL)", name ,_connection.Database, _connection.DataSource);
+        }
+
+        private void CreateDatabase(string name)
+        {
+            MySqlCommand command = new MySqlCommand($"CREATE DATABASE {name}", _connection);
+            command.ExecuteNonQuery();
+        }
+    }
+}

+ 125 - 0
VeloeMonitorDataCollector/DatabaseConnectors/SignalRConnector.cs

@@ -0,0 +1,125 @@
+using Microsoft.Extensions.Configuration;
+using MinecraftStatus;
+using VeloeMonitorDataCollector.Dependencies;
+using VeloeMonitorDataCollector.Models;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.AspNetCore.SignalR;
+using System.Text.Json;
+using Serilog;
+
+namespace VeloeMonitorDataCollector.DatabaseConnectors
+{
+    internal class SignalRConnector : IDataSendable
+    {
+        WebApplication app;
+        public SignalRConnector(in IConfigurationSection section, in Serilog.ILogger logger)
+        {
+            string url = section["url"];
+
+            if (string.IsNullOrEmpty(url))
+                url = "http://*:5000";
+
+            var builder = WebApplication.CreateBuilder();
+
+            builder.WebHost.UseUrls(url);
+
+            builder.Services.AddSignalR();
+            builder.Services.AddSingleton<DataSender>();
+
+            builder.Host.UseSerilog(logger);
+
+            app = builder.Build();
+
+            app.MapHub<DataHub>("/hubs/data");
+
+            app.RunAsync();
+        }
+
+        public bool CheckGameServer(in string name, in string type)
+        {
+            return true;
+        }
+
+        public bool CheckHardware(in Dictionary<string, float> input)
+        {
+            return true;
+        }
+
+        public void Close()
+        {
+            app.StopAsync();
+        }
+
+        public void SendGamespy3(in Gs3Status.Status data, in string name)
+        {
+            app.Services.GetService<DataSender>().SendGamespy3(data, name);
+        }
+
+        public void SendHardware(in Dictionary<string, float> data)
+        {
+            app.Services.GetService<DataSender>().SendHardware(data);
+        }
+
+        public void SendMinecraft(in McStatus data, in string name)
+        {
+            app.Services.GetService<DataSender>().SendMinecraft(data, name);
+        }
+
+        public void SendSteam(in SteamData data, in string name)
+        {
+            app.Services.GetService<DataSender>().SendSteam(data, name);
+        }
+    }
+
+    public class DataSender
+    {
+        IHubContext<DataHub> _hubContext;
+        Serilog.ILogger _logger;
+        public DataSender(IHubContext<DataHub> hubContext, ILogger logger)
+        {
+            _hubContext = hubContext; 
+            _logger = logger;
+        }
+
+        public void SendGamespy3(in Gs3Status.Status data, in string name)
+        {
+            _hubContext.Clients.Groups(name).SendAsync($"Update{name}",JsonSerializer.Serialize(data));
+        }
+
+        public void SendHardware(in Dictionary<string, float> data)
+        {
+            _hubContext.Clients.Groups("hardware").SendAsync($"Update", JsonSerializer.Serialize(data));
+        }
+
+        public void SendMinecraft(in McStatus data, in string name)
+        {
+            _hubContext.Clients.Groups(name).SendAsync($"Update{name}", JsonSerializer.Serialize(data));
+        }
+
+        public void SendSteam(in SteamData data, in string name)
+        {
+            _hubContext.Clients.Groups(name).SendAsync($"Update{name}", JsonSerializer.Serialize(data));
+        }
+    }
+
+    public class DataHub : Hub
+    {
+        public async Task SendMessage(string user, string message)
+        {
+
+            await Clients.All.SendAsync("ReceiveMessage", user, message);
+        }
+
+        public async Task ConnectToGroup(string groupName)
+        {
+            await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
+        }
+
+        public async Task DisconnectFromGroup(string groupName)
+        {
+            await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
+        }
+    }
+}

+ 458 - 0
VeloeMonitorDataCollector/Dependencies/Gs3Status.cs

@@ -0,0 +1,458 @@
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+
+
+//https://github.com/opengsq/opengsq-dotnet/blob/main/OpenGSQ/Protocols/GameSpy3.cs
+//https://github.com/opengsq/opengsq-dotnet/blob/main/OpenGSQ/ProtocolBase.cs
+//https://github.com/opengsq/opengsq-dotnet/blob/main/OpenGSQ/BinaryReaderExtensions.cs
+
+/*
+MIT License
+
+Copyright (c) 2021 OpenGSQ
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE. 
+ 
+ */
+
+
+namespace VeloeMonitorDataCollector.Dependencies
+{
+    public class Gs3Status
+    {
+
+        /// <summary>
+        /// Represents a network endpoint as an IP address and a port number.
+        /// </summary>
+        protected IPEndPoint _EndPoint;
+
+        /// <summary>
+        /// Timeout in millisecond
+        /// </summary>
+        protected int _Timeout;
+
+        /// <summary>
+        /// Cached challenge bytes
+        /// </summary>
+        //protected byte[] _Challenge = new byte[0];
+
+        /// <summary>
+        /// Gamespy Query Protocol version 3
+        /// </summary>
+        /// <param name="address"></param>
+        /// <param name="port"></param>
+        /// <param name="timeout"></param>
+        public Gs3Status(string address, int port, int timeout = 5000)
+        {
+            if (IPAddress.TryParse(address, out var ipAddress))
+            {
+                _EndPoint = new IPEndPoint(ipAddress, port);
+            }
+            else
+            {
+                _EndPoint = new IPEndPoint(Dns.GetHostAddresses(address)[0], port);
+            }
+
+            _Timeout = timeout;
+        }
+
+#pragma warning disable 1591
+        protected bool _Challenge;
+#pragma warning restore 1591
+
+        /// <summary>
+        /// Retrieves information about the server including, Info, Players, and Teams.
+        /// </summary>
+        /// <returns></returns>
+        /// <exception cref="SocketException"></exception>
+        public Status GetStatus()
+        {
+            using (var udpClient = new UdpClient())
+            {
+                var responseData = ConnectAndSendPackets(udpClient);
+
+                using (var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8))
+                {
+                    return new Status
+                    {
+                        // Save Status Info
+                        Info = GetInfo(br),
+
+                        // Save Status Players
+                        Players = GetPlayers(br),
+
+                        // Save Status Teams
+                        Teams = GetTeams(br)
+                    };
+                }
+            }
+        }
+
+        private byte[] ConnectAndSendPackets(UdpClient udpClient)
+        {
+            // Connect to remote host
+            udpClient.Connect(_EndPoint);
+            udpClient.Client.SendTimeout = _Timeout;
+            udpClient.Client.ReceiveTimeout = _Timeout;
+
+            // Packet 1: Initial request
+            byte[] responseData, challenge = new byte[] { }, requestData = new byte[] { 0xFE, 0xFD, 0x09, 0x04, 0x05, 0x06, 0x07 };
+
+            if (_Challenge)
+            {
+                udpClient.Send(requestData, requestData.Length);
+
+                // Packet 2: First response
+                responseData = udpClient.Receive(ref _EndPoint);
+
+                // Get challenge
+                if (int.TryParse(Encoding.ASCII.GetString(responseData.Skip(5).ToArray()).Trim(), out int result) && result != 0)
+                {
+                    challenge = BitConverter.GetBytes(result);
+
+                    if (BitConverter.IsLittleEndian)
+                    {
+                        Array.Reverse(challenge);
+                    }
+                }
+            }
+
+            // Packet 3: Second request
+            requestData[2] = 0x00;
+            requestData = requestData.Concat(challenge).Concat(new byte[] { 0xFF, 0xFF, 0xFF, 0x01 }).ToArray();
+            udpClient.Send(requestData, requestData.Length);
+
+            // Packet 4: Server response
+            responseData = Receive(udpClient);
+
+            return responseData;
+        }
+
+        private byte[] Receive(UdpClient udpClient)
+        {
+            int totalPackets = -1;
+            var payloads = new SortedDictionary<int, byte[]>();
+
+            do
+            {
+                var responseData = udpClient.Receive(ref _EndPoint);
+
+                using (var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8))
+                {
+                    var header = br.ReadByte();
+
+                    if (header != 0)
+                    {
+                        throw new Exception($"Packet header mismatch. Received: {header}. Expected: 0.");
+                    }
+
+                    // Skip the timestamp and splitnum
+                    br.ReadBytes(13);
+
+                    // The 'numPackets' byte
+                    var numPackets = br.ReadByte();
+
+                    // The low 7 bits are the packet index (starting at zero)
+                    var number = numPackets & 0x7F;
+
+                    // The high bit is whether or not this is the last packet
+                    var isLastPacket = numPackets >> 7 == 1;
+
+                    // Save totalPackets as packet number + 1
+                    if (isLastPacket)
+                    {
+                        totalPackets = number + 1;
+                    }
+
+                    // The object id. Example: \x01
+                    var objectId = br.ReadByte();
+
+                    // The object header
+                    byte[] objectHeader = new byte[] { };
+
+                    if (objectId >= 1)
+                    {
+                        // The object name. Example: "player_"
+                        string objectName = br.ReadStringEx();
+
+                        // The object items appear count
+                        int count = br.ReadByte();
+
+                        // If the object item doesn't appear before, set the header back
+                        if (count == 0)
+                        {
+                            // Set the header. Example: \x00\x01player_\x00\x00
+                            objectHeader = new byte[] { 0x00, objectId }.Concat(Encoding.UTF8.GetBytes(objectName)).Concat(new byte[] { 0x00, 0x00 }).ToArray();
+                        }
+                    }
+
+                    // Save the payload
+                    byte[] payload = objectHeader.Concat(responseData.Skip((int)br.BaseStream.Position)).ToArray();
+
+                    payloads.Add(number, TrimPayload(payload));
+                }
+            } while (totalPackets == -1 || payloads.Count < totalPackets);
+
+            // Combine the payloads
+            var combinedPayload = payloads.Values.Aggregate((a, b) => a.Concat(b).ToArray());
+
+            return combinedPayload;
+        }
+
+        /// <summary>
+        /// Remove the last trash string on the payload 
+        /// </summary>
+        /// <param name="payload"></param>
+        /// <returns></returns>
+        private byte[] TrimPayload(byte[] payload)
+        {
+            int i = payload.Length;
+
+            while (payload[--i] != 0) ;
+
+            return payload.Take(i).ToArray();
+        }
+
+        private Dictionary<string, string> GetInfo(BinaryReader br)
+        {
+            var info = new Dictionary<string, string>();
+
+            // Read all key values
+            while (br.TryReadStringEx(out var key))
+            {
+                info[key] = br.ReadStringEx().Trim();
+            }
+
+            return info;
+        }
+
+        private List<Dictionary<string, string>> GetPlayers(BinaryReader br)
+        {
+            var players = new List<Dictionary<string, string>>();
+
+            // Return if BaseStream is end
+            if (br.BaseStream.Position == br.BaseStream.Length)
+            {
+                return players;
+            }
+
+            // Skip \x01player_\x00\x00
+            br.ReadByte();
+            string key = br.ReadStringEx().TrimEnd('_');
+            br.ReadByte();
+
+            // Team index
+            int i = 0;
+
+            // Loop all values and save
+            while (br.BaseStream.Position < br.BaseStream.Length)
+            {
+                if (br.TryReadStringEx(out var value))
+                {
+                    // Add a Dictionary object if not exists
+                    if (players.Count < i + 1)
+                    {
+                        players.Add(new Dictionary<string, string>());
+                    }
+
+                    // Save the value
+                    players[i++][key] = value.Trim();
+                }
+                else
+                {
+                    // Return if no player
+                    if (br.BaseStream.Position == br.BaseStream.Length)
+                    {
+                        break;
+                    }
+
+                    // Set new key
+                    if (br.TryReadStringEx(out key))
+                    {
+                        // Remove the trailing "_"
+                        key = key.TrimEnd('_');
+                    }
+                    else
+                    {
+                        break;
+                    }
+
+                    // Reset the team index
+                    i = br.ReadByte();
+                }
+            }
+
+            return players;
+        }
+
+        private List<Dictionary<string, string>> GetTeams(BinaryReader br)
+        {
+            var teams = new List<Dictionary<string, string>>();
+
+            // Return if BaseStream is end
+            if (br.BaseStream.Position == br.BaseStream.Length)
+            {
+                return teams;
+            }
+
+            // Skip \x00\x02team_t\x00\x00
+            br.ReadBytes(2);
+            string key = br.ReadStringEx().TrimEnd('t').TrimEnd('_');
+            br.ReadByte();
+
+            // Player index
+            int i = 0;
+
+            // Loop all values and save
+            while (br.BaseStream.Position < br.BaseStream.Length)
+            {
+                if (br.TryReadStringEx(out var value))
+                {
+                    // Add a Dictionary object if not exists
+                    if (teams.Count < i + 1)
+                    {
+                        teams.Add(new Dictionary<string, string>());
+                    }
+
+                    // Save the value
+                    teams[i++][key] = value.Trim();
+                }
+                else
+                {
+                    // Return if no team
+                    if (br.BaseStream.Position == br.BaseStream.Length)
+                    {
+                        break;
+                    }
+
+                    // Set new key
+                    if (br.TryReadStringEx(out key))
+                    {
+                        // Remove the trailing "_t"
+                        key = key.TrimEnd('t').TrimEnd('_');
+                    }
+                    else
+                    {
+                        break;
+                    }
+
+                    // Reset the team index
+                    i = br.ReadByte();
+                }
+            }
+
+            return teams;
+        }
+
+        /// <summary>
+        /// Status object
+        /// </summary>
+        public class Status
+        {
+            /// <summary>
+            /// Status Info
+            /// </summary>
+            public Dictionary<string, string> Info { get; set; }
+
+            /// <summary>
+            /// Status Players
+            /// </summary>
+            public List<Dictionary<string, string>> Players { get; set; }
+
+            /// <summary>
+            /// Status Teams
+            /// </summary>
+            public List<Dictionary<string, string>> Teams { get; set; }
+        }
+    }
+
+    /// <summary>
+    /// BinaryReader Extensions
+    /// </summary>
+    public static class BinaryReaderExtensions
+    {
+        /// <summary>
+        /// Reads a string from the current stream until charByte.
+        /// </summary>
+        /// <param name="br"></param>
+        /// <param name="charBytes"></param>
+        /// <returns>The string being read.</returns>
+        /// <exception cref="EndOfStreamException">The end of the stream is reached.</exception>
+        /// <exception cref="ObjectDisposedException">The stream is closed.</exception>
+        /// <exception cref="IOException">An I/O error occurs.</exception>
+        public static string ReadStringEx(this BinaryReader br, byte[] charBytes)
+        {
+            charBytes = charBytes ?? new byte[] { 0 };
+
+            var bytes = new List<byte>();
+            byte streamByte;
+
+            while (Array.IndexOf(charBytes, streamByte = br.ReadByte()) == -1)
+            {
+                bytes.Add(streamByte);
+            }
+
+            return Encoding.UTF8.GetString(bytes.ToArray());
+        }
+
+        /// <summary>
+        /// Reads a string from the current stream until charByte.
+        /// </summary>
+        /// <param name="br"></param>
+        /// <param name="charByte"></param>
+        /// <returns>The string being read.</returns>
+        /// <exception cref="EndOfStreamException">The end of the stream is reached.</exception>
+        /// <exception cref="ObjectDisposedException">The stream is closed.</exception>
+        /// <exception cref="IOException">An I/O error occurs.</exception>
+        public static string ReadStringEx(this BinaryReader br, byte charByte = 0)
+        {
+            return br.ReadStringEx(new byte[] { charByte });
+        }
+
+        /// <summary>
+        /// Reads a string from the current stream until charByte. Return true if is not null and empty.
+        /// </summary>
+        /// <param name="br"></param>
+        /// <param name="outString"></param>
+        /// <param name="charBytes"></param>
+        /// <returns></returns>
+        public static bool TryReadStringEx(this BinaryReader br, out string outString, byte[] charBytes)
+        {
+            outString = br.ReadStringEx(charBytes);
+
+            return !string.IsNullOrEmpty(outString);
+        }
+
+        /// <summary>
+        /// Reads a string from the current stream until charByte. Return true if is not null and empty.
+        /// </summary>
+        /// <param name="br"></param>
+        /// <param name="outString"></param>
+        /// <param name="charByte"></param>
+        /// <returns></returns>
+        public static bool TryReadStringEx(this BinaryReader br, out string outString, byte charByte = 0)
+        {
+            return br.TryReadStringEx(out outString, new byte[] { charByte });
+        }
+    }
+
+}
+
+

+ 241 - 0
VeloeMonitorDataCollector/Dependencies/McStatus.cs

@@ -0,0 +1,241 @@
+using System.Diagnostics;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+
+// Used this with some changes
+// https://github.com/maxime-paquatte/csharp-minecraft-query
+
+namespace MinecraftStatus
+{
+    /// <summary>
+    /// Query a minecraft server to obtains the status (according this documentation : http://wiki.vg/Query).
+    /// </summary>
+    public class McStatus
+    {
+
+        const Byte Statistic = 0x00;
+        const Byte Handshake = 0x09;
+
+        private readonly Dictionary<string, string> _keyValues;
+        private List<string> _players;
+
+        public string MessageOfTheDay
+        {
+            get { return _keyValues["hostname"]; }
+        }
+
+        public string Gametype
+        {
+            get { return _keyValues["gametype"]; }
+        }
+
+        public string GameId
+        {
+            get { return _keyValues["game_id"]; }
+        }
+
+        public string Version
+        {
+            get { return _keyValues["version"]; }
+        }
+
+        public string Plugins
+        {
+            get { return _keyValues["plugins"]; }
+        }
+
+        public string Map
+        {
+            get { return _keyValues["map"]; }
+        }
+        public string NumPlayers
+        {
+            get { return _keyValues["numplayers"]; }
+        }
+        public string MaxPlayers
+        {
+            get { return _keyValues["maxplayers"]; }
+        }
+        public string HostPort
+        {
+            get { return _keyValues["hostport"]; }
+        }
+        public string HostIp
+        {
+            get { return _keyValues["hostip"]; }
+        }
+
+        public IEnumerable<string> Players
+        {
+            get { return _players; }
+        }
+
+        internal McStatus(byte[] message)
+        {
+            _keyValues = new Dictionary<string, string>();
+            _players = new List<string>();
+
+            var buffer = new byte[256];
+            Stream stream = new MemoryStream(message);
+
+            stream.Read(buffer, 0, 5);// Read Type + SessionID
+            stream.Read(buffer, 0, 11); // Padding: 11 bytes constant
+            var constant1 = new byte[] { 0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00 };
+            for (int i = 0; i < constant1.Length; i++) Debug.Assert(constant1[i] == buffer[i], "Byte mismatch at " + i + " Val :" + String.Join(" ", buffer));
+
+            var sb = new StringBuilder();
+            string lastKey = string.Empty;
+            int currentByte;
+            while ((currentByte = stream.ReadByte()) != -1)
+            {
+                if (currentByte == 0x00)
+                {
+                    if (!string.IsNullOrEmpty(lastKey))
+                    {
+                        _keyValues.Add(lastKey, sb.ToString());
+                        lastKey = string.Empty;
+                    }
+                    else
+                    {
+                        lastKey = sb.ToString();
+                        if (string.IsNullOrEmpty(lastKey)) break;
+                    }
+                    sb.Clear();
+                }
+                else sb.Append((char)currentByte);
+            }
+
+            stream.Read(buffer, 0, 10); // Padding: 10 bytes constant
+            var constant2 = new byte[] { 0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00 };
+            //for (int i = 0; i < constant2.Length; i++)  Debug.Assert(constant2[i] == buffer[i], "Byte mismatch at " + i + " Val :" + buffer[i]);
+
+            while ((currentByte = stream.ReadByte()) != -1)
+            {
+                if (currentByte == 0x00)
+                {
+                    var player = sb.ToString();
+                    if (string.IsNullOrEmpty(player)) break;
+                    _players.Add(player);
+                    sb.Clear();
+                }
+                else sb.Append((char)currentByte);
+            }
+        }
+
+
+        /// <summary>
+        /// Get the status of the given host and optional port
+        /// </summary>
+        /// <param name="host">The host name or address (monserver.com or 123.123.123.123)</param>
+        /// <param name="port">The query port, by default is 25565</param>
+        public static McStatus? GetStatus(string host, int port = 25565)
+        {
+            var e = new IPEndPoint(IPAddress.Any, port);
+            using (var u = new UdpClient())
+            {
+                u.Client.SendTimeout = 1000;
+                u.Client.ReceiveTimeout = 1000;
+                u.ExclusiveAddressUse = false;
+                u.Client.SetSocketOption(System.Net.Sockets.SocketOptionLevel.Socket, System.Net.Sockets.SocketOptionName.ReuseAddress, true);
+                u.Client.Bind(e);
+
+                try
+                {
+                    var s = new UdpState { EndPoint = e, Client = u };
+                    u.Connect(host, port);
+                    var status = GetStatus(s);
+
+                    if (status == null)
+                        throw new SocketException();
+
+                    //check answer is correct
+                    var buffer = new byte[256];
+                    Stream stream = new MemoryStream(status);
+                    stream.Read(buffer, 0, 5);// Read Type + SessionID
+                    stream.Read(buffer, 0, 11); // Padding: 11 bytes constant
+                    var constant1 = new byte[] { 0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00 };
+                    for (int i = 0; i < constant1.Length; i++) if (constant1[i] != buffer[i]) throw new Exception(String.Join(" ", status));
+
+                    u.Close();
+                    return new McStatus(status);
+                }
+                catch (SocketException)
+                {
+                    u.Close();
+                    return null;
+                }
+                catch (Exception ex)
+                {
+                    Console.Out.WriteLine(ex.StackTrace + "\n" + ex.Message);
+                    u.Close();
+                    return null;
+                }
+                finally
+                {
+                    u.Close();
+                    u.Dispose();
+                    //u.Close();
+                }
+            }
+        }
+
+
+        static byte[]? GetStatus(UdpState s)
+        {
+            var challengeToken = GetChallengeToken(s);
+            if (challengeToken == null)
+                return null;
+            //append 4 bytes to obtains the Full status
+            WriteData(s, Statistic, challengeToken, new byte[] { 0x00, 0x00, 0x00, 0x00 });
+            return ReceiveMessages(s);
+        }
+
+        static byte[]? GetChallengeToken(UdpState s)
+        {
+            WriteData(s, Handshake);
+
+            var message = ReceiveMessages(s);
+
+            var challangeBytes = new byte[16];
+            Array.Copy(message, 5, challangeBytes, 0, message.Length - 5);
+            //string challengeStr = new string(Encoding.ASCII.GetString(challangeBytes).Where(char.IsDigit).ToArray());
+            //Console.Out.WriteLineAsync("|"+challengeStr + "|" + s.EndPoint.Port);
+            //var challengeInt = Int64.Parse(challengeStr);
+            try
+            {
+                var challengeInt = int.Parse(Encoding.ASCII.GetString(challangeBytes));
+                return BitConverter.GetBytes(challengeInt).Reverse().ToArray();
+            }
+            catch (FormatException)
+            {
+                return null;
+            }
+
+
+        }
+
+
+        static void WriteData(UdpState s, byte cmd, byte[] append = null, byte[] append2 = null)
+        {
+            var cmdData = new byte[] { 0xFE, 0xFD, cmd, 0x01, 0x02, 0x03, 0x04 };
+            var dataLength = cmdData.Length + (append != null ? append.Length : 0) + (append2 != null ? append2.Length : 0);
+            var data = new byte[dataLength];
+            cmdData.CopyTo(data, 0);
+            if (append != null) append.CopyTo(data, cmdData.Length);
+            if (append2 != null) append2.CopyTo(data, cmdData.Length + (append != null ? append.Length : 0));
+            s.Client.Send(data, data.Length);
+        }
+
+        static byte[] ReceiveMessages(UdpState s)
+        {
+            return s.Client.Receive(ref s.EndPoint);
+        }
+
+        class UdpState
+        {
+            public UdpClient Client;
+            public IPEndPoint EndPoint;
+        }
+    }
+}

+ 23 - 0
VeloeMonitorDataCollector/Interfaces/IDataSendable.cs

@@ -0,0 +1,23 @@
+using MinecraftStatus;
+using VeloeMonitorDataCollector.Dependencies;
+using VeloeMonitorDataCollector.Models;
+
+namespace VeloeMonitorDataCollector;
+
+public interface IDataSendable
+{
+    public void SendHardware(in Dictionary<string, float> data);
+    
+    public void SendMinecraft(in McStatus data, in string name);
+    
+    public void SendSteam(in SteamData data,in string name);
+
+    public void SendGamespy3(in Gs3Status.Status data, in string name);
+
+    public void Close();
+
+    public bool CheckHardware(in Dictionary<string,float> input);
+
+    public bool CheckGameServer(in string name,in string type);
+    
+}

+ 6 - 0
VeloeMonitorDataCollector/Models/GameServerType.cs

@@ -0,0 +1,6 @@
+namespace VeloeMonitorDataCollector.Models;
+
+public enum GameServerType
+{
+    Minecraft, Steam, GameSpy3
+}

+ 10 - 0
VeloeMonitorDataCollector/Models/SteamData.cs

@@ -0,0 +1,10 @@
+using SteamQueryNet.Models;
+
+namespace VeloeMonitorDataCollector.Models;
+
+public struct SteamData
+{
+    public ServerInfo ServerInfo;
+    public List<Player> Players;
+
+}

+ 49 - 0
VeloeMonitorDataCollector/Program.cs

@@ -0,0 +1,49 @@
+using Microsoft.Extensions.Configuration;
+using MinecraftStatus;
+using Serilog;
+using Serilog.Events;
+using System.Text.Json;
+
+// See https://aka.ms/new-console-template for more information
+using VeloeMonitorDataCollector;
+using VeloeMonitorDataCollector.Dependencies;
+
+IConfiguration configuration = new ConfigurationBuilder()
+    .AddIniFile("config.ini", optional: true, reloadOnChange: true)
+    .Build();
+
+var logger = new LoggerConfiguration()
+   .MinimumLevel.Debug()
+   .WriteTo.Console(LogEventLevel.Debug)
+   .WriteTo.File("logfile.log", LogEventLevel.Debug)// restricted... is Optional
+   .CreateLogger();
+/*
+var status = new Gs3Status("192.168.1.86",5446).GetStatus();
+
+foreach (KeyValuePair<string, string> kvp in status.Info)
+{
+    Console.WriteLine(kvp.Key + " " + kvp.Value);
+}
+
+Console.ReadKey();
+*/
+try
+{
+    DataCollector collector = new(configuration, logger);
+
+    collector.Start();
+
+    logger.Information("Wait for any input to stop");
+    Console.ReadKey();
+    //Console.Read();
+    collector.Stop();
+    logger.Information("Wait for any input to exit");
+    Console.ReadKey();
+    //Console.Read();        
+}
+catch (Exception ex)
+{
+    logger.Error(ex.Message);
+    logger.Error(ex.StackTrace);
+    throw;
+}

+ 45 - 0
VeloeMonitorDataCollector/VeloeMonitorDataCollector.csproj

@@ -0,0 +1,45 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net6.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <PlatformTarget>x64</PlatformTarget>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+    <WarningLevel>0</WarningLevel>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+    <WarningLevel>0</WarningLevel>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <None Remove="logfile.log" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="ClrHeapAllocationAnalyzer" Version="3.0.0" />
+    <PackageReference Include="LibreHardwareMonitorLib" Version="0.9.0" />
+    <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="6.0.0" />
+    <PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
+    <PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
+    <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
+    <PackageReference Include="SonarAnalyzer.CSharp" Version="8.44.0.52574">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
+    <PackageReference Include="SteamQueryNet" Version="1.0.6" />
+    <PackageReference Include="MySql.Data" Version="8.0.30" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Update="config.ini">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
+</Project>

+ 29 - 0
VeloeMonitorDataCollector/default-config.ini

@@ -0,0 +1,29 @@
+#hardware = true
+
+#[MySQL]
+#Ip = 127.0.0.1
+#Port = 8806
+#Username = User
+#Password = Password
+#Scheme = values
+
+#[WebSoket]
+#url = http://192.168.1.2:5000
+
+#[MinecraftServer]
+#Ip = 127.0.0.1
+#Port = 25565
+#Type = Minecraft
+#updateInterval = 30
+
+#[SteamAPIServer]
+#Ip = 127.0.0.1
+#Port = 27015
+#Type = Steam
+#updateInterval = 30
+
+#[Gamespy3Server]
+#Ip = 127.0.0.1
+#Port = 5446
+#Type = Gamespy3
+#updateInterval = 30