Improved comments. Started implementing docker components and other base components. Updated dependencies

This commit is contained in:
2025-09-06 21:44:22 +02:00
parent 348e9560ab
commit 282096595d
16 changed files with 672 additions and 52 deletions

View File

@@ -9,19 +9,16 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Docker.DotNet" Version="3.125.15" /> <PackageReference Include="Docker.DotNet" Version="3.125.15" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" /> <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageReference Include="MoonCore" Version="1.9.4" /> <PackageReference Include="MoonCore" Version="1.9.7" />
<PackageReference Include="MoonCore.Extended" Version="1.3.6" /> <PackageReference Include="MoonCore.Extended" Version="1.3.7" />
<PackageReference Include="MoonCore.Unix" Version="1.0.8" /> <PackageReference Include="MoonCore.Unix" Version="1.0.8" />
<PackageReference Include="SharpZipLib" Version="1.4.2" /> <PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Stateless" Version="5.17.0" /> <PackageReference Include="Stateless" Version="5.19.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1"/>
<PackageReference Include="System.Reactive.Async" Version="6.0.0-alpha.18" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Http\Controllers\Servers\" /> <Folder Include="Http\Controllers\Servers\" />
<Folder Include="Http\Middleware\" /> <Folder Include="Http\Middleware\" />
<Folder Include="ServerSystem\Docker\Components\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -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<string> StdOutEventSource = new();
private readonly ConcurrentList<string> 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<IEnumerable<string>> GetCacheAsync()
{
return Task.FromResult<IEnumerable<string>>(StdOutCache);
}
public async Task<IAsyncDisposable> SubscribeStdOutAsync(Func<string, ValueTask> callback)
=> await StdOutEventSource.SubscribeAsync(callback);
public async ValueTask DisposeAsync()
{
if (!Cts.IsCancellationRequested)
await Cts.CancelAsync();
if (BaseStream != null)
BaseStream.Dispose();
}
}

View File

@@ -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}";
}

View File

@@ -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<int> 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<bool> 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<IAsyncDisposable> SubscribeExited(Func<int, ValueTask> 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();
}
}

View File

@@ -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<string> GetPathAsync()
=> Task.FromResult(BaseDirectory);
public Task<bool> CheckExistsAsync()
{
var exists = Directory.Exists(BaseDirectory);
return Task.FromResult(exists);
}
public Task<bool> 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;
}

View File

@@ -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<string> GetPathAsync()
=> Task.FromResult(BaseDirectory);
public Task<bool> CheckExistsAsync()
{
var exists = Directory.Exists(BaseDirectory);
return Task.FromResult(exists);
}
public Task<bool> 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;
}

View File

@@ -51,6 +51,9 @@ public class StartupHandler : IServerStateHandler
// 5. Create runtime // 5. Create runtime
var hostPath = await Server.RuntimeFileSystem.GetPathAsync(); var hostPath = await Server.RuntimeFileSystem.GetPathAsync();
if (await Server.Runtime.CheckExistsAsync())
await Server.Runtime.DestroyAsync();
await Server.Runtime.CreateAsync(hostPath); await Server.Runtime.CreateAsync(hostPath);
if (ExitSubscription == null) if (ExitSubscription == null)

View File

@@ -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<bool> 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;
}
}

View File

@@ -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;
}

View File

@@ -7,7 +7,7 @@ public interface IConsole : IServerComponent
/// would write into the containers standard input. /// would write into the containers standard input.
/// <remarks>This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required</remarks> /// <remarks>This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required</remarks>
/// </summary> /// </summary>
/// <param name="content">The content to write</param> /// <param name="content">Content to write</param>
/// <returns></returns> /// <returns></returns>
public Task WriteStdInAsync(string content); public Task WriteStdInAsync(string content);
/// <summary> /// <summary>
@@ -15,17 +15,9 @@ public interface IConsole : IServerComponent
/// would write into the containers standard output. /// would write into the containers standard output.
/// <remarks>This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required</remarks> /// <remarks>This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required</remarks>
/// </summary> /// </summary>
/// <param name="content">The content to write</param> /// <param name="content">Content to write</param>
/// <returns></returns> /// <returns></returns>
public Task WriteStdOutAsync(string content); public Task WriteStdOutAsync(string content);
/// <summary>
/// Writes a system message to the standard output with the moonlight console prefix
/// <remarks>This method *does* add the newline separator at the end</remarks>
/// </summary>
/// <param name="content">The content to write into the standard output</param>
/// <returns></returns>
public Task WriteMoonlightAsync(string content);
/// <summary> /// <summary>
/// Attaches the console to the runtime environment /// Attaches the console to the runtime environment
@@ -60,7 +52,7 @@ public interface IConsole : IServerComponent
/// <summary> /// <summary>
/// Gets the content from the standard output cache /// Gets the content from the standard output cache
/// </summary> /// </summary>
/// <returns>The content from the cache</returns> /// <returns>Content from the cache</returns>
public Task<IEnumerable<string>> GetCacheAsync(); public Task<IEnumerable<string>> GetCacheAsync();
/// <summary> /// <summary>
@@ -68,5 +60,5 @@ public interface IConsole : IServerComponent
/// </summary> /// </summary>
/// <param name="callback">Callback which will be invoked whenever a new line is received</param> /// <param name="callback">Callback which will be invoked whenever a new line is received</param>
/// <returns>Subscription disposable to unsubscribe from the event</returns> /// <returns>Subscription disposable to unsubscribe from the event</returns>
public Task<IAsyncDisposable> SubscribeStdOutAsync(Func<string, Task> callback); public Task<IAsyncDisposable> SubscribeStdOutAsync(Func<string, ValueTask> callback);
} }

