Implemented online detection. Extended ServerContext to include self reference so sub components can subscribe to the state. Improved console module detach handling. Implemented new server service to replace the old one. Added log restore when restoring
This commit is contained in:
@@ -12,9 +12,9 @@ namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
|||||||
[Route("api/servers")]
|
[Route("api/servers")]
|
||||||
public class ServerPowerController : Controller
|
public class ServerPowerController : Controller
|
||||||
{
|
{
|
||||||
private readonly ServerService ServerService;
|
private readonly NewServerService ServerService;
|
||||||
|
|
||||||
public ServerPowerController(ServerService serverService)
|
public ServerPowerController(NewServerService serverService)
|
||||||
{
|
{
|
||||||
ServerService = serverService;
|
ServerService = serverService;
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ public class ServerPowerController : Controller
|
|||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
await server.Trigger(ServerTrigger.Start);
|
await server.StateMachine.FireAsync(ServerTrigger.Start);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{serverId:int}/stop")]
|
[HttpPost("{serverId:int}/stop")]
|
||||||
@@ -38,7 +38,7 @@ public class ServerPowerController : Controller
|
|||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
await server.Trigger(ServerTrigger.Stop);
|
await server.StateMachine.FireAsync(ServerTrigger.Stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{serverId:int}/install")]
|
[HttpPost("{serverId:int}/install")]
|
||||||
@@ -49,7 +49,7 @@ public class ServerPowerController : Controller
|
|||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
await server.Trigger(ServerTrigger.Install);
|
await server.StateMachine.FireAsync(ServerTrigger.Install);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{serverId:int}/kill")]
|
[HttpPost("{serverId:int}/kill")]
|
||||||
@@ -60,6 +60,6 @@ public class ServerPowerController : Controller
|
|||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
await server.Trigger(ServerTrigger.Kill);
|
await server.StateMachine.FireAsync(ServerTrigger.Kill);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,9 +14,9 @@ namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
|||||||
[Route("api/servers/{serverId:int}")]
|
[Route("api/servers/{serverId:int}")]
|
||||||
public class ServersController : Controller
|
public class ServersController : Controller
|
||||||
{
|
{
|
||||||
private readonly ServerService ServerService;
|
private readonly NewServerService ServerService;
|
||||||
|
|
||||||
public ServersController(ServerService serverService)
|
public ServersController(NewServerService serverService)
|
||||||
{
|
{
|
||||||
ServerService = serverService;
|
ServerService = serverService;
|
||||||
}
|
}
|
||||||
@@ -57,8 +57,7 @@ public class ServersController : Controller
|
|||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
var consoleSubSystem = server.GetRequiredSubSystem<ConsoleSubSystem>();
|
var messages = server.Console.GetOutput();
|
||||||
var messages = await consoleSubSystem.RetrieveCache();
|
|
||||||
|
|
||||||
return new ServerLogsResponse()
|
return new ServerLogsResponse()
|
||||||
{
|
{
|
||||||
@@ -73,7 +72,7 @@ public class ServersController : Controller
|
|||||||
|
|
||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
/*
|
||||||
var statsSubSystem = server.GetRequiredSubSystem<StatsSubSystem>();
|
var statsSubSystem = server.GetRequiredSubSystem<StatsSubSystem>();
|
||||||
|
|
||||||
return Task.FromResult<ServerStatsResponse>(new()
|
return Task.FromResult<ServerStatsResponse>(new()
|
||||||
@@ -84,6 +83,16 @@ public class ServersController : Controller
|
|||||||
NetworkWrite = statsSubSystem.CurrentStats.NetworkWrite,
|
NetworkWrite = statsSubSystem.CurrentStats.NetworkWrite,
|
||||||
IoRead = statsSubSystem.CurrentStats.IoRead,
|
IoRead = statsSubSystem.CurrentStats.IoRead,
|
||||||
IoWrite = statsSubSystem.CurrentStats.IoWrite
|
IoWrite = statsSubSystem.CurrentStats.IoWrite
|
||||||
|
});*/
|
||||||
|
|
||||||
|
return Task.FromResult<ServerStatsResponse>(new()
|
||||||
|
{
|
||||||
|
CpuUsage = 0,
|
||||||
|
MemoryUsage = 0,
|
||||||
|
NetworkRead = 0,
|
||||||
|
NetworkWrite = 0,
|
||||||
|
IoRead = 0,
|
||||||
|
IoWrite = 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,8 +104,6 @@ public class ServersController : Controller
|
|||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
var consoleSubSystem = server.GetRequiredSubSystem<ConsoleSubSystem>();
|
await server.Console.WriteToInput(request.Command);
|
||||||
|
|
||||||
await consoleSubSystem.WriteInput(request.Command);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,29 @@ public class ServerConfigurationMapper
|
|||||||
AppConfiguration = appConfiguration;
|
AppConfiguration = appConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ServerConfiguration FromServerDataResponse(ServerDataResponse response)
|
||||||
|
{
|
||||||
|
return new ServerConfiguration()
|
||||||
|
{
|
||||||
|
Id = response.Id,
|
||||||
|
StartupCommand = response.StartupCommand,
|
||||||
|
Allocations = response.Allocations.Select(y => new ServerConfiguration.AllocationConfiguration()
|
||||||
|
{
|
||||||
|
IpAddress = y.IpAddress,
|
||||||
|
Port = y.Port
|
||||||
|
}).ToArray(),
|
||||||
|
Variables = response.Variables,
|
||||||
|
OnlineDetection = response.OnlineDetection,
|
||||||
|
DockerImage = response.DockerImage,
|
||||||
|
UseVirtualDisk = response.UseVirtualDisk,
|
||||||
|
Bandwidth = response.Bandwidth,
|
||||||
|
Cpu = response.Cpu,
|
||||||
|
Disk = response.Disk,
|
||||||
|
Memory = response.Memory,
|
||||||
|
StopCommand = response.StopCommand
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public CreateContainerParameters ToRuntimeParameters(
|
public CreateContainerParameters ToRuntimeParameters(
|
||||||
ServerConfiguration serverConfiguration,
|
ServerConfiguration serverConfiguration,
|
||||||
string hostPath,
|
string hostPath,
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ public interface IConsole : IServerComponent
|
|||||||
public Task AttachToRuntime();
|
public Task AttachToRuntime();
|
||||||
public Task AttachToInstallation();
|
public Task AttachToInstallation();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detaches any attached consoles. Usually either runtime or install is attached
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task Detach();
|
||||||
|
|
||||||
|
public Task CollectFromRuntime();
|
||||||
|
public Task CollectFromInstallation();
|
||||||
|
|
||||||
public Task WriteToOutput(string content);
|
public Task WriteToOutput(string content);
|
||||||
public Task WriteToInput(string content);
|
public Task WriteToInput(string content);
|
||||||
public Task WriteToMoonlight(string content);
|
public Task WriteToMoonlight(string content);
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
|
||||||
|
|
||||||
|
public interface IOnlineDetection : IServerComponent
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using MoonCore.Observability;
|
using MoonCore.Observability;
|
||||||
using MoonlightServers.Daemon.Extensions;
|
using MoonlightServers.Daemon.Extensions;
|
||||||
using MoonlightServers.Daemon.Helpers;
|
using MoonlightServers.Daemon.Http.Hubs;
|
||||||
|
using MoonlightServers.Daemon.Mappers;
|
||||||
using MoonlightServers.Daemon.ServerSystem;
|
using MoonlightServers.Daemon.ServerSystem;
|
||||||
|
using MoonlightServers.Daemon.Services;
|
||||||
using Stateless;
|
using Stateless;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
|
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
|
||||||
@@ -14,15 +17,20 @@ public class Server : IAsyncDisposable
|
|||||||
public IProvisioner Provisioner { get; }
|
public IProvisioner Provisioner { get; }
|
||||||
public IRestorer Restorer { get; }
|
public IRestorer Restorer { get; }
|
||||||
public IStatistics Statistics { get; }
|
public IStatistics Statistics { get; }
|
||||||
|
public IOnlineDetection OnlineDetection { get; }
|
||||||
public StateMachine<ServerState, ServerTrigger> StateMachine { get; private set; }
|
public StateMachine<ServerState, ServerTrigger> StateMachine { get; private set; }
|
||||||
public ServerContext Context { get; }
|
public ServerContext Context { get; }
|
||||||
public IAsyncObservable<ServerState> OnState => OnStateSubject;
|
public IAsyncObservable<ServerState> OnState => OnStateSubject;
|
||||||
|
|
||||||
private readonly EventSubject<ServerState> OnStateSubject = new();
|
private readonly EventSubject<ServerState> OnStateSubject = new();
|
||||||
private readonly ILogger<Server> Logger;
|
private readonly ILogger<Server> Logger;
|
||||||
|
private readonly RemoteService RemoteService;
|
||||||
|
private readonly ServerConfigurationMapper Mapper;
|
||||||
|
private readonly IHubContext<ServerWebSocketHub> HubContext;
|
||||||
|
|
||||||
private IAsyncDisposable? ProvisionExitSubscription;
|
private IAsyncDisposable? ProvisionExitSubscription;
|
||||||
private IAsyncDisposable? InstallerExitSubscription;
|
private IAsyncDisposable? InstallerExitSubscription;
|
||||||
|
private IAsyncDisposable? ConsoleSubscription;
|
||||||
|
|
||||||
public Server(
|
public Server(
|
||||||
ILogger<Server> logger,
|
ILogger<Server> logger,
|
||||||
@@ -32,8 +40,11 @@ public class Server : IAsyncDisposable
|
|||||||
IProvisioner provisioner,
|
IProvisioner provisioner,
|
||||||
IRestorer restorer,
|
IRestorer restorer,
|
||||||
IStatistics statistics,
|
IStatistics statistics,
|
||||||
ServerContext context
|
IOnlineDetection onlineDetection,
|
||||||
)
|
ServerContext context,
|
||||||
|
RemoteService remoteService,
|
||||||
|
ServerConfigurationMapper mapper,
|
||||||
|
IHubContext<ServerWebSocketHub> hubContext)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
Console = console;
|
Console = console;
|
||||||
@@ -43,13 +54,17 @@ public class Server : IAsyncDisposable
|
|||||||
Restorer = restorer;
|
Restorer = restorer;
|
||||||
Statistics = statistics;
|
Statistics = statistics;
|
||||||
Context = context;
|
Context = context;
|
||||||
|
RemoteService = remoteService;
|
||||||
|
Mapper = mapper;
|
||||||
|
HubContext = hubContext;
|
||||||
|
OnlineDetection = onlineDetection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Initialize()
|
public async Task Initialize()
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Initializing server components");
|
Logger.LogDebug("Initializing server components");
|
||||||
|
|
||||||
IServerComponent[] components = [Console, Restorer, FileSystem, Installer, Provisioner, Statistics];
|
IServerComponent[] components = [Console, Restorer, FileSystem, Installer, Provisioner, Statistics, OnlineDetection];
|
||||||
|
|
||||||
foreach (var serverComponent in components)
|
foreach (var serverComponent in components)
|
||||||
{
|
{
|
||||||
@@ -79,6 +94,8 @@ public class Server : IAsyncDisposable
|
|||||||
|
|
||||||
CreateStateMachine(restoredState);
|
CreateStateMachine(restoredState);
|
||||||
|
|
||||||
|
await SetupHubEvents();
|
||||||
|
|
||||||
// Setup event handling
|
// Setup event handling
|
||||||
ProvisionExitSubscription = await Provisioner.OnExited.SubscribeEventAsync(async _ =>
|
ProvisionExitSubscription = await Provisioner.OnExited.SubscribeEventAsync(async _ =>
|
||||||
await StateMachine.FireAsync(ServerTrigger.Exited)
|
await StateMachine.FireAsync(ServerTrigger.Exited)
|
||||||
@@ -89,6 +106,14 @@ public class Server : IAsyncDisposable
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task Sync()
|
||||||
|
{
|
||||||
|
IServerComponent[] components = [Console, Restorer, FileSystem, Installer, Provisioner, Statistics, OnlineDetection];
|
||||||
|
|
||||||
|
foreach (var component in components)
|
||||||
|
await component.Sync();
|
||||||
|
}
|
||||||
|
|
||||||
private void CreateStateMachine(ServerState initialState)
|
private void CreateStateMachine(ServerState initialState)
|
||||||
{
|
{
|
||||||
StateMachine = new StateMachine<ServerState, ServerTrigger>(initialState, FiringMode.Queued);
|
StateMachine = new StateMachine<ServerState, ServerTrigger>(initialState, FiringMode.Queued);
|
||||||
@@ -140,9 +165,31 @@ public class Server : IAsyncDisposable
|
|||||||
|
|
||||||
StateMachine.Configure(ServerState.Stopping)
|
StateMachine.Configure(ServerState.Stopping)
|
||||||
.OnEntryFromAsync(ServerTrigger.Stop, HandleStop)
|
.OnEntryFromAsync(ServerTrigger.Stop, HandleStop)
|
||||||
|
.OnEntryFromAsync(ServerTrigger.Kill, HandleKill)
|
||||||
.OnExitFromAsync(ServerTrigger.Exited, HandleRuntimeExit);
|
.OnExitFromAsync(ServerTrigger.Exited, HandleRuntimeExit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SetupHubEvents()
|
||||||
|
{
|
||||||
|
var groupName = Context.Configuration.Id.ToString();
|
||||||
|
|
||||||
|
ConsoleSubscription = await Console.OnOutput.SubscribeAsync(async line =>
|
||||||
|
{
|
||||||
|
await HubContext.Clients.Group(groupName).SendAsync(
|
||||||
|
"ConsoleOutput",
|
||||||
|
line
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
StateMachine.OnTransitionedAsync(async transition =>
|
||||||
|
{
|
||||||
|
await HubContext.Clients.Group(groupName).SendAsync(
|
||||||
|
"StateChanged",
|
||||||
|
transition.Destination.ToString()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#region State machine handlers
|
#region State machine handlers
|
||||||
|
|
||||||
private async Task HandleStart()
|
private async Task HandleStart()
|
||||||
@@ -158,7 +205,11 @@ public class Server : IAsyncDisposable
|
|||||||
// 6. Start the container
|
// 6. Start the container
|
||||||
|
|
||||||
// 1. Fetch latest configuration from panel
|
// 1. Fetch latest configuration from panel
|
||||||
// TODO: Implement
|
Logger.LogDebug("Fetching latest server configuration");
|
||||||
|
await Console.WriteToMoonlight("Fetching latest server configuration");
|
||||||
|
|
||||||
|
var serverDataResponse = await RemoteService.GetServer(Context.Configuration.Id);
|
||||||
|
Context.Configuration = Mapper.FromServerDataResponse(serverDataResponse);
|
||||||
|
|
||||||
// 2. Ensure that the file system exists
|
// 2. Ensure that the file system exists
|
||||||
if (!FileSystem.Exists)
|
if (!FileSystem.Exists)
|
||||||
@@ -187,9 +238,6 @@ public class Server : IAsyncDisposable
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.LogError(e, "An error occured while starting the server");
|
Logger.LogError(e, "An error occured while starting the server");
|
||||||
await Console.WriteToMoonlight("Failed to start the server. More details can be found in the daemon logs");
|
|
||||||
|
|
||||||
await StateMachine.FireAsync(ServerTrigger.FailSafe);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,53 +246,61 @@ public class Server : IAsyncDisposable
|
|||||||
await Provisioner.Stop();
|
await Provisioner.Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task HandleKill()
|
||||||
|
{
|
||||||
|
await Provisioner.Kill();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task HandleRuntimeExit()
|
private async Task HandleRuntimeExit()
|
||||||
{
|
{
|
||||||
|
Logger.LogDebug("Detected runtime exit");
|
||||||
|
|
||||||
|
Logger.LogDebug("Detaching from console");
|
||||||
|
await Console.Detach();
|
||||||
|
|
||||||
Logger.LogDebug("Deprovisioning");
|
Logger.LogDebug("Deprovisioning");
|
||||||
await Console.WriteToMoonlight("Deprovisioning");
|
await Console.WriteToMoonlight("Deprovisioning");
|
||||||
|
|
||||||
await Provisioner.Deprovision();
|
await Provisioner.Deprovision();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleInstall()
|
private async Task HandleInstall()
|
||||||
{
|
{
|
||||||
try
|
// Plan:
|
||||||
{
|
// 1. Fetch the latest installation data
|
||||||
Logger.LogDebug("Installing");
|
// 2. Setup installation environment
|
||||||
|
// 3. Attach console to installation
|
||||||
|
// 4. Start the installation
|
||||||
|
|
||||||
Logger.LogDebug("Setting up");
|
Logger.LogDebug("Installing");
|
||||||
await Console.WriteToMoonlight("Setting up installation");
|
|
||||||
|
|
||||||
// TODO: Extract to service
|
Logger.LogDebug("Setting up");
|
||||||
|
await Console.WriteToMoonlight("Setting up installation");
|
||||||
|
|
||||||
Context.InstallConfiguration = new()
|
// 1. Fetch the latest installation data
|
||||||
{
|
Logger.LogDebug("Fetching installation data");
|
||||||
Shell = "/bin/ash",
|
await Console.WriteToMoonlight("Fetching installation data");
|
||||||
DockerImage = "ghcr.io/parkervcp/installers:alpine",
|
|
||||||
Script =
|
|
||||||
"#!/bin/ash\n# Paper Installation Script\n#\n# Server Files: /mnt/server\nPROJECT=paper\n\nif [ -n \"${DL_PATH}\" ]; then\n\techo -e \"Using supplied download url: ${DL_PATH}\"\n\tDOWNLOAD_URL=`eval echo $(echo ${DL_PATH} | sed -e 's/{{/${/g' -e 's/}}/}/g')`\nelse\n\tVER_EXISTS=`curl -s https://api.papermc.io/v2/projects/${PROJECT} | jq -r --arg VERSION $MINECRAFT_VERSION '.versions[] | contains($VERSION)' | grep -m1 true`\n\tLATEST_VERSION=`curl -s https://api.papermc.io/v2/projects/${PROJECT} | jq -r '.versions' | jq -r '.[-1]'`\n\n\tif [ \"${VER_EXISTS}\" == \"true\" ]; then\n\t\techo -e \"Version is valid. Using version ${MINECRAFT_VERSION}\"\n\telse\n\t\techo -e \"Specified version not found. Defaulting to the latest ${PROJECT} version\"\n\t\tMINECRAFT_VERSION=${LATEST_VERSION}\n\tfi\n\n\tBUILD_EXISTS=`curl -s https://api.papermc.io/v2/projects/${PROJECT}/versions/${MINECRAFT_VERSION} | jq -r --arg BUILD ${BUILD_NUMBER} '.builds[] | tostring | contains($BUILD)' | grep -m1 true`\n\tLATEST_BUILD=`curl -s https://api.papermc.io/v2/projects/${PROJECT}/versions/${MINECRAFT_VERSION} | jq -r '.builds' | jq -r '.[-1]'`\n\n\tif [ \"${BUILD_EXISTS}\" == \"true\" ]; then\n\t\techo -e \"Build is valid for version ${MINECRAFT_VERSION}. Using build ${BUILD_NUMBER}\"\n\telse\n\t\techo -e \"Using the latest ${PROJECT} build for version ${MINECRAFT_VERSION}\"\n\t\tBUILD_NUMBER=${LATEST_BUILD}\n\tfi\n\n\tJAR_NAME=${PROJECT}-${MINECRAFT_VERSION}-${BUILD_NUMBER}.jar\n\n\techo \"Version being downloaded\"\n\techo -e \"MC Version: ${MINECRAFT_VERSION}\"\n\techo -e \"Build: ${BUILD_NUMBER}\"\n\techo -e \"JAR Name of Build: ${JAR_NAME}\"\n\tDOWNLOAD_URL=https://api.papermc.io/v2/projects/${PROJECT}/versions/${MINECRAFT_VERSION}/builds/${BUILD_NUMBER}/downloads/${JAR_NAME}\nfi\n\ncd /mnt/server\n\necho -e \"Running curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\"\n\nif [ -f ${SERVER_JARFILE} ]; then\n\tmv ${SERVER_JARFILE} ${SERVER_JARFILE}.old\nfi\n\ncurl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\n\nif [ ! -f server.properties ]; then\n echo -e \"Downloading MC server.properties\"\n curl -o server.properties https://raw.githubusercontent.com/parkervcp/eggs/master/minecraft/java/server.properties\nfi"
|
|
||||||
};
|
|
||||||
|
|
||||||
await Installer.Setup();
|
Context.InstallConfiguration = await RemoteService.GetServerInstallation(Context.Configuration.Id);
|
||||||
|
|
||||||
await Console.AttachToInstallation();
|
// 2. Setup installation environment
|
||||||
|
await Installer.Setup();
|
||||||
|
|
||||||
await Installer.Start();
|
// 3. Attach console to installation
|
||||||
}
|
await Console.AttachToInstallation();
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogError(e, "An error occured while starting installation");
|
|
||||||
await Console.WriteToMoonlight("An error occured while starting installation");
|
|
||||||
|
|
||||||
await StateMachine.FireAsync(ServerTrigger.FailSafe);
|
// 4. Start the installation
|
||||||
}
|
await Installer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleInstallExit()
|
private async Task HandleInstallExit()
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Installation done");
|
Logger.LogDebug("Detected install exit");
|
||||||
await Console.WriteToMoonlight("Cleaning up");
|
|
||||||
|
|
||||||
|
Logger.LogDebug("Detaching from console");
|
||||||
|
await Console.Detach();
|
||||||
|
|
||||||
|
Logger.LogDebug("Cleaning up");
|
||||||
|
await Console.WriteToMoonlight("Cleaning up");
|
||||||
await Installer.Cleanup();
|
await Installer.Cleanup();
|
||||||
|
|
||||||
await Console.WriteToMoonlight("Installation completed");
|
await Console.WriteToMoonlight("Installation completed");
|
||||||
@@ -260,6 +316,9 @@ public class Server : IAsyncDisposable
|
|||||||
if (InstallerExitSubscription != null)
|
if (InstallerExitSubscription != null)
|
||||||
await InstallerExitSubscription.DisposeAsync();
|
await InstallerExitSubscription.DisposeAsync();
|
||||||
|
|
||||||
|
if (ConsoleSubscription != null)
|
||||||
|
await ConsoleSubscription.DisposeAsync();
|
||||||
|
|
||||||
await Console.DisposeAsync();
|
await Console.DisposeAsync();
|
||||||
await FileSystem.DisposeAsync();
|
await FileSystem.DisposeAsync();
|
||||||
await Installer.DisposeAsync();
|
await Installer.DisposeAsync();
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ public record ServerContext
|
|||||||
public ServerConfiguration Configuration { get; set; }
|
public ServerConfiguration Configuration { get; set; }
|
||||||
public AsyncServiceScope ServiceScope { get; set; }
|
public AsyncServiceScope ServiceScope { get; set; }
|
||||||
public ServerInstallDataResponse InstallConfiguration { get; set; }
|
public ServerInstallDataResponse InstallConfiguration { get; set; }
|
||||||
|
public Server Self { get; set; }
|
||||||
}
|
}
|
||||||
@@ -38,11 +38,10 @@ public class DefaultRestorer : IRestorer
|
|||||||
{
|
{
|
||||||
Logger.LogDebug("Detected runtime to restore");
|
Logger.LogDebug("Detected runtime to restore");
|
||||||
|
|
||||||
|
await Console.CollectFromRuntime();
|
||||||
await Console.AttachToRuntime();
|
await Console.AttachToRuntime();
|
||||||
await Statistics.SubscribeToRuntime();
|
await Statistics.SubscribeToRuntime();
|
||||||
|
|
||||||
// TODO: Read out existing container log in order to search if the server is online
|
|
||||||
|
|
||||||
return ServerState.Online;
|
return ServerState.Online;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +49,7 @@ public class DefaultRestorer : IRestorer
|
|||||||
{
|
{
|
||||||
Logger.LogDebug("Detected installation to restore");
|
Logger.LogDebug("Detected installation to restore");
|
||||||
|
|
||||||
|
await Console.CollectFromInstallation();
|
||||||
await Console.AttachToInstallation();
|
await Console.AttachToInstallation();
|
||||||
await Statistics.SubscribeToInstallation();
|
await Statistics.SubscribeToInstallation();
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using System.Reactive.Linq;
|
|
||||||
using System.Reactive.Subjects;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Docker.DotNet;
|
using Docker.DotNet;
|
||||||
using Docker.DotNet.Models;
|
using Docker.DotNet.Models;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonCore.Observability;
|
using MoonCore.Observability;
|
||||||
using MoonlightServers.Daemon.Helpers;
|
|
||||||
using MoonlightServers.Daemon.ServerSys.Abstractions;
|
using MoonlightServers.Daemon.ServerSys.Abstractions;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.ServerSys.Implementations;
|
namespace MoonlightServers.Daemon.ServerSys.Implementations;
|
||||||
@@ -26,11 +23,12 @@ public class DockerConsole : IConsole
|
|||||||
private MultiplexedStream? CurrentStream;
|
private MultiplexedStream? CurrentStream;
|
||||||
private CancellationTokenSource Cts = new();
|
private CancellationTokenSource Cts = new();
|
||||||
|
|
||||||
|
private const string MlPrefix = "\x1b[1;38;2;200;90;200mM\x1b[1;38;2;204;110;230mo\x1b[1;38;2;170;130;245mo\x1b[1;38;2;140;150;255mn\x1b[1;38;2;110;180;255ml\x1b[1;38;2;100;200;255mi\x1b[1;38;2;100;220;255mg\x1b[1;38;2;120;235;255mh\x1b[1;38;2;140;250;255mt\x1b[0m \x1b[3;38;2;200;200;200m{0}\x1b[0m\n\r";
|
||||||
|
|
||||||
public DockerConsole(
|
public DockerConsole(
|
||||||
ServerContext context,
|
ServerContext context,
|
||||||
DockerClient dockerClient,
|
DockerClient dockerClient,
|
||||||
ILogger<DockerConsole> logger
|
ILogger<DockerConsole> logger)
|
||||||
)
|
|
||||||
{
|
{
|
||||||
Context = context;
|
Context = context;
|
||||||
DockerClient = dockerClient;
|
DockerClient = dockerClient;
|
||||||
@@ -55,8 +53,45 @@ public class DockerConsole : IConsole
|
|||||||
await AttachStream(containerName);
|
await AttachStream(containerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task AttachStream(string containerName)
|
public async Task Detach()
|
||||||
{
|
{
|
||||||
|
Logger.LogDebug("Detaching stream");
|
||||||
|
|
||||||
|
if (!Cts.IsCancellationRequested)
|
||||||
|
await Cts.CancelAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CollectFromRuntime()
|
||||||
|
=> await CollectFromContainer($"moonlight-runtime-{Context.Configuration.Id}");
|
||||||
|
|
||||||
|
public async Task CollectFromInstallation()
|
||||||
|
=> await CollectFromContainer($"moonlight-install-{Context.Configuration.Id}");
|
||||||
|
|
||||||
|
private async Task CollectFromContainer(string containerName)
|
||||||
|
{
|
||||||
|
var logStream = await DockerClient.Containers.GetContainerLogsAsync(containerName, true, new()
|
||||||
|
{
|
||||||
|
Follow = false,
|
||||||
|
ShowStderr = true,
|
||||||
|
ShowStdout = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var combinedOutput = await logStream.ReadOutputToEndAsync(CancellationToken.None);
|
||||||
|
var contentToAdd = combinedOutput.stdout + combinedOutput.stderr;
|
||||||
|
|
||||||
|
await WriteToOutput(contentToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AttachStream(string containerName)
|
||||||
|
{
|
||||||
|
// This stops any previously existing stream reading if
|
||||||
|
// any is currently running
|
||||||
|
if (!Cts.IsCancellationRequested)
|
||||||
|
await Cts.CancelAsync();
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
Cts = new();
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
// This loop is here to reconnect to the container if for some reason the container
|
// This loop is here to reconnect to the container if for some reason the container
|
||||||
@@ -138,8 +173,6 @@ public class DockerConsole : IConsole
|
|||||||
|
|
||||||
Logger.LogDebug("Disconnected from container stream");
|
Logger.LogDebug("Disconnected from container stream");
|
||||||
}, Cts.Token);
|
}, Cts.Token);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task WriteToOutput(string content)
|
public async Task WriteToOutput(string content)
|
||||||
@@ -170,8 +203,7 @@ public class DockerConsole : IConsole
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task WriteToMoonlight(string content)
|
public async Task WriteToMoonlight(string content)
|
||||||
=> await WriteToOutput(
|
=> await WriteToOutput(string.Format(MlPrefix, content));
|
||||||
$"\x1b[0;38;2;255;255;255;48;2;124;28;230m Moonlight \x1b[0m\x1b[38;5;250m\x1b[3m {content}\x1b[0m\n\r");
|
|
||||||
|
|
||||||
public Task ClearOutput()
|
public Task ClearOutput()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -170,13 +170,13 @@ public class DockerInstaller : IInstaller
|
|||||||
|
|
||||||
ContainerId = createdContainer.ID;
|
ContainerId = createdContainer.ID;
|
||||||
|
|
||||||
Logger.LogInformation("Created container");
|
Logger.LogDebug("Created container");
|
||||||
await Console.WriteToMoonlight("Created container");
|
await Console.WriteToMoonlight("Created container");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Start()
|
public async Task Start()
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Starting container");
|
Logger.LogDebug("Starting container");
|
||||||
await Console.WriteToMoonlight("Starting container");
|
await Console.WriteToMoonlight("Starting container");
|
||||||
await DockerClient.Containers.StartContainerAsync(ContainerId, new());
|
await DockerClient.Containers.StartContainerAsync(ContainerId, new());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ public class DockerProvisioner : IProvisioner
|
|||||||
|
|
||||||
await EnsureContainerOffline(possibleContainer);
|
await EnsureContainerOffline(possibleContainer);
|
||||||
|
|
||||||
Logger.LogInformation("Removing orphan container");
|
Logger.LogDebug("Removing orphan container");
|
||||||
await DockerClient.Containers.RemoveContainerAsync(ContainerName, new());
|
await DockerClient.Containers.RemoveContainerAsync(ContainerName, new());
|
||||||
}
|
}
|
||||||
catch (DockerContainerNotFoundException)
|
catch (DockerContainerNotFoundException)
|
||||||
@@ -151,7 +151,7 @@ public class DockerProvisioner : IProvisioner
|
|||||||
|
|
||||||
ContainerId = createdContainer.ID;
|
ContainerId = createdContainer.ID;
|
||||||
|
|
||||||
Logger.LogInformation("Created container");
|
Logger.LogDebug("Created container");
|
||||||
await Console.WriteToMoonlight("Created container");
|
await Console.WriteToMoonlight("Created container");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@ public class DockerProvisioner : IProvisioner
|
|||||||
await EnsureContainerOffline(container);
|
await EnsureContainerOffline(container);
|
||||||
|
|
||||||
// 3. Remove the container
|
// 3. Remove the container
|
||||||
Logger.LogInformation("Removing container");
|
Logger.LogDebug("Removing container");
|
||||||
await Console.WriteToMoonlight("Removing container");
|
await Console.WriteToMoonlight("Removing container");
|
||||||
|
|
||||||
await DockerClient.Containers.RemoveContainerAsync(container.ID, new());
|
await DockerClient.Containers.RemoveContainerAsync(container.ID, new());
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using MoonCore.Observability;
|
||||||
|
using MoonlightServers.Daemon.ServerSys.Abstractions;
|
||||||
|
using MoonlightServers.Daemon.ServerSystem;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.ServerSys.Implementations;
|
||||||
|
|
||||||
|
public class RegexOnlineDetection : IOnlineDetection
|
||||||
|
{
|
||||||
|
private readonly ServerContext Context;
|
||||||
|
private readonly IConsole Console;
|
||||||
|
private readonly ILogger<RegexOnlineDetection> Logger;
|
||||||
|
|
||||||
|
private Regex? Regex;
|
||||||
|
private IAsyncDisposable? ConsoleSubscription;
|
||||||
|
private IAsyncDisposable? StateSubscription;
|
||||||
|
|
||||||
|
public RegexOnlineDetection(ServerContext context, IConsole console, ILogger<RegexOnlineDetection> logger)
|
||||||
|
{
|
||||||
|
Context = context;
|
||||||
|
Console = console;
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Initialize()
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Subscribing to state changes");
|
||||||
|
|
||||||
|
StateSubscription = await Context.Self.OnState.SubscribeAsync(async state =>
|
||||||
|
{
|
||||||
|
if (state == ServerState.Starting) // Subscribe to console when starting
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Detected state change to online. Subscribing to console in order to check for the regex matches");
|
||||||
|
|
||||||
|
if(ConsoleSubscription != null)
|
||||||
|
await ConsoleSubscription.DisposeAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Regex = new(Context.Configuration.OnlineDetection, RegexOptions.Compiled);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "An error occured while building regex expression. Please make sure the regex is valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsoleSubscription = await Console.OnOutput.SubscribeEventAsync(HandleOutput);
|
||||||
|
}
|
||||||
|
else if (ConsoleSubscription != null) // Unsubscribe from console when any other state and not already unsubscribed
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Detected state change to {state}. Unsubscribing from console", state);
|
||||||
|
|
||||||
|
await ConsoleSubscription.DisposeAsync();
|
||||||
|
ConsoleSubscription = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask HandleOutput(string line)
|
||||||
|
{
|
||||||
|
// Handle here just to make sure. Shouldn't be required as we
|
||||||
|
// unsubscribe from the console, as soon as we go online (or any other state).
|
||||||
|
// The regex should also not be null as we initialize it in the handler above but whatevers
|
||||||
|
if(Context.Self.StateMachine.State != ServerState.Starting || Regex == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(Regex.Matches(line).Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await Context.Self.StateMachine.FireAsync(ServerTrigger.OnlineDetected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Sync()
|
||||||
|
{
|
||||||
|
Regex = new(Context.Configuration.OnlineDetection, RegexOptions.Compiled);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if(ConsoleSubscription != null)
|
||||||
|
await ConsoleSubscription.DisposeAsync();
|
||||||
|
|
||||||
|
if(StateSubscription != null)
|
||||||
|
await StateSubscription.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,10 +17,12 @@ public class ServerFactory
|
|||||||
var scope = ServiceProvider.CreateAsyncScope();
|
var scope = ServiceProvider.CreateAsyncScope();
|
||||||
|
|
||||||
var context = scope.ServiceProvider.GetRequiredService<ServerContext>();
|
var context = scope.ServiceProvider.GetRequiredService<ServerContext>();
|
||||||
|
var server = scope.ServiceProvider.GetRequiredService<Server>();
|
||||||
|
|
||||||
context.Configuration = configuration;
|
context.Configuration = configuration;
|
||||||
context.ServiceScope = scope;
|
context.ServiceScope = scope;
|
||||||
|
context.Self = server;
|
||||||
|
|
||||||
return scope.ServiceProvider.GetRequiredService<Server>();
|
return server;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
122
MoonlightServers.Daemon/Services/NewServerService.cs
Normal file
122
MoonlightServers.Daemon/Services/NewServerService.cs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using MoonCore.Helpers;
|
||||||
|
using MoonCore.Models;
|
||||||
|
using MoonlightServers.Daemon.Mappers;
|
||||||
|
using MoonlightServers.Daemon.Models.Cache;
|
||||||
|
using MoonlightServers.Daemon.ServerSys;
|
||||||
|
using MoonlightServers.Daemon.ServerSys.Abstractions;
|
||||||
|
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.Services;
|
||||||
|
|
||||||
|
public class NewServerService : IHostedLifecycleService
|
||||||
|
{
|
||||||
|
private readonly ILogger<ServerService> Logger;
|
||||||
|
private readonly ServerFactory ServerFactory;
|
||||||
|
private readonly RemoteService RemoteService;
|
||||||
|
private readonly ServerConfigurationMapper Mapper;
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<int, Server> Servers = new();
|
||||||
|
|
||||||
|
public NewServerService(
|
||||||
|
ILogger<ServerService> logger,
|
||||||
|
ServerFactory serverFactory,
|
||||||
|
RemoteService remoteService,
|
||||||
|
ServerConfigurationMapper mapper
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
ServerFactory = serverFactory;
|
||||||
|
RemoteService = remoteService;
|
||||||
|
Mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAllFromPanel()
|
||||||
|
{
|
||||||
|
var servers = await PagedData<ServerDataResponse>.All(async (page, pageSize) =>
|
||||||
|
await RemoteService.GetServers(page, pageSize)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var serverDataResponse in servers)
|
||||||
|
{
|
||||||
|
var configuration = Mapper.FromServerDataResponse(serverDataResponse);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Initialize(configuration);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "An error occured while initializing server: {id}", serverDataResponse.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Server> Initialize(ServerConfiguration serverConfiguration)
|
||||||
|
{
|
||||||
|
var server = ServerFactory.CreateServer(serverConfiguration);
|
||||||
|
|
||||||
|
Servers[serverConfiguration.Id] = server;
|
||||||
|
|
||||||
|
await server.Initialize();
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Server? Find(int serverId)
|
||||||
|
=> Servers.GetValueOrDefault(serverId);
|
||||||
|
|
||||||
|
public async Task Sync(int serverId)
|
||||||
|
{
|
||||||
|
var server = Find(serverId);
|
||||||
|
|
||||||
|
if (server == null)
|
||||||
|
throw new ArgumentException("No server with this id found", nameof(serverId));
|
||||||
|
|
||||||
|
var serverData = await RemoteService.GetServer(serverId);
|
||||||
|
var config = Mapper.FromServerDataResponse(serverData);
|
||||||
|
|
||||||
|
server.Context.Configuration = config;
|
||||||
|
|
||||||
|
await server.Sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Delete(int serverId)
|
||||||
|
{
|
||||||
|
var server = Find(serverId);
|
||||||
|
|
||||||
|
if (server == null)
|
||||||
|
throw new ArgumentException("No server with this id found", nameof(serverId));
|
||||||
|
|
||||||
|
await server.DisposeAsync();
|
||||||
|
|
||||||
|
Servers.Remove(serverId, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Lifetime
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
|
public async Task StartedAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await InitializeAllFromPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartingAsync(CancellationToken cancellationToken)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task StoppedAsync(CancellationToken cancellationToken)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
|
public async Task StoppingAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
foreach (var server in Servers.Values)
|
||||||
|
await server.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -331,8 +331,8 @@ public class Startup
|
|||||||
|
|
||||||
private Task RegisterServers()
|
private Task RegisterServers()
|
||||||
{
|
{
|
||||||
WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<ServerService>()
|
WebApplicationBuilder.Services.AddSingleton<NewServerService>();
|
||||||
);
|
WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<NewServerService>());
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddSingleton<DockerEventService>();
|
WebApplicationBuilder.Services.AddSingleton<DockerEventService>();
|
||||||
WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<DockerEventService>());
|
WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<DockerEventService>());
|
||||||
@@ -348,6 +348,7 @@ public class Startup
|
|||||||
WebApplicationBuilder.Services.AddScoped<IInstaller, DockerInstaller>();
|
WebApplicationBuilder.Services.AddScoped<IInstaller, DockerInstaller>();
|
||||||
WebApplicationBuilder.Services.AddScoped<IProvisioner, DockerProvisioner>();
|
WebApplicationBuilder.Services.AddScoped<IProvisioner, DockerProvisioner>();
|
||||||
WebApplicationBuilder.Services.AddScoped<IStatistics, DockerStatistics>();
|
WebApplicationBuilder.Services.AddScoped<IStatistics, DockerStatistics>();
|
||||||
|
WebApplicationBuilder.Services.AddScoped<IOnlineDetection, RegexOnlineDetection>();
|
||||||
WebApplicationBuilder.Services.AddScoped<ServerContext>();
|
WebApplicationBuilder.Services.AddScoped<ServerContext>();
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddScoped<ServerSys.Abstractions.Server>();
|
WebApplicationBuilder.Services.AddScoped<ServerSys.Abstractions.Server>();
|
||||||
|
|||||||
Reference in New Issue
Block a user