mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
Server status framework. (#709)
Adds a HTTP server to the server that exposes `/status` to fetch the server status.
This commit is contained in:
committed by
GitHub
parent
baff29362a
commit
21fd3e5d96
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
18
SS14.Server/Interfaces/ServerStatus/IStatusHost.cs
Normal file
18
SS14.Server/Interfaces/ServerStatus/IStatusHost.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
178
SS14.Server/ServerStatus/StatusHost.cs
Normal file
178
SS14.Server/ServerStatus/StatusHost.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,10 @@ tickrate = 60
|
||||
port = 1212
|
||||
bindto = "::,0.0.0.0"
|
||||
|
||||
[status]
|
||||
enabled = false
|
||||
bind = "localhost:1212"
|
||||
|
||||
[game]
|
||||
hostname = "MyServer"
|
||||
mapname = "stationstation"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user