From c1b385c41dc4b66dcdd89ad1c51ec9a49e71d54a Mon Sep 17 00:00:00 2001 From: CommonLoon102 <321850+CommonLoon102@users.noreply.github.com> Date: Mon, 27 Jan 2020 16:25:45 +0000 Subject: [PATCH] fix port ranges (#6) --- Common/CommandLineUtils.cs | 12 +++ Common/Constants.cs | 1 - Common/NBloodServerStartInfo.cs | 37 --------- Common/PortUtils.cs | 26 ------ Common/ProcessSpawner.cs | 86 ++++++++++++++++++++ Common/SpawnedServerInfo.cs | 22 +++++ README.md | 2 + Supervisor/NBloodServerListener.cs | 39 +++++---- Supervisor/PrivateServerManager.cs | 23 ++++-- Supervisor/Program.cs | 19 +++-- Supervisor/PublicServerManager.cs | 41 ++++++---- Supervisor/WebApiListener.cs | 27 +++--- WebInterface/CommandLineUtils.cs | 17 ---- WebInterface/Controllers/NBloodController.cs | 32 ++------ WebInterface/Services/StateService.cs | 13 ++- WebInterface/Views/Home/Index.cshtml | 2 +- WebInterface/appsettings.json | 1 - 17 files changed, 236 insertions(+), 164 deletions(-) create mode 100644 Common/CommandLineUtils.cs delete mode 100644 Common/NBloodServerStartInfo.cs delete mode 100644 Common/PortUtils.cs create mode 100644 Common/ProcessSpawner.cs create mode 100644 Common/SpawnedServerInfo.cs delete mode 100644 WebInterface/CommandLineUtils.cs diff --git a/Common/CommandLineUtils.cs b/Common/CommandLineUtils.cs new file mode 100644 index 0000000..90a60f0 --- /dev/null +++ b/Common/CommandLineUtils.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Common +{ + public static class CommandLineUtils + { + public static string GetClientLaunchCommand(string host, int port, string modCommandLine) => + $"nblood -client {host} -port {port} {modCommandLine}"; + } +} diff --git a/Common/Constants.cs b/Common/Constants.cs index 3c89c45..d4c3c5b 100644 --- a/Common/Constants.cs +++ b/Common/Constants.cs @@ -7,7 +7,6 @@ namespace Common { public class Constants { - public const string NBloodExecutable = "nblood_server"; public static readonly IReadOnlyDictionary SupportedMods = new Dictionary() { { "BLOOD", new Mod("BLOOD", "Blood", "") }, diff --git a/Common/NBloodServerStartInfo.cs b/Common/NBloodServerStartInfo.cs deleted file mode 100644 index 9e8c0b0..0000000 --- a/Common/NBloodServerStartInfo.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Model; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Text; - -namespace Common -{ - public static class NBloodServerStartInfo - { - private static readonly string workingDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "blood"); - - public static ProcessStartInfo Get(int maxPlayers, int port, Mod mod) - { - var psi = new ProcessStartInfo(GetExecutable(), $"-server {maxPlayers} -port {port} -pname Server {mod.CommandLine}") - { - UseShellExecute = true, - WorkingDirectory = workingDir - }; - - return psi; - } - - private static string GetExecutable() - { - string nbloodServer = Constants.NBloodExecutable; - bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - if (isWindows) - nbloodServer += ".exe"; - - return nbloodServer; - } - } -} diff --git a/Common/PortUtils.cs b/Common/PortUtils.cs deleted file mode 100644 index 1fa494d..0000000 --- a/Common/PortUtils.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.NetworkInformation; -using System.Text; - -namespace Common -{ - public class PortUtils - { - public static int GetPort() - { - IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); - var usedPorts = ipGlobalProperties.GetActiveTcpConnections().Select(c => c.LocalEndPoint.Port).ToList(); - usedPorts.AddRange(ipGlobalProperties.GetActiveTcpListeners().Select(c => c.Port).ToList()); - usedPorts.AddRange(ipGlobalProperties.GetActiveUdpListeners().Select(c => c.Port).ToList()); - - var availablePorts = Enumerable.Range(1025, ushort.MaxValue).ToList().Except(usedPorts).ToList(); - - Random rnd = new Random(); - int index = rnd.Next(0, availablePorts.Count() - 1); - int port = availablePorts[index]; - return port; - } - } -} diff --git a/Common/ProcessSpawner.cs b/Common/ProcessSpawner.cs new file mode 100644 index 0000000..b5b4b82 --- /dev/null +++ b/Common/ProcessSpawner.cs @@ -0,0 +1,86 @@ +using Model; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Linq; +using System.Net.NetworkInformation; + +namespace Common +{ + public static class ProcessSpawner + { + private const int minPort = 23581; + private const int maxPort = 23700; + private const int maximumServers = 80; + private const string nBloodExecutable = "nblood_server"; + + private static readonly string workingDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "blood"); + private static readonly Random rnd = new Random(); + + public static SpawnedServerInfo SpawnServer(int players, string modName) + { + int serversRunning = Process.GetProcessesByName(nBloodExecutable).Count(); + if (serversRunning >= maximumServers) + throw new Exception("The maximum number of servers are already running."); + + Mod mod = GetModByName(modName); + int port = GetPort(); + + var process = Process.Start(GetProcessStartInfo(players, port, mod)); + return new SpawnedServerInfo(process, port, mod); + } + + private static Mod GetModByName(string modName) + { + if (string.IsNullOrWhiteSpace(modName)) + return Constants.SupportedMods["BLOOD"]; + + if (!Constants.SupportedMods.ContainsKey(modName.ToUpper())) + throw new Exception("This mod is not supported: " + modName); + + return Constants.SupportedMods[modName.ToUpper()]; + } + + private static int GetPort() + { + IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); + var usedPorts = ipGlobalProperties.GetActiveTcpConnections().Select(c => c.LocalEndPoint.Port).ToList(); + usedPorts.AddRange(ipGlobalProperties.GetActiveTcpListeners().Select(c => c.Port).ToList()); + usedPorts.AddRange(ipGlobalProperties.GetActiveUdpListeners().Select(c => c.Port).ToList()); + + var availablePorts = Enumerable.Range(minPort, maxPort - minPort + 1).ToList().Except(usedPorts).ToList(); + + if (availablePorts.Count == 0) + throw new Exception($"Cannot obtain free port in range {minPort}-{maxPort}."); + + int index = rnd.Next(0, availablePorts.Count() - 1); + int port = availablePorts[index]; + return port; + } + + private static ProcessStartInfo GetProcessStartInfo(int maxPlayers, int port, Mod mod) + { + var psi = new ProcessStartInfo(GetExecutableName(), $"-server {maxPlayers} -port {port} -pname Server {mod.CommandLine}") + { + UseShellExecute = true, + WorkingDirectory = workingDir + }; + + return psi; + } + + private static string GetExecutableName() + { + string nbloodServer = nBloodExecutable; + bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + if (isWindows) + nbloodServer += ".exe"; + + return nbloodServer; + } + } +} diff --git a/Common/SpawnedServerInfo.cs b/Common/SpawnedServerInfo.cs new file mode 100644 index 0000000..b6990ea --- /dev/null +++ b/Common/SpawnedServerInfo.cs @@ -0,0 +1,22 @@ +using Model; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace Common +{ + public class SpawnedServerInfo + { + public Process Process { get; } + public int Port { get; } + public Mod Mod { get; } + + public SpawnedServerInfo(Process process, int port, Mod mod) + { + Process = process; + Port = port; + Mod = mod; + } + } +} diff --git a/README.md b/README.md index 0a24150..ff99b28 100644 --- a/README.md +++ b/README.md @@ -89,3 +89,5 @@ http://your.ip.goes.here:23580/nblood/api/startserver?players=3&modName=cryptic& The number of players must be at least 3 and maximum 8. The servers started with this URL won't be visible publicly via the `listservers` URL. The modName parameter can be `cryptic`, `dw`, `fo`, `twoira` or it can be missing. You can see the port and the command line command to join in the response. + +Port range used: 23580-23700 diff --git a/Supervisor/NBloodServerListener.cs b/Supervisor/NBloodServerListener.cs index 6dff515..b1bf705 100644 --- a/Supervisor/NBloodServerListener.cs +++ b/Supervisor/NBloodServerListener.cs @@ -26,23 +26,30 @@ namespace Supervisor private static void ProcessPacket(byte[] buffer) { - string message = Encoding.ASCII.GetString(buffer); - switch (message[0]) + try { - case 'A': - ProcessPlayerCountsPacket(message); - break; - case 'B': - ProcessPlayerNamesPacket(message); - break; - case 'C': - ProcessFragsPacket(buffer); - break; - case 'D': - ProcessRemovePacket(message); - break; - default: - break; + string message = Encoding.ASCII.GetString(buffer); + switch (message[0]) + { + case 'A': + ProcessPlayerCountsPacket(message); + break; + case 'B': + ProcessPlayerNamesPacket(message); + break; + case 'C': + ProcessFragsPacket(buffer); + break; + case 'D': + ProcessRemovePacket(message); + break; + default: + break; + } + } + catch + { + // Log... } } diff --git a/Supervisor/PrivateServerManager.cs b/Supervisor/PrivateServerManager.cs index cef5fe2..80f078c 100644 --- a/Supervisor/PrivateServerManager.cs +++ b/Supervisor/PrivateServerManager.cs @@ -24,15 +24,22 @@ namespace Supervisor private static void KillUnusedServers() { - var killables = Program.State.Servers.Values.Where(s => - s.IsPrivate - && !s.IsStarted - && s.CurrentPlayers < 2 - && (DateTime.UtcNow - s.SpawnedAtUtc) > TimeSpan.FromMinutes(10)); - - foreach (var server in killables) + try { - Process.GetProcessById(server.ProcessId).Kill(); + var killables = Program.State.Servers.Values.Where(s => + s.IsPrivate + && !s.IsStarted + && s.CurrentPlayers < 2 + && (DateTime.UtcNow - s.SpawnedAtUtc) > TimeSpan.FromMinutes(10)); + + foreach (var server in killables) + { + Process.GetProcessById(server.ProcessId).Kill(); + } + } + catch + { + //Log... } } } diff --git a/Supervisor/Program.cs b/Supervisor/Program.cs index f074955..2effb2c 100644 --- a/Supervisor/Program.cs +++ b/Supervisor/Program.cs @@ -26,13 +26,20 @@ namespace Supervisor private static void RemoveCrashedServers() { - var crashedServers = State.Servers.Values - .Where(s => s.IsStarted && DateTime.UtcNow - s.LastHeartBeatUtc < TimeSpan.FromMinutes(15)) - .Select(s => s.Port); - - foreach (var port in crashedServers) + try { - State.Servers.TryRemove(port, out _); + var crashedServers = State.Servers.Values + .Where(s => s.IsStarted && DateTime.UtcNow - s.LastHeartBeatUtc < TimeSpan.FromMinutes(15)) + .Select(s => s.Port); + + foreach (var port in crashedServers) + { + State.Servers.TryRemove(port, out _); + } + } + catch + { + // Log... } } } diff --git a/Supervisor/PublicServerManager.cs b/Supervisor/PublicServerManager.cs index 63a03cb..456b913 100644 --- a/Supervisor/PublicServerManager.cs +++ b/Supervisor/PublicServerManager.cs @@ -46,24 +46,33 @@ namespace Supervisor { if (IsNewServerNeeded(i, mod)) { - int port = PortUtils.GetPort(); - var process = Process.Start(NBloodServerStartInfo.Get(i, port, mod)); - Program.State.Servers.AddOrUpdate(port, new Server() + try { - Port = port, - ProcessId = process.Id, - MaximumPlayers = i, - CurrentPlayers = 1, - Mod = mod, - }, - (prt, server) => + var spawnedServer = ProcessSpawner.SpawnServer(i, mod.Name); + int port = spawnedServer.Port; + int processId = spawnedServer.Process.Id; + Program.State.Servers.AddOrUpdate(port, new Server() + { + Port = port, + ProcessId = processId, + MaximumPlayers = i, + CurrentPlayers = 1, + Mod = mod, + }, + (prt, server) => + { + server.ProcessId = processId; + server.MaximumPlayers = i; + server.CurrentPlayers = 1; + server.Mod = mod; + return server; + }); + } + catch { - server.ProcessId = process.Id; - server.MaximumPlayers = i; - server.CurrentPlayers = 1; - server.Mod = mod; - return server; - }); + // No free ports, cannot create process + // Log... + } } } } diff --git a/Supervisor/WebApiListener.cs b/Supervisor/WebApiListener.cs index 377760c..e4299c0 100644 --- a/Supervisor/WebApiListener.cs +++ b/Supervisor/WebApiListener.cs @@ -31,17 +31,24 @@ namespace Supervisor private static void ProcessWebApiMessage(byte[] buffer) { - string message = Encoding.ASCII.GetString(buffer); - switch (message[0]) + try { - case 'A': - ProcessGetCurrentStateRequest(); - break; - case 'B': - StorePrivateServerInfo(message); - break; - default: - break; + string message = Encoding.ASCII.GetString(buffer); + switch (message[0]) + { + case 'A': + ProcessGetCurrentStateRequest(); + break; + case 'B': + StorePrivateServerInfo(message); + break; + default: + break; + } + } + catch + { + // Log... } } diff --git a/WebInterface/CommandLineUtils.cs b/WebInterface/CommandLineUtils.cs deleted file mode 100644 index a5f85b5..0000000 --- a/WebInterface/CommandLineUtils.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Model; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace WebInterface -{ - public static class CommandLineUtils - { - public static string GetLaunchCommand(string host, int port, Mod mod) - { - return $"nblood -client {host} -port {port} {mod.CommandLine}"; - } - } -} diff --git a/WebInterface/Controllers/NBloodController.cs b/WebInterface/Controllers/NBloodController.cs index 3f292a5..d1069fa 100644 --- a/WebInterface/Controllers/NBloodController.cs +++ b/WebInterface/Controllers/NBloodController.cs @@ -58,23 +58,20 @@ namespace WebInterface.Controllers if (parameters.ApiKey != _config.GetValue("ApiKey")) return new StartServerResponse("Invalid ApiKey."); - string processName = Constants.NBloodExecutable; - int serversRunning = Process.GetProcessesByName(processName).Count(); - if (serversRunning >= _config.GetValue("MaximumServers")) - return new StartServerResponse("The maximum number of servers are already running."); - - Mod mod = GetMod(parameters.ModName); - int port = PortUtils.GetPort(); - - var process = Process.Start(NBloodServerStartInfo.Get(parameters.Players, port, mod)); - byte[] payload = Encoding.ASCII.GetBytes($"B{port}\t{process.Id}\0"); + SpawnedServerInfo serverProcess = ProcessSpawner.SpawnServer(parameters.Players, parameters.ModName); + byte[] payload = Encoding.ASCII.GetBytes($"B{serverProcess.Port}\t{serverProcess.Process.Id}\0"); socket.SendTo(payload, webApiListenerEndPoint); _logger.LogInformation("Server started waiting for {0} players on port {1}.", - parameters.Players, port); + parameters.Players, serverProcess.Port); Thread.Sleep(TimeSpan.FromSeconds(2)); - return new StartServerResponse(port) { CommandLine = CommandLineUtils.GetLaunchCommand(HttpContext.Request.Host.Host, port, mod) }; + return new StartServerResponse(serverProcess.Port) + { + CommandLine = CommandLineUtils.GetClientLaunchCommand(HttpContext.Request.Host.Host, + serverProcess.Port, + serverProcess.Mod.CommandLine) + }; } catch (Exception ex) { @@ -108,16 +105,5 @@ namespace WebInterface.Controllers return new ListServersResponse("Unhandled exception has been occured. Check the logs for details."); } } - - private Mod GetMod(string modName) - { - if (string.IsNullOrWhiteSpace(modName)) - return Constants.SupportedMods["BLOOD"]; - - if (!Constants.SupportedMods.ContainsKey(modName.ToUpper())) - throw new Exception("This mod is not supported: " + modName); - - return Constants.SupportedMods[modName.ToUpper()]; - } } } \ No newline at end of file diff --git a/WebInterface/Services/StateService.cs b/WebInterface/Services/StateService.cs index f7221e5..9551370 100644 --- a/WebInterface/Services/StateService.cs +++ b/WebInterface/Services/StateService.cs @@ -1,4 +1,5 @@ -using Model; +using Common; +using Model; using System.IO; using System.Linq; using System.Net; @@ -28,7 +29,7 @@ namespace WebInterface.Services { Port = s.Port, IsStarted = s.IsStarted, - CommandLine = s.CurrentPlayers == s.MaximumPlayers ? "Sorry, the game is already started." : CommandLineUtils.GetLaunchCommand(host, s.Port, s.Mod), + CommandLine = GetCommandLine(s, host), GameType = s.GameType, Mod = s.Mod.FriendlyName, CurrentPlayers = s.CurrentPlayers, @@ -41,6 +42,14 @@ namespace WebInterface.Services return serversResponse; } + public string GetCommandLine(Model.Server server, string host) + { + if (server.CurrentPlayers == server.MaximumPlayers) + return "Sorry, the game is already started."; + + return CommandLineUtils.GetClientLaunchCommand(host, server.Port, server.Mod.CommandLine); + } + public GetStatisticsResponse GetStatistics() { StateResponse stateResponse = RequestState(); diff --git a/WebInterface/Views/Home/Index.cshtml b/WebInterface/Views/Home/Index.cshtml index e9989be..1f63976 100644 --- a/WebInterface/Views/Home/Index.cshtml +++ b/WebInterface/Views/Home/Index.cshtml @@ -159,7 +159,7 @@

-

Do you think this page is ugly? You are right! If you want to make it better, PRs are welcomed (but please, don't use any JavaScript, thanks): https://github.com/CommonLoon102/NBloodServerSupervisor

+

Do you think this page is ugly? You are right! If you want to make it look better, PRs are welcomed (but please, don't use any JavaScript, thanks): https://github.com/CommonLoon102/NBloodServerSupervisor

diff --git a/WebInterface/appsettings.json b/WebInterface/appsettings.json index e708deb..563ad16 100644 --- a/WebInterface/appsettings.json +++ b/WebInterface/appsettings.json @@ -7,6 +7,5 @@ } }, "AllowedHosts": "*", - "MaximumServers": 80, "ApiKey": "CHANGEME" }