From 282096595d851d1f5b5b4eacaa09b2d8bac051e6 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Sat, 6 Sep 2025 21:44:22 +0200 Subject: [PATCH] Improved comments. Started implementing docker components and other base components. Updated dependencies --- .../MoonlightServers.Daemon.csproj | 9 +- .../ServerSystem/Docker/DockerConsole.cs | 203 ++++++++++++++++++ .../ServerSystem/Docker/DockerConstants.cs | 7 + .../ServerSystem/Docker/DockerInstallation.cs | 185 ++++++++++++++++ .../FileSystems/RawInstallationFs.cs | 58 +++++ .../ServerSystem/FileSystems/RawRuntimeFs.cs | 58 +++++ .../ServerSystem/Handlers/StartupHandler.cs | 3 + .../Implementations/RegexOnlineDetector.cs | 61 ++++++ .../Implementations/ServerReporter.cs | 45 ++++ .../ServerSystem/Interfaces/IConsole.cs | 16 +- .../ServerSystem/Interfaces/IInstallation.cs | 13 +- .../Interfaces/IOnlineDetector.cs | 2 +- .../ServerSystem/Interfaces/IReporter.cs | 4 +- .../ServerSystem/Interfaces/IRuntime.cs | 4 +- .../ServerSystem/ServerFactory.cs | 17 +- .../Services/DockerEventService.cs | 39 ++-- 16 files changed, 672 insertions(+), 52 deletions(-) create mode 100644 MoonlightServers.Daemon/ServerSystem/Docker/DockerConsole.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Docker/DockerConstants.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Docker/DockerInstallation.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/FileSystems/RawInstallationFs.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/FileSystems/RawRuntimeFs.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Implementations/RegexOnlineDetector.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Implementations/ServerReporter.cs diff --git a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj index 42dc3d1..009d473 100644 --- a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj +++ b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj @@ -9,19 +9,16 @@ - - + + - - - + - diff --git a/MoonlightServers.Daemon/ServerSystem/Docker/DockerConsole.cs b/MoonlightServers.Daemon/ServerSystem/Docker/DockerConsole.cs new file mode 100644 index 0000000..7d6b51d --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Docker/DockerConsole.cs @@ -0,0 +1,203 @@ +using System.Text; +using Docker.DotNet; +using MoonCore.Events; +using MoonCore.Helpers; +using MoonlightServers.Daemon.ServerSystem.Interfaces; +using MoonlightServers.Daemon.ServerSystem.Models; + +namespace MoonlightServers.Daemon.ServerSystem.Docker; + +public class DockerConsole : IConsole +{ + private readonly EventSource StdOutEventSource = new(); + private readonly ConcurrentList StdOutCache = new(); + private readonly DockerClient DockerClient; + private readonly ServerContext Context; + private readonly ILogger Logger; + + private MultiplexedStream? BaseStream; + private CancellationTokenSource Cts = new(); + + public DockerConsole(DockerClient dockerClient, ServerContext context) + { + DockerClient = dockerClient; + Context = context; + } + + public Task InitializeAsync() + => Task.CompletedTask; + + public async Task WriteStdInAsync(string content) + { + if (BaseStream == null) + { + Logger.LogWarning("Unable to write to stdin as no stream is connected"); + return; + } + + var contextBuffer = Encoding.UTF8.GetBytes(content); + + await BaseStream.WriteAsync(contextBuffer, 0, contextBuffer.Length, Cts.Token); + } + + public async Task WriteStdOutAsync(string content) + { + // Add output cache + if (StdOutCache.Count > 250) // TODO: Config + StdOutCache.RemoveRange(0, 100); + + StdOutCache.Add(content); + + // Fire event + await StdOutEventSource.InvokeAsync(content); + } + + public async Task AttachRuntimeAsync() + { + var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id); + + await AttachToContainer(containerName); + } + + public async Task AttachInstallationAsync() + { + var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id); + + await AttachToContainer(containerName); + } + + private async Task AttachToContainer(string containerName) + { + // Cancels previous active read task if it exists + if (!Cts.IsCancellationRequested) + await Cts.CancelAsync(); + + // Reset cancellation token + Cts = new(); + + // Start reading task + Task.Run(async () => + { + // This loop is here to reconnect to the stream when connection is lost. + // This can occur when docker restarts for example + + while (!Cts.IsCancellationRequested) + { + try + { + using var stream = await DockerClient.Containers.AttachContainerAsync( + containerName, + true, + new() + { + Stderr = true, + Stdin = true, + Stdout = true, + Stream = true + }, + Cts.Token + ); + + BaseStream = stream; + + var buffer = new byte[1024]; + + try + { + // Read while server tasks are not canceled + while (!Cts.Token.IsCancellationRequested) + { + var readResult = await BaseStream.ReadOutputAsync( + buffer, + 0, + buffer.Length, + Cts.Token + ); + + if (readResult.EOF) + break; + + var decodedText = Encoding.UTF8.GetString(buffer, 0, readResult.Count); + + await WriteStdOutAsync(decodedText); + } + } + catch (TaskCanceledException) + { + // Ignored + } + catch (OperationCanceledException) + { + // Ignored + } + catch (Exception e) + { + Logger.LogWarning(e, "An unhandled error occured while reading from container stream"); + } + } + catch (TaskCanceledException) + { + // ignored + } + catch (Exception e) + { + Logger.LogError(e, "An error occured while attaching to container"); + } + } + + Logger.LogDebug("Disconnected from container stream"); + }); + } + + public async Task FetchRuntimeAsync() + { + var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id); + + await FetchFromContainer(containerName); + } + + public async Task FetchInstallationAsync() + { + var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id); + + await FetchFromContainer(containerName); + } + + private async Task FetchFromContainer(string containerName) + { + var logStream = await DockerClient.Containers.GetContainerLogsAsync(containerName, true, new() + { + Follow = false, + ShowStderr = true, + ShowStdout = true + }); + + var combinedOutput = await logStream.ReadOutputToEndAsync(Cts.Token); + var contentToAdd = combinedOutput.stdout + combinedOutput.stderr; + + await WriteStdOutAsync(contentToAdd); + } + + public Task ClearCacheAsync() + { + StdOutCache.Clear(); + return Task.CompletedTask; + } + + public Task> GetCacheAsync() + { + return Task.FromResult>(StdOutCache); + } + + public async Task SubscribeStdOutAsync(Func callback) + => await StdOutEventSource.SubscribeAsync(callback); + + public async ValueTask DisposeAsync() + { + if (!Cts.IsCancellationRequested) + await Cts.CancelAsync(); + + if (BaseStream != null) + BaseStream.Dispose(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Docker/DockerConstants.cs b/MoonlightServers.Daemon/ServerSystem/Docker/DockerConstants.cs new file mode 100644 index 0000000..e7efc5b --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Docker/DockerConstants.cs @@ -0,0 +1,7 @@ +namespace MoonlightServers.Daemon.ServerSystem.Docker; + +public static class DockerConstants +{ + public const string RuntimeNameTemplate = "monnlight-runtime-{0}"; + public const string InstallationNameTemplate = "monnlight-installation-{0}"; +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Docker/DockerInstallation.cs b/MoonlightServers.Daemon/ServerSystem/Docker/DockerInstallation.cs new file mode 100644 index 0000000..7c40d35 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Docker/DockerInstallation.cs @@ -0,0 +1,185 @@ +using Docker.DotNet; +using Docker.DotNet.Models; +using MoonCore.Events; +using MoonlightServers.Daemon.Mappers; +using MoonlightServers.Daemon.ServerSystem.Interfaces; +using MoonlightServers.Daemon.ServerSystem.Models; +using MoonlightServers.Daemon.Services; +using MoonlightServers.DaemonShared.PanelSide.Http.Responses; + +namespace MoonlightServers.Daemon.ServerSystem.Docker; + +public class DockerInstallation : IInstallation +{ + private readonly DockerEventService DockerEventService; + private readonly ServerConfigurationMapper Mapper; + private readonly DockerImageService ImageService; + private readonly ServerContext ServerContext; + private readonly DockerClient DockerClient; + private readonly IReporter Reporter; + + private readonly EventSource ExitEventSource = new(); + + private IAsyncDisposable ContainerEventSubscription; + private string ContainerId; + + public DockerInstallation( + DockerClient dockerClient, + ServerContext serverContext, + ServerConfigurationMapper mapper, + DockerImageService imageService, + IReporter reporter, + DockerEventService dockerEventService + ) + { + DockerClient = dockerClient; + ServerContext = serverContext; + Mapper = mapper; + ImageService = imageService; + Reporter = reporter; + DockerEventService = dockerEventService; + } + + public async Task InitializeAsync() + { + ContainerEventSubscription = await DockerEventService.SubscribeContainerAsync(OnContainerEvent); + } + + private async ValueTask OnContainerEvent(Message message) + { + // Only handle events for our own container + if (message.ID != ContainerId) + return; + + // Only handle die events + if (message.Action != "die") + return; + + int exitCode; + + if (message.Actor.Attributes.TryGetValue("exitCode", out var exitCodeStr)) + { + if (!int.TryParse(exitCodeStr, out exitCode)) + exitCode = 0; + } + else + exitCode = 0; + + + await ExitEventSource.InvokeAsync(exitCode); + } + + public async Task CheckExistsAsync() + { + try + { + var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id); + + await DockerClient.Containers.InspectContainerAsync( + containerName + ); + + return true; + } + catch (DockerContainerNotFoundException) + { + return false; + } + } + + public async Task CreateAsync( + string runtimePath, + string hostPath, + ServerInstallDataResponse data + ) + { + var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id); + + var parameters = Mapper.ToInstallParameters( + ServerContext.Configuration, + data, + runtimePath, + hostPath, + containerName + ); + + // Docker image + await Reporter.StatusAsync("Downloading docker image"); + + await ImageService.Download(data.DockerImage, async status => { await Reporter.StatusAsync(status); }); + + await Reporter.StatusAsync("Downloaded docker image"); + + // Write install script to install fs + + await File.WriteAllTextAsync( + Path.Combine(hostPath, "install.sh"), + data.Script + ); + + // + + await DockerClient.Containers.CreateContainerAsync(parameters); + + await Reporter.StatusAsync("Created container"); + } + + public async Task StartAsync() + { + var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id); + + await DockerClient.Containers.StartContainerAsync(containerName, new()); + } + + public async Task KillAsync() + { + var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id); + + await DockerClient.Containers.KillContainerAsync(containerName, new()); + } + + public async Task DestroyAsync() + { + var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id); + + try + { + var container = await DockerClient.Containers.InspectContainerAsync(containerName); + + if (container.State.Running) + await DockerClient.Containers.KillContainerAsync(containerName, new()); + + await DockerClient.Containers.RemoveContainerAsync(containerName, new() + { + Force = true + }); + } + catch (DockerContainerNotFoundException) + { + // Ignored + } + } + + public async Task SubscribeExited(Func callback) + => await ExitEventSource.SubscribeAsync(callback); + + public async Task RestoreAsync() + { + try + { + var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id); + + var container = await DockerClient.Containers.InspectContainerAsync(containerName); + ContainerId = container.ID; + } + catch (DockerContainerNotFoundException) + { + // Ignore + } + } + + public async ValueTask DisposeAsync() + { + await ContainerEventSubscription.DisposeAsync(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/FileSystems/RawInstallationFs.cs b/MoonlightServers.Daemon/ServerSystem/FileSystems/RawInstallationFs.cs new file mode 100644 index 0000000..f5ea780 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/FileSystems/RawInstallationFs.cs @@ -0,0 +1,58 @@ +using MoonlightServers.Daemon.ServerSystem.Interfaces; +using MoonlightServers.Daemon.ServerSystem.Models; + +namespace MoonlightServers.Daemon.ServerSystem.FileSystems; + +public class RawInstallationFs : IFileSystem +{ + private readonly string BaseDirectory; + + public RawInstallationFs(ServerContext context) + { + BaseDirectory = Path.Combine( + Directory.GetCurrentDirectory(), + "storage", + "volumes", + context.Configuration.Id.ToString() + ); + } + + public Task InitializeAsync() + => Task.CompletedTask; + + public Task GetPathAsync() + => Task.FromResult(BaseDirectory); + + public Task CheckExistsAsync() + { + var exists = Directory.Exists(BaseDirectory); + return Task.FromResult(exists); + } + + public Task CheckMountedAsync() + => Task.FromResult(true); + + public Task CreateAsync() + { + Directory.CreateDirectory(BaseDirectory); + return Task.CompletedTask; + } + + public Task PerformChecksAsync() + => Task.CompletedTask; + + public Task MountAsync() + => Task.CompletedTask; + + public Task UnmountAsync() + => Task.CompletedTask; + + public Task DestroyAsync() + { + Directory.Delete(BaseDirectory, true); + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + => ValueTask.CompletedTask; +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/FileSystems/RawRuntimeFs.cs b/MoonlightServers.Daemon/ServerSystem/FileSystems/RawRuntimeFs.cs new file mode 100644 index 0000000..6f98ac7 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/FileSystems/RawRuntimeFs.cs @@ -0,0 +1,58 @@ +using MoonlightServers.Daemon.ServerSystem.Interfaces; +using MoonlightServers.Daemon.ServerSystem.Models; + +namespace MoonlightServers.Daemon.ServerSystem.FileSystems; + +public class RawRuntimeFs : IFileSystem +{ + private readonly string BaseDirectory; + + public RawRuntimeFs(ServerContext context) + { + BaseDirectory = Path.Combine( + Directory.GetCurrentDirectory(), + "storage", + "volumes", + context.Configuration.Id.ToString() + ); + } + + public Task InitializeAsync() + => Task.CompletedTask; + + public Task GetPathAsync() + => Task.FromResult(BaseDirectory); + + public Task CheckExistsAsync() + { + var exists = Directory.Exists(BaseDirectory); + return Task.FromResult(exists); + } + + public Task CheckMountedAsync() + => Task.FromResult(true); + + public Task CreateAsync() + { + Directory.CreateDirectory(BaseDirectory); + return Task.CompletedTask; + } + + public Task PerformChecksAsync() + => Task.CompletedTask; + + public Task MountAsync() + => Task.CompletedTask; + + public Task UnmountAsync() + => Task.CompletedTask; + + public Task DestroyAsync() + { + Directory.Delete(BaseDirectory, true); + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + => ValueTask.CompletedTask; +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Handlers/StartupHandler.cs b/MoonlightServers.Daemon/ServerSystem/Handlers/StartupHandler.cs index 3fbfe62..b5e0846 100644 --- a/MoonlightServers.Daemon/ServerSystem/Handlers/StartupHandler.cs +++ b/MoonlightServers.Daemon/ServerSystem/Handlers/StartupHandler.cs @@ -51,6 +51,9 @@ public class StartupHandler : IServerStateHandler // 5. Create runtime var hostPath = await Server.RuntimeFileSystem.GetPathAsync(); + if (await Server.Runtime.CheckExistsAsync()) + await Server.Runtime.DestroyAsync(); + await Server.Runtime.CreateAsync(hostPath); if (ExitSubscription == null) diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/RegexOnlineDetector.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/RegexOnlineDetector.cs new file mode 100644 index 0000000..0f9a238 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/RegexOnlineDetector.cs @@ -0,0 +1,61 @@ +using System.Text.RegularExpressions; +using MoonlightServers.Daemon.ServerSystem.Interfaces; +using MoonlightServers.Daemon.ServerSystem.Models; + +namespace MoonlightServers.Daemon.ServerSystem.Implementations; + +public class RegexOnlineDetector : IOnlineDetector +{ + private readonly ServerContext Context; + private readonly ILogger Logger; + + private Regex? Expression; + + public RegexOnlineDetector(ServerContext context, ILogger logger) + { + Context = context; + Logger = logger; + } + + public Task InitializeAsync() + => Task.CompletedTask; + + public Task CreateAsync() + { + if(string.IsNullOrEmpty(Context.Configuration.OnlineDetection)) + return Task.CompletedTask; + + try + { + Expression = new Regex(Context.Configuration.OnlineDetection, RegexOptions.Compiled); + } + catch (Exception e) + { + Logger.LogError(e, "An error occured while creating regex. Check the regex expression"); + } + + return Task.CompletedTask; + } + + public Task HandleOutputAsync(string line) + { + if (Expression == null) + return Task.FromResult(false); + + var result = Expression.Matches(line).Count > 0; + + return Task.FromResult(result); + } + + public Task DestroyAsync() + { + Expression = null; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + { + Expression = null; + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/ServerReporter.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/ServerReporter.cs new file mode 100644 index 0000000..7e3d63b --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/ServerReporter.cs @@ -0,0 +1,45 @@ +using MoonlightServers.Daemon.ServerSystem.Interfaces; + +namespace MoonlightServers.Daemon.ServerSystem.Implementations; + +public class ServerReporter : IReporter +{ + private readonly IConsole Console; + private readonly ILogger Logger; + + private const string StatusTemplate = + "\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"; + + private const string ErrorTemplate = + "\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[1;38;2;255;0;0m{0}\x1b[0m\n\r"; + + public ServerReporter(IConsole console, ILogger logger) + { + Console = console; + Logger = logger; + } + + public Task InitializeAsync() + => Task.CompletedTask; + + public async Task StatusAsync(string message) + { + Logger.LogInformation("Status: {message}", message); + + await Console.WriteStdOutAsync( + string.Format(StatusTemplate, message) + ); + } + + public async Task ErrorAsync(string message) + { + Logger.LogError("Error: {message}", message); + + await Console.WriteStdOutAsync( + string.Format(ErrorTemplate, message) + ); + } + + public ValueTask DisposeAsync() + => ValueTask.CompletedTask; +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IConsole.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IConsole.cs index fe891e3..ba798ce 100644 --- a/MoonlightServers.Daemon/ServerSystem/Interfaces/IConsole.cs +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IConsole.cs @@ -7,7 +7,7 @@ public interface IConsole : IServerComponent /// would write into the containers standard input. /// This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required /// - /// The content to write + /// Content to write /// public Task WriteStdInAsync(string content); /// @@ -15,17 +15,9 @@ public interface IConsole : IServerComponent /// would write into the containers standard output. /// This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required /// - /// The content to write + /// Content to write /// public Task WriteStdOutAsync(string content); - - /// - /// Writes a system message to the standard output with the moonlight console prefix - /// This method *does* add the newline separator at the end - /// - /// The content to write into the standard output - /// - public Task WriteMoonlightAsync(string content); /// /// Attaches the console to the runtime environment @@ -60,7 +52,7 @@ public interface IConsole : IServerComponent /// /// Gets the content from the standard output cache /// - /// The content from the cache + /// Content from the cache public Task> GetCacheAsync(); /// @@ -68,5 +60,5 @@ public interface IConsole : IServerComponent /// /// Callback which will be invoked whenever a new line is received /// Subscription disposable to unsubscribe from the event - public Task SubscribeStdOutAsync(Func callback); + public Task SubscribeStdOutAsync(Func callback); } \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IInstallation.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IInstallation.cs index 1ac540d..27236dc 100644 --- a/MoonlightServers.Daemon/ServerSystem/Interfaces/IInstallation.cs +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IInstallation.cs @@ -1,3 +1,5 @@ +using MoonlightServers.DaemonShared.PanelSide.Http.Responses; + namespace MoonlightServers.Daemon.ServerSystem.Interfaces; public interface IInstallation : IServerComponent @@ -11,10 +13,11 @@ public interface IInstallation : IServerComponent /// /// Creates the installation environment /// - /// The host path of the runtime storage location - /// The host path of the installation file system + /// Host path of the runtime storage location + /// Host path of the installation file system + /// Installation data for the server /// - public Task CreateAsync(string runtimePath, string hostPath); + public Task CreateAsync(string runtimePath, string hostPath, ServerInstallDataResponse data); /// /// Starts the installation @@ -37,9 +40,9 @@ public interface IInstallation : IServerComponent /// /// Subscribes to the event when the installation exists /// - /// The callback to invoke whenever the installation exists + /// Callback to invoke whenever the installation exists /// Subscription disposable to unsubscribe from the event - public Task SubscribeExited(Func callback); + public Task SubscribeExited(Func callback); /// /// Connects an existing installation to this abstraction in order to restore it. diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IOnlineDetector.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IOnlineDetector.cs index 21cf894..dc7314e 100644 --- a/MoonlightServers.Daemon/ServerSystem/Interfaces/IOnlineDetector.cs +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IOnlineDetector.cs @@ -11,7 +11,7 @@ public interface IOnlineDetector : IServerComponent /// /// Handles the detection of the online state based on the received output /// - /// The excerpt of the output + /// Excerpt of the output /// True if the detection showed that the server is online. False if the detection didnt find anything public Task HandleOutputAsync(string line); diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IReporter.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IReporter.cs index 4ae4552..ba9f2bb 100644 --- a/MoonlightServers.Daemon/ServerSystem/Interfaces/IReporter.cs +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IReporter.cs @@ -5,14 +5,14 @@ public interface IReporter : IServerComponent /// /// Writes both in the server logs as well in the server console the provided message as a status update /// - /// The message to write + /// Message to write /// public Task StatusAsync(string message); /// /// Writes both in the server logs as well in the server console the provided message as an error /// - /// The message to write + /// Message to write /// public Task ErrorAsync(string message); } \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IRuntime.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IRuntime.cs index 6490d9f..3a27b2d 100644 --- a/MoonlightServers.Daemon/ServerSystem/Interfaces/IRuntime.cs +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IRuntime.cs @@ -11,7 +11,7 @@ public interface IRuntime : IServerComponent /// /// Creates the runtime with the specified path as the storage path where the server files should be stored in /// - /// + /// Path where the server files are located /// public Task CreateAsync(string path); @@ -42,7 +42,7 @@ public interface IRuntime : IServerComponent /// /// This subscribes to the exited event of the runtime /// - /// The callback gets invoked whenever the runtime exites + /// Callback gets invoked whenever the runtime exites /// Subscription disposable to unsubscribe from the event public Task SubscribeExited(Func callback); diff --git a/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs b/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs index 78bac7f..1dc86cd 100644 --- a/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs +++ b/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs @@ -1,4 +1,7 @@ using MoonlightServers.Daemon.Models.Cache; +using MoonlightServers.Daemon.ServerSystem.Docker; +using MoonlightServers.Daemon.ServerSystem.FileSystems; +using MoonlightServers.Daemon.ServerSystem.Implementations; using MoonlightServers.Daemon.ServerSystem.Interfaces; using MoonlightServers.Daemon.ServerSystem.Models; @@ -27,7 +30,7 @@ public class ServerFactory context.ServiceScope = scope; // Define all required components - + IConsole console; IFileSystem runtimeFs; IFileSystem installFs; @@ -37,10 +40,18 @@ public class ServerFactory IRestorer restorer; IRuntime runtime; IStatistics statistics; - + // Resolve the components + + console = ActivatorUtilities.CreateInstance(scope.ServiceProvider, logger); + reporter = ActivatorUtilities.CreateInstance(scope.ServiceProvider, console, logger); + runtimeFs = ActivatorUtilities.CreateInstance(scope.ServiceProvider, logger, reporter); + installFs = ActivatorUtilities.CreateInstance(scope.ServiceProvider, logger, reporter); + installation = ActivatorUtilities.CreateInstance(scope.ServiceProvider, logger, reporter); + onlineDetector = ActivatorUtilities.CreateInstance(scope.ServiceProvider, logger, reporter); + // TODO: Add a plugin hook for dynamically resolving components and checking if any is unset - + // Resolve server from di var server = new Server( logger, diff --git a/MoonlightServers.Daemon/Services/DockerEventService.cs b/MoonlightServers.Daemon/Services/DockerEventService.cs index 4bb9f88..1b794dc 100644 --- a/MoonlightServers.Daemon/Services/DockerEventService.cs +++ b/MoonlightServers.Daemon/Services/DockerEventService.cs @@ -3,6 +3,7 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using Docker.DotNet; using Docker.DotNet.Models; +using MoonCore.Events; using MoonCore.Observability; using MoonlightServers.Daemon.Helpers; @@ -13,13 +14,9 @@ public class DockerEventService : BackgroundService private readonly ILogger Logger; private readonly DockerClient DockerClient; - public IAsyncObservable OnContainerEvent => OnContainerSubject; - public IAsyncObservable OnImageEvent => OnImageSubject; - public IAsyncObservable OnNetworkEvent => OnNetworkSubject; - - private readonly EventSubject OnContainerSubject = new(); - private readonly EventSubject OnImageSubject = new(); - private readonly EventSubject OnNetworkSubject = new(); + private readonly EventSource ContainerSource = new(); + private readonly EventSource ImageSource = new(); + private readonly EventSource NetworkSource = new(); public DockerEventService( ILogger logger, @@ -30,10 +27,19 @@ public class DockerEventService : BackgroundService DockerClient = dockerClient; } + public async ValueTask SubscribeContainerAsync(Func callback) + => await ContainerSource.SubscribeAsync(callback); + + public async ValueTask SubscribeImageAsync(Func callback) + => await ImageSource.SubscribeAsync(callback); + + public async ValueTask SubscribeNetworkAsync(Func callback) + => await NetworkSource.SubscribeAsync(callback); + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { Logger.LogInformation("Starting docker event service"); - + while (!stoppingToken.IsCancellationRequested) { try @@ -47,15 +53,15 @@ public class DockerEventService : BackgroundService switch (message.Type) { case "container": - await OnContainerSubject.OnNextAsync(message); + await ContainerSource.InvokeAsync(message); break; case "image": - await OnImageSubject.OnNextAsync(message); + await ImageSource.InvokeAsync(message); break; case "network": - await OnNetworkSubject.OnNextAsync(message); + await NetworkSource.InvokeAsync(message); break; } } @@ -76,16 +82,7 @@ public class DockerEventService : BackgroundService Logger.LogError(e, "An error occured while listening for docker events: {message}", e.Message); } } - + Logger.LogInformation("Stopping docker event service"); } - - public override void Dispose() - { - base.Dispose(); - - OnContainerSubject.Dispose(); - OnImageSubject.Dispose(); - OnNetworkSubject.Dispose(); - } } \ No newline at end of file