diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs index 7441757..c04232f 100644 --- a/MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs +++ b/MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs @@ -12,9 +12,9 @@ namespace MoonlightServers.Daemon.Http.Controllers.Servers; [Route("api/servers")] public class ServerPowerController : Controller { - private readonly ServerService ServerService; + private readonly NewServerService ServerService; - public ServerPowerController(ServerService serverService) + public ServerPowerController(NewServerService serverService) { ServerService = serverService; } @@ -27,7 +27,7 @@ public class ServerPowerController : Controller if (server == null) 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")] @@ -38,7 +38,7 @@ public class ServerPowerController : Controller if (server == null) 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")] @@ -49,7 +49,7 @@ public class ServerPowerController : Controller if (server == null) 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")] @@ -60,6 +60,6 @@ public class ServerPowerController : Controller if (server == null) throw new HttpApiException("No server with this id found", 404); - await server.Trigger(ServerTrigger.Kill); + await server.StateMachine.FireAsync(ServerTrigger.Kill); } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs index 1930530..519c793 100644 --- a/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs +++ b/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs @@ -14,9 +14,9 @@ namespace MoonlightServers.Daemon.Http.Controllers.Servers; [Route("api/servers/{serverId:int}")] public class ServersController : Controller { - private readonly ServerService ServerService; + private readonly NewServerService ServerService; - public ServersController(ServerService serverService) + public ServersController(NewServerService serverService) { ServerService = serverService; } @@ -57,8 +57,7 @@ public class ServersController : Controller if (server == null) throw new HttpApiException("No server with this id found", 404); - var consoleSubSystem = server.GetRequiredSubSystem(); - var messages = await consoleSubSystem.RetrieveCache(); + var messages = server.Console.GetOutput(); return new ServerLogsResponse() { @@ -73,7 +72,7 @@ public class ServersController : Controller if (server == null) throw new HttpApiException("No server with this id found", 404); - +/* var statsSubSystem = server.GetRequiredSubSystem(); return Task.FromResult(new() @@ -84,6 +83,16 @@ public class ServersController : Controller NetworkWrite = statsSubSystem.CurrentStats.NetworkWrite, IoRead = statsSubSystem.CurrentStats.IoRead, IoWrite = statsSubSystem.CurrentStats.IoWrite + });*/ + + return Task.FromResult(new() + { + CpuUsage = 0, + MemoryUsage = 0, + NetworkRead = 0, + NetworkWrite = 0, + IoRead = 0, + IoWrite = 0 }); } @@ -94,9 +103,7 @@ public class ServersController : Controller if (server == null) throw new HttpApiException("No server with this id found", 404); - - var consoleSubSystem = server.GetRequiredSubSystem(); - - await consoleSubSystem.WriteInput(request.Command); + + await server.Console.WriteToInput(request.Command); } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Mappers/ServerConfigurationMapper.cs b/MoonlightServers.Daemon/Mappers/ServerConfigurationMapper.cs index a980c29..5256d13 100644 --- a/MoonlightServers.Daemon/Mappers/ServerConfigurationMapper.cs +++ b/MoonlightServers.Daemon/Mappers/ServerConfigurationMapper.cs @@ -15,6 +15,29 @@ public class ServerConfigurationMapper { 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( ServerConfiguration serverConfiguration, diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IConsole.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IConsole.cs index 7545189..986bae5 100644 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/IConsole.cs +++ b/MoonlightServers.Daemon/ServerSys/Abstractions/IConsole.cs @@ -8,6 +8,15 @@ public interface IConsole : IServerComponent public Task AttachToRuntime(); public Task AttachToInstallation(); + /// + /// Detaches any attached consoles. Usually either runtime or install is attached + /// + /// + public Task Detach(); + + public Task CollectFromRuntime(); + public Task CollectFromInstallation(); + public Task WriteToOutput(string content); public Task WriteToInput(string content); public Task WriteToMoonlight(string content); diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IOnlineDetection.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IOnlineDetection.cs new file mode 100644 index 0000000..b0a6905 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSys/Abstractions/IOnlineDetection.cs @@ -0,0 +1,6 @@ +namespace MoonlightServers.Daemon.ServerSys.Abstractions; + +public interface IOnlineDetection : IServerComponent +{ + +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/Server.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/Server.cs index 34d9d54..257195a 100644 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/Server.cs +++ b/MoonlightServers.Daemon/ServerSys/Abstractions/Server.cs @@ -1,7 +1,10 @@ +using Microsoft.AspNetCore.SignalR; using MoonCore.Observability; using MoonlightServers.Daemon.Extensions; -using MoonlightServers.Daemon.Helpers; +using MoonlightServers.Daemon.Http.Hubs; +using MoonlightServers.Daemon.Mappers; using MoonlightServers.Daemon.ServerSystem; +using MoonlightServers.Daemon.Services; using Stateless; namespace MoonlightServers.Daemon.ServerSys.Abstractions; @@ -14,15 +17,20 @@ public class Server : IAsyncDisposable public IProvisioner Provisioner { get; } public IRestorer Restorer { get; } public IStatistics Statistics { get; } + public IOnlineDetection OnlineDetection { get; } public StateMachine StateMachine { get; private set; } public ServerContext Context { get; } public IAsyncObservable OnState => OnStateSubject; private readonly EventSubject OnStateSubject = new(); private readonly ILogger Logger; + private readonly RemoteService RemoteService; + private readonly ServerConfigurationMapper Mapper; + private readonly IHubContext HubContext; private IAsyncDisposable? ProvisionExitSubscription; private IAsyncDisposable? InstallerExitSubscription; + private IAsyncDisposable? ConsoleSubscription; public Server( ILogger logger, @@ -32,8 +40,11 @@ public class Server : IAsyncDisposable IProvisioner provisioner, IRestorer restorer, IStatistics statistics, - ServerContext context - ) + IOnlineDetection onlineDetection, + ServerContext context, + RemoteService remoteService, + ServerConfigurationMapper mapper, + IHubContext hubContext) { Logger = logger; Console = console; @@ -43,13 +54,17 @@ public class Server : IAsyncDisposable Restorer = restorer; Statistics = statistics; Context = context; + RemoteService = remoteService; + Mapper = mapper; + HubContext = hubContext; + OnlineDetection = onlineDetection; } public async Task Initialize() { 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) { @@ -79,6 +94,8 @@ public class Server : IAsyncDisposable CreateStateMachine(restoredState); + await SetupHubEvents(); + // Setup event handling ProvisionExitSubscription = await Provisioner.OnExited.SubscribeEventAsync(async _ => 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) { StateMachine = new StateMachine(initialState, FiringMode.Queued); @@ -140,9 +165,31 @@ public class Server : IAsyncDisposable StateMachine.Configure(ServerState.Stopping) .OnEntryFromAsync(ServerTrigger.Stop, HandleStop) + .OnEntryFromAsync(ServerTrigger.Kill, HandleKill) .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 private async Task HandleStart() @@ -158,7 +205,11 @@ public class Server : IAsyncDisposable // 6. Start the container // 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 if (!FileSystem.Exists) @@ -187,9 +238,6 @@ public class Server : IAsyncDisposable catch (Exception e) { 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(); } + private async Task HandleKill() + { + await Provisioner.Kill(); + } + private async Task HandleRuntimeExit() { + Logger.LogDebug("Detected runtime exit"); + + Logger.LogDebug("Detaching from console"); + await Console.Detach(); + Logger.LogDebug("Deprovisioning"); await Console.WriteToMoonlight("Deprovisioning"); - await Provisioner.Deprovision(); } private async Task HandleInstall() { - try - { - Logger.LogDebug("Installing"); + // Plan: + // 1. Fetch the latest installation data + // 2. Setup installation environment + // 3. Attach console to installation + // 4. Start the installation - Logger.LogDebug("Setting up"); - await Console.WriteToMoonlight("Setting up installation"); + Logger.LogDebug("Installing"); - // TODO: Extract to service + Logger.LogDebug("Setting up"); + await Console.WriteToMoonlight("Setting up installation"); - Context.InstallConfiguration = new() - { - Shell = "/bin/ash", - 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" - }; + // 1. Fetch the latest installation data + Logger.LogDebug("Fetching installation data"); + await Console.WriteToMoonlight("Fetching installation data"); - await Installer.Setup(); + Context.InstallConfiguration = await RemoteService.GetServerInstallation(Context.Configuration.Id); - await Console.AttachToInstallation(); + // 2. Setup installation environment + await Installer.Setup(); - await Installer.Start(); - } - catch (Exception e) - { - Logger.LogError(e, "An error occured while starting installation"); - await Console.WriteToMoonlight("An error occured while starting installation"); + // 3. Attach console to installation + await Console.AttachToInstallation(); - await StateMachine.FireAsync(ServerTrigger.FailSafe); - } + // 4. Start the installation + await Installer.Start(); } private async Task HandleInstallExit() { - Logger.LogDebug("Installation done"); + Logger.LogDebug("Detected install exit"); + + Logger.LogDebug("Detaching from console"); + await Console.Detach(); + + Logger.LogDebug("Cleaning up"); await Console.WriteToMoonlight("Cleaning up"); - await Installer.Cleanup(); await Console.WriteToMoonlight("Installation completed"); @@ -260,6 +316,9 @@ public class Server : IAsyncDisposable if (InstallerExitSubscription != null) await InstallerExitSubscription.DisposeAsync(); + if (ConsoleSubscription != null) + await ConsoleSubscription.DisposeAsync(); + await Console.DisposeAsync(); await FileSystem.DisposeAsync(); await Installer.DisposeAsync(); diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/ServerMeta.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/ServerContext.cs similarity index 91% rename from MoonlightServers.Daemon/ServerSys/Abstractions/ServerMeta.cs rename to MoonlightServers.Daemon/ServerSys/Abstractions/ServerContext.cs index 9d72949..93899d4 100644 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/ServerMeta.cs +++ b/MoonlightServers.Daemon/ServerSys/Abstractions/ServerContext.cs @@ -8,4 +8,5 @@ public record ServerContext public ServerConfiguration Configuration { get; set; } public AsyncServiceScope ServiceScope { get; set; } public ServerInstallDataResponse InstallConfiguration { get; set; } + public Server Self { get; set; } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Implementations/DefaultRestorer.cs b/MoonlightServers.Daemon/ServerSys/Implementations/DefaultRestorer.cs index a0c890a..b0e0bb4 100644 --- a/MoonlightServers.Daemon/ServerSys/Implementations/DefaultRestorer.cs +++ b/MoonlightServers.Daemon/ServerSys/Implementations/DefaultRestorer.cs @@ -38,10 +38,9 @@ public class DefaultRestorer : IRestorer { Logger.LogDebug("Detected runtime to restore"); + await Console.CollectFromRuntime(); await Console.AttachToRuntime(); await Statistics.SubscribeToRuntime(); - - // TODO: Read out existing container log in order to search if the server is online return ServerState.Online; } @@ -50,6 +49,7 @@ public class DefaultRestorer : IRestorer { Logger.LogDebug("Detected installation to restore"); + await Console.CollectFromInstallation(); await Console.AttachToInstallation(); await Statistics.SubscribeToInstallation(); diff --git a/MoonlightServers.Daemon/ServerSys/Implementations/DockerConsole.cs b/MoonlightServers.Daemon/ServerSys/Implementations/DockerConsole.cs index 8d8663f..f324c8d 100644 --- a/MoonlightServers.Daemon/ServerSys/Implementations/DockerConsole.cs +++ b/MoonlightServers.Daemon/ServerSys/Implementations/DockerConsole.cs @@ -1,11 +1,8 @@ -using System.Reactive.Linq; -using System.Reactive.Subjects; using System.Text; using Docker.DotNet; using Docker.DotNet.Models; using MoonCore.Helpers; using MoonCore.Observability; -using MoonlightServers.Daemon.Helpers; using MoonlightServers.Daemon.ServerSys.Abstractions; namespace MoonlightServers.Daemon.ServerSys.Implementations; @@ -26,11 +23,12 @@ public class DockerConsole : IConsole private MultiplexedStream? CurrentStream; 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( ServerContext context, DockerClient dockerClient, - ILogger logger - ) + ILogger logger) { Context = context; DockerClient = dockerClient; @@ -55,8 +53,45 @@ public class DockerConsole : IConsole 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 () => { // 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"); }, Cts.Token); - - return Task.CompletedTask; } public async Task WriteToOutput(string content) @@ -165,13 +198,12 @@ public class DockerConsole : IConsole contentBuffer.Length, Cts.Token ); - + await OnInputSubject.OnNextAsync(content); } public async Task WriteToMoonlight(string content) - => await WriteToOutput( - $"\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"); + => await WriteToOutput(string.Format(MlPrefix, content)); public Task ClearOutput() { diff --git a/MoonlightServers.Daemon/ServerSys/Implementations/DockerInstaller.cs b/MoonlightServers.Daemon/ServerSys/Implementations/DockerInstaller.cs index 1a29bb9..2539d59 100644 --- a/MoonlightServers.Daemon/ServerSys/Implementations/DockerInstaller.cs +++ b/MoonlightServers.Daemon/ServerSys/Implementations/DockerInstaller.cs @@ -170,13 +170,13 @@ public class DockerInstaller : IInstaller ContainerId = createdContainer.ID; - Logger.LogInformation("Created container"); + Logger.LogDebug("Created container"); await Console.WriteToMoonlight("Created container"); } public async Task Start() { - Logger.LogInformation("Starting container"); + Logger.LogDebug("Starting container"); await Console.WriteToMoonlight("Starting container"); await DockerClient.Containers.StartContainerAsync(ContainerId, new()); } diff --git a/MoonlightServers.Daemon/ServerSys/Implementations/DockerProvisioner.cs b/MoonlightServers.Daemon/ServerSys/Implementations/DockerProvisioner.cs index 6da65ff..6bb2f13 100644 --- a/MoonlightServers.Daemon/ServerSys/Implementations/DockerProvisioner.cs +++ b/MoonlightServers.Daemon/ServerSys/Implementations/DockerProvisioner.cs @@ -115,7 +115,7 @@ public class DockerProvisioner : IProvisioner await EnsureContainerOffline(possibleContainer); - Logger.LogInformation("Removing orphan container"); + Logger.LogDebug("Removing orphan container"); await DockerClient.Containers.RemoveContainerAsync(ContainerName, new()); } catch (DockerContainerNotFoundException) @@ -151,7 +151,7 @@ public class DockerProvisioner : IProvisioner ContainerId = createdContainer.ID; - Logger.LogInformation("Created container"); + Logger.LogDebug("Created container"); await Console.WriteToMoonlight("Created container"); } @@ -214,7 +214,7 @@ public class DockerProvisioner : IProvisioner await EnsureContainerOffline(container); // 3. Remove the container - Logger.LogInformation("Removing container"); + Logger.LogDebug("Removing container"); await Console.WriteToMoonlight("Removing container"); await DockerClient.Containers.RemoveContainerAsync(container.ID, new()); diff --git a/MoonlightServers.Daemon/ServerSys/Implementations/RegexOnlineDetection.cs b/MoonlightServers.Daemon/ServerSys/Implementations/RegexOnlineDetection.cs new file mode 100644 index 0000000..b220341 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSys/Implementations/RegexOnlineDetection.cs @@ -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 Logger; + + private Regex? Regex; + private IAsyncDisposable? ConsoleSubscription; + private IAsyncDisposable? StateSubscription; + + public RegexOnlineDetection(ServerContext context, IConsole console, ILogger 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(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/ServerFactory.cs b/MoonlightServers.Daemon/ServerSys/ServerFactory.cs index c3c6c1a..6313ab2 100644 --- a/MoonlightServers.Daemon/ServerSys/ServerFactory.cs +++ b/MoonlightServers.Daemon/ServerSys/ServerFactory.cs @@ -17,10 +17,12 @@ public class ServerFactory var scope = ServiceProvider.CreateAsyncScope(); var context = scope.ServiceProvider.GetRequiredService(); + var server = scope.ServiceProvider.GetRequiredService(); context.Configuration = configuration; context.ServiceScope = scope; + context.Self = server; - return scope.ServiceProvider.GetRequiredService(); + return server; } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Services/NewServerService.cs b/MoonlightServers.Daemon/Services/NewServerService.cs new file mode 100644 index 0000000..9001d59 --- /dev/null +++ b/MoonlightServers.Daemon/Services/NewServerService.cs @@ -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 Logger; + private readonly ServerFactory ServerFactory; + private readonly RemoteService RemoteService; + private readonly ServerConfigurationMapper Mapper; + + private readonly ConcurrentDictionary Servers = new(); + + public NewServerService( + ILogger logger, + ServerFactory serverFactory, + RemoteService remoteService, + ServerConfigurationMapper mapper + ) + { + Logger = logger; + ServerFactory = serverFactory; + RemoteService = remoteService; + Mapper = mapper; + } + + public async Task InitializeAllFromPanel() + { + var servers = await PagedData.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 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 +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Startup.cs b/MoonlightServers.Daemon/Startup.cs index 3fdb2c1..9fa8f5d 100644 --- a/MoonlightServers.Daemon/Startup.cs +++ b/MoonlightServers.Daemon/Startup.cs @@ -331,8 +331,8 @@ public class Startup private Task RegisterServers() { - WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService() - ); + WebApplicationBuilder.Services.AddSingleton(); + WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService()); WebApplicationBuilder.Services.AddSingleton(); WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService()); @@ -348,6 +348,7 @@ public class Startup WebApplicationBuilder.Services.AddScoped(); WebApplicationBuilder.Services.AddScoped(); WebApplicationBuilder.Services.AddScoped(); + WebApplicationBuilder.Services.AddScoped(); WebApplicationBuilder.Services.AddScoped(); WebApplicationBuilder.Services.AddScoped();