Refactored/recreated server system. Seperated into sub systems. Still wip

This commit is contained in:
2025-05-29 21:56:38 +02:00
parent f2771acb49
commit b955bd3527
32 changed files with 1642 additions and 1174 deletions

View File

@@ -0,0 +1,183 @@
using Microsoft.AspNetCore.SignalR;
using MoonCore.Exceptions;
using MoonlightServers.Daemon.Http.Hubs;
using MoonlightServers.Daemon.Models.Cache;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem;
public class Server : IAsyncDisposable
{
public ServerConfiguration Configuration { get; set; }
public CancellationToken TaskCancellation => TaskCancellationSource.Token;
internal StateMachine<ServerState, ServerTrigger> StateMachine { get; private set; }
private CancellationTokenSource TaskCancellationSource;
private Dictionary<Type, ServerSubSystem> SubSystems = new();
private ServerState InternalState = ServerState.Offline;
private readonly IHubContext<ServerWebSocketHub> HubContext;
private readonly IServiceScope ServiceScope;
private readonly ILoggerFactory LoggerFactory;
private readonly ILogger Logger;
public Server(
ServerConfiguration configuration,
IServiceScope serviceScope,
IHubContext<ServerWebSocketHub> hubContext
)
{
Configuration = configuration;
ServiceScope = serviceScope;
HubContext = hubContext;
TaskCancellationSource = new CancellationTokenSource();
LoggerFactory = serviceScope.ServiceProvider.GetRequiredService<ILoggerFactory>();
Logger = LoggerFactory.CreateLogger($"Server {Configuration.Id}");
StateMachine = new StateMachine<ServerState, ServerTrigger>(
() => InternalState,
state => InternalState = state,
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);
// Configure task reset when server goes offline
StateMachine.Configure(ServerState.Offline)
.OnEntryAsync(async () =>
{
if (!TaskCancellationSource.IsCancellationRequested)
await TaskCancellationSource.CancelAsync();
TaskCancellationSource = new();
});
// Setup websocket notify for state changes
StateMachine.OnTransitionedAsync(async transition =>
{
await HubContext.Clients
.Group(Configuration.Id.ToString())
.SendAsync("StateChanged", transition.Destination.ToString());
});
}
public async Task Initialize(Type[] subSystemTypes)
{
foreach (var type in subSystemTypes)
{
var logger = LoggerFactory.CreateLogger($"Server {Configuration.Id} - {type.Name}");
var subSystem = ActivatorUtilities.CreateInstance(
ServiceScope.ServiceProvider,
type,
this,
logger
) as ServerSubSystem;
if (subSystem == null)
{
Logger.LogError("Unable to construct server sub system: {name}", type.Name);
continue;
}
SubSystems.Add(type, subSystem);
}
foreach (var type in SubSystems.Keys)
{
try
{
await SubSystems[type].Initialize();
}
catch (Exception e)
{
Logger.LogError("An unhandled error occured while initializing sub system {name}: {e}", type.Name, e);
}
}
}
public async Task Trigger(ServerTrigger trigger)
{
if (!StateMachine.CanFire(trigger))
throw new HttpApiException($"The trigger {trigger} is not supported during the state {StateMachine.State}", 400);
await StateMachine.FireAsync(trigger);
}
public async Task Delete()
{
foreach (var subSystem in SubSystems.Values)
await subSystem.Delete();
}
// This method completely bypasses the state machine.
// Using this method without any checks will lead to
// broken server states. Use with caution
public void OverrideState(ServerState state)
{
InternalState = state;
}
public T? GetSubSystem<T>() where T : ServerSubSystem
{
var type = typeof(T);
var subSystem = SubSystems.GetValueOrDefault(type);
if (subSystem == null)
return null;
return subSystem as T;
}
public T GetRequiredSubSystem<T>() where T : ServerSubSystem
{
var subSystem = GetSubSystem<T>();
if (subSystem == null)
throw new AggregateException("Unable to resolve requested sub system");
return subSystem;
}
public async ValueTask DisposeAsync()
{
if (!TaskCancellationSource.IsCancellationRequested)
await TaskCancellationSource.CancelAsync();
foreach (var subSystem in SubSystems.Values)
await subSystem.DisposeAsync();
ServiceScope.Dispose();
}
}

View File

@@ -0,0 +1,10 @@
namespace MoonlightServers.Daemon.ServerSystem;
public enum ServerState
{
Offline = 0,
Starting = 1,
Online = 2,
Stopping = 3,
Installing = 4
}

View File

@@ -0,0 +1,27 @@
using MoonlightServers.Daemon.Models.Cache;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem;
public abstract class ServerSubSystem : IAsyncDisposable
{
protected Server Server { get; private set; }
protected ServerConfiguration Configuration => Server.Configuration;
protected ILogger Logger { get; private set; }
protected StateMachine<ServerState, ServerTrigger> StateMachine => Server.StateMachine;
protected ServerSubSystem(Server server, ILogger logger)
{
Server = server;
Logger = logger;
}
public virtual Task Initialize()
=> Task.CompletedTask;
public virtual Task Delete()
=> Task.CompletedTask;
public virtual ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -0,0 +1,12 @@
namespace MoonlightServers.Daemon.ServerSystem;
public enum ServerTrigger
{
Start = 0,
Stop = 1,
Kill = 2,
Install = 3,
Exited = 4,
OnlineDetected = 5,
FailSafe = 6
}

View File

@@ -0,0 +1,145 @@
using System.Text;
using Docker.DotNet;
using Docker.DotNet.Models;
using Microsoft.AspNetCore.SignalR;
using MoonlightServers.Daemon.Http.Hubs;
namespace MoonlightServers.Daemon.ServerSystem.SubSystems;
public class ConsoleSubSystem : ServerSubSystem
{
public event Func<string, Task>? OnOutput;
public event Func<string, Task>? OnInput;
private MultiplexedStream? Stream;
private readonly List<string> OutputCache = new();
private readonly IHubContext<ServerWebSocketHub> HubContext;
private readonly DockerClient DockerClient;
public ConsoleSubSystem(
Server server,
ILogger logger,
IHubContext<ServerWebSocketHub> hubContext,
DockerClient dockerClient
) : base(server, logger)
{
HubContext = hubContext;
DockerClient = dockerClient;
}
public override Task Initialize()
{
OnInput += async content =>
{
if(Stream == null)
return;
var contentBuffer = Encoding.UTF8.GetBytes(content);
await Stream.WriteAsync(contentBuffer, 0, contentBuffer.Length, Server.TaskCancellation);
};
return Task.CompletedTask;
}
public async Task Attach(string containerId)
{
Stream = await DockerClient.Containers.AttachContainerAsync(containerId,
true,
new ContainerAttachParameters()
{
Stderr = true,
Stdin = true,
Stdout = true,
Stream = true
},
Server.TaskCancellation
);
// Reading
Task.Run(async () =>
{
while (!Server.TaskCancellation.IsCancellationRequested)
{
var buffer = new byte[1024];
try
{
var readResult = await Stream.ReadOutputAsync(
buffer,
0,
buffer.Length,
Server.TaskCancellation
);
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 WriteOutput(decodedText);
}
catch (TaskCanceledException)
{
// Ignored
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception e)
{
Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", e);
}
}
// Reset stream so no further inputs will be piped to it
Stream = null;
Logger.LogDebug("Disconnected from container stream");
});
}
public async Task WriteOutput(string output)
{
lock (OutputCache)
{
// Shrink cache if it exceeds the maximum
if (OutputCache.Count > 400)
OutputCache.RemoveRange(0, 100);
OutputCache.Add(output);
}
if (OnOutput != null)
await OnOutput.Invoke(output);
await HubContext.Clients
.Group(Configuration.Id.ToString())
.SendAsync("ConsoleOutput", output);
}
public async Task WriteMoonlight(string output)
{
await WriteOutput($"\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m\x1b[38;5;250m\x1b[3m {output}\x1b[0m\n\r");
}
public async Task WriteInput(string input)
{
if (OnInput != null)
await OnInput.Invoke(input);
}
public Task<string[]> RetrieveCache()
{
string[] result;
lock (OutputCache)
result = OutputCache.ToArray();
return Task.FromResult(result);
}
}

View File

@@ -0,0 +1,19 @@
namespace MoonlightServers.Daemon.ServerSystem.SubSystems;
public class DebugSubSystem : ServerSubSystem
{
public DebugSubSystem(Server server, ILogger logger) : base(server, logger)
{
}
public override Task Initialize()
{
StateMachine.OnTransitioned(transition =>
{
Logger.LogTrace("State: {state} via {trigger}", transition.Destination, transition.Trigger);
});
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,238 @@
using Docker.DotNet;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Extensions;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.ServerSystem.SubSystems;
public class InstallationSubSystem : ServerSubSystem
{
public string? CurrentContainerId { get; set; }
private readonly DockerClient DockerClient;
private readonly RemoteService RemoteService;
private readonly DockerImageService DockerImageService;
private readonly AppConfiguration AppConfiguration;
public InstallationSubSystem(
Server server,
ILogger logger,
DockerClient dockerClient,
RemoteService remoteService,
DockerImageService dockerImageService,
AppConfiguration appConfiguration
) : base(server, logger)
{
DockerClient = dockerClient;
RemoteService = remoteService;
DockerImageService = dockerImageService;
AppConfiguration = appConfiguration;
}
public override Task Initialize()
{
StateMachine.Configure(ServerState.Installing)
.OnEntryAsync(HandleProvision);
StateMachine.Configure(ServerState.Installing)
.OnExitAsync(HandleDeprovision);
return Task.CompletedTask;
}
#region Provision
private async Task HandleProvision()
{
try
{
await Provision();
}
catch (Exception e)
{
Logger.LogError("An error occured while provisioning installation: {e}", e);
await StateMachine.FireAsync(ServerTrigger.FailSafe);
}
}
private async Task Provision()
{
// What will happen here:
// 1. Remove possible existing container
// 2. Fetch latest configuration & install configuration
// 3. Ensure the storage location exists
// 4. Copy script to set location
// 5. Ensure the docker image has been downloaded
// 6. Create the docker container
// 7. Attach the console
// 8. Start the container
// Define some shared variables:
var containerName = $"moonlight-install-{Configuration.Id}";
var consoleSubSystem = Server.GetRequiredSubSystem<ConsoleSubSystem>();
// Reset container tracking id, so if we kill an old container it won't
// trigger an Exited event :>
CurrentContainerId = null;
// 1. Remove possible existing container
try
{
var existingContainer = await DockerClient.Containers
.InspectContainerAsync(containerName);
if (existingContainer.State.Running)
{
Logger.LogDebug("Killing old docker container");
await consoleSubSystem.WriteMoonlight("Killing old container");
await DockerClient.Containers.KillContainerAsync(existingContainer.ID, new());
}
Logger.LogDebug("Removing old docker container");
await consoleSubSystem.WriteMoonlight("Removing old container");
await DockerClient.Containers.RemoveContainerAsync(existingContainer.ID, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
// 2. Fetch latest configuration
Logger.LogDebug("Fetching latest configuration from panel");
await consoleSubSystem.WriteMoonlight("Updating configuration");
var serverData = await RemoteService.GetServer(Configuration.Id);
var latestConfiguration = serverData.ToServerConfiguration();
Server.Configuration = latestConfiguration;
var installData = await RemoteService.GetServerInstallation(Configuration.Id);
// 3. Ensure the storage location exists
Logger.LogDebug("Ensuring storage");
var storageSubSystem = Server.GetRequiredSubSystem<StorageSubSystem>();
if (!await storageSubSystem.IsRuntimeVolumeReady())
{
Logger.LogDebug("Unable to continue provision because the server file system isn't ready");
await consoleSubSystem.WriteMoonlight("Server file system is not ready yet. Try again later");
await StateMachine.FireAsync(ServerTrigger.FailSafe);
return;
}
var runtimePath = await storageSubSystem.GetRuntimeHostPath();
var installPath = await storageSubSystem.EnsureInstallVolume();
// 4. Copy script to location
var content = installData.Script.Replace("\r\n", "\n");
await File.WriteAllTextAsync(Path.Combine(installPath, "install.sh"), content);
// 5. Ensure the docker image is downloaded
Logger.LogDebug("Downloading docker image");
await consoleSubSystem.WriteMoonlight("Downloading docker image");
await DockerImageService.Download(installData.DockerImage,
async updateMessage => { await consoleSubSystem.WriteMoonlight(updateMessage); });
Logger.LogDebug("Docker image downloaded");
await consoleSubSystem.WriteMoonlight("Downloaded docker image");
// 6. Create the docker container
Logger.LogDebug("Creating docker container");
await consoleSubSystem.WriteMoonlight("Creating container");
var containerParams = Configuration.ToInstallationCreateParameters(
AppConfiguration,
runtimePath,
installPath,
containerName,
installData.DockerImage,
installData.Shell
);
var creationResult = await DockerClient.Containers.CreateContainerAsync(containerParams);
CurrentContainerId = creationResult.ID;
// 7. Attach the console
Logger.LogDebug("Attaching console");
await consoleSubSystem.Attach(CurrentContainerId);
// 8. Start the docker container
Logger.LogDebug("Starting docker container");
await consoleSubSystem.WriteMoonlight("Starting container");
await DockerClient.Containers.StartContainerAsync(containerName, new());
}
#endregion
#region Deprovision
private async Task HandleDeprovision()
{
try
{
await Deprovision();
}
catch (Exception e)
{
Logger.LogError("An error occured while deprovisioning installation: {e}", e);
await StateMachine.FireAsync(ServerTrigger.FailSafe);
}
}
private async Task Deprovision()
{
// Handle possible unknown container id calls
if (string.IsNullOrEmpty(CurrentContainerId))
{
Logger.LogDebug("Skipping deprovisioning as the current container id is not set");
return;
}
var consoleSubSystem = Server.GetRequiredSubSystem<ConsoleSubSystem>();
// Destroy container
try
{
Logger.LogDebug("Removing docker container");
await consoleSubSystem.WriteMoonlight("Removing container");
await DockerClient.Containers.RemoveContainerAsync(CurrentContainerId, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
CurrentContainerId = null;
// Remove install volume
var storageSubSystem = Server.GetRequiredSubSystem<StorageSubSystem>();
Logger.LogDebug("Removing installation data");
await consoleSubSystem.WriteMoonlight("Removing installation data");
await storageSubSystem.DeleteInstallVolume();
}
#endregion
}

View File

@@ -0,0 +1,45 @@
using System.Text.RegularExpressions;
namespace MoonlightServers.Daemon.ServerSystem.SubSystems;
public class OnlineDetectionService : ServerSubSystem
{
// We are compiling the regex when the first output has been received
// and resetting it after the server has stopped to maximize the performance
// but allowing the startup detection string to change :>
private Regex? CompiledRegex = null;
public OnlineDetectionService(Server server, ILogger logger) : base(server, logger)
{
}
public override Task Initialize()
{
var consoleSubSystem = Server.GetRequiredSubSystem<ConsoleSubSystem>();
consoleSubSystem.OnOutput += async line =>
{
if(StateMachine.State != ServerState.Starting)
return;
if (CompiledRegex == null)
CompiledRegex = new Regex(Configuration.OnlineDetection, RegexOptions.Compiled);
if (Regex.Matches(line, Configuration.OnlineDetection).Count == 0)
return;
await StateMachine.FireAsync(ServerTrigger.OnlineDetected);
};
StateMachine.Configure(ServerState.Offline)
.OnEntryAsync(_ =>
{
CompiledRegex = null;
return Task.CompletedTask;
});
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,219 @@
using Docker.DotNet;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Extensions;
using MoonlightServers.Daemon.Services;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.SubSystems;
public class ProvisionSubSystem : ServerSubSystem
{
public string? CurrentContainerId { get; set; }
private readonly DockerClient DockerClient;
private readonly AppConfiguration AppConfiguration;
private readonly RemoteService RemoteService;
private readonly DockerImageService DockerImageService;
public ProvisionSubSystem(
Server server,
ILogger logger,
DockerClient dockerClient,
AppConfiguration appConfiguration,
RemoteService remoteService,
DockerImageService dockerImageService
) : base(server, logger)
{
DockerClient = dockerClient;
AppConfiguration = appConfiguration;
RemoteService = remoteService;
DockerImageService = dockerImageService;
}
public override Task Initialize()
{
StateMachine.Configure(ServerState.Starting)
.OnEntryFromAsync(ServerTrigger.Start, HandleProvision);
StateMachine.Configure(ServerState.Offline)
.OnEntryAsync(HandleDeprovision);
return Task.CompletedTask;
}
#region Provisioning
private async Task HandleProvision()
{
try
{
await Provision();
}
catch (Exception e)
{
Logger.LogError("An error occured while provisioning server: {e}", e);
await StateMachine.FireAsync(ServerTrigger.FailSafe);
}
}
private async Task Provision()
{
// What will happen here:
// 1. Remove possible existing container
// 2. Fetch latest configuration
// 3. Ensure the storage location exists
// 4. Ensure the docker image has been downloaded
// 5. Create the docker container
// 6. Attach the console
// 7. Start the container
// Define some shared variables:
var containerName = $"moonlight-runtime-{Configuration.Id}";
var consoleSubSystem = Server.GetRequiredSubSystem<ConsoleSubSystem>();
// Reset container tracking id, so if we kill an old container it won't
// trigger an Exited event :>
CurrentContainerId = null;
// 1. Remove possible existing container
try
{
var existingContainer = await DockerClient.Containers
.InspectContainerAsync(containerName);
if (existingContainer.State.Running)
{
Logger.LogDebug("Killing old docker container");
await consoleSubSystem.WriteMoonlight("Killing old container");
await DockerClient.Containers.KillContainerAsync(existingContainer.ID, new());
}
Logger.LogDebug("Removing old docker container");
await consoleSubSystem.WriteMoonlight("Removing old container");
await DockerClient.Containers.RemoveContainerAsync(existingContainer.ID, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
// 2. Fetch latest configuration
Logger.LogDebug("Fetching latest configuration from panel");
await consoleSubSystem.WriteMoonlight("Updating configuration");
var serverData = await RemoteService.GetServer(Configuration.Id);
var latestConfiguration = serverData.ToServerConfiguration();
Server.Configuration = latestConfiguration;
// 3. Ensure the storage location exists
Logger.LogDebug("Ensuring storage");
var storageSubSystem = Server.GetRequiredSubSystem<StorageSubSystem>();
if (!await storageSubSystem.IsRuntimeVolumeReady())
{
Logger.LogDebug("Unable to continue provision because the server file system isn't ready");
await consoleSubSystem.WriteMoonlight("Server file system is not ready yet. Try again later");
await StateMachine.FireAsync(ServerTrigger.FailSafe);
return;
}
var volumePath = await storageSubSystem.GetRuntimeHostPath();
// 4. Ensure the docker image is downloaded
Logger.LogDebug("Downloading docker image");
await consoleSubSystem.WriteMoonlight("Downloading docker image");
await DockerImageService.Download(Configuration.DockerImage, async updateMessage =>
{
await consoleSubSystem.WriteMoonlight(updateMessage);
});
Logger.LogDebug("Docker image downloaded");
await consoleSubSystem.WriteMoonlight("Downloaded docker image");
// 5. Create the docker container
Logger.LogDebug("Creating docker container");
await consoleSubSystem.WriteMoonlight("Creating container");
var containerParams = Configuration.ToRuntimeCreateParameters(
AppConfiguration,
volumePath,
containerName
);
var creationResult = await DockerClient.Containers.CreateContainerAsync(containerParams);
CurrentContainerId = creationResult.ID;
// 6. Attach the console
Logger.LogDebug("Attaching console");
await consoleSubSystem.Attach(CurrentContainerId);
// 7. Start the docker container
Logger.LogDebug("Starting docker container");
await consoleSubSystem.WriteMoonlight("Starting container");
await DockerClient.Containers.StartContainerAsync(containerName, new());
}
#endregion
#region Deprovision
private async Task HandleDeprovision(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
try
{
await Deprovision();
}
catch (Exception e)
{
Logger.LogError("An error occured while provisioning server: {e}", e);
await StateMachine.FireAsync(ServerTrigger.FailSafe);
}
}
private async Task Deprovision()
{
// Handle possible unknown container id calls
if (string.IsNullOrEmpty(CurrentContainerId))
{
Logger.LogDebug("Skipping deprovisioning as the current container id is not set");
return;
}
var consoleSubSystem = Server.GetRequiredSubSystem<ConsoleSubSystem>();
// Destroy container
try
{
Logger.LogDebug("Removing docker container");
await consoleSubSystem.WriteMoonlight("Removing container");
await DockerClient.Containers.RemoveContainerAsync(CurrentContainerId, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
CurrentContainerId = null;
}
#endregion
}

View File

@@ -0,0 +1,110 @@
using Docker.DotNet;
namespace MoonlightServers.Daemon.ServerSystem.SubSystems;
public class RestoreSubSystem : ServerSubSystem
{
private readonly DockerClient DockerClient;
public RestoreSubSystem(Server server, ILogger logger, DockerClient dockerClient) : base(server, logger)
{
DockerClient = dockerClient;
}
public override async Task Initialize()
{
Logger.LogDebug("Searching for restorable container");
// Handle possible runtime container
var runtimeContainerName = $"moonlight-runtime-{Configuration.Id}";
try
{
var runtimeContainer = await DockerClient.Containers.InspectContainerAsync(runtimeContainerName);
if (runtimeContainer.State.Running)
{
var provisionSubSystem = Server.GetRequiredSubSystem<ProvisionSubSystem>();
// Override values
provisionSubSystem.CurrentContainerId = runtimeContainer.ID;
Server.OverrideState(ServerState.Online);
var consoleSubSystem = Server.GetRequiredSubSystem<ConsoleSubSystem>();
var logStream = await DockerClient.Containers.GetContainerLogsAsync(runtimeContainerName, true, new ()
{
Follow = false,
ShowStderr = true,
ShowStdout = true
});
var (standardOutput, standardError) = await logStream.ReadOutputToEndAsync(CancellationToken.None);
// We split up the read output data into their lines to prevent overloading
// the console by one large string
foreach (var line in standardOutput.Split("\n"))
await consoleSubSystem.WriteOutput(line + "\n");
foreach (var line in standardError.Split("\n"))
await consoleSubSystem.WriteOutput(line + "\n");
await consoleSubSystem.Attach(provisionSubSystem.CurrentContainerId);
Logger.LogInformation("Restored runtime container successfully");
return;
}
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
// Handle possible installation container
var installContainerName = $"moonlight-install-{Configuration.Id}";
try
{
var installContainer = await DockerClient.Containers.InspectContainerAsync(installContainerName);
if (installContainer.State.Running)
{
var installationSubSystem = Server.GetRequiredSubSystem<InstallationSubSystem>();
// Override values
installationSubSystem.CurrentContainerId = installContainer.ID;
Server.OverrideState(ServerState.Installing);
var consoleSubSystem = Server.GetRequiredSubSystem<ConsoleSubSystem>();
var logStream = await DockerClient.Containers.GetContainerLogsAsync(installContainerName, true, new ()
{
Follow = false,
ShowStderr = true,
ShowStdout = true
});
var (standardOutput, standardError) = await logStream.ReadOutputToEndAsync(CancellationToken.None);
// We split up the read output data into their lines to prevent overloading
// the console by one large string
foreach (var line in standardOutput.Split("\n"))
await consoleSubSystem.WriteOutput(line + "\n");
foreach (var line in standardError.Split("\n"))
await consoleSubSystem.WriteOutput(line + "\n");
await consoleSubSystem.Attach(installationSubSystem.CurrentContainerId);
return;
}
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
}
}

View File

@@ -0,0 +1,85 @@
using Docker.DotNet;
namespace MoonlightServers.Daemon.ServerSystem.SubSystems;
public class ShutdownSubSystem : ServerSubSystem
{
private readonly DockerClient DockerClient;
public ShutdownSubSystem(
Server server,
ILogger logger,
DockerClient dockerClient
) : base(server, logger)
{
DockerClient = dockerClient;
}
public override Task Initialize()
{
StateMachine.Configure(ServerState.Stopping)
.OnEntryFromAsync(ServerTrigger.Stop, HandleStop)
.OnEntryFromAsync(ServerTrigger.Kill, HandleKill);
return Task.CompletedTask;
}
#region Stopping
private async Task HandleStop()
{
try
{
await Stop();
}
catch (Exception e)
{
Logger.LogError("An error occured while stopping container: {e}", e);
await StateMachine.FireAsync(ServerTrigger.FailSafe);
}
}
private async Task Stop()
{
var provisionSubSystem = Server.GetRequiredSubSystem<ProvisionSubSystem>();
// Handle signal stopping
if (Configuration.StopCommand.StartsWith('^'))
{
await DockerClient.Containers.KillContainerAsync(provisionSubSystem.CurrentContainerId, new()
{
Signal = Configuration.StopCommand.Replace("^", "")
});
}
else // Handle input stopping
{
var consoleSubSystem = Server.GetRequiredSubSystem<ConsoleSubSystem>();
await consoleSubSystem.WriteInput($"{Configuration.StopCommand}\n\r");
}
}
#endregion
private async Task HandleKill()
{
try
{
await Kill();
}
catch (Exception e)
{
Logger.LogError("An error occured while killing container: {e}", e);
await StateMachine.FireAsync(ServerTrigger.FailSafe);
}
}
private async Task Kill()
{
var provisionSubSystem = Server.GetRequiredSubSystem<ProvisionSubSystem>();
await DockerClient.Containers.KillContainerAsync(
provisionSubSystem.CurrentContainerId,
new()
);
}
}

View File

@@ -0,0 +1,147 @@
using MoonCore.Exceptions;
using MoonCore.Unix.SecureFs;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Helpers;
namespace MoonlightServers.Daemon.ServerSystem.SubSystems;
public class StorageSubSystem : ServerSubSystem
{
private readonly AppConfiguration AppConfiguration;
private SecureFileSystem SecureFileSystem;
private ServerFileSystem ServerFileSystem;
private bool IsInitialized = false;
public StorageSubSystem(
Server server,
ILogger logger,
AppConfiguration appConfiguration
) : base(server, logger)
{
AppConfiguration = appConfiguration;
}
public override async Task Initialize()
{
Logger.LogDebug("Lazy initializing server file system");
Task.Run(async () =>
{
try
{
await EnsureRuntimeVolume();
var hostPath = await GetRuntimeHostPath();
SecureFileSystem = new(hostPath);
ServerFileSystem = new(SecureFileSystem);
IsInitialized = true;
}
catch (Exception e)
{
Logger.LogError("An unhandled error occured while lazy initializing server file system: {e}", e);
}
});
}
#region Runtime
public Task<ServerFileSystem> GetFileSystem()
{
if (!IsInitialized)
throw new HttpApiException("The file system is still initializing. Please try again later", 503);
return Task.FromResult(ServerFileSystem);
}
public Task<bool> IsRuntimeVolumeReady()
{
return Task.FromResult(IsInitialized);
}
private async Task EnsureRuntimeVolume()
{
var path = await GetRuntimeHostPath();
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
var consoleSubSystem = Server.GetRequiredSubSystem<ConsoleSubSystem>();
await consoleSubSystem.WriteMoonlight("Creating virtual disk file. Please be patient");
await Task.Delay(TimeSpan.FromSeconds(8));
await consoleSubSystem.WriteMoonlight("Formatting virtual disk. This can take a bit");
await Task.Delay(TimeSpan.FromSeconds(8));
await consoleSubSystem.WriteMoonlight("Mounting virtual disk. Please be patient");
await Task.Delay(TimeSpan.FromSeconds(3));
await consoleSubSystem.WriteMoonlight("Virtual disk ready");
// TODO: Implement virtual disk
}
public Task<string> GetRuntimeHostPath()
{
var path = Path.Combine(
AppConfiguration.Storage.Volumes,
Configuration.Id.ToString()
);
if (!path.StartsWith('/'))
path = Path.Combine(Directory.GetCurrentDirectory(), path);
return Task.FromResult(path);
}
#endregion
#region Installation
public async Task<string> EnsureInstallVolume()
{
var path = await GetInstallHostPath();
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
return path;
}
public Task<string> GetInstallHostPath()
{
var path = Path.Combine(
AppConfiguration.Storage.Install,
Configuration.Id.ToString()
);
if (!path.StartsWith('/'))
path = Path.Combine(Directory.GetCurrentDirectory(), path);
return Task.FromResult(path);
}
public async Task DeleteInstallVolume()
{
var path = await GetInstallHostPath();
if(!Directory.Exists(path))
return;
Directory.Delete(path, true);
}
#endregion
public override ValueTask DisposeAsync()
{
if (IsInitialized)
{
if(!SecureFileSystem.IsDisposed)
SecureFileSystem.Dispose();
}
return ValueTask.CompletedTask;
}
}