Refactored/recreated server system. Seperated into sub systems. Still wip
This commit is contained in:
183
MoonlightServers.Daemon/ServerSystem/Server.cs
Normal file
183
MoonlightServers.Daemon/ServerSystem/Server.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
10
MoonlightServers.Daemon/ServerSystem/ServerState.cs
Normal file
10
MoonlightServers.Daemon/ServerSystem/ServerState.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public enum ServerState
|
||||
{
|
||||
Offline = 0,
|
||||
Starting = 1,
|
||||
Online = 2,
|
||||
Stopping = 3,
|
||||
Installing = 4
|
||||
}
|
||||
27
MoonlightServers.Daemon/ServerSystem/ServerSubSystem.cs
Normal file
27
MoonlightServers.Daemon/ServerSystem/ServerSubSystem.cs
Normal 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;
|
||||
}
|
||||
12
MoonlightServers.Daemon/ServerSystem/ServerTrigger.cs
Normal file
12
MoonlightServers.Daemon/ServerSystem/ServerTrigger.cs
Normal 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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user