Server status framework. (#709)

Adds a HTTP server to the server that exposes `/status` to fetch the server status.
This commit is contained in:
Pieter-Jan Briers
2018-11-26 09:58:58 +01:00
committed by GitHub
parent baff29362a
commit 21fd3e5d96
9 changed files with 341 additions and 23 deletions

View File

@@ -32,6 +32,7 @@ using SS14.Shared.Network.Messages;
using SS14.Shared.Prototypes;
using SS14.Shared.Map;
using SS14.Server.Interfaces.Maps;
using SS14.Server.Interfaces.ServerStatus;
using SS14.Server.Player;
using SS14.Server.ViewVariables;
using SS14.Shared.Asynchronous;
@@ -215,6 +216,7 @@ namespace SS14.Server
AssemblyLoader.BroadcastRunLevel(AssemblyLoader.RunLevel.PostInit);
_entities.Startup();
IoCManager.Resolve<IStatusHost>().Start();
return false;
}

View File

@@ -18,6 +18,7 @@ namespace SS14.Server.Interfaces.Player
{
/// <summary>
/// Number of players currently connected to this server.
/// Fetching this is thread safe.
/// </summary>
int PlayerCount { get; }

View File

@@ -0,0 +1,18 @@
using System;
using Newtonsoft.Json.Linq;
namespace SS14.Server.Interfaces.ServerStatus
{
public interface IStatusHost
{
void Start();
/// <summary>
/// Invoked when a client queries a status request from the server.
/// THIS IS INVOKED FROM ANOTHER THREAD.
/// I REPEAT, THIS DOES NOT RUN ON THE MAIN THREAD.
/// MAKE TRIPLE SURE EVERYTHING IN HERE IS THREAD SAFE DEAR GOD.
/// </summary>
event Action<JObject> OnStatusRequest;
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using SS14.Server.Interfaces;
using SS14.Server.Interfaces.GameObjects;
using SS14.Server.Interfaces.Player;
@@ -37,6 +38,8 @@ namespace SS14.Server.Player
private bool NeedsStateUpdate;
private readonly ReaderWriterLockSlim _sessionsLock = new ReaderWriterLockSlim();
/// <summary>
/// Active sessions of connected clients to the server.
/// </summary>
@@ -45,7 +48,21 @@ namespace SS14.Server.Player
private Dictionary<NetSessionId, PlayerData> _playerData;
/// <inheritdoc />
public int PlayerCount => _sessions.Count;
public int PlayerCount
{
get
{
_sessionsLock.EnterReadLock();
try
{
return _sessions.Count;
}
finally
{
_sessionsLock.ExitReadLock();
}
}
}
/// <inheritdoc />
public int MaxPlayers { get; private set; } = 32;
@@ -77,27 +94,59 @@ namespace SS14.Server.Player
IPlayerSession IPlayerManager.GetSessionByChannel(INetChannel channel) => GetSessionByChannel(channel);
private PlayerSession GetSessionByChannel(INetChannel channel)
{
// Should only be one session per client. Returns that session, in theory.
return _sessions[channel.SessionId];
_sessionsLock.EnterReadLock();
try
{
// Should only be one session per client. Returns that session, in theory.
return _sessions[channel.SessionId];
}
finally
{
_sessionsLock.ExitReadLock();
}
}
/// <inheritdoc />
public IPlayerSession GetSessionById(NetSessionId index)
{
return _sessions[index];
_sessionsLock.EnterReadLock();
try
{
return _sessions[index];
}
finally
{
_sessionsLock.ExitReadLock();
}
}
public bool ValidSessionId(NetSessionId index)
{
return _sessions.ContainsKey(index);
_sessionsLock.EnterReadLock();
try
{
return _sessions.ContainsKey(index);
}
finally
{
_sessionsLock.ExitReadLock();
}
}
public bool TryGetSessionById(NetSessionId sessionId, out IPlayerSession session)
{
if (_sessions.TryGetValue(sessionId, out var _session))
_sessionsLock.EnterReadLock();
try
{
session = _session;
return true;
if (_sessions.TryGetValue(sessionId, out var _session))
{
session = _session;
return true;
}
}
finally
{
_sessionsLock.ExitReadLock();
}
session = default;
return false;
@@ -108,8 +157,16 @@ namespace SS14.Server.Player
/// </summary>
public void SendJoinGameToAll()
{
foreach (var s in _sessions.Values)
s.JoinGame();
_sessionsLock.EnterReadLock();
try
{
foreach (var s in _sessions.Values)
s.JoinGame();
}
finally
{
_sessionsLock.ExitReadLock();
}
}
public IEnumerable<IPlayerData> GetAllPlayerData()
@@ -122,8 +179,16 @@ namespace SS14.Server.Player
/// </summary>
public void DetachAll()
{
foreach (var s in _sessions.Values)
s.DetachFromEntity();
_sessionsLock.EnterReadLock();
try
{
foreach (var s in _sessions.Values)
s.DetachFromEntity();
}
finally
{
_sessionsLock.ExitReadLock();
}
}
/// <summary>
@@ -134,12 +199,22 @@ namespace SS14.Server.Player
/// <returns></returns>
public List<IPlayerSession> GetPlayersInRange(GridLocalCoordinates worldPos, int range)
{
_sessionsLock.EnterReadLock();
//TODO: This needs to be moved to the PVS system.
return
_sessions.Values.Where(x => x.AttachedEntity != null &&
worldPos.InRange(x.AttachedEntity.GetComponent<ITransformComponent>().LocalPosition, range))
.Cast<IPlayerSession>()
.ToList();
try
{
return
_sessions.Values.Where(x => x.AttachedEntity != null &&
worldPos.InRange(
x.AttachedEntity.GetComponent<ITransformComponent>().LocalPosition,
range))
.Cast<IPlayerSession>()
.ToList();
}
finally
{
_sessionsLock.ExitReadLock();
}
}
/// <summary>
@@ -148,7 +223,15 @@ namespace SS14.Server.Player
/// <returns></returns>
public List<IPlayerSession> GetAllPlayers()
{
return _sessions.Values.Cast<IPlayerSession>().ToList();
_sessionsLock.EnterReadLock();
try
{
return _sessions.Values.Cast<IPlayerSession>().ToList();
}
finally
{
_sessionsLock.ExitReadLock();
}
}
/// <summary>
@@ -162,9 +245,17 @@ namespace SS14.Server.Player
return null;
}
NeedsStateUpdate = false;
return _sessions.Values
.Select(s => s.PlayerState)
.ToList();
_sessionsLock.EnterReadLock();
try
{
return _sessions.Values
.Select(s => s.PlayerState)
.ToList();
}
finally
{
_sessionsLock.ExitReadLock();
}
}
private void OnConnecting(object sender, NetConnectingArgs args)
@@ -189,7 +280,15 @@ namespace SS14.Server.Player
session.PlayerStatusChanged += (obj, sessionArgs) => OnPlayerStatusChanged(session, sessionArgs.OldStatus, sessionArgs.NewStatus);
_sessions[args.Channel.SessionId] = session;
_sessionsLock.EnterWriteLock();
try
{
_sessions.Add(args.Channel.SessionId, session);
}
finally
{
_sessionsLock.ExitWriteLock();
}
}
private void OnPlayerStatusChanged(IPlayerSession session, SessionStatus oldStatus, SessionStatus newStatus)
@@ -209,7 +308,15 @@ namespace SS14.Server.Player
//Detach the entity and (don't)delete it.
session.OnDisconnect();
_sessions.Remove(session.SessionId);
_sessionsLock.EnterWriteLock();
try
{
_sessions.Remove(session.SessionId);
}
finally
{
_sessionsLock.ExitWriteLock();
}
}
private void HandleWelcomeMessageReq(MsgServerInfoReq message)