View File

@@ -1,3 +1,5 @@
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.ServerSystem.Interfaces; namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IInstallation : IServerComponent public interface IInstallation : IServerComponent
@@ -11,10 +13,11 @@ public interface IInstallation : IServerComponent
/// <summary> /// <summary>
/// Creates the installation environment /// Creates the installation environment
/// </summary> /// </summary>
/// <param name="runtimePath">The host path of the runtime storage location</param> /// <param name="runtimePath">Host path of the runtime storage location</param>
/// <param name="hostPath">The host path of the installation file system</param> /// <param name="hostPath">Host path of the installation file system</param>
/// <param name="data">Installation data for the server</param>
/// <returns></returns> /// <returns></returns>
public Task CreateAsync(string runtimePath, string hostPath); public Task CreateAsync(string runtimePath, string hostPath, ServerInstallDataResponse data);
/// <summary> /// <summary>
/// Starts the installation /// Starts the installation
@@ -37,9 +40,9 @@ public interface IInstallation : IServerComponent
/// <summary> /// <summary>
/// Subscribes to the event when the installation exists /// Subscribes to the event when the installation exists
/// </summary> /// </summary>
/// <param name="callback">The callback to invoke whenever the installation exists</param> /// <param name="callback">Callback to invoke whenever the installation exists</param>
/// <returns>Subscription disposable to unsubscribe from the event</returns> /// <returns>Subscription disposable to unsubscribe from the event</returns>
public Task<IAsyncDisposable> SubscribeExited(Func<int, Task> callback); public Task<IAsyncDisposable> SubscribeExited(Func<int, ValueTask> callback);
/// <summary> /// <summary>
/// Connects an existing installation to this abstraction in order to restore it. /// Connects an existing installation to this abstraction in order to restore it.

View File

@@ -11,7 +11,7 @@ public interface IOnlineDetector : IServerComponent
/// <summary> /// <summary>
/// Handles the detection of the online state based on the received output /// Handles the detection of the online state based on the received output
/// </summary> /// </summary>
/// <param name="line">The excerpt of the output</param> /// <param name="line">Excerpt of the output</param>
/// <returns>True if the detection showed that the server is online. False if the detection didnt find anything</returns> /// <returns>True if the detection showed that the server is online. False if the detection didnt find anything</returns>
public Task<bool> HandleOutputAsync(string line); public Task<bool> HandleOutputAsync(string line);

View File

@@ -5,14 +5,14 @@ public interface IReporter : IServerComponent
/// <summary> /// <summary>
/// Writes both in the server logs as well in the server console the provided message as a status update /// Writes both in the server logs as well in the server console the provided message as a status update
/// </summary> /// </summary>
/// <param name="message">The message to write</param> /// <param name="message">Message to write</param>
/// <returns></returns> /// <returns></returns>
public Task StatusAsync(string message); public Task StatusAsync(string message);
/// <summary> /// <summary>
/// Writes both in the server logs as well in the server console the provided message as an error /// Writes both in the server logs as well in the server console the provided message as an error
/// </summary> /// </summary>
/// <param name="message">The message to write</param> /// <param name="message">Message to write</param>
/// <returns></returns> /// <returns></returns>
public Task ErrorAsync(string message); public Task ErrorAsync(string message);
} }

View File

@@ -11,7 +11,7 @@ public interface IRuntime : IServerComponent
/// <summary> /// <summary>
/// Creates the runtime with the specified path as the storage path where the server files should be stored in /// Creates the runtime with the specified path as the storage path where the server files should be stored in
/// </summary> /// </summary>
/// <param name="path"></param> /// <param name="path">Path where the server files are located</param>
/// <returns></returns> /// <returns></returns>
public Task CreateAsync(string path); public Task CreateAsync(string path);
@@ -42,7 +42,7 @@ public interface IRuntime : IServerComponent
/// <summary> /// <summary>
/// This subscribes to the exited event of the runtime /// This subscribes to the exited event of the runtime
/// </summary> /// </summary>
/// <param name="callback">The callback gets invoked whenever the runtime exites</param> /// <param name="callback">Callback gets invoked whenever the runtime exites</param>
/// <returns>Subscription disposable to unsubscribe from the event</returns> /// <returns>Subscription disposable to unsubscribe from the event</returns>
public Task<IAsyncDisposable> SubscribeExited(Func<int, Task> callback); public Task<IAsyncDisposable> SubscribeExited(Func<int, Task> callback);

