Application cleanup (#485)

* Changed application to use background services and removed hacky http server startup

* Small improvements and method removals

* Removed Core dependency on Web application SDK

* Fixed wrong imported type

---------

Co-authored-by: Alex <clodanSPT@hotmail.com>
Co-authored-by: Chomp <27521899+chompDev@users.noreply.github.com>
This commit is contained in:
clodanSPT
2025-07-18 16:21:24 +01:00
committed by GitHub
parent 67b886127c
commit 1af50bfd34
24 changed files with 133 additions and 166 deletions
@@ -1,21 +0,0 @@
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.DI;
using SPTarkov.Server.Core.Servers;
namespace SPTarkov.Server.Core.Callbacks;
[Injectable(InjectionType.Singleton, TypePriority = OnLoadOrder.HttpCallbacks)]
public class HttpCallbacks(HttpServer httpServer) : IOnLoad
{
public Task OnLoad()
{
httpServer.Load();
return Task.CompletedTask;
}
public string GetImage()
{
return "";
}
}
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Http;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Controllers;
using SPTarkov.Server.Core.Helpers;
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Http;
using SPTarkov.Server.Core.Models.Common;
namespace SPTarkov.Server.Core.DI;
@@ -1,6 +1,7 @@
using System.Collections.Frozen;
using System.Net;
using System.Net.Sockets;
using Microsoft.AspNetCore.Http;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Models.Spt.Config;
using SPTarkov.Server.Core.Servers;
@@ -85,8 +85,8 @@ namespace SPTarkov.Server.Core.Migration.Migrations
);
//Directly injecting CreateProfileService causes a circular dependency which I can't be bothered to fix just for this
serviceProvider
.GetService<CreateProfileService>()!
(serviceProvider
.GetService(typeof(CreateProfileService)) as CreateProfileService)!
.AddMissingInternalContainersToProfile(profile.CharacterData.PmcData);
}
@@ -135,8 +135,8 @@ namespace SPTarkov.Server.Core.Migration.Migrations
.ToList();
//Directly injecting RewardHelper causes a circular dependency which I can't be bothered to fix just for this
serviceProvider
.GetService<RewardHelper>()!
(serviceProvider
.GetService(typeof(RewardHelper)) as RewardHelper)!
.ApplyRewards(
filteredRewards,
CustomisationSource.ACHIEVEMENT,
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Http;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.DI;
using SPTarkov.Server.Core.Models.Common;
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Http;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Models.Common;
using SPTarkov.Server.Core.Models.Utils;
@@ -1,4 +1,5 @@
using SPTarkov.DI.Annotations;
using Microsoft.AspNetCore.Http;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.DI;
using SPTarkov.Server.Core.Loaders;
using SPTarkov.Server.Core.Models.Common;
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Http;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.DI;
using SPTarkov.Server.Core.Models.Common;
@@ -1,4 +1,5 @@
using SPTarkov.DI.Annotations;
using Microsoft.AspNetCore.Http;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Controllers;
using SPTarkov.Server.Core.DI;
using SPTarkov.Server.Core.Helpers;
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\Build.props" />
<PropertyGroup>
<PackageId>SPTarkov.Server.Core</PackageId>
@@ -1,4 +1,6 @@
namespace SPTarkov.Server.Core.Servers.Http;
using Microsoft.AspNetCore.Http;
namespace SPTarkov.Server.Core.Servers.Http;
public interface IHttpListener
{
@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.IO.Compression;
using System.Text;
using Microsoft.AspNetCore.Http;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.DI;
using SPTarkov.Server.Core.Models.Enums;
@@ -1,10 +1,7 @@
using System.Net;
using System.Security.Authentication;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using SPTarkov.Common.Extensions;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Helpers;
using SPTarkov.Server.Core.Models.Spt.Config;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Servers.Http;
@@ -14,82 +11,17 @@ namespace SPTarkov.Server.Core.Servers;
[Injectable(InjectionType.Singleton)]
public class HttpServer(
WebApplicationBuilder _builder,
ISptLogger<HttpServer> _logger,
ServerLocalisationService _serverLocalisationService,
ConfigServer _configServer,
CertificateHelper _certificateHelper,
WebSocketServer _webSocketServer,
ProfileActivityService _profileActivityService,
IEnumerable<IHttpListener> _httpListeners
)
{
private readonly HttpConfig _httpConfig = _configServer.GetConfig<HttpConfig>();
private bool _started;
private WebApplication? _webApplication;
/// <summary>
/// Handle server loading event
/// </summary>
/// <exception cref="Exception"> Throws Exception when WebApplicationBuilder or WebApplication are null </exception>
public void Load()
{
if (_builder is null)
{
throw new Exception("WebApplicationBuilder is null in HttpServer.Load()");
}
_builder.WebHost.ConfigureKestrel(options =>
{
options.Listen(
IPAddress.Parse(_httpConfig.Ip),
_httpConfig.Port,
listenOptions =>
{
listenOptions.UseHttps(opts =>
{
opts.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13;
opts.ServerCertificate = _certificateHelper.LoadOrGenerateCertificatePfx();
opts.ClientCertificateMode = ClientCertificateMode.NoCertificate;
});
}
);
});
_webApplication = _builder.Build();
if (_webApplication is null)
{
throw new Exception("WebApplication is null in HttpServer.Load()");
}
// Enable web socket
_webApplication.UseWebSockets(
new WebSocketOptions
{
// Every minute a heartbeat is sent to keep the connection alive.
KeepAliveInterval = TimeSpan.FromSeconds(60),
}
);
_webApplication.Use(
async (HttpContext req, RequestDelegate _) =>
{
await HandleFallback(req);
}
);
}
public async Task StartAsync()
{
if (_webApplication != null && !_started)
{
_started = true;
await _webApplication.RunAsync();
}
}
private async Task HandleFallback(HttpContext context)
public async Task HandleRequest(HttpContext context)
{
if (context.WebSockets.IsWebSocketRequest)
{
@@ -203,11 +135,6 @@ public class HttpServer(
return found;
}
public bool IsStarted()
{
return _started;
}
public string ListeningUrl()
{
return $"https://{_httpConfig.Ip}:{_httpConfig.Port}";
@@ -1,4 +1,5 @@
using System.Net.WebSockets;
using Microsoft.AspNetCore.Http;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Servers.Ws;
@@ -1,4 +1,5 @@
using System.Net.WebSockets;
using Microsoft.AspNetCore.Http;
namespace SPTarkov.Server.Core.Servers.Ws;
@@ -1,5 +1,6 @@
using System.Net.WebSockets;
using System.Text;
using Microsoft.AspNetCore.Http;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Helpers;
using SPTarkov.Server.Core.Models.Eft.Ws;
@@ -1,4 +1,5 @@
using System.Text;
using Microsoft.AspNetCore.Http;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Models.Spt.Config;
using SPTarkov.Server.Core.Servers;
+14 -21
View File
@@ -1,3 +1,4 @@
using Microsoft.Extensions.Hosting;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.DI;
using SPTarkov.Server.Core.Extensions;
@@ -75,29 +76,21 @@ public class App(
// Discard here, as this task will run indefinitely
_ = Task.Run(Update);
}
public async Task StartAsync()
{
if (!_httpServer.IsStarted())
{
_logger.Success(
_serverLocalisationService.GetText(
"started_webserver_success",
_httpServer.ListeningUrl()
)
);
_logger.Success(
_serverLocalisationService.GetText(
"websocket-started",
_httpServer.ListeningUrl().Replace("https://", "wss://")
)
);
}
_logger.Success(
_serverLocalisationService.GetText(
"started_webserver_success",
_httpServer.ListeningUrl()
)
);
_logger.Success(
_serverLocalisationService.GetText(
"websocket-started",
_httpServer.ListeningUrl().Replace("https://", "wss://")
)
);
_logger.Success(GetRandomisedStartMessage());
await _httpServer.StartAsync();
}
protected string GetRandomisedStartMessage()
@@ -117,7 +110,7 @@ public class App(
while (!_appLifeTime.ApplicationStopping.IsCancellationRequested)
{
// If the server has failed to start, skip any update calls
if (!_httpServer.IsStarted() || !_databaseService.IsDatabaseValid())
if (!_databaseService.IsDatabaseValid())
{
await Task.Delay(5000, _appLifeTime.ApplicationStopping);
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Http;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Helpers;
@@ -1,5 +1,6 @@
using System.Reflection;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using SPTarkov.DI.Annotations;
namespace SPTarkov.Server.Core.Utils.Json.Converters;
+59 -36
View File
@@ -1,16 +1,23 @@
using System.Net;
using System.Runtime;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Text;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using SPTarkov.Common.Semver;
using SPTarkov.Common.Semver.Implementations;
using SPTarkov.DI;
using SPTarkov.Server.Core.Helpers;
using SPTarkov.Server.Core.Loaders;
using SPTarkov.Server.Core.Models.Spt.Config;
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;
using SPTarkov.Server.Modding;
using SPTarkov.Server.Services;
namespace SPTarkov.Server;
@@ -60,57 +67,73 @@ public static class Program
builder.Services.AddSingleton(builder);
builder.Services.AddSingleton<IReadOnlyList<SptMod>>(loadedMods);
var serviceProvider = builder.Services.BuildServiceProvider();
var logger = serviceProvider.GetService<ILoggerFactory>().CreateLogger("Server");
// Load bundles for bundle mods
if (ProgramStatics.MODS())
{
var bundleLoader = serviceProvider.GetService<BundleLoader>();
foreach (var mod in loadedMods)
{
if (mod.ModMetadata?.IsBundleMod == true)
{
// Convert to relative path
string relativeModPath = Path.GetRelativePath(
Directory.GetCurrentDirectory(),
mod.Directory
)
.Replace('\\', '/');
builder.Services.AddHostedService<SptServerBackgroundService>();
// Configure Kestrel options
ConfigureKestrel(builder);
bundleLoader.AddBundles(relativeModPath);
}
}
}
var app = builder.Build();
// Configure Kestrel WS options and Handle fallback requests
ConfigureWebApp(app);
// In case of exceptions we snatch a Server logger
var serverExceptionLogger = app.Services.GetService<ILoggerFactory>()!.CreateLogger("Server");
// We need any logger instance to use as a finalizer when the app closes
var loggerFinalizer = app.Services.GetService<ISptLogger<App>>()!;
try
{
SetConsoleOutputMode();
// Get the Built app and run it
var app = serviceProvider.GetService<App>();
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);
await app.StartAsync();
}
await app.RunAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex);
logger.LogCritical(ex, "Critical exception, stopping server...");
serverExceptionLogger.LogCritical(ex, "Critical exception, stopping server...");
}
finally
{
serviceProvider.GetService<SptLogger<object>>()?.DumpAndStop();
loggerFinalizer.DumpAndStop();
}
}
private static void ConfigureWebApp(WebApplication app)
{
app.UseWebSockets(
new WebSocketOptions
{
// Every minute a heartbeat is sent to keep the connection alive.
KeepAliveInterval = TimeSpan.FromSeconds(60)
}
);
app.Use(async (HttpContext context, RequestDelegate _) =>
{
await context.RequestServices.GetService<HttpServer>()!.HandleRequest(context);
});
}
private static void ConfigureKestrel(WebApplicationBuilder builder)
{
builder.WebHost.ConfigureKestrel((_, options) =>
{
var httpConfig = options.ApplicationServices.GetService<ConfigServer>()?.GetConfig<HttpConfig>()!;
var certHelper = options.ApplicationServices.GetService<CertificateHelper>()!;
options.Listen(
IPAddress.Parse(httpConfig.Ip),
httpConfig.Port,
listenOptions =>
{
listenOptions.UseHttps(opts =>
{
opts.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13;
opts.ServerCertificate = certHelper.LoadOrGenerateCertificatePfx();
opts.ClientCertificateMode = ClientCertificateMode.NoCertificate;
});
}
);
});
}
private static WebApplicationBuilder CreateNewHostBuilder(string[]? args = null)
{
var builder = WebApplication.CreateBuilder(args);
@@ -0,0 +1,33 @@
using System.Runtime;
using SPTarkov.Server.Core.Loaders;
using SPTarkov.Server.Core.Models.Spt.Mod;
using SPTarkov.Server.Core.Utils;
namespace SPTarkov.Server.Services;
public class SptServerBackgroundService(IReadOnlyList<SptMod> loadedMods, BundleLoader bundleLoader, App app) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (ProgramStatics.MODS())
{
foreach (var mod in loadedMods)
{
if (mod.ModMetadata?.IsBundleMod == true)
{
// Convert to relative path
var relativeModPath = Path.GetRelativePath(Directory.GetCurrentDirectory(), mod.Directory)
.Replace('\\', '/');
bundleLoader.AddBundles(relativeModPath);
}
}
}
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);
}
}
@@ -35,11 +35,6 @@ public class ItemTplGenerator(
// Load all onload components, this gives us access to most of SPTs injections
foreach (var onLoad in _onLoadComponents)
{
if (onLoad is HttpCallbacks)
{
continue;
}
await onLoad.OnLoad();
}