View File

@@ -42,6 +42,8 @@ using System.Reflection;
using SS14.Shared.Interfaces.Resources;
using SS14.Server.Console;
using SS14.Server.Interfaces.Console;
using SS14.Server.Interfaces.ServerStatus;
using SS14.Server.ServerStatus;
using SS14.Server.ViewVariables;
using SS14.Shared.Asynchronous;
@@ -135,6 +137,7 @@ namespace SS14.Server
IoCManager.Register<IPrototypeManager, ServerPrototypeManager>();
IoCManager.Register<IViewVariablesHost, ViewVariablesHost>();
IoCManager.Register<IConGroupController, ConGroupController>();
IoCManager.Register<IStatusHost, StatusHost>();
IoCManager.BuildGraph();
}

View File

@@ -126,6 +126,7 @@
<Compile Include="GameObjects\EntitySystems\MoverSystem.cs" />
<Compile Include="GameObjects\EntitySystems\UserInterfaceSystem.cs" />
<Compile Include="Interfaces\Player\IPlayerData.cs" />
<Compile Include="Interfaces\ServerStatus\IStatusHost.cs" />
<Compile Include="Player\PlayerData.cs" />
<Compile Include="Prototypes\ServerPrototypeManager.cs" />
<Compile Include="Console\Commands\SpawnCommand.cs" />
@@ -148,6 +149,7 @@
<Compile Include="Reflection\ServerReflectionManager.cs" />
<Compile Include="BaseServer.cs" />
<Compile Include="Console\Commands\LogCommands.cs" />
<Compile Include="ServerStatus\StatusHost.cs" />
<Compile Include="Signals.cs" />
<Compile Include="ViewVariables\IViewVariablesHost.cs" />
<Compile Include="ViewVariables\Traits\ViewVariablesTraitEntity.cs" />