View File

@@ -1,4 +1,7 @@
using MoonlightServers.Daemon.Models.Cache; 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.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models; using MoonlightServers.Daemon.ServerSystem.Models;
@@ -27,7 +30,7 @@ public class ServerFactory
context.ServiceScope = scope; context.ServiceScope = scope;
// Define all required components // Define all required components
IConsole console; IConsole console;
IFileSystem runtimeFs; IFileSystem runtimeFs;
IFileSystem installFs; IFileSystem installFs;
@@ -37,10 +40,18 @@ public class ServerFactory
IRestorer restorer; IRestorer restorer;
IRuntime runtime; IRuntime runtime;
IStatistics statistics; IStatistics statistics;
// Resolve the components // Resolve the components
console = ActivatorUtilities.CreateInstance<DockerConsole>(scope.ServiceProvider, logger);
reporter = ActivatorUtilities.CreateInstance<ServerReporter>(scope.ServiceProvider, console, logger);
runtimeFs = ActivatorUtilities.CreateInstance<RawRuntimeFs>(scope.ServiceProvider, logger, reporter);
installFs = ActivatorUtilities.CreateInstance<RawInstallationFs>(scope.ServiceProvider, logger, reporter);
installation = ActivatorUtilities.CreateInstance<DockerInstallation>(scope.ServiceProvider, logger, reporter);
onlineDetector = ActivatorUtilities.CreateInstance<RegexOnlineDetector>(scope.ServiceProvider, logger, reporter);
// TODO: Add a plugin hook for dynamically resolving components and checking if any is unset // TODO: Add a plugin hook for dynamically resolving components and checking if any is unset
// Resolve server from di // Resolve server from di
var server = new Server( var server = new Server(
logger, logger,

View File

@@ -3,6 +3,7 @@ using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Docker.DotNet; using Docker.DotNet;
using Docker.DotNet.Models; using Docker.DotNet.Models;
using MoonCore.Events;
using MoonCore.Observability; using MoonCore.Observability;
using MoonlightServers.Daemon.Helpers; using MoonlightServers.Daemon.Helpers;
@@ -13,13 +14,9 @@ public class DockerEventService : BackgroundService
private readonly ILogger<DockerEventService> Logger; private readonly ILogger<DockerEventService> Logger;
private readonly DockerClient DockerClient; private readonly DockerClient DockerClient;
public IAsyncObservable<Message> OnContainerEvent => OnContainerSubject; private readonly EventSource<Message> ContainerSource = new();
public IAsyncObservable<Message> OnImageEvent => OnImageSubject; private readonly EventSource<Message> ImageSource = new();
public IAsyncObservable<Message> OnNetworkEvent => OnNetworkSubject; private readonly EventSource<Message> NetworkSource = new();
private readonly EventSubject<Message> OnContainerSubject = new();
private readonly EventSubject<Message> OnImageSubject = new();
private readonly EventSubject<Message> OnNetworkSubject = new();
public DockerEventService( public DockerEventService(
ILogger<DockerEventService> logger, ILogger<DockerEventService> logger,
@@ -30,10 +27,19 @@ public class DockerEventService : BackgroundService
DockerClient = dockerClient; DockerClient = dockerClient;
} }
public async ValueTask<IAsyncDisposable> SubscribeContainerAsync(Func<Message, ValueTask> callback)
=> await ContainerSource.SubscribeAsync(callback);
public async ValueTask<IAsyncDisposable> SubscribeImageAsync(Func<Message, ValueTask> callback)
=> await ImageSource.SubscribeAsync(callback);
public async ValueTask<IAsyncDisposable> SubscribeNetworkAsync(Func<Message, ValueTask> callback)
=> await NetworkSource.SubscribeAsync(callback);
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
Logger.LogInformation("Starting docker event service"); Logger.LogInformation("Starting docker event service");
while (!stoppingToken.IsCancellationRequested) while (!stoppingToken.IsCancellationRequested)
{ {
try try
@@ -47,15 +53,15 @@ public class DockerEventService : BackgroundService
switch (message.Type) switch (message.Type)
{ {
case "container": case "container":
await OnContainerSubject.OnNextAsync(message); await ContainerSource.InvokeAsync(message);
break; break;
case "image": case "image":
await OnImageSubject.OnNextAsync(message); await ImageSource.InvokeAsync(message);
break; break;
case "network": case "network":
await OnNetworkSubject.OnNextAsync(message); await NetworkSource.InvokeAsync(message);
break; break;
} }
} }
@@ -76,16 +82,7 @@ public class DockerEventService : BackgroundService
Logger.LogError(e, "An error occured while listening for docker events: {message}", e.Message); Logger.LogError(e, "An error occured while listening for docker events: {message}", e.Message);
} }
} }
Logger.LogInformation("Stopping docker event service"); Logger.LogInformation("Stopping docker event service");
} }
public override void Dispose()
{
base.Dispose();
OnContainerSubject.Dispose();
OnImageSubject.Dispose();
OnNetworkSubject.Dispose();
}
} }