Implemented factory pattern for server abstraction creation. Implemented raw fs and docker provisioner. Implemented docker event service with observer pattern

This commit is contained in:
2025-07-26 19:14:02 +02:00
parent 0bef60dbc8
commit 84b3d1caf6
13 changed files with 956 additions and 51 deletions

View File

@@ -1,6 +1,9 @@
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text;
using Docker.DotNet;
using Docker.DotNet.Models;
using Microsoft.Extensions.Options;
using MoonCore.Helpers;
using MoonlightServers.Daemon.ServerSys.Abstractions;
@@ -15,11 +18,17 @@ public class DockerConsole : IConsole
private readonly AsyncSubject<string> OnInputSubject = new();
private readonly ConcurrentList<string> OutputCache = new();
private readonly DockerClient DockerClient;
private readonly ILogger<DockerConsole> Logger;
private readonly ServerMeta Meta;
private MultiplexedStream? CurrentStream;
private CancellationTokenSource Cts = new();
public DockerConsole(ServerMeta meta)
{
Meta = meta;
}
public Task Initialize()
=> Task.CompletedTask;
@@ -29,12 +38,101 @@ public class DockerConsole : IConsole
public async Task AttachToRuntime()
{
throw new NotImplementedException();
var containerName = $"moonlight-runtime-{Meta.Configuration.Id}";
await AttachStream(containerName);
}
public async Task AttachToInstallation()
{
throw new NotImplementedException();
var containerName = $"moonlight-install-{Meta.Configuration.Id}";
await AttachStream(containerName);
}
private Task AttachStream(string containerName)
{
Task.Run(async () =>
{
// This loop is here to reconnect to the container if for some reason the container
// attach stream fails before the server tasks have been canceled i.e. the before the server
// goes offline
while (!Cts.Token.IsCancellationRequested)
{
try
{
CurrentStream = await DockerClient.Containers.AttachContainerAsync(
containerName,
true,
new ContainerAttachParameters()
{
Stderr = true,
Stdin = true,
Stdout = true,
Stream = true
},
Cts.Token
);
var buffer = new byte[1024];
try
{
// Read while server tasks are not canceled
while (!Cts.Token.IsCancellationRequested)
{
var readResult = await CurrentStream.ReadOutputAsync(
buffer,
0,
buffer.Length,
Cts.Token
);
if (readResult.EOF)
break;
var resizedBuffer = new byte[readResult.Count];
Array.Copy(buffer, resizedBuffer, readResult.Count);
buffer = new byte[buffer.Length];
var decodedText = Encoding.UTF8.GetString(resizedBuffer);
await WriteToOutput(decodedText);
}
}
catch (TaskCanceledException)
{
// Ignored
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception e)
{
Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", e);
}
finally
{
CurrentStream.Dispose();
}
}
catch (TaskCanceledException)
{
// ignored
}
catch (Exception e)
{
Logger.LogError("An error occured while attaching to container: {e}", e);
}
}
// Reset stream so no further inputs will be piped to it
CurrentStream = null;
Logger.LogDebug("Disconnected from container stream");
}, Cts.Token);
return Task.CompletedTask;
}
public Task WriteToOutput(string content)
@@ -56,9 +154,22 @@ public class DockerConsole : IConsole
public async Task WriteToInput(string content)
{
throw new NotImplementedException();
if (CurrentStream == null)
return;
var contentBuffer = Encoding.UTF8.GetBytes(content);
await CurrentStream.WriteAsync(
contentBuffer,
0,
contentBuffer.Length,
Cts.Token
);
}
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");
public Task ClearOutput()
{
OutputCache.Clear();
@@ -75,8 +186,8 @@ public class DockerConsole : IConsole
await Cts.CancelAsync();
Cts.Dispose();
}
if(CurrentStream != null)
if (CurrentStream != null)
CurrentStream.Dispose();
}
}

View File

@@ -0,0 +1,246 @@
using System.Reactive.Subjects;
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.ServerSys.Abstractions;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.ServerSys.Implementations;
public class DockerProvisioner : IProvisioner
{
public IAsyncObservable<object> OnExited { get; set; }
private readonly DockerClient DockerClient;
private readonly ILogger<DockerProvisioner> Logger;
private readonly DockerEventService EventService;
private readonly ServerMeta Meta;
private readonly IConsole Console;
private readonly DockerImageService ImageService;
private readonly ServerConfigurationMapper Mapper;
private readonly IFileSystem FileSystem;
private AsyncSubject<object> OnExitedSubject = new();
private string? ContainerId;
private string ContainerName;
private IAsyncDisposable? ContainerEventSubscription;
public DockerProvisioner(
DockerClient dockerClient,
ILogger<DockerProvisioner> logger,
DockerEventService eventService,
ServerMeta meta,
IConsole console,
DockerImageService imageService,
ServerConfigurationMapper mapper,
IFileSystem fileSystem
)
{
DockerClient = dockerClient;
Logger = logger;
EventService = eventService;
Meta = meta;
Console = console;
ImageService = imageService;
Mapper = mapper;
FileSystem = fileSystem;
}
public async Task Initialize()
{
ContainerName = $"moonlight-runtime-{Meta.Configuration.Id}";
ContainerEventSubscription = await EventService
.OnContainerEvent
.SubscribeAsync(HandleContainerEvent);
// Check for any already existing runtime container
// TODO: Implement a way for restoring the state
// Needs to be able to be consumed by the restorer
}
private ValueTask HandleContainerEvent(Message message)
{
// Only handle events for our own container
if (message.ID != ContainerId)
return ValueTask.CompletedTask;
// Only handle die events
if (message.Action != "die")
return ValueTask.CompletedTask;
OnExitedSubject.OnNext(message);
return ValueTask.CompletedTask;
}
public Task Sync()
{
return Task.CompletedTask; // TODO: Implement
}
public async Task Provision()
{
// Plan of action:
// 1. Ensure no other container with that name exist
// 2. Ensure the docker image has been downloaded
// 3. Create the container from the configuration in the meta
// 1. Ensure no other container with that name exist
try
{
Logger.LogDebug("Searching for orphan container");
var possibleContainer = await DockerClient.Containers.InspectContainerAsync(ContainerName);
Logger.LogDebug("Orphan container found. Removing it");
await Console.WriteToMoonlight("Found orphan container. Removing it");
await EnsureContainerOffline(possibleContainer);
Logger.LogInformation("Removing orphan container");
await DockerClient.Containers.RemoveContainerAsync(ContainerName, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
// 2. Ensure the docker image has been downloaded
await Console.WriteToMoonlight("Downloading docker image");
await ImageService.Download(Meta.Configuration.DockerImage, async message =>
{
try
{
await Console.WriteToMoonlight(message);
}
catch (Exception)
{
// Ignored. Not handling it here could cause an application wide crash afaik
}
});
// 3. Create the container from the configuration in the meta
var hostFsPath = FileSystem.GetExternalPath();
var parameters = Mapper.ToRuntimeParameters(
Meta.Configuration,
hostFsPath,
ContainerName
);
var createdContainer = await DockerClient.Containers.CreateContainerAsync(parameters);
ContainerId = createdContainer.ID;
Logger.LogInformation("Created container");
await Console.WriteToMoonlight("Created container");
}
public async Task Start()
{
if(string.IsNullOrEmpty(ContainerId))
throw new ArgumentNullException(nameof(ContainerId), "Container id of runtime is unknown");
await Console.WriteToMoonlight("Starting container");
await DockerClient.Containers.StartContainerAsync(ContainerId, new());
}
public async Task Stop()
{
if (Meta.Configuration.StopCommand.StartsWith('^'))
{
await DockerClient.Containers.KillContainerAsync(ContainerId, new()
{
Signal = Meta.Configuration.StopCommand.Substring(1)
});
}
else
await Console.WriteToInput(Meta.Configuration.StopCommand);
}
public async Task Kill()
{
await EnsureContainerOffline();
}
public async Task Deprovision()
{
// Plan of action:
// 1. Search for the container by id or name
// 2. Ensure container is offline
// 3. Remove the container
// 1. Search for the container by id or name
ContainerInspectResponse? container = null;
try
{
if (string.IsNullOrEmpty(ContainerId))
container = await DockerClient.Containers.InspectContainerAsync(ContainerName);
else
container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
}
catch (DockerContainerNotFoundException)
{
// Ignored
Logger.LogDebug("Runtime container could not be found. Reporting deprovision success");
}
// No container found? We are done here then
if (container == null)
return;
// 2. Ensure container is offline
await EnsureContainerOffline(container);
// 3. Remove the container
Logger.LogInformation("Removing container");
await Console.WriteToMoonlight("Removing container");
await DockerClient.Containers.RemoveContainerAsync(container.ID, new());
}
private async Task EnsureContainerOffline(ContainerInspectResponse? container = null)
{
try
{
if (string.IsNullOrEmpty(ContainerId))
container = await DockerClient.Containers.InspectContainerAsync(ContainerName);
else
container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
// No container found? We are done here then
if (container == null)
return;
// Check if container is running
if (!container.State.Running)
return;
await Console.WriteToMoonlight("Killing container");
await DockerClient.Containers.KillContainerAsync(ContainerId, new());
}
public Task<ServerCrash?> SearchForCrash()
{
throw new NotImplementedException();
}
public async ValueTask DisposeAsync()
{
OnExitedSubject.Dispose();
if (ContainerEventSubscription != null)
await ContainerEventSubscription.DisposeAsync();
}
}

View File

@@ -0,0 +1,55 @@
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.ServerSys.Abstractions;
namespace MoonlightServers.Daemon.ServerSys.Implementations;
public class RawFileSystem : IFileSystem
{
public bool IsMounted { get; private set; }
public bool Exists { get; private set; }
private readonly ServerMeta Meta;
private readonly AppConfiguration Configuration;
private string HostPath => Path.Combine(Configuration.Storage.Volumes, Meta.Configuration.Id.ToString());
public RawFileSystem(ServerMeta meta, AppConfiguration configuration)
{
Meta = meta;
Configuration = configuration;
}
public Task Initialize()
=> Task.CompletedTask;
public Task Sync()
=> Task.CompletedTask;
public Task Create()
{
return Task.CompletedTask;
}
public Task Mount()
{
IsMounted = true;
return Task.CompletedTask;
}
public Task Unmount()
{
IsMounted = false;
return Task.CompletedTask;
}
public Task Delete()
{
Directory.Delete(HostPath, true);
return Task.CompletedTask;
}
public string GetExternalPath()
=> HostPath;
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}