fix port ranges (#6)

This commit is contained in:
CommonLoon102 2020-01-27 16:25:45 +00:00 committed by GitHub
parent 4f96ad89db
commit c1b385c41d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 236 additions and 164 deletions

View File

@ -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}";
}
}

View File

@ -7,7 +7,6 @@ namespace Common
{ {
public class Constants public class Constants
{ {
public const string NBloodExecutable = "nblood_server";
public static readonly IReadOnlyDictionary<string, Mod> SupportedMods = new Dictionary<string, Mod>() public static readonly IReadOnlyDictionary<string, Mod> SupportedMods = new Dictionary<string, Mod>()
{ {
{ "BLOOD", new Mod("BLOOD", "Blood", "") }, { "BLOOD", new Mod("BLOOD", "Blood", "") },

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

86
Common/ProcessSpawner.cs Normal file
View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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 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. 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. You can see the port and the command line command to join in the response.
Port range used: 23580-23700

View File

@ -26,23 +26,30 @@ namespace Supervisor
private static void ProcessPacket(byte[] buffer) private static void ProcessPacket(byte[] buffer)
{ {
string message = Encoding.ASCII.GetString(buffer); try
switch (message[0])
{ {
case 'A': string message = Encoding.ASCII.GetString(buffer);
ProcessPlayerCountsPacket(message); switch (message[0])
break; {
case 'B': case 'A':
ProcessPlayerNamesPacket(message); ProcessPlayerCountsPacket(message);
break; break;
case 'C': case 'B':
ProcessFragsPacket(buffer); ProcessPlayerNamesPacket(message);
break; break;
case 'D': case 'C':
ProcessRemovePacket(message); ProcessFragsPacket(buffer);
break; break;
default: case 'D':
break; ProcessRemovePacket(message);
break;
default:
break;
}
}
catch
{
// Log...
} }
} }

View File

@ -24,15 +24,22 @@ namespace Supervisor
private static void KillUnusedServers() private static void KillUnusedServers()
{ {
var killables = Program.State.Servers.Values.Where(s => try
s.IsPrivate
&& !s.IsStarted
&& s.CurrentPlayers < 2
&& (DateTime.UtcNow - s.SpawnedAtUtc) > TimeSpan.FromMinutes(10));
foreach (var server in killables)
{ {
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...
} }
} }
} }

View File

@ -26,13 +26,20 @@ namespace Supervisor
private static void RemoveCrashedServers() private static void RemoveCrashedServers()
{ {
var crashedServers = State.Servers.Values try
.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 _); 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...
} }
} }
} }

View File

@ -46,24 +46,33 @@ namespace Supervisor
{ {
if (IsNewServerNeeded(i, mod)) if (IsNewServerNeeded(i, mod))
{ {
int port = PortUtils.GetPort(); try
var process = Process.Start(NBloodServerStartInfo.Get(i, port, mod));
Program.State.Servers.AddOrUpdate(port, new Server()
{ {
Port = port, var spawnedServer = ProcessSpawner.SpawnServer(i, mod.Name);
ProcessId = process.Id, int port = spawnedServer.Port;
MaximumPlayers = i, int processId = spawnedServer.Process.Id;
CurrentPlayers = 1, Program.State.Servers.AddOrUpdate(port, new Server()
Mod = mod, {
}, Port = port,
(prt, server) => 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; // No free ports, cannot create process
server.MaximumPlayers = i; // Log...
server.CurrentPlayers = 1; }
server.Mod = mod;
return server;
});
} }
} }
} }

View File

@ -31,17 +31,24 @@ namespace Supervisor
private static void ProcessWebApiMessage(byte[] buffer) private static void ProcessWebApiMessage(byte[] buffer)
{ {
string message = Encoding.ASCII.GetString(buffer); try
switch (message[0])
{ {
case 'A': string message = Encoding.ASCII.GetString(buffer);
ProcessGetCurrentStateRequest(); switch (message[0])
break; {
case 'B': case 'A':
StorePrivateServerInfo(message); ProcessGetCurrentStateRequest();
break; break;
default: case 'B':
break; StorePrivateServerInfo(message);
break;
default:
break;
}
}
catch
{
// Log...
} }
} }

View File

@ -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}";
}
}
}

View File

@ -58,23 +58,20 @@ namespace WebInterface.Controllers
if (parameters.ApiKey != _config.GetValue<string>("ApiKey")) if (parameters.ApiKey != _config.GetValue<string>("ApiKey"))
return new StartServerResponse("Invalid ApiKey."); return new StartServerResponse("Invalid ApiKey.");
string processName = Constants.NBloodExecutable; SpawnedServerInfo serverProcess = ProcessSpawner.SpawnServer(parameters.Players, parameters.ModName);
int serversRunning = Process.GetProcessesByName(processName).Count(); byte[] payload = Encoding.ASCII.GetBytes($"B{serverProcess.Port}\t{serverProcess.Process.Id}\0");
if (serversRunning >= _config.GetValue<int>("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");
socket.SendTo(payload, webApiListenerEndPoint); socket.SendTo(payload, webApiListenerEndPoint);
_logger.LogInformation("Server started waiting for {0} players on port {1}.", _logger.LogInformation("Server started waiting for {0} players on port {1}.",
parameters.Players, port); parameters.Players, serverProcess.Port);
Thread.Sleep(TimeSpan.FromSeconds(2)); 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) catch (Exception ex)
{ {
@ -108,16 +105,5 @@ namespace WebInterface.Controllers
return new ListServersResponse("Unhandled exception has been occured. Check the logs for details."); 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()];
}
} }
} }

View File

@ -1,4 +1,5 @@
using Model; using Common;
using Model;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@ -28,7 +29,7 @@ namespace WebInterface.Services
{ {
Port = s.Port, Port = s.Port,
IsStarted = s.IsStarted, 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, GameType = s.GameType,
Mod = s.Mod.FriendlyName, Mod = s.Mod.FriendlyName,
CurrentPlayers = s.CurrentPlayers, CurrentPlayers = s.CurrentPlayers,
@ -41,6 +42,14 @@ namespace WebInterface.Services
return serversResponse; 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() public GetStatisticsResponse GetStatistics()
{ {
StateResponse stateResponse = RequestState(); StateResponse stateResponse = RequestState();

View File

@ -159,7 +159,7 @@
</details> </details>
<br /> <br />
<hr /> <hr />
<p>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): <a href="https://github.com/CommonLoon102/NBloodServerSupervisor">https://github.com/CommonLoon102/NBloodServerSupervisor</a></p> <p>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): <a href="https://github.com/CommonLoon102/NBloodServerSupervisor">https://github.com/CommonLoon102/NBloodServerSupervisor</a></p>
</div> </div>
</body> </body>
</html> </html>

View File

@ -7,6 +7,5 @@
} }
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"MaximumServers": 80,
"ApiKey": "CHANGEME" "ApiKey": "CHANGEME"
} }