Return of the HttpListener. (#1423)

Microsoft isn't supporting NuGet-components ASP.NET Core ever since 3.x so using Kestrel is out.

New implementation is 100% thread pool compared to the old one which was a single specific thread.
This commit is contained in:
Pieter-Jan Briers
2020-11-26 23:57:52 +01:00
committed by GitHub
parent a41f64f30e
commit 2b39c05472
12 changed files with 121 additions and 396 deletions

View File

@@ -1,11 +1,14 @@
using System;
using System.Net;
using System.Net.Http;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json.Linq;
namespace Robust.Server.Interfaces.ServerStatus
{
public delegate bool StatusHostHandler(HttpMethod method, HttpRequest request, HttpResponse response);
public delegate bool StatusHostHandler(
HttpMethod method,
HttpListenerRequest request,
HttpListenerResponse response);
public interface IStatusHost
{

View File

@@ -14,8 +14,6 @@
<Import Project="..\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.1.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Core" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" Version="2.2.1" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="5.0.0" />
<PackageReference Include="prometheus-net" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Loki" Version="3.0.0" />

View File

@@ -2,7 +2,6 @@ using System;
using System.IO;
using System.Net;
using System.Net.Http;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Robust.Shared;
@@ -22,21 +21,20 @@ namespace Robust.Server.ServerStatus
AddHandler(HandleInfo);
}
private static bool HandleTeapot(HttpMethod method, HttpRequest request, HttpResponse response)
private static bool HandleTeapot(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
{
if (!method.IsGetLike() || request.Path != "/teapot")
if (!method.IsGetLike() || request.Url!.AbsolutePath != "/teapot")
{
return false;
}
response.StatusCode = StatusCodes.Status418ImATeapot;
response.Respond("I am a teapot.", StatusCodes.Status418ImATeapot);
response.Respond(method, "I am a teapot.", (HttpStatusCode) 418);
return true;
}
private bool HandleStatus(HttpMethod method, HttpRequest request, HttpResponse response)
private bool HandleStatus(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
{
if (!method.IsGetLike() || request.Path != "/status")
if (!method.IsGetLike() || request.Url!.AbsolutePath != "/status")
{
return false;
}
@@ -44,7 +42,7 @@ namespace Robust.Server.ServerStatus
if (OnStatusRequest == null)
{
Logger.WarningS(Sawmill, "OnStatusRequest is not set, responding with a 501.");
response.Respond("Not Implemented", HttpStatusCode.NotImplemented);
response.Respond(method, "Not Implemented", HttpStatusCode.NotImplemented);
return true;
}
@@ -60,7 +58,7 @@ namespace Robust.Server.ServerStatus
OnStatusRequest?.Invoke(jObject);
using var streamWriter = new StreamWriter(response.Body, EncodingHelpers.UTF8);
using var streamWriter = new StreamWriter(response.OutputStream, EncodingHelpers.UTF8);
using var jsonWriter = new JsonTextWriter(streamWriter);
@@ -71,9 +69,9 @@ namespace Robust.Server.ServerStatus
return true;
}
private bool HandleInfo(HttpMethod method, HttpRequest request, HttpResponse response)
private bool HandleInfo(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
{
if (!method.IsGetLike() || request.Path != "/info")
if (!method.IsGetLike() || request.Url!.AbsolutePath != "/info")
{
return false;
}
@@ -132,7 +130,7 @@ namespace Robust.Server.ServerStatus
OnInfoRequest?.Invoke(jObject);
using var streamWriter = new StreamWriter(response.Body, EncodingHelpers.UTF8);
using var streamWriter = new StreamWriter(response.OutputStream, EncodingHelpers.UTF8);
using var jsonWriter = new JsonTextWriter(streamWriter);

View File

@@ -1,58 +0,0 @@
using System;
using System.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Options;
using Robust.Shared.Interfaces.Log;
using Robust.Shared.IoC;
using Robust.Shared.Log;
namespace Robust.Server.ServerStatus
{
internal sealed partial class StatusHost
{
private HttpContextFactory _ctxFactory = default!;
public HttpContext CreateContext(IFeatureCollection contextFeatures) => _ctxFactory.Create(contextFeatures);
public void DisposeContext(HttpContext context, Exception exception)
{
if (exception != null)
{
Logger.ErrorS(Sawmill, $"Context disposed due to exception: {exception}");
}
_ctxFactory.Dispose(context);
}
private static HttpContextFactory CreateHttpContextFactory()
{
var ctxFacOptions = Options.Create(new FormOptions
{
});
var ctxFactory = new HttpContextFactory(ctxFacOptions, new HttpContextAccessor());
return ctxFactory;
}
private void InitHttpContextThread()
{
if (SynchronizationContext.Current == _syncCtx)
{
// maybe assert instead?
return;
}
ILogManager? logMgr = null;
WaitSync(() =>
{
logMgr = IoCManager.Resolve<ILogManager>();
}, ApplicationStopping);
var deps = new DependencyCollection();
deps.RegisterInstance<ILogManager>(new ProxyLogManager(logMgr!));
deps.BuildGraph();
IoCManager.InitThread(deps, true);
}
}
}

View File

@@ -1,63 +0,0 @@
using System.Threading;
namespace Robust.Server.ServerStatus
{
internal sealed partial class StatusHost
{
private readonly CancellationTokenSource _startedSource = new();
private readonly CancellationTokenSource _stoppedSource = new();
private readonly CancellationTokenSource _stoppingSource = new();
public void StopApplication() => Dispose();
private static CancellationTokenSource? _cancelled;
private static CancellationToken GetCancelledToken()
{
if (_cancelled == null)
{
_cancelled = new CancellationTokenSource();
_cancelled.Cancel();
}
return _cancelled.Token;
}
/// <summary>
/// Triggered when the application host has fully started and is about to wait
/// for a graceful shutdown.
/// </summary>
public CancellationToken ApplicationStarted => _startedSource?.Token ?? GetCancelledToken();
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// Request may still be in flight. Shutdown will block until this event completes.
/// </summary>
public CancellationToken ApplicationStopping => _stoppingSource?.Token ?? GetCancelledToken();
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// All requests should be complete at this point. Shutdown will block
/// until this event completes.
/// </summary>
public CancellationToken ApplicationStopped => _stoppedSource?.Token ?? GetCancelledToken();
public void Dispose()
{
if (_stoppingSource?.IsCancellationRequested ?? true)
{
return;
}
_stoppingSource?.Cancel();
_server?.StopAsync(ApplicationStopped);
_stoppedSource?.Cancel();
}
}
}

View File

@@ -1,50 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Interfaces.Log;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Microsoft.Extensions.Logging;
using LogLevel = Robust.Shared.Log.LogLevel;
namespace Robust.Server.ServerStatus
{
internal sealed partial class StatusHost
{
private Dictionary<string, SawmillWrapper> _sawmillCache = new();
public ILogger CreateLogger(string categoryName)
{
if (!_sawmillCache.TryGetValue(categoryName, out var wrapper))
{
var newCatName = categoryName;
if (newCatName.StartsWith("Microsoft.AspNetCore.Server.Kestrel"))
{
newCatName = "http";
}
else
{
newCatName = newCatName.Replace("Microsoft.AspNetCore.", "aspnet.");
}
wrapper = new SawmillWrapper(Logger.GetSawmill($"{Sawmill}.{newCatName}"));
_sawmillCache[categoryName] = wrapper;
}
return wrapper;
}
public void AddProvider(ILoggerProvider provider)
=> throw new NotImplementedException();
private static void ConfigureSawmills()
{
var logMgr = IoCManager.Resolve<ILogManager>();
logMgr.GetSawmill("statushost.http").Level = LogLevel.Warning;
logMgr.GetSawmill("statushost.aspnet").Level = LogLevel.Warning;
}
}
}

View File

@@ -1,36 +0,0 @@
using System;
using Microsoft.Extensions.Logging;
using Robust.Shared.Interfaces.Log;
namespace Robust.Server.ServerStatus
{
internal sealed partial class StatusHost
{
private class SawmillWrapper : ILogger
{
private ISawmill _sawmill;
public SawmillWrapper(ISawmill sawmill)
=> _sawmill = sawmill;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
=> _sawmill.Log((Shared.Log.LogLevel) (int) logLevel, formatter(state, exception));
public bool IsEnabled(LogLevel logLevel)
=> (int) logLevel >= (int) (_sawmill.Level ?? (Shared.Log.LogLevel) 0);
public IDisposable BeginScope<TState>(TState state)
=> new DummyDisposable();
// @formatter:off
private struct DummyDisposable : IDisposable { public void Dispose() { } }
// @formatter:on
}
}
}

View File

@@ -1,55 +0,0 @@
using System;
using System.Threading;
namespace Robust.Server.ServerStatus
{
internal sealed partial class StatusHost
{
private SynchronizationContext _syncCtx = default!;
public void DeferSync(Action a)
{
if (ExecuteInlineIfOnSyncCtx(a))
{
return;
}
_syncCtx.Post(x => ((Action) x!)(), a);
}
public void WaitSync(Action a, CancellationToken ct = default)
{
if (ExecuteInlineIfOnSyncCtx(a))
{
return;
}
// throws not implemented
//_syncCtx.Send(x => ((Action) x)(), a);
using var e = new ManualResetEventSlim(false, 0);
_syncCtx.Post(_ =>
{
a();
// ReSharper disable once AccessToDisposedClosure
e.Set();
}, null);
e.Wait(ct);
}
private bool ExecuteInlineIfOnSyncCtx(Action a)
{
if (_syncCtx == SynchronizationContext.Current)
{
a();
return true;
}
return false;
}
}
}

View File

@@ -5,20 +5,13 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Robust.Server.Interfaces.ServerStatus;
using Robust.Shared;
using Robust.Shared.ContentPack;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Interfaces.Log;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -29,34 +22,26 @@ using Robust.Shared.Log;
namespace Robust.Server.ServerStatus
{
internal sealed partial class StatusHost
: IStatusHost, IDisposable,
IHttpApplication<HttpContext>,
IApplicationLifetime,
ILoggerFactory
internal sealed partial class StatusHost : IStatusHost, IDisposable
{
private const string Sawmill = "statushost";
private static readonly JsonSerializer JsonSerializer = new();
private readonly List<StatusHostHandler> _handlers = new();
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IServerNetManager _netManager = default!;
private KestrelServer _server = default!;
private static readonly JsonSerializer JsonSerializer = new();
private readonly List<StatusHostHandler> _handlers = new();
private HttpListener? _listener;
private TaskCompletionSource? _stopSource;
private ISawmill _httpSawmill = default!;
public Task ProcessRequestAsync(HttpContext context)
public Task ProcessRequestAsync(HttpListenerContext context)
{
var response = context.Response;
var request = context.Request;
var method = new HttpMethod(request.Method);
InitHttpContextThread();
var method = new HttpMethod(request.HttpMethod);
Logger.InfoS(Sawmill, $"{method} {context.Request.Path} from " +
$"{context.Connection.RemoteIpAddress}:{context.Connection.RemotePort}");
_httpSawmill.Info($"{method} {context.Request.Url?.PathAndQuery} from {request.RemoteEndPoint}");
try
{
@@ -70,16 +55,18 @@ namespace Robust.Server.ServerStatus
// No handler returned true, assume no handlers care about this.
// 404.
response.Respond("Not Found", HttpStatusCode.NotFound);
response.Respond(method, "Not Found", HttpStatusCode.NotFound);
}
catch (Exception e)
{
response.Respond("Internal Server Error", HttpStatusCode.InternalServerError);
response.Respond(method, "Internal Server Error", HttpStatusCode.InternalServerError);
Logger.ErrorS(Sawmill, $"Exception in StatusHost: {e}");
}
Logger.DebugS(Sawmill, $"{method} {context.Request.Path} {context.Response.StatusCode} " +
$"{(HttpStatusCode) context.Response.StatusCode} to {context.Connection.RemoteIpAddress}:{context.Connection.RemotePort}");
/*
_httpSawmill.Debug(Sawmill, $"{method} {context.Request.Url!.PathAndQuery} {context.Response.StatusCode} " +
$"{(HttpStatusCode) context.Response.StatusCode} to {context.Request.RemoteEndPoint}");
*/
return Task.CompletedTask;
}
@@ -88,10 +75,14 @@ namespace Robust.Server.ServerStatus
public event Action<JObject>? OnInfoRequest;
public void AddHandler(StatusHostHandler handler) => _handlers.Add(handler);
public void AddHandler(StatusHostHandler handler)
{
_handlers.Add(handler);
}
public void Start()
{
_httpSawmill = Logger.GetSawmill($"{Sawmill}.http");
RegisterCVars();
if (!_configurationManager.GetCVar(CVars.StatusEnabled))
@@ -99,64 +90,53 @@ namespace Robust.Server.ServerStatus
return;
}
ConfigureSawmills();
_ctxFactory = CreateHttpContextFactory();
var kestrelOpts = new KestrelServerOptions
{
AllowSynchronousIO = true,
ApplicationSchedulingMode = SchedulingMode.ThreadPool
};
kestrelOpts.Listen(GetBinding());
_server = new KestrelServer(
Options.Create(
kestrelOpts
),
GetSocketTransportFactory(),
this
);
RegisterHandlers();
_server.StartAsync(this, ApplicationStopping);
_stopSource = new TaskCompletionSource();
_listener = new HttpListener();
_listener.Prefixes.Add($"http://{_configurationManager.GetCVar(CVars.StatusBind)}/");
_listener.Start();
_syncCtx = SynchronizationContext.Current!;
if (_syncCtx == null)
{
SynchronizationContext.SetSynchronizationContext(_syncCtx = new SynchronizationContext());
}
Task.Run(ListenerThread);
}
private IPEndPoint GetBinding()
// Not a real thread but whatever.
private async Task ListenerThread()
{
var binding = _configurationManager.GetCVar(CVars.StatusBind).Split(':');
var ipAddrStr = binding[0];
if (ipAddrStr == "+" || ipAddrStr == "*")
var maxConnections = _configurationManager.GetCVar(CVars.StatusMaxConnections);
var connectionsSemaphore = new SemaphoreSlim(maxConnections, maxConnections);
while (true)
{
ipAddrStr = "0.0.0.0";
}
var getContextTask = _listener!.GetContextAsync();
var task = await Task.WhenAny(getContextTask, _stopSource!.Task);
var ipAddress = IPAddress.Parse(ipAddrStr);
var port = int.Parse(binding[1]);
var ipEndPoint = new IPEndPoint(ipAddress, port);
return ipEndPoint;
}
private SocketTransportFactory GetSocketTransportFactory()
{
var transportFactory = new SocketTransportFactory(
Options.Create(new SocketTransportOptions
if (task == _stopSource.Task)
{
IOQueueCount = 42
}),
this,
this
);
return transportFactory;
return;
}
await connectionsSemaphore.WaitAsync();
// Task.Run this so it gets run on another thread pool thread.
#pragma warning disable 4014
Task.Run(async () =>
#pragma warning restore 4014
{
try
{
var ctx = await getContextTask;
await ProcessRequestAsync(ctx);
}
catch (Exception e)
{
_httpSawmill.Error($"Error inside ProcessRequestAsync:\n{e}");
}
finally
{
connectionsSemaphore.Release();
}
});
}
}
private void RegisterCVars()
@@ -181,6 +161,17 @@ namespace Robust.Server.ServerStatus
_configurationManager.SetCVar(CVars.BuildHashLinux, info?.Hashes.Linux ?? "");
}
public void Dispose()
{
if (_stopSource == null)
{
return;
}
_stopSource.SetResult();
_listener!.Stop();
}
[JsonObject(ItemRequired = Required.DisallowNull)]
private sealed class BuildInfo
{
@@ -198,5 +189,4 @@ namespace Robust.Server.ServerStatus
[JsonProperty("macos")] public string MacOS { get; set; } = default!;
}
}
}

View File

@@ -2,8 +2,6 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Robust.Shared.Utility;
@@ -11,33 +9,45 @@ namespace Robust.Server.ServerStatus
{
public static class StatusHostHelpers
{
public static bool IsGetLike(this HttpMethod method) =>
method == HttpMethod.Get || method == HttpMethod.Head;
public static bool IsGetLike(this HttpMethod method)
{
return method == HttpMethod.Get || method == HttpMethod.Head;
}
public static void Respond(this HttpResponse response, string text, HttpStatusCode code = HttpStatusCode.OK,
string contentType = "text/plain") =>
response.Respond(text, (int) code, contentType);
public static void Respond(
this HttpListenerResponse response,
HttpMethod method,
string text,
HttpStatusCode code = HttpStatusCode.OK,
string contentType = "text/plain")
{
response.Respond(method, text, (int) code, contentType);
}
public static void Respond(this HttpResponse response, string text, int code = 200,
public static void Respond(
this HttpListenerResponse response,
HttpMethod method,
string text,
int code = 200,
string contentType = "text/plain")
{
response.StatusCode = code;
response.ContentType = contentType;
if (response.HttpContext.Request.Method == "HEAD")
if (method == HttpMethod.Head)
{
return;
}
using var writer = new StreamWriter(response.Body, EncodingHelpers.UTF8);
using var writer = new StreamWriter(response.OutputStream, EncodingHelpers.UTF8);
writer.Write(text);
}
[return: MaybeNull]
public static T GetFromJson<T>(this HttpRequest request)
public static T GetFromJson<T>(this HttpListenerRequest request)
{
using var streamReader = new StreamReader(request.Body, EncodingHelpers.UTF8);
using var streamReader = new StreamReader(request.InputStream, EncodingHelpers.UTF8);
using var jsonReader = new JsonTextReader(streamReader);
var serializer = new JsonSerializer();

View File

@@ -4,7 +4,6 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Robust.Server.Interfaces;
using Robust.Server.Interfaces.ServerStatus;
@@ -42,9 +41,9 @@ namespace Robust.Server.ServerStatus
_statusHost.AddHandler(ShutdownHandler);
}
private bool UpdateHandler(HttpMethod method, HttpRequest request, HttpResponse response)
private bool UpdateHandler(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
{
if (method != HttpMethod.Post || request.Path != "/update")
if (method != HttpMethod.Post || request.Url!.AbsolutePath != "/update")
{
return false;
}
@@ -56,18 +55,11 @@ namespace Robust.Server.ServerStatus
}
var auth = request.Headers["WatchdogToken"];
if (auth.Count != 1)
{
response.StatusCode = (int) HttpStatusCode.BadRequest;
return true;
}
var authVal = auth[0];
if (authVal != _watchdogToken)
if (auth != _watchdogToken)
{
// Holy shit nobody read these logs please.
Logger.InfoS("watchdogApi", @"Failed auth: ""{0}"" vs ""{1}""", authVal, _watchdogToken);
Logger.InfoS("watchdogApi", @"Failed auth: ""{0}"" vs ""{1}""", auth, _watchdogToken);
response.StatusCode = (int) HttpStatusCode.Unauthorized;
return true;
}
@@ -79,9 +71,9 @@ namespace Robust.Server.ServerStatus
return true;
}
private bool ShutdownHandler(HttpMethod method, HttpRequest request, HttpResponse response)
private bool ShutdownHandler(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
{
if (method != HttpMethod.Post || request.Path != "/shutdown")
if (method != HttpMethod.Post || request.Url!.AbsolutePath != "/shutdown")
{
return false;
}
@@ -94,18 +86,11 @@ namespace Robust.Server.ServerStatus
}
var auth = request.Headers["WatchdogToken"];
if (auth.Count != 1)
{
response.StatusCode = (int) HttpStatusCode.BadRequest;
return true;
}
var authVal = auth[0];
if (authVal != _watchdogToken)
if (auth != _watchdogToken)
{
Logger.WarningS("watchdogApi",
"received POST /shutdown with invalid authentication token. Ignoring {0}, {1}", authVal,
"received POST /shutdown with invalid authentication token. Ignoring {0}, {1}", auth,
_watchdogToken);
response.StatusCode = (int) HttpStatusCode.Unauthorized;
return true;

View File

@@ -83,13 +83,16 @@ namespace Robust.Shared
CVarDef.Create("metrics.port", 44880);
public static readonly CVarDef<bool> StatusEnabled =
CVarDef.Create("status.enabled", true, CVar.ARCHIVE);
CVarDef.Create("status.enabled", true, CVar.ARCHIVE | CVar.SERVERONLY);
public static readonly CVarDef<string> StatusBind =
CVarDef.Create("status.bind", "*:1212", CVar.ARCHIVE);
CVarDef.Create("status.bind", "*:1212", CVar.ARCHIVE | CVar.SERVERONLY);
public static readonly CVarDef<int> StatusMaxConnections =
CVarDef.Create("status.max_connections", 5, CVar.SERVERONLY);
public static readonly CVarDef<string> StatusConnectAddress =
CVarDef.Create("status.connectaddress", "", CVar.ARCHIVE);
CVarDef.Create("status.connectaddress", "", CVar.ARCHIVE | CVar.SERVERONLY);
public static readonly CVarDef<string> BuildForkId =
CVarDef.Create("build.fork_id", "", CVar.ARCHIVE);