using LightReflectiveMirror.Debug; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; namespace LightReflectiveMirror.LoadBalancing { partial class Program { /// /// Keeps track of all the LRM nodes registered to the Load Balancer. /// public Dictionary availableRelayServers = new(); public static Dictionary cachedRooms = new(); private int _pingDelay = 10000; public static bool showDebugLogs = false; public static DateTime startupTime; const string API_PATH = "/api/stats"; readonly string CONFIG_PATH = System.Environment.GetEnvironmentVariable("LRM_LB_CONFIG_PATH") ?? "config.json"; public static Config conf; public static Program instance; public static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); public async Task MainAsync() { WriteTitle(); instance = this; startupTime = DateTime.Now; if (!File.Exists(CONFIG_PATH)) { File.WriteAllText(CONFIG_PATH, JsonConvert.SerializeObject(new Config(), Formatting.Indented)); Logger.ForceLogMessage("A config.json file was generated. Please configure it to the proper settings and re-run!", ConsoleColor.Yellow); Console.ReadKey(); Environment.Exit(0); } else { conf = JsonConvert.DeserializeObject(File.ReadAllText(CONFIG_PATH)); Logger.ConfigureLogger(new Logger.LogConfiguration { sendLogs = conf.ShowDebugLogs }); _pingDelay = conf.ConnectedServerPingRate; showDebugLogs = conf.ShowDebugLogs; if (new EndpointServer().Start(conf.EndpointPort)) Logger.ForceLogMessage("Endpoint server started successfully", ConsoleColor.Green); else Logger.ForceLogMessage("Endpoint server started unsuccessfully", ConsoleColor.Red); } var pingThread = new Thread(new ThreadStart(PingServers)); pingThread.Start(); // keep console alive await Task.Delay(-1); } /// /// Called when a new server requested that we add them to our load balancer. /// /// /// /// /// /// public async Task AddServer(string serverIP, ushort port, ushort endpointPort, string publicIP, int regionId) { var relayAddr = new RelayAddress { port = port, endpointPort = endpointPort, address = publicIP, endpointAddress = serverIP.Trim(), serverRegion = (LRMRegions)regionId }; if (availableRelayServers.ContainsKey(relayAddr)) { Logger.ForceLogMessage($"LRM Node {serverIP}:{port} tried to register while already registered!"); return; } var stats = await RequestStatsFromNode(serverIP, endpointPort); if (stats.HasValue) { Logger.ForceLogMessage($"LRM Node Registered! {serverIP}:{port}", ConsoleColor.Green); availableRelayServers.Add(relayAddr, stats.Value); } else { Logger.ForceLogMessage($"LRM Node Failed to respond to ping back. Make sure {serverIP}:{port} is port forwarded!"); } } /// /// Called when we want to get the server info from a server. /// /// /// /// public async Task RequestStatsFromNode(string serverIP, ushort port) { using (WebClient wc = new WebClient()) { try { string receivedStats = await wc.DownloadStringTaskAsync($"http://{serverIP}:{port}{API_PATH}"); var stats = JsonConvert.DeserializeObject(receivedStats); if (stats.serversConnectedToRelay == null) stats.serversConnectedToRelay = new List(); return stats; } catch (Exception e) { // Server failed to respond to stats, dont add to load balancer. return null; } } } /// /// Called when we want to check if a server is alive. /// /// /// /// public async Task HealthCheckNode(string serverIP, ushort port) { using (WebClient wc = new WebClient()) { try { await wc.DownloadStringTaskAsync($"http://{serverIP}:{port}{API_PATH}"); // If it got to here, then the server is healthy! return true; } catch (Exception e) { // Server failed to respond return false; } } } /// /// Called when we want to get the list of rooms in a specific LRM node. /// /// /// /// public async Task> RequestServerListFromNode(string serverIP, ushort port) { using (WebClient wc = new WebClient()) { try { string receivedStats = await wc.DownloadStringTaskAsync($"http://{serverIP}:{port}/api/servers"); var stats = JsonConvert.DeserializeObject>(receivedStats); // If they have no servers, it will return null as json for some reason. if (stats == null) return new List(); else return stats; } catch (Exception e) { // Server failed to respond return new List(); } } } /// /// A thread constantly running and making sure LRM nodes are still healthy. /// async void PingServers() { while (true) { Logger.WriteLogMessage("Pinging " + availableRelayServers.Count + " available relays"); // Create a new list so we can modify the collection in our loop. var keys = new List(availableRelayServers.Keys); for (int i = 0; i < keys.Count; i++) { if(!await HealthCheckNode(keys[i].endpointAddress, keys[i].endpointPort)) { Logger.ForceLogMessage($"Server {keys[i].address}:{keys[i].port} failed a health check, removing from load balancer.", ConsoleColor.Red); availableRelayServers.Remove(keys[i]); } } GC.Collect(); await Task.Delay(_pingDelay); } } void WriteTitle() { string t = @" _ _____ __ __ | | | __ \ | \/ | | | | |__) | | \ / | | | | _ / | |\/| | | |____ | | \ \ | | | | w c(..)o ( |______| |_| \_\ |_| |_| \__(-) __) _ ____ _____ /\ ( | | / __ \ /\ | __ \ /(_)___) | | | | | | / \ | | | | w /| | | | | | | / /\ \ | | | | | \ | |____ | |__| | / ____ \ | |__| | m m copyright monkesoft 2021 |______| \____/ /_/ \_\ |_____/ ____ _ _ _ _____ ______ _____ | _ \ /\ | | /\ | \ | | / ____| | ____| | __ \ | |_) | / \ | | / \ | \| | | | | |__ | |__) | | _ < / /\ \ | | / /\ \ | . ` | | | | __| | _ / | |_) | / ____ \ | |____ / ____ \ | |\ | | |____ | |____ | | \ \ |____/ /_/ \_\ |______| /_/ \_\ |_| \_| \_____| |______| |_| \_\ "; string load = $"Chimp Event Listener Initializing... OK" + "\nHarambe Memorial Initializing... OK" + "\nBananas Initializing... OK\n"; Logger.ForceLogMessage(t, ConsoleColor.Green); Logger.ForceLogMessage(load, ConsoleColor.Cyan); } } }