From 0bef60dbc8be79a30192cc543926e66ea00686d7 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Fri, 25 Jul 2025 13:45:47 +0200 Subject: [PATCH] For extensions of base system like podman and btrfs: Started improving server abstractions to make it more extendable in order to support multiple implementations --- .../Services/NodeBootService.cs | 30 ---- .../MoonlightServers.Daemon.csproj | 7 +- .../ServerSys/Abstractions/IConsole.cs | 16 ++ .../ServerSys/Abstractions/IFileSystem.cs | 14 ++ .../ServerSys/Abstractions/IInstaller.cs | 12 ++ .../ServerSys/Abstractions/IProvisioner.cs | 13 ++ .../ServerSys/Abstractions/IRestorer.cs | 8 + .../Abstractions/IServerComponent.cs | 7 + .../ServerSys/Abstractions/IStatistics.cs | 11 ++ .../ServerSys/Abstractions/Server.cs | 139 ++++++++++++++++++ .../ServerSys/Abstractions/ServerCrash.cs | 3 + .../ServerSys/Abstractions/ServerStats.cs | 3 + .../Implementations/DockerConsole.cs | 82 +++++++++++ 13 files changed, 312 insertions(+), 33 deletions(-) delete mode 100644 MoonlightServers.ApiServer/Services/NodeBootService.cs create mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IConsole.cs create mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IFileSystem.cs create mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IInstaller.cs create mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IProvisioner.cs create mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IRestorer.cs create mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IServerComponent.cs create mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IStatistics.cs create mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/Server.cs create mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/ServerCrash.cs create mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/ServerStats.cs create mode 100644 MoonlightServers.Daemon/ServerSys/Implementations/DockerConsole.cs diff --git a/MoonlightServers.ApiServer/Services/NodeBootService.cs b/MoonlightServers.ApiServer/Services/NodeBootService.cs deleted file mode 100644 index ec77091..0000000 --- a/MoonlightServers.ApiServer/Services/NodeBootService.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.Extensions.Hosting; - -namespace MoonlightServers.ApiServer.Services; - -public class NodeBootService : IHostedLifecycleService -{ - public async Task StartedAsync(CancellationToken cancellationToken) - { - // TODO: Add node boot calls here - } - - #region Unused - - public Task StartAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - public Task StopAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - public Task StartingAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - public Task StoppedAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - public Task StoppingAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - #endregion -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj index b129cad..bcca05e 100644 --- a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj +++ b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj @@ -9,12 +9,13 @@ - - - + + + + diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IConsole.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IConsole.cs new file mode 100644 index 0000000..6905e31 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSys/Abstractions/IConsole.cs @@ -0,0 +1,16 @@ +namespace MoonlightServers.Daemon.ServerSys.Abstractions; + +public interface IConsole : IServerComponent +{ + public IAsyncObservable OnOutput { get; } + public IAsyncObservable OnInput { get; } + + public Task AttachToRuntime(); + public Task AttachToInstallation(); + + public Task WriteToOutput(string content); + public Task WriteToInput(string content); + + public Task ClearOutput(); + public string[] GetOutput(); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IFileSystem.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IFileSystem.cs new file mode 100644 index 0000000..419ec53 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSys/Abstractions/IFileSystem.cs @@ -0,0 +1,14 @@ +namespace MoonlightServers.Daemon.ServerSys.Abstractions; + +public interface IFileSystem : IServerComponent +{ + public bool IsMounted { get; } + public bool Exists { get; } + + public Task Create(); + public Task Mount(); + public Task Unmount(); + public Task Delete(); + + public string GetExternalPath(); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IInstaller.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IInstaller.cs new file mode 100644 index 0000000..603f435 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSys/Abstractions/IInstaller.cs @@ -0,0 +1,12 @@ +namespace MoonlightServers.Daemon.ServerSys.Abstractions; + +public interface IInstaller : IServerComponent +{ + public IAsyncObservable OnExited { get; set; } + + public Task Start(); + public Task Abort(); + public Task Cleanup(); + + public Task SearchForCrash(); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IProvisioner.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IProvisioner.cs new file mode 100644 index 0000000..ad9108b --- /dev/null +++ b/MoonlightServers.Daemon/ServerSys/Abstractions/IProvisioner.cs @@ -0,0 +1,13 @@ +namespace MoonlightServers.Daemon.ServerSys.Abstractions; + +public interface IProvisioner : IServerComponent +{ + public IAsyncObservable OnExited { get; set; } + + public Task Start(); + public Task Stop(); + public Task Kill(); + public Task Cleanup(); + + public Task SearchForCrash(); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IRestorer.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IRestorer.cs new file mode 100644 index 0000000..3b2eb65 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSys/Abstractions/IRestorer.cs @@ -0,0 +1,8 @@ +using MoonlightServers.Daemon.ServerSystem; + +namespace MoonlightServers.Daemon.ServerSys.Abstractions; + +public interface IRestorer : IServerComponent +{ + public Task Restore(); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IServerComponent.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IServerComponent.cs new file mode 100644 index 0000000..e2ad11e --- /dev/null +++ b/MoonlightServers.Daemon/ServerSys/Abstractions/IServerComponent.cs @@ -0,0 +1,7 @@ +namespace MoonlightServers.Daemon.ServerSys.Abstractions; + +public interface IServerComponent : IAsyncDisposable +{ + public Task Initialize(); + public Task Sync(); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IStatistics.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IStatistics.cs new file mode 100644 index 0000000..cf355c9 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSys/Abstractions/IStatistics.cs @@ -0,0 +1,11 @@ +namespace MoonlightServers.Daemon.ServerSys.Abstractions; + +public interface IStatistics : IServerComponent +{ + public IAsyncObservable Stats { get; } + + public Task SubscribeToRuntime(); + public Task SubscribeToInstallation(); + + public ServerStats[] GetStats(int count); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/Server.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/Server.cs new file mode 100644 index 0000000..48a7ec1 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSys/Abstractions/Server.cs @@ -0,0 +1,139 @@ +using MoonlightServers.Daemon.ServerSystem; +using Stateless; + +namespace MoonlightServers.Daemon.ServerSys.Abstractions; + +public class Server : IAsyncDisposable +{ + public IConsole Console { get; private set; } + public IFileSystem FileSystem { get; private set; } + public IInstaller Installer { get; private set; } + public IProvisioner Provisioner { get; private set; } + public IRestorer Restorer { get; private set; } + public IStatistics Statistics { get; private set; } + public StateMachine StateMachine { get; private set; } + + private readonly ILogger Logger; + + private IAsyncDisposable? ProvisionExitSubscription; + private IAsyncDisposable? InstallerExitSubscription; + + public Server( + ILogger logger, + IConsole console, + IFileSystem fileSystem, + IInstaller installer, + IProvisioner provisioner, + IRestorer restorer, + IStatistics statistics, + StateMachine stateMachine + ) + { + Logger = logger; + Console = console; + FileSystem = fileSystem; + Installer = installer; + Provisioner = provisioner; + Restorer = restorer; + Statistics = statistics; + StateMachine = stateMachine; + } + + public async Task Initialize() + { + Logger.LogDebug("Initializing server components"); + + IServerComponent[] components = [Console, Restorer, FileSystem, Installer, Provisioner, Statistics]; + + foreach (var serverComponent in components) + { + try + { + await serverComponent.Initialize(); + } + catch (Exception e) + { + Logger.LogError( + e, + "Error initializing server component: {type}", + serverComponent.GetType().Name.GetType().FullName + ); + + throw; + } + } + + Logger.LogDebug("Restoring server"); + var restoredState = await Restorer.Restore(); + + if (restoredState == ServerState.Offline) + Logger.LogDebug("Restorer didnt find anything to restore. State is offline"); + else + Logger.LogDebug("Restored server to state: {state}", restoredState); + + CreateStateMachine(restoredState); + + // Setup event handling + ProvisionExitSubscription = await Provisioner.OnExited.SubscribeAsync(async o => + { + await StateMachine.FireAsync(ServerTrigger.Exited); + }); + + InstallerExitSubscription = await Installer.OnExited.SubscribeAsync(async o => + { + await StateMachine.FireAsync(ServerTrigger.Exited); + }); + } + + private void CreateStateMachine(ServerState initialState) + { + StateMachine = new StateMachine(initialState, FiringMode.Queued); + + // Configure basic state machine flow + + StateMachine.Configure(ServerState.Offline) + .Permit(ServerTrigger.Start, ServerState.Starting) + .Permit(ServerTrigger.Install, ServerState.Installing) + .PermitReentry(ServerTrigger.FailSafe); + + StateMachine.Configure(ServerState.Starting) + .Permit(ServerTrigger.OnlineDetected, ServerState.Online) + .Permit(ServerTrigger.FailSafe, ServerState.Offline) + .Permit(ServerTrigger.Exited, ServerState.Offline) + .Permit(ServerTrigger.Stop, ServerState.Stopping) + .Permit(ServerTrigger.Kill, ServerState.Stopping); + + StateMachine.Configure(ServerState.Online) + .Permit(ServerTrigger.Stop, ServerState.Stopping) + .Permit(ServerTrigger.Kill, ServerState.Stopping) + .Permit(ServerTrigger.Exited, ServerState.Offline); + + StateMachine.Configure(ServerState.Stopping) + .PermitReentry(ServerTrigger.FailSafe) + .PermitReentry(ServerTrigger.Kill) + .Permit(ServerTrigger.Exited, ServerState.Offline); + + StateMachine.Configure(ServerState.Installing) + .Permit(ServerTrigger.FailSafe, ServerState.Offline) + .Permit(ServerTrigger.Exited, ServerState.Offline); + + // Handle transitions + + } + + public async ValueTask DisposeAsync() + { + if (ProvisionExitSubscription != null) + await ProvisionExitSubscription.DisposeAsync(); + + if(InstallerExitSubscription != null) + await InstallerExitSubscription.DisposeAsync(); + + await Console.DisposeAsync(); + await FileSystem.DisposeAsync(); + await Installer.DisposeAsync(); + await Provisioner.DisposeAsync(); + await Restorer.DisposeAsync(); + await Statistics.DisposeAsync(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/ServerCrash.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/ServerCrash.cs new file mode 100644 index 0000000..1e696a7 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSys/Abstractions/ServerCrash.cs @@ -0,0 +1,3 @@ +namespace MoonlightServers.Daemon.ServerSys.Abstractions; + +public record ServerCrash(); \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/ServerStats.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/ServerStats.cs new file mode 100644 index 0000000..a6c1605 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSys/Abstractions/ServerStats.cs @@ -0,0 +1,3 @@ +namespace MoonlightServers.Daemon.ServerSys.Abstractions; + +public record ServerStats(); \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Implementations/DockerConsole.cs b/MoonlightServers.Daemon/ServerSys/Implementations/DockerConsole.cs new file mode 100644 index 0000000..5e3d140 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSys/Implementations/DockerConsole.cs @@ -0,0 +1,82 @@ +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Docker.DotNet; +using MoonCore.Helpers; +using MoonlightServers.Daemon.ServerSys.Abstractions; + +namespace MoonlightServers.Daemon.ServerSys.Implementations; + +public class DockerConsole : IConsole +{ + public IAsyncObservable OnOutput => OnOutputSubject.ToAsyncObservable(); + public IAsyncObservable OnInput => OnInputSubject.ToAsyncObservable(); + + private readonly AsyncSubject OnOutputSubject = new(); + private readonly AsyncSubject OnInputSubject = new(); + + private readonly ConcurrentList OutputCache = new(); + + private readonly DockerClient DockerClient; + private readonly ILogger Logger; + private MultiplexedStream? CurrentStream; + private CancellationTokenSource Cts = new(); + + public Task Initialize() + => Task.CompletedTask; + + public Task Sync() + => Task.CompletedTask; + + public async Task AttachToRuntime() + { + throw new NotImplementedException(); + } + + public async Task AttachToInstallation() + { + throw new NotImplementedException(); + } + + public Task WriteToOutput(string content) + { + OutputCache.Add(content); + + if (OutputCache.Count > 250) // TODO: Config + { + // TODO: Replace with remove range once it becomes available in mooncore + for (var i = 0; i < 100; i++) + { + OutputCache.RemoveAt(i); + } + } + + OnOutputSubject.OnNext(content); + return Task.CompletedTask; + } + + public async Task WriteToInput(string content) + { + throw new NotImplementedException(); + } + + public Task ClearOutput() + { + OutputCache.Clear(); + return Task.CompletedTask; + } + + public string[] GetOutput() + => OutputCache.ToArray(); + + public async ValueTask DisposeAsync() + { + if (!Cts.IsCancellationRequested) + { + await Cts.CancelAsync(); + Cts.Dispose(); + } + + if(CurrentStream != null) + CurrentStream.Dispose(); + } +} \ No newline at end of file