Implemented restorer, wired up for basic testing. Improved abstractions and fixed observer pattern issues

This commit is contained in:
2025-07-26 23:19:57 +02:00
parent 84b3d1caf6
commit b546a168d2
17 changed files with 355 additions and 97 deletions

View File

@@ -0,0 +1,56 @@
using MoonlightServers.Daemon.ServerSys.Abstractions;
using MoonlightServers.Daemon.ServerSystem;
namespace MoonlightServers.Daemon.ServerSys.Implementations;
public class DefaultRestorer : IRestorer
{
private readonly ILogger<DefaultRestorer> Logger;
private readonly IConsole Console;
private readonly IProvisioner Provisioner;
private readonly IStatistics Statistics;
public DefaultRestorer(
ILogger<DefaultRestorer> logger,
IConsole console,
IProvisioner provisioner,
IStatistics statistics
)
{
Logger = logger;
Console = console;
Provisioner = provisioner;
Statistics = statistics;
}
public Task Initialize()
=> Task.CompletedTask;
public Task Sync()
=> Task.CompletedTask;
public async Task<ServerState> Restore()
{
Logger.LogDebug("Restoring server state");
if (Provisioner.IsProvisioned)
{
Logger.LogDebug("Detected runtime to restore");
await Console.AttachToRuntime();
await Statistics.SubscribeToRuntime();
// TODO: Read out existing container log in order to search if the server is online
return ServerState.Online;
}
else
{
Logger.LogDebug("Nothing found to restore");
return ServerState.Offline;
}
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -11,23 +11,29 @@ namespace MoonlightServers.Daemon.ServerSys.Implementations;
public class DockerConsole : IConsole
{
public IAsyncObservable<string> OnOutput => OnOutputSubject.ToAsyncObservable();
public IAsyncObservable<string> OnInput => OnInputSubject.ToAsyncObservable();
public IObservable<string> OnOutput => OnOutputSubject;
public IObservable<string> OnInput => OnInputSubject;
private readonly AsyncSubject<string> OnOutputSubject = new();
private readonly AsyncSubject<string> OnInputSubject = new();
private readonly Subject<string> OnOutputSubject = new();
private readonly Subject<string> OnInputSubject = new();
private readonly ConcurrentList<string> OutputCache = new();
private readonly DockerClient DockerClient;
private readonly ILogger<DockerConsole> Logger;
private readonly ServerMeta Meta;
private readonly ServerContext Context;
private MultiplexedStream? CurrentStream;
private CancellationTokenSource Cts = new();
public DockerConsole(ServerMeta meta)
public DockerConsole(
ServerContext context,
DockerClient dockerClient,
ILogger<DockerConsole> logger
)
{
Meta = meta;
Context = context;
DockerClient = dockerClient;
Logger = logger;
}
public Task Initialize()
@@ -38,13 +44,13 @@ public class DockerConsole : IConsole
public async Task AttachToRuntime()
{
var containerName = $"moonlight-runtime-{Meta.Configuration.Id}";
var containerName = $"moonlight-runtime-{Context.Configuration.Id}";
await AttachStream(containerName);
}
public async Task AttachToInstallation()
{
var containerName = $"moonlight-install-{Meta.Configuration.Id}";
var containerName = $"moonlight-install-{Context.Configuration.Id}";
await AttachStream(containerName);
}
@@ -108,7 +114,7 @@ public class DockerConsole : IConsole
}
catch (Exception e)
{
Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", e);
Logger.LogWarning(e, "An unhandled error occured while reading from container stream");
}
finally
{
@@ -121,7 +127,7 @@ public class DockerConsole : IConsole
}
catch (Exception e)
{
Logger.LogError("An error occured while attaching to container: {e}", e);
Logger.LogError(e, "An error occured while attaching to container");
}
}
@@ -131,7 +137,7 @@ public class DockerConsole : IConsole
Logger.LogDebug("Disconnected from container stream");
}, Cts.Token);
return Task.CompletedTask;
}
@@ -168,7 +174,8 @@ public class DockerConsole : IConsole
}
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(
$"\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()
{

View File

@@ -0,0 +1,55 @@
using System.Reactive.Linq;
using System.Reactive.Subjects;
using MoonlightServers.Daemon.ServerSys.Abstractions;
namespace MoonlightServers.Daemon.ServerSys.Implementations;
public class DockerInstaller : IInstaller
{
public IObservable<object> OnExited => OnExitedSubject;
public bool IsRunning { get; private set; } = false;
private readonly Subject<string> OnExitedSubject = new();
private readonly ILogger<DockerInstaller> Logger;
public DockerInstaller(ILogger<DockerInstaller> logger)
{
Logger = logger;
}
public Task Initialize()
{
return Task.CompletedTask;
}
public Task Sync()
{
throw new NotImplementedException();
}
public Task Start()
{
throw new NotImplementedException();
}
public Task Abort()
{
throw new NotImplementedException();
}
public Task Cleanup()
{
throw new NotImplementedException();
}
public Task<ServerCrash?> SearchForCrash()
{
throw new NotImplementedException();
}
public async ValueTask DisposeAsync()
{
OnExitedSubject.Dispose();
}
}

View File

@@ -1,3 +1,5 @@
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Docker.DotNet;
using Docker.DotNet.Models;
@@ -9,28 +11,29 @@ namespace MoonlightServers.Daemon.ServerSys.Implementations;
public class DockerProvisioner : IProvisioner
{
public IAsyncObservable<object> OnExited { get; set; }
public IObservable<object> OnExited => OnExitedSubject;
public bool IsProvisioned { get; private set; }
private readonly DockerClient DockerClient;
private readonly ILogger<DockerProvisioner> Logger;
private readonly DockerEventService EventService;
private readonly ServerMeta Meta;
private readonly ServerContext Context;
private readonly IConsole Console;
private readonly DockerImageService ImageService;
private readonly ServerConfigurationMapper Mapper;
private readonly IFileSystem FileSystem;
private AsyncSubject<object> OnExitedSubject = new();
private Subject<object> OnExitedSubject = new();
private string? ContainerId;
private string ContainerName;
private IAsyncDisposable? ContainerEventSubscription;
private IDisposable? ContainerEventSubscription;
public DockerProvisioner(
DockerClient dockerClient,
ILogger<DockerProvisioner> logger,
DockerEventService eventService,
ServerMeta meta,
ServerContext context,
IConsole console,
DockerImageService imageService,
ServerConfigurationMapper mapper,
@@ -40,7 +43,7 @@ public class DockerProvisioner : IProvisioner
DockerClient = dockerClient;
Logger = logger;
EventService = eventService;
Meta = meta;
Context = context;
Console = console;
ImageService = imageService;
Mapper = mapper;
@@ -49,30 +52,39 @@ public class DockerProvisioner : IProvisioner
public async Task Initialize()
{
ContainerName = $"moonlight-runtime-{Meta.Configuration.Id}";
ContainerName = $"moonlight-runtime-{Context.Configuration.Id}";
ContainerEventSubscription = await EventService
ContainerEventSubscription = EventService
.OnContainerEvent
.SubscribeAsync(HandleContainerEvent);
.Subscribe(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
// Check for any already existing runtime container to reclaim
Logger.LogDebug("Searching for orphan container to reclaim");
try
{
var container = await DockerClient.Containers.InspectContainerAsync(ContainerName);
ContainerId = container.ID;
IsProvisioned = container.State.Running;
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
}
private ValueTask HandleContainerEvent(Message message)
private void HandleContainerEvent(Message message)
{
// Only handle events for our own container
if (message.ID != ContainerId)
return ValueTask.CompletedTask;
return;
// Only handle die events
if (message.Action != "die")
return ValueTask.CompletedTask;
return;
OnExitedSubject.OnNext(message);
return ValueTask.CompletedTask;
}
public Task Sync()
@@ -111,7 +123,7 @@ public class DockerProvisioner : IProvisioner
// 2. Ensure the docker image has been downloaded
await Console.WriteToMoonlight("Downloading docker image");
await ImageService.Download(Meta.Configuration.DockerImage, async message =>
await ImageService.Download(Context.Configuration.DockerImage, async message =>
{
try
{
@@ -127,7 +139,7 @@ public class DockerProvisioner : IProvisioner
var hostFsPath = FileSystem.GetExternalPath();
var parameters = Mapper.ToRuntimeParameters(
Meta.Configuration,
Context.Configuration,
hostFsPath,
ContainerName
);
@@ -151,15 +163,15 @@ public class DockerProvisioner : IProvisioner
public async Task Stop()
{
if (Meta.Configuration.StopCommand.StartsWith('^'))
if (Context.Configuration.StopCommand.StartsWith('^'))
{
await DockerClient.Containers.KillContainerAsync(ContainerId, new()
{
Signal = Meta.Configuration.StopCommand.Substring(1)
Signal = Context.Configuration.StopCommand.Substring(1)
});
}
else
await Console.WriteToInput(Meta.Configuration.StopCommand);
await Console.WriteToInput(Context.Configuration.StopCommand + "\n\r");
}
public async Task Kill()
@@ -241,6 +253,6 @@ public class DockerProvisioner : IProvisioner
OnExitedSubject.Dispose();
if (ContainerEventSubscription != null)
await ContainerEventSubscription.DisposeAsync();
ContainerEventSubscription.Dispose();
}
}

View File

@@ -0,0 +1,34 @@
using System.Reactive.Linq;
using System.Reactive.Subjects;
using MoonlightServers.Daemon.ServerSys.Abstractions;
namespace MoonlightServers.Daemon.ServerSys.Implementations;
public class DockerStatistics : IStatistics
{
public IAsyncObservable<ServerStats> OnStats => OnStatsSubject.ToAsyncObservable();
private readonly Subject<ServerStats> OnStatsSubject = new();
public Task Initialize()
=> Task.CompletedTask;
public Task Sync()
=> Task.CompletedTask;
public Task SubscribeToRuntime()
=> Task.CompletedTask;
public Task SubscribeToInstallation()
=> Task.CompletedTask;
public ServerStats[] GetStats(int count)
{
return [];
}
public async ValueTask DisposeAsync()
{
OnStatsSubject.Dispose();
}
}

View File

@@ -8,24 +8,29 @@ public class RawFileSystem : IFileSystem
public bool IsMounted { get; private set; }
public bool Exists { get; private set; }
private readonly ServerMeta Meta;
private readonly ServerContext Context;
private readonly AppConfiguration Configuration;
private string HostPath => Path.Combine(Configuration.Storage.Volumes, Meta.Configuration.Id.ToString());
private string HostPath;
public RawFileSystem(ServerMeta meta, AppConfiguration configuration)
public RawFileSystem(ServerContext context, AppConfiguration configuration)
{
Meta = meta;
Context = context;
Configuration = configuration;
}
public Task Initialize()
=> Task.CompletedTask;
{
HostPath = Path.Combine(Directory.GetCurrentDirectory(), Configuration.Storage.Volumes, Context.Configuration.Id.ToString());
return Task.CompletedTask;
}
public Task Sync()
=> Task.CompletedTask;
public Task Create()
{
Directory.CreateDirectory(HostPath);
return Task.CompletedTask;
}