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 const string NBloodExecutable = "nblood_server";
public static readonly IReadOnlyDictionary<string, Mod> SupportedMods = new Dictionary<string, Mod>()
{
{ "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 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

View File

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

View File

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

View File

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

View File

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

View File

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

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"))
return new StartServerResponse("Invalid ApiKey.");
string processName = Constants.NBloodExecutable;
int serversRunning = Process.GetProcessesByName(processName).Count();
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");
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()];
}
}
}

View File

@ -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();

View File

@ -159,7 +159,7 @@
</details>
<br />
<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>
</body>
</html>

View File

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