For extensions of base system like podman and btrfs: Started improving server abstractions to make it more extendable in order to support multiple implementations

This commit is contained in:
2025-07-25 13:45:47 +02:00
parent bdc4ad8265
commit 0bef60dbc8
13 changed files with 312 additions and 33 deletions

View File

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

View File

@@ -9,12 +9,13 @@
<ItemGroup>
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageReference Include="MoonCore" Version="1.8.6" />
<PackageReference Include="MoonCore.Extended" Version="1.3.3" />
<PackageReference Include="MoonCore.Unix" Version="1.0.7" />
<PackageReference Include="MoonCore" Version="1.9.2" />
<PackageReference Include="MoonCore.Extended" Version="1.3.6" />
<PackageReference Include="MoonCore.Unix" Version="1.0.8" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Stateless" Version="5.17.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1"/>
<PackageReference Include="System.Reactive.Async" Version="6.0.0-alpha.18" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,16 @@
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
public interface IConsole : IServerComponent
{
public IAsyncObservable<string> OnOutput { get; }
public IAsyncObservable<string> OnInput { get; }
public Task AttachToRuntime();
public Task AttachToInstallation();
public Task WriteToOutput(string content);
public Task WriteToInput(string content);
public Task ClearOutput();
public string[] GetOutput();
}

View File

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

View File

@@ -0,0 +1,12 @@
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
public interface IInstaller : IServerComponent
{
public IAsyncObservable<object> OnExited { get; set; }
public Task Start();
public Task Abort();
public Task Cleanup();
public Task<ServerCrash?> SearchForCrash();
}

View File

@@ -0,0 +1,13 @@
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
public interface IProvisioner : IServerComponent
{
public IAsyncObservable<object> OnExited { get; set; }
public Task Start();
public Task Stop();
public Task Kill();
public Task Cleanup();
public Task<ServerCrash?> SearchForCrash();
}

View File

@@ -0,0 +1,8 @@
using MoonlightServers.Daemon.ServerSystem;
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
public interface IRestorer : IServerComponent
{
public Task<ServerState> Restore();
}

View File

@@ -0,0 +1,7 @@
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
public interface IServerComponent : IAsyncDisposable
{
public Task Initialize();
public Task Sync();
}

View File

@@ -0,0 +1,11 @@
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
public interface IStatistics : IServerComponent
{
public IAsyncObservable<ServerStats> Stats { get; }
public Task SubscribeToRuntime();
public Task SubscribeToInstallation();
public ServerStats[] GetStats(int count);
}

View File

@@ -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<ServerState, ServerTrigger> StateMachine { get; private set; }
private readonly ILogger<Server> Logger;
private IAsyncDisposable? ProvisionExitSubscription;
private IAsyncDisposable? InstallerExitSubscription;
public Server(
ILogger<Server> logger,
IConsole console,
IFileSystem fileSystem,
IInstaller installer,
IProvisioner provisioner,
IRestorer restorer,
IStatistics statistics,
StateMachine<ServerState, ServerTrigger> 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<ServerState, ServerTrigger>(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();
}
}

View File

@@ -0,0 +1,3 @@
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
public record ServerCrash();

View File

@@ -0,0 +1,3 @@
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
public record ServerStats();

View File

@@ -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<string> OnOutput => OnOutputSubject.ToAsyncObservable();
public IAsyncObservable<string> OnInput => OnInputSubject.ToAsyncObservable();
private readonly AsyncSubject<string> OnOutputSubject = new();
private readonly AsyncSubject<string> OnInputSubject = new();
private readonly ConcurrentList<string> OutputCache = new();
private readonly DockerClient DockerClient;
private readonly ILogger<DockerConsole> 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();
}
}