Move more initialization, mod loading and http requests over to async

This commit is contained in:
Archangel
2025-05-29 19:05:55 +02:00
committed by Jesse
parent c930197942
commit fa1368fb47
19 changed files with 136 additions and 139 deletions
@@ -2,6 +2,6 @@ namespace SPTarkov.Server.Core.DI;
public interface ISerializer
{
public void Serialize(string sessionID, HttpRequest req, HttpResponse resp, object? body);
public Task Serialize(string sessionID, HttpRequest req, HttpResponse resp, object? body);
public bool CanHandle(string route);
}
@@ -9,7 +9,7 @@ namespace SPTarkov.Server.Core.Loaders;
[Injectable(TypePriority = OnLoadOrder.PostDBModLoader)]
public class PostDBModLoader(
ISptLogger<PostDBModLoader> _logger,
IEnumerable<IPostDBLoadMod> _postDbLoadMods
IEnumerable<IPostSptLoadModAsync> _postDbLoadMods
) : IOnLoad
{
public async Task OnLoad()
@@ -19,7 +19,7 @@ public class PostDBModLoader(
_logger.Info("Loading PostDBMods...");
foreach (var postDbLoadMod in _postDbLoadMods)
{
postDbLoadMod.PostDBLoad();
await postDbLoadMod.PostSptLoadAsync();
}
_logger.Info("Finished loading PostDBMods...");
@@ -9,7 +9,7 @@ namespace SPTarkov.Server.Core.Loaders;
[Injectable(TypePriority = OnLoadOrder.PostSptModLoader)]
public class PostSptModLoader(
ISptLogger<PostSptModLoader> _logger,
IEnumerable<IPostSptLoadMod> _postSptLoadMods
IEnumerable<IPostSptLoadModAsync> _postSptLoadMods
) : IOnLoad
{
public async Task OnLoad()
@@ -19,7 +19,7 @@ public class PostSptModLoader(
_logger.Info("Loading PostSptMods...");
foreach (var postSptLoadMod in _postSptLoadMods)
{
postSptLoadMod.PostSptLoad();
await postSptLoadMod.PostSptLoadAsync();
}
_logger.Info("Finished loading PostSptMods...");
@@ -1,6 +0,0 @@
namespace SPTarkov.Server.Core.Models.External;
public interface IPostDBLoadMod
{
void PostDBLoad();
}
@@ -0,0 +1,6 @@
namespace SPTarkov.Server.Core.Models.External;
public interface IPostDBLoadModAsync
{
Task PostDBLoadAsync();
}
@@ -1,6 +0,0 @@
namespace SPTarkov.Server.Core.Models.External;
public interface IPostSptLoadMod
{
void PostSptLoad();
}
@@ -0,0 +1,6 @@
namespace SPTarkov.Server.Core.Models.External;
public interface IPostSptLoadModAsync
{
Task PostSptLoadAsync();
}
@@ -1,6 +0,0 @@
namespace SPTarkov.Server.Core.Models.External;
public interface IPreSptLoadMod
{
void PreSptLoad();
}
@@ -0,0 +1,6 @@
namespace SPTarkov.Server.Core.Models.External;
public interface IPreSptLoadModAsync
{
Task PreSptLoadAsync();
}
@@ -31,7 +31,7 @@ public class ImageRouter
_imageRouterService.AddRoute(key.ToLower(), valueToAdd);
}
public void SendImage(string sessionId, HttpRequest req, HttpResponse resp, object body)
public async Task SendImage(string sessionId, HttpRequest req, HttpResponse resp, object body)
{
// remove file extension
var url = _fileUtil.StripExtension(req.Path, true);
@@ -40,7 +40,7 @@ public class ImageRouter
var urlKeyLower = url.ToLower();
if (_imageRouterService.ExistsByKey(urlKeyLower))
{
_httpFileUtil.SendFile(resp, _imageRouterService.GetByKey(urlKeyLower));
await _httpFileUtil.SendFile(resp, _imageRouterService.GetByKey(urlKeyLower));
return;
}
@@ -13,7 +13,7 @@ public class BundleSerializer(
HttpFileUtil httpFileUtil
) : ISerializer
{
public void Serialize(string sessionID, HttpRequest req, HttpResponse resp, object? body)
public async Task Serialize(string sessionID, HttpRequest req, HttpResponse resp, object? body)
{
var key = req.Path.Value.Split("/bundle/")[1];
var bundle = bundleLoader.GetBundle(key);
@@ -30,7 +30,7 @@ public class BundleSerializer(
}
var bundlePath = Path.Join(bundle.ModPath, "/bundles/", bundle.FileName);
httpFileUtil.SendFile(resp, bundlePath);
await httpFileUtil.SendFile(resp, bundlePath);
}
public bool CanHandle(string route)
@@ -13,9 +13,9 @@ public class ImageSerializer : ISerializer
_imageRouter = imageRouter;
}
public void Serialize(string sessionID, HttpRequest req, HttpResponse resp, object? body)
public async Task Serialize(string sessionID, HttpRequest req, HttpResponse resp, object? body)
{
_imageRouter.SendImage(sessionID, req, resp, body);
await _imageRouter.SendImage(sessionID, req, resp, body);
}
public bool CanHandle(string route)
@@ -13,7 +13,7 @@ public class NotifySerializer(
HttpServerHelper httpServerHelper
) : ISerializer
{
public void Serialize(string sessionID, HttpRequest req, HttpResponse resp, object? body)
public async Task Serialize(string sessionID, HttpRequest req, HttpResponse resp, object? body)
{
var splittedUrl = req.Path.Value.Split("/");
var tmpSessionID = splittedUrl[^1].Split("?last_id")[0];
@@ -22,7 +22,7 @@ public class NotifySerializer(
* Take our array of JSON message objects and cast them to JSON strings, so that they can then
* be sent to client as NEWLINE separated strings... yup.
*/
notifierController.NotifyAsync(tmpSessionID)
await notifierController.NotifyAsync(tmpSessionID)
.ContinueWith(messages => messages.Result.Select(message => string.Join("\n", jsonUtil.Serialize(message))))
.ContinueWith(text => httpServerHelper.SendTextJson(resp, text));
}
@@ -3,5 +3,5 @@
public interface IHttpListener
{
bool CanHandle(string sessionId, HttpRequest req);
void Handle(string sessionId, HttpRequest req, HttpResponse resp);
Task Handle(string sessionId, HttpRequest req, HttpResponse resp);
}
@@ -13,26 +13,7 @@ using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel;
namespace SPTarkov.Server.Core.Servers.Http;
[Injectable]
public class SptHttpListener : IHttpListener
{
// we want to reserve on the list 512KB capacity before it needs to expand, should be enough for most requests
private const int InitialCapacityForListBuffer = 1024 * 512;
// We want to read 1KB at a time, for most request this is already big enough
private const int BodyReadBufferSize = 1024 * 1;
private static readonly ImmutableHashSet<string> SupportedMethods = ["GET", "PUT", "POST"];
protected readonly HttpResponseUtil _httpResponseUtil;
protected readonly JsonUtil _jsonUtil;
protected readonly LocalisationService _localisationService;
protected readonly ISptLogger<SptHttpListener> _logger;
protected readonly ISptLogger<RequestLogger> _requestLogger;
protected readonly HttpRouter _router;
protected readonly IEnumerable<ISerializer> _serializers;
public SptHttpListener(
public class SptHttpListener(
HttpRouter httpRouter,
IEnumerable<ISerializer> serializers,
ISptLogger<SptHttpListener> logger,
@@ -40,30 +21,35 @@ public class SptHttpListener : IHttpListener
JsonUtil jsonUtil,
HttpResponseUtil httpHttpResponseUtil,
LocalisationService localisationService
)
) : IHttpListener
{
_router = httpRouter;
_serializers = serializers;
_logger = logger;
_requestLogger = requestsLogger;
_httpResponseUtil = httpHttpResponseUtil;
_localisationService = localisationService;
_jsonUtil = jsonUtil;
}
// We want to read 1KB at a time, for most request this is already big enough
private const int BodyReadBufferSize = 1024 * 1;
private static readonly ImmutableHashSet<string> SupportedMethods = ["GET", "PUT", "POST"];
protected readonly HttpResponseUtil _httpResponseUtil = httpHttpResponseUtil;
protected readonly JsonUtil _jsonUtil = jsonUtil;
protected readonly LocalisationService _localisationService = localisationService;
protected readonly ISptLogger<SptHttpListener> _logger = logger;
protected readonly ISptLogger<RequestLogger> _requestLogger = requestsLogger;
protected readonly HttpRouter _router = httpRouter;
protected readonly IEnumerable<ISerializer> _serializers = serializers;
public bool CanHandle(string _, HttpRequest req)
{
return SupportedMethods.Contains(req.Method);
}
public void Handle(string sessionId, HttpRequest req, HttpResponse resp)
public async Task Handle(string sessionId, HttpRequest req, HttpResponse resp)
{
switch (req.Method)
{
case "GET":
{
var response = GetResponse(sessionId, req, null);
SendResponse(sessionId, req, resp, null, response);
await SendResponse(sessionId, req, resp, null, response);
break;
}
// these are handled almost identically.
@@ -78,46 +64,47 @@ public class SptHttpListener : IHttpListener
compressHeader != "0";
var requestCompressed = req.Method == "PUT" || requestIsCompressed;
// reserve some capacity to avoid having the list to resize
var totalRead = new List<byte>(InitialCapacityForListBuffer);
// read 1KB at a time
var memory = new Memory<byte>(new byte[BodyReadBufferSize]);
var readTask = req.Body.ReadAsync(memory).AsTask();
readTask.Wait();
var readBytes = 0;
while (readTask.Result != 0)
var body = string.Empty;
using MemoryStream bufferStream = new();
var buffer = new byte[BodyReadBufferSize];
int bytesRead;
while ((bytesRead = await req.Body.ReadAsync(buffer)) > 0)
{
readBytes += readTask.Result;
totalRead.AddRange(memory[..readTask.Result].ToArray());
memory = new Memory<byte>(new byte[BodyReadBufferSize]);
readTask = req.Body.ReadAsync(memory).AsTask();
readTask.Wait();
await bufferStream.WriteAsync(buffer.AsMemory(0, bytesRead));
}
string value;
bufferStream.Position = 0;
if (requestCompressed)
{
using var uncompressedDataStream = new MemoryStream();
using var compressedDataStream = new MemoryStream(totalRead[..readBytes].ToArray());
using var deflateStream = new ZLibStream(compressedDataStream, CompressionMode.Decompress, true);
deflateStream.CopyTo(uncompressedDataStream);
value = Encoding.UTF8.GetString(uncompressedDataStream.ToArray());
using var deflateStream = new ZLibStream(bufferStream, CompressionMode.Decompress);
using var decompressedStream = new MemoryStream();
await deflateStream.CopyToAsync(decompressedStream);
decompressedStream.Position = 0;
using var reader = new StreamReader(decompressedStream, Encoding.UTF8);
body = await reader.ReadToEndAsync();
}
else
{
value = Encoding.UTF8.GetString(totalRead[..readBytes].ToArray());
// No decompression needed, decode directly from the bufferStream's buffer
bufferStream.Position = 0;
using var reader = new StreamReader(bufferStream, Encoding.UTF8);
body = await reader.ReadToEndAsync();
}
if (!requestIsCompressed)
{
if (_logger.IsLogEnabled(LogLevel.Debug))
{
_logger.Debug(value);
_logger.Debug(body);
}
}
var response = GetResponse(sessionId, req, value);
SendResponse(sessionId, req, resp, value, response);
var response = GetResponse(sessionId, req, body);
await SendResponse(sessionId, req, resp, body, response);
break;
}
@@ -137,7 +124,7 @@ public class SptHttpListener : IHttpListener
/// <param name="resp"> Outgoing response </param>
/// <param name="body"> Buffer </param>
/// <param name="output"> Server generated response data</param>
public void SendResponse(
public async Task SendResponse(
string sessionID,
HttpRequest req,
HttpResponse resp,
@@ -155,7 +142,7 @@ public class SptHttpListener : IHttpListener
if (IsDebugRequest(req))
{
// Send only raw response without transformation
SendJson(resp, output, sessionID);
await SendJson(resp, output, sessionID);
if (_logger.IsLogEnabled(LogLevel.Debug))
{
_logger.Debug($"Response: {output}");
@@ -169,12 +156,12 @@ public class SptHttpListener : IHttpListener
var serialiser = _serializers.FirstOrDefault(x => x.CanHandle(output));
if (serialiser != null)
{
serialiser.Serialize(sessionID, req, resp, bodyInfo);
await serialiser.Serialize(sessionID, req, resp, bodyInfo);
}
else
// No serializer can handle the request (majority of requests dont), zlib the output and send response back
{
SendZlibJson(resp, output, sessionID);
await SendZlibJson(resp, output, sessionID);
}
LogRequest(req, output);
@@ -225,35 +212,35 @@ public class SptHttpListener : IHttpListener
return output;
}
public void SendJson(HttpResponse resp, string? output, string sessionID)
public async Task SendJson(HttpResponse resp, string? output, string sessionID)
{
resp.StatusCode = 200;
resp.ContentType = "application/json";
resp.Headers.Append("Set-Cookie", $"PHPSESSID={sessionID}");
if (!string.IsNullOrEmpty(output))
{
resp.Body.WriteAsync(Encoding.UTF8.GetBytes(output)).AsTask().Wait();
await resp.Body.WriteAsync(Encoding.UTF8.GetBytes(output));
}
resp.StartAsync().Wait();
resp.CompleteAsync().Wait();
await resp.StartAsync();
await resp.CompleteAsync();
}
public void SendZlibJson(HttpResponse resp, string? output, string sessionID)
public async Task SendZlibJson(HttpResponse resp, string? output, string sessionID)
{
using (var ms = new MemoryStream())
{
using (var deflateStream = new ZLibStream(ms, CompressionLevel.SmallestSize))
{
deflateStream.WriteAsync(Encoding.UTF8.GetBytes(output)).AsTask().Wait();
await deflateStream.WriteAsync(Encoding.UTF8.GetBytes(output));
}
var bytes = ms.ToArray();
resp.Body.WriteAsync(bytes, 0, bytes.Length).Wait();
await resp.Body.WriteAsync(bytes);
}
resp.StartAsync().Wait();
resp.CompleteAsync().Wait();
await resp.StartAsync();
await resp.CompleteAsync();
}
private record Response(string Method, string jsonData);
@@ -15,11 +15,11 @@ namespace SPTarkov.Server.Core.Servers;
[Injectable(InjectionType.Singleton)]
public class HttpServer(
WebApplicationBuilder _builder,
ISptLogger<HttpServer> _logger,
LocalisationService _localisationService,
ConfigServer _configServer,
CertificateHelper _certificateHelper,
ApplicationContext _applicationContext,
WebSocketServer _webSocketServer,
ProfileActivityService _profileActivityService,
IEnumerable<IHttpListener> _httpListeners
@@ -27,20 +27,21 @@ public class HttpServer(
{
private readonly HttpConfig _httpConfig = _configServer.GetConfig<HttpConfig>();
private bool _started;
private WebApplication? _webApplication;
/// <summary>
/// Handle server loading event
/// </summary>
/// <param name="builder"> Server builder </param>
/// <exception cref="Exception"> Throws Exception when WebApplicationBuiler or WebApplication are null </exception>
public void Load(WebApplicationBuilder? builder)
public void Load()
{
if (builder is null)
if (_builder is null)
{
throw new Exception("WebApplicationBuilder is null in HttpServer.Load()");
}
builder.WebHost.ConfigureKestrel(options =>
_builder.WebHost.ConfigureKestrel(options =>
{
options.Listen(IPAddress.Parse(_httpConfig.Ip), _httpConfig.Port, listenOptions =>
{
@@ -53,29 +54,35 @@ public class HttpServer(
});
});
var app = builder.Build();
_webApplication = _builder.Build();
if (app is null)
if (_webApplication is null)
{
throw new Exception("WebApplication is null in HttpServer.Load()");
}
// Enable web socket
app.UseWebSockets(new WebSocketOptions
_webApplication.UseWebSockets(new WebSocketOptions
{
// Every minute a heartbeat is sent to keep the connection alive.
KeepAliveInterval = TimeSpan.FromSeconds(60)
});
app?.Use((HttpContext req, RequestDelegate _) =>
_webApplication.Use(async (HttpContext req, RequestDelegate _) =>
{
return Task.Factory.StartNew(async () => await HandleFallback(req));
await HandleFallback(req);
}
);
}
public async Task StartAsync()
{
if (_webApplication != null && !_started)
{
_started = true;
await _webApplication.RunAsync();
}
_applicationContext.AddValue(ContextVariableType.WEB_APPLICATION, app);
}
private async Task HandleFallback(HttpContext context)
@@ -103,7 +110,12 @@ public class HttpServer(
try
{
_httpListeners.SingleOrDefault(l => l.CanHandle(sessionId, context.Request))?.Handle(sessionId, context.Request, context.Response);
var listener = _httpListeners.FirstOrDefault(l => l.CanHandle(sessionId, context.Request));
if (listener != null)
{
await listener.Handle(sessionId, context.Request, context.Response);
}
}
catch (Exception ex)
{
+7 -2
View File
@@ -57,7 +57,7 @@ public class App
_coreConfig = configServer.GetConfig<CoreConfig>();
}
public async Task Run()
public async Task InitializeAsync()
{
// execute onLoad callbacks
_logger.Info(_localisationService.GetText("executing_startup_callbacks"));
@@ -96,14 +96,19 @@ public class App
}
_timer = new Timer(_ => Update(_onUpdate), null, TimeSpan.Zero, TimeSpan.FromMilliseconds(5000));
}
if (_httpServer.IsStarted())
public async Task StartAsync()
{
if(!_httpServer.IsStarted())
{
_logger.Success(_localisationService.GetText("started_webserver_success", _httpServer.ListeningUrl()));
_logger.Success(_localisationService.GetText("websocket-started", _httpServer.ListeningUrl().Replace("https://", "wss://")));
}
_logger.Success(GetRandomisedStartMessage());
await _httpServer.StartAsync();
}
protected string GetRandomisedStartMessage()
@@ -4,21 +4,16 @@ using SPTarkov.Server.Core.Helpers;
namespace SPTarkov.Server.Core.Utils;
[Injectable]
public class HttpFileUtil
public class HttpFileUtil(HttpServerHelper httpServerHelper)
{
protected HttpServerHelper _httpServerHelper;
protected HttpServerHelper _httpServerHelper = httpServerHelper;
public HttpFileUtil(HttpServerHelper httpServerHelper)
{
_httpServerHelper = httpServerHelper;
}
public void SendFile(HttpResponse resp, string filePath)
public async Task SendFile(HttpResponse resp, string filePath)
{
var pathSlice = filePath.Split("/");
var mimePath = _httpServerHelper.GetMimeText(pathSlice[^1].Split(".")[^1]);
var type = string.IsNullOrWhiteSpace(mimePath) ? _httpServerHelper.GetMimeText("txt") : mimePath;
resp.Headers.Append("Content-Type", type);
resp.SendFileAsync(filePath, CancellationToken.None).Wait();
await resp.SendFileAsync(filePath, CancellationToken.None);
}
}
+10 -12
View File
@@ -8,6 +8,7 @@ using SPTarkov.Server.Core.Helpers;
using SPTarkov.Server.Core.Models.External;
using SPTarkov.Server.Core.Models.Spt.Mod;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Servers;
using SPTarkov.Server.Core.Utils;
using SPTarkov.Server.Core.Utils.Logger;
using SPTarkov.Server.Logger;
@@ -17,7 +18,7 @@ namespace SPTarkov.Server;
public static class Program
{
public static void Main(string[] args)
public static async Task Main(string[] args)
{
// Initialize the program variables
ProgramStatics.Initialize();
@@ -30,7 +31,7 @@ public static class Program
diHandler.AddInjectableTypesFromTypeAssembly(typeof(Program));
diHandler.AddInjectableTypesFromTypeAssembly(typeof(App));
List<SptMod> loadedMods = null;
List<SptMod> loadedMods = [];
if (ProgramStatics.MODS())
{
// Search for mod dlls
@@ -61,28 +62,25 @@ public static class Program
if (ProgramStatics.MODS())
{
// Initialize PreSptMods
var preSptLoadMods = serviceProvider.GetServices<IPreSptLoadMod>();
var preSptLoadMods = serviceProvider.GetServices<IPreSptLoadModAsync>();
foreach (var preSptLoadMod in preSptLoadMods)
{
preSptLoadMod.PreSptLoad();
await preSptLoadMod.PreSptLoadAsync();
}
}
// Get the Built app and run it
var app = serviceProvider.GetService<App>();
app?.Run().Wait();
if (app != null)
{
await app.InitializeAsync();
// Run garbage collection now the server is ready to start
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
var httpServerHelper = serviceProvider.GetService<HttpServerHelper>();
// When the application is started by the HttpServer it will be added into the AppContext of the WebApplication
// object, which we can use here to start the webapp.
if (httpServerHelper != null)
{
appContext?.GetLatestValue(ContextVariableType.WEB_APPLICATION)?.GetValue<WebApplication>().Run();
await app.StartAsync();
}
}
catch (Exception ex)