View File

@@ -0,0 +1,178 @@
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SS14.Server.Interfaces.Player;
using SS14.Server.Interfaces.ServerStatus;
using SS14.Shared.Configuration;
using SS14.Shared.Interfaces.Configuration;
using SS14.Shared.IoC;
// This entire file is NIHing a REST server because pulling in libraries is effort.
// Also it was fun to write.
// Just slap this thing behind an Nginx reverse proxy. It's not supposed to be directly exposed to the web.
namespace SS14.Server.ServerStatus
{
public sealed class StatusHost : IStatusHost, IDisposable
{
[Dependency] private IConfigurationManager _configurationManager;
// See this SO post for inspiration: https://stackoverflow.com/a/4673210
private HttpListener _listener;
private Thread _listenerThread;
private ManualResetEventSlim _stop;
public void Start()
{
_configurationManager.RegisterCVar("status.enabled", false, CVar.ARCHIVE);
_configurationManager.RegisterCVar("status.bind", "localhost:1212", CVar.ARCHIVE);
if (!_configurationManager.GetCVar<bool>("status.enabled"))
{
return;
}
_stop = new ManualResetEventSlim();
_listener = new HttpListener();
_listener.Prefixes.Add($"http://{_configurationManager.GetCVar<string>("status.bind")}/");
_listener.Start();
_listenerThread = new Thread(_worker)
{
Name = "REST API Thread",
IsBackground = true,
Priority = ThreadPriority.BelowNormal
};
_listenerThread.Start();
}
public event Action<JObject> OnStatusRequest;
public void Dispose()
{
if (_stop == null)
{
return;
}
_stop.Set();
_listenerThread.Join(1000);
_listener.Stop();
}
private void _worker()
{
while (_listener.IsListening)
{
var context = _listener.BeginGetContext(ar =>
{
var actualContext = _listener.EndGetContext(ar);
_processRequest(actualContext);
}, null);
if (0 == WaitHandle.WaitAny(new[] {_stop.WaitHandle, context.AsyncWaitHandle}))
{
return;
}
}
}
private void _processRequest(HttpListenerContext context)
{
var response = context.Response;
var request = context.Request;
if (request.HttpMethod != "GET" && request.HttpMethod != "HEAD")
{
response.StatusCode = (int) HttpStatusCode.BadRequest;
response.StatusDescription = "Bad Request";
response.ContentType = "text/plain";
_respondText(response, "400 Bad Request", false);
return;
}
var head = request.HttpMethod == "HEAD";
try
{
var uri = request.Url;
if (uri.AbsolutePath == "/teapot")
{
response.StatusCode = 418; // >HttpStatusCode doesn't include 418.
response.StatusDescription = "I'm a teapot";
response.ContentType = "text/plain";
_respondText(response, "The requested entity body is short and stout.", head);
}
else if (uri.AbsolutePath == "/status")
{
if (OnStatusRequest == null)
{
response.StatusCode = (int) HttpStatusCode.NotImplemented;
response.StatusDescription = "Not Implemented";
response.ContentType = "text/plain";
_respondText(response, "501 Not Implemented", head);
return;
}
response.StatusCode = (int) HttpStatusCode.OK;
response.StatusDescription = "OK";
response.ContentType = "application/json";
response.ContentEncoding = Encoding.UTF8;
if (head)
{
response.OutputStream.Close();
return;
}
var jObject = new JObject();
OnStatusRequest?.Invoke(jObject);
using (var streamWriter = new StreamWriter(response.OutputStream, Encoding.UTF8))
using (var jsonWriter = new JsonTextWriter(streamWriter))
{
var serializer = new JsonSerializer();
serializer.Serialize(jsonWriter, jObject);
jsonWriter.Flush();
}
response.OutputStream.Close();
}
else
{
response.StatusCode = (int) HttpStatusCode.NotFound;
response.StatusDescription = "Not Found";
response.ContentType = "text/plain";
_respondText(response, "404 Not Found", head);
}
}
catch
{
response.StatusCode = (int) HttpStatusCode.InternalServerError;
response.StatusDescription = "Internal Server Error";
response.ContentType = "text/plain";
_respondText(response, "500 Internal Server Error", head);
// TODO: Logging.
// Logger is not thread safe atm.
// This is a problem.
}
}
private static void _respondText(HttpListenerResponse response, string contents, bool head)
{
response.ContentEncoding = Encoding.UTF8;
if (head)
{
response.OutputStream.Close();
return;
}
using (var writer = new StreamWriter(response.OutputStream, Encoding.UTF8))
{
writer.Write(contents);
}
response.OutputStream.Close();
}
}
}

View File

@@ -10,6 +10,10 @@ tickrate = 60
port = 1212
bindto = "::,0.0.0.0"
[status]
enabled = false
bind = "localhost:1212"
[game]
hostname = "MyServer"
mapname = "stationstation"

View File

@@ -37,11 +37,13 @@ using SS14.Server.Interfaces.GameState;
using SS14.Server.Interfaces.Maps;
using SS14.Server.Interfaces.Placement;
using SS14.Server.Interfaces.Player;
using SS14.Server.Interfaces.ServerStatus;
using SS14.Server.Maps;
using SS14.Server.Placement;
using SS14.Server.Player;
using SS14.Server.Prototypes;
using SS14.Server.Reflection;
using SS14.Server.ServerStatus;
using SS14.Server.ViewVariables;
using SS14.Shared.Asynchronous;
using SS14.Shared.Configuration;
@@ -236,6 +238,7 @@ namespace SS14.UnitTesting
IoCManager.Register<IPrototypeManager, ServerPrototypeManager>();
IoCManager.Register<IViewVariablesHost, ViewVariablesHost>();
IoCManager.Register<IConGroupController, ConGroupController>();
IoCManager.Register<IStatusHost, StatusHost>();
break;
default: