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:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user