Cleaned up interfaces. Extracted server state machine trigger handler to seperated classes. Removed legacy code
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
|
||||
public enum ServerState
|
||||
{
|
||||
@@ -6,5 +6,6 @@ public enum ServerState
|
||||
Starting = 1,
|
||||
Online = 2,
|
||||
Stopping = 3,
|
||||
Installing = 4
|
||||
Installing = 4,
|
||||
Locked = 5
|
||||
}
|
||||
12
MoonlightServers.Daemon/ServerSystem/Enums/ServerTrigger.cs
Normal file
12
MoonlightServers.Daemon/ServerSystem/Enums/ServerTrigger.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
|
||||
public enum ServerTrigger
|
||||
{
|
||||
Start = 0,
|
||||
Stop = 1,
|
||||
Kill = 2,
|
||||
DetectOnline = 3,
|
||||
Install = 4,
|
||||
Fail = 5,
|
||||
Exited = 6
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
using Stateless;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
|
||||
|
||||
public class ShutdownHandler : IServerStateHandler
|
||||
{
|
||||
private readonly ServerContext ServerContext;
|
||||
|
||||
public ShutdownHandler(ServerContext serverContext)
|
||||
{
|
||||
ServerContext = serverContext;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
|
||||
{
|
||||
// Filter (we only want to handle exists from the runtime, so we filter out the installing state)
|
||||
if (transition is
|
||||
{
|
||||
Destination: ServerState.Offline,
|
||||
Source: not ServerState.Installing,
|
||||
Trigger: ServerTrigger.Exited
|
||||
})
|
||||
return;
|
||||
|
||||
// Plan:
|
||||
// 1. Handle possible crash
|
||||
// 2. Remove runtime
|
||||
|
||||
// 1. Handle possible crash
|
||||
// TODO: Handle crash here
|
||||
|
||||
// 2. Remove runtime
|
||||
|
||||
await ServerContext.Server.Runtime.DestroyAsync();
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
=> ValueTask.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
using Stateless;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
|
||||
|
||||
public class StartupHandler : IServerStateHandler
|
||||
{
|
||||
private IAsyncDisposable? ExitSubscription;
|
||||
|
||||
private readonly ServerContext Context;
|
||||
private Server Server => Context.Server;
|
||||
|
||||
public StartupHandler(ServerContext context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
|
||||
{
|
||||
// Filter
|
||||
if (transition is not {Source: ServerState.Offline, Destination: ServerState.Starting, Trigger: ServerTrigger.Start})
|
||||
return;
|
||||
|
||||
// Plan:
|
||||
// 1. Fetch latest configuration
|
||||
// 2. Check if file system exists
|
||||
// 3. Check if file system is mounted
|
||||
// 4. Run file system checks
|
||||
// 5. Create runtime
|
||||
// 6. Attach console
|
||||
// 7. Attach statistics collector
|
||||
// 8. Create online detector
|
||||
// 9. Start runtime
|
||||
|
||||
// 1. Fetch latest configuration
|
||||
// TODO
|
||||
|
||||
// 2. Check if file system exists
|
||||
if (!await Server.RuntimeFileSystem.CheckExistsAsync())
|
||||
await Server.RuntimeFileSystem.CreateAsync();
|
||||
|
||||
// 3. Check if file system is mounted
|
||||
if (!await Server.RuntimeFileSystem.CheckMountedAsync())
|
||||
await Server.RuntimeFileSystem.CheckMountedAsync();
|
||||
|
||||
// 4. Run file system checks
|
||||
await Server.RuntimeFileSystem.PerformChecksAsync();
|
||||
|
||||
// 5. Create runtime
|
||||
var hostPath = await Server.RuntimeFileSystem.GetPathAsync();
|
||||
|
||||
await Server.Runtime.CreateAsync(hostPath);
|
||||
|
||||
if (ExitSubscription == null)
|
||||
{
|
||||
ExitSubscription = await Server.Runtime.SubscribeExited(OnRuntimeExited);
|
||||
}
|
||||
|
||||
// 6. Attach console
|
||||
|
||||
await Server.Console.AttachRuntimeAsync();
|
||||
|
||||
// 7. Attach statistics collector
|
||||
|
||||
await Server.Statistics.AttachRuntimeAsync();
|
||||
|
||||
// 8. Create online detector
|
||||
|
||||
await Server.OnlineDetector.CreateAsync();
|
||||
await Server.OnlineDetector.DestroyAsync();
|
||||
|
||||
// 9. Start runtime
|
||||
|
||||
await Server.Runtime.StartAsync();
|
||||
}
|
||||
|
||||
private async Task OnRuntimeExited(int exitCode)
|
||||
{
|
||||
// TODO: Notify the crash handler component of the exit code
|
||||
|
||||
await Server.StateMachine.FireAsync(ServerTrigger.Exited);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (ExitSubscription != null)
|
||||
await ExitSubscription.DisposeAsync();
|
||||
}
|
||||
}
|
||||
72
MoonlightServers.Daemon/ServerSystem/Interfaces/IConsole.cs
Normal file
72
MoonlightServers.Daemon/ServerSystem/Interfaces/IConsole.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IConsole : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes to the standard input of the console. If attached to the runtime when using docker for example this
|
||||
/// would write into the containers standard input.
|
||||
/// <remarks>This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required</remarks>
|
||||
/// </summary>
|
||||
/// <param name="content">The content to write</param>
|
||||
/// <returns></returns>
|
||||
public Task WriteStdInAsync(string content);
|
||||
/// <summary>
|
||||
/// Writes to the standard output of the console. If attached to the runtime when using docker for example this
|
||||
/// would write into the containers standard output.
|
||||
/// <remarks>This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required</remarks>
|
||||
/// </summary>
|
||||
/// <param name="content">The content to write</param>
|
||||
/// <returns></returns>
|
||||
public Task WriteStdOutAsync(string content);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a system message to the standard output with the moonlight console prefix
|
||||
/// <remarks>This method *does* add the newline separator at the end</remarks>
|
||||
/// </summary>
|
||||
/// <param name="content">The content to write into the standard output</param>
|
||||
/// <returns></returns>
|
||||
public Task WriteMoonlightAsync(string content);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches the console to the runtime environment
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task AttachRuntimeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Attaches the console to the installation environment
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task AttachInstallationAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Fetches all output from the runtime environment and write them into the cache without triggering any events
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task FetchRuntimeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Fetches all output from the installation environment and write them into the cache without triggering any events
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task FetchInstallationAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Clears the cache of the standard output received by the environments
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task ClearCacheAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content from the standard output cache
|
||||
/// </summary>
|
||||
/// <returns>The content from the cache</returns>
|
||||
public Task<IEnumerable<string>> GetCacheAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to standard output receive events
|
||||
/// </summary>
|
||||
/// <param name="callback">Callback which will be invoked whenever a new line is received</param>
|
||||
/// <returns>Subscription disposable to unsubscribe from the event</returns>
|
||||
public Task<IAsyncDisposable> SubscribeStdOutAsync(Func<string, Task> callback);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IFileSystem : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the path of the file system on the host operating system to be reused by other components
|
||||
/// </summary>
|
||||
/// <returns>Path to the file systems storage location</returns>
|
||||
public Task<string> GetPathAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the file system exists
|
||||
/// </summary>
|
||||
/// <returns>True if it does exist. False if it doesn't exist</returns>
|
||||
public Task<bool> CheckExistsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the file system is mounted
|
||||
/// </summary>
|
||||
/// <returns>True if its mounted, False if it is not mounted</returns>
|
||||
public Task<bool> CheckMountedAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the file system. E.g. Creating a virtual disk, formatting it
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task CreateAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Performs checks and optimisations on the file system.
|
||||
/// E.g. checking for corrupted files, resizing a virtual disk or adjusting file permissions
|
||||
/// <remarks>Requires <see cref="MountAsync"/> to be called before or the file system to be in a mounted state</remarks>
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task PerformChecksAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Mounts the file system
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task MountAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Unmounts the file system
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task UnmountAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the file system and its contents
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task DestroyAsync();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IInstallation : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the installation environment exists. It doesn't matter if it is currently running or not
|
||||
/// </summary>
|
||||
/// <returns>True if it exists, False if it doesn't</returns>
|
||||
public Task<bool> CheckExistsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the installation environment
|
||||
/// </summary>
|
||||
/// <param name="runtimePath">The host path of the runtime storage location</param>
|
||||
/// <param name="hostPath">The host path of the installation file system</param>
|
||||
/// <returns></returns>
|
||||
public Task CreateAsync(string runtimePath, string hostPath);
|
||||
|
||||
/// <summary>
|
||||
/// Starts the installation
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task StartAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Kills the current installation immediately
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task KillAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Removes the installation. E.g. removes the docker container
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task DestroyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the event when the installation exists
|
||||
/// </summary>
|
||||
/// <param name="callback">The callback to invoke whenever the installation exists</param>
|
||||
/// <returns>Subscription disposable to unsubscribe from the event</returns>
|
||||
public Task<IAsyncDisposable> SubscribeExited(Func<int, Task> callback);
|
||||
|
||||
/// <summary>
|
||||
/// Connects an existing installation to this abstraction in order to restore it.
|
||||
/// E.g. fetching the container id and using it for exit events
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task RestoreAsync();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IOnlineDetector : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the detection engine for the online state
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task CreateAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Handles the detection of the online state based on the received output
|
||||
/// </summary>
|
||||
/// <param name="line">The excerpt of the output</param>
|
||||
/// <returns>True if the detection showed that the server is online. False if the detection didnt find anything</returns>
|
||||
public Task<bool> HandleOutputAsync(string line);
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the detection engine for the online state
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task DestroyAsync();
|
||||
}
|
||||
18
MoonlightServers.Daemon/ServerSystem/Interfaces/IReporter.cs
Normal file
18
MoonlightServers.Daemon/ServerSystem/Interfaces/IReporter.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IReporter : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes both in the server logs as well in the server console the provided message as a status update
|
||||
/// </summary>
|
||||
/// <param name="message">The message to write</param>
|
||||
/// <returns></returns>
|
||||
public Task StatusAsync(string message);
|
||||
|
||||
/// <summary>
|
||||
/// Writes both in the server logs as well in the server console the provided message as an error
|
||||
/// </summary>
|
||||
/// <param name="message">The message to write</param>
|
||||
/// <returns></returns>
|
||||
public Task ErrorAsync(string message);
|
||||
}
|
||||
16
MoonlightServers.Daemon/ServerSystem/Interfaces/IRestorer.cs
Normal file
16
MoonlightServers.Daemon/ServerSystem/Interfaces/IRestorer.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IRestorer : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks for any running runtime environment from which the state can be restored from
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task<bool> HandleRuntimeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Checks for any running installation environment from which the state can be restored from
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task<bool> HandleInstallationAsync();
|
||||
}
|
||||
55
MoonlightServers.Daemon/ServerSystem/Interfaces/IRuntime.cs
Normal file
55
MoonlightServers.Daemon/ServerSystem/Interfaces/IRuntime.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IRuntime : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the runtime does exist. This includes already running instances
|
||||
/// </summary>
|
||||
/// <returns>True if it exists, False if it doesn't</returns>
|
||||
public Task<bool> CheckExistsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the runtime with the specified path as the storage path where the server files should be stored in
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public Task CreateAsync(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Starts the runtime. This requires <see cref="CreateAsync"/> to be called before this function
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task StartAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Performs a live update on the runtime. When this method is called the current server configuration has already been updated
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task UpdateAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Kills the current runtime immediately
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task KillAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the runtime. When implemented using docker this would remove the container used for hosting the runtime
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task DestroyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// This subscribes to the exited event of the runtime
|
||||
/// </summary>
|
||||
/// <param name="callback">The callback gets invoked whenever the runtime exites</param>
|
||||
/// <returns>Subscription disposable to unsubscribe from the event</returns>
|
||||
public Task<IAsyncDisposable> SubscribeExited(Func<int, Task> callback);
|
||||
|
||||
/// <summary>
|
||||
/// Connects an existing runtime to this abstraction in order to restore it.
|
||||
/// E.g. fetching the container id and using it for exit events
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task RestoreAsync();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IServerComponent : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the server component
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task InitializeAsync();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
using Stateless;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IServerStateHandler : IAsyncDisposable
|
||||
{
|
||||
public Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IStatistics : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Attaches the statistics collector to the currently running runtime
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task AttachRuntimeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Attaches the statistics collector to the currently running installation
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task AttachInstallationAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Clears the statistics cache
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task ClearCacheAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the statistics data from the cache
|
||||
/// </summary>
|
||||
/// <returns>All data from the cache</returns>
|
||||
public Task<IEnumerable<StatisticsData>> GetCacheAsync();
|
||||
}
|
||||
11
MoonlightServers.Daemon/ServerSystem/Models/ServerContext.cs
Normal file
11
MoonlightServers.Daemon/ServerSystem/Models/ServerContext.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using MoonlightServers.Daemon.Models.Cache;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
public class ServerContext
|
||||
{
|
||||
public ServerConfiguration Configuration { get; set; }
|
||||
public int Identifier { get; set; }
|
||||
public AsyncServiceScope ServiceScope { get; set; }
|
||||
public Server Server { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
public class StatisticsData
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,57 +1,82 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using MoonCore.Exceptions;
|
||||
using MoonlightServers.Daemon.Http.Hubs;
|
||||
using MoonlightServers.Daemon.Models.Cache;
|
||||
using MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
using Stateless;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public class Server : IAsyncDisposable
|
||||
public partial class Server : IAsyncDisposable
|
||||
{
|
||||
public ServerConfiguration Configuration { get; set; }
|
||||
public CancellationToken TaskCancellation => TaskCancellationSource.Token;
|
||||
internal StateMachine<ServerState, ServerTrigger> StateMachine { get; private set; }
|
||||
private CancellationTokenSource TaskCancellationSource;
|
||||
public int Identifier => InnerContext.Identifier;
|
||||
public ServerContext Context => InnerContext;
|
||||
|
||||
private Dictionary<Type, ServerSubSystem> SubSystems = new();
|
||||
private ServerState InternalState = ServerState.Offline;
|
||||
public IConsole Console { get; }
|
||||
public IFileSystem RuntimeFileSystem { get; }
|
||||
public IFileSystem InstallationFileSystem { get; }
|
||||
public IInstallation Installation { get; }
|
||||
public IOnlineDetector OnlineDetector { get; }
|
||||
public IReporter Reporter { get; }
|
||||
public IRestorer Restorer { get; }
|
||||
public IRuntime Runtime { get; }
|
||||
public IStatistics Statistics { get; }
|
||||
public StateMachine<ServerState, ServerTrigger> StateMachine { get; private set; }
|
||||
|
||||
private readonly IHubContext<ServerWebSocketHub> HubContext;
|
||||
private readonly IServiceScope ServiceScope;
|
||||
private readonly ILoggerFactory LoggerFactory;
|
||||
private readonly IServerStateHandler[] Handlers;
|
||||
|
||||
private readonly IServerComponent[] AllComponents;
|
||||
private readonly ServerContext InnerContext;
|
||||
private readonly ILogger Logger;
|
||||
|
||||
|
||||
public Server(
|
||||
ServerConfiguration configuration,
|
||||
IServiceScope serviceScope,
|
||||
IHubContext<ServerWebSocketHub> hubContext
|
||||
ILogger logger,
|
||||
ServerContext context,
|
||||
IConsole console,
|
||||
IFileSystem runtimeFileSystem,
|
||||
IFileSystem installationFileSystem,
|
||||
IInstallation installation,
|
||||
IOnlineDetector onlineDetector,
|
||||
IReporter reporter,
|
||||
IRestorer restorer,
|
||||
IRuntime runtime,
|
||||
IStatistics statistics,
|
||||
IServerStateHandler[] handlers
|
||||
)
|
||||
{
|
||||
Configuration = configuration;
|
||||
ServiceScope = serviceScope;
|
||||
HubContext = hubContext;
|
||||
Logger = logger;
|
||||
InnerContext = context;
|
||||
Console = console;
|
||||
RuntimeFileSystem = runtimeFileSystem;
|
||||
InstallationFileSystem = installationFileSystem;
|
||||
Installation = installation;
|
||||
OnlineDetector = onlineDetector;
|
||||
Reporter = reporter;
|
||||
Restorer = restorer;
|
||||
Runtime = runtime;
|
||||
Statistics = statistics;
|
||||
|
||||
TaskCancellationSource = new CancellationTokenSource();
|
||||
AllComponents =
|
||||
[
|
||||
Console, RuntimeFileSystem, InstallationFileSystem, Installation, OnlineDetector, Reporter, Restorer,
|
||||
Runtime, Statistics
|
||||
];
|
||||
|
||||
LoggerFactory = serviceScope.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
Logger = LoggerFactory.CreateLogger($"Server {Configuration.Id}");
|
||||
Handlers = handlers;
|
||||
}
|
||||
|
||||
private void ConfigureStateMachine(ServerState initialState)
|
||||
{
|
||||
StateMachine = new StateMachine<ServerState, ServerTrigger>(
|
||||
() => InternalState,
|
||||
state => InternalState = state,
|
||||
FiringMode.Queued
|
||||
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);
|
||||
.PermitReentry(ServerTrigger.Fail);
|
||||
|
||||
StateMachine.Configure(ServerState.Starting)
|
||||
.Permit(ServerTrigger.OnlineDetected, ServerState.Online)
|
||||
.Permit(ServerTrigger.FailSafe, ServerState.Offline)
|
||||
.Permit(ServerTrigger.DetectOnline, ServerState.Online)
|
||||
.Permit(ServerTrigger.Fail, ServerState.Offline)
|
||||
.Permit(ServerTrigger.Exited, ServerState.Offline)
|
||||
.Permit(ServerTrigger.Stop, ServerState.Stopping)
|
||||
.Permit(ServerTrigger.Kill, ServerState.Stopping);
|
||||
@@ -62,128 +87,98 @@ public class Server : IAsyncDisposable
|
||||
.Permit(ServerTrigger.Exited, ServerState.Offline);
|
||||
|
||||
StateMachine.Configure(ServerState.Stopping)
|
||||
.PermitReentry(ServerTrigger.FailSafe)
|
||||
.PermitReentry(ServerTrigger.Fail)
|
||||
.PermitReentry(ServerTrigger.Kill)
|
||||
.Permit(ServerTrigger.Exited, ServerState.Offline);
|
||||
|
||||
StateMachine.Configure(ServerState.Installing)
|
||||
.Permit(ServerTrigger.FailSafe, ServerState.Offline)
|
||||
.Permit(ServerTrigger.Fail, ServerState.Offline) // TODO: Add kill
|
||||
.Permit(ServerTrigger.Exited, ServerState.Offline);
|
||||
|
||||
StateMachine.Configure(ServerState.Offline)
|
||||
.OnEntryAsync(async () =>
|
||||
{
|
||||
// Configure task reset when server goes offline
|
||||
|
||||
if (!TaskCancellationSource.IsCancellationRequested)
|
||||
await TaskCancellationSource.CancelAsync();
|
||||
})
|
||||
.OnExit(() =>
|
||||
{
|
||||
// Activate tasks when the server goes online
|
||||
// If we don't separate the disabling and enabling
|
||||
// of the tasks and would do both it in just the offline handler
|
||||
// we would have edge cases where reconnect loops would already have the new task activated
|
||||
// while they are supposed to shut down. I tested the handling of the state machine,
|
||||
// and it executes on exit before the other listeners from the other sub systems
|
||||
TaskCancellationSource = new();
|
||||
});
|
||||
}
|
||||
|
||||
// Setup websocket notify for state changes
|
||||
private void ConfigureStateMachineEvents()
|
||||
{
|
||||
// Configure the calling of the handlers
|
||||
StateMachine.OnTransitionedAsync(async transition =>
|
||||
{
|
||||
await HubContext.Clients
|
||||
.Group(Configuration.Id.ToString())
|
||||
.SendAsync("StateChanged", transition.Destination.ToString());
|
||||
var hasFailed = false;
|
||||
|
||||
foreach (var handler in Handlers)
|
||||
{
|
||||
try
|
||||
{
|
||||
await handler.ExecuteAsync(transition);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(
|
||||
e,
|
||||
"Handler {name} has thrown an unexpected exception",
|
||||
handler.GetType().FullName
|
||||
);
|
||||
|
||||
hasFailed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!hasFailed)
|
||||
return; // Everything went fine, we can exit now
|
||||
|
||||
// Something has failed, lets check if we can handle the error
|
||||
// via a fail trigger
|
||||
|
||||
if(!StateMachine.CanFire(ServerTrigger.Fail))
|
||||
return;
|
||||
|
||||
// Trigger the fail so the server gets a chance to handle the error softly
|
||||
await StateMachine.FireAsync(ServerTrigger.Fail);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task Initialize(Type[] subSystemTypes)
|
||||
private async Task HandleSaveAsync(Func<Task> callback)
|
||||
{
|
||||
foreach (var type in subSystemTypes)
|
||||
try
|
||||
{
|
||||
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);
|
||||
await callback.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e, "An error occured while handling");
|
||||
|
||||
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);
|
||||
}
|
||||
await StateMachine.FireAsync(ServerTrigger.Fail);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Trigger(ServerTrigger trigger)
|
||||
private async Task HandleIgnoredAsync(Func<Task> callback)
|
||||
{
|
||||
if (!StateMachine.CanFire(trigger))
|
||||
throw new HttpApiException($"The trigger {trigger} is not supported during the state {StateMachine.State}", 400);
|
||||
|
||||
await StateMachine.FireAsync(trigger);
|
||||
try
|
||||
{
|
||||
await callback.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e, "An error occured while handling");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Delete()
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
foreach (var subSystem in SubSystems.Values)
|
||||
await subSystem.Delete();
|
||||
}
|
||||
foreach (var component in AllComponents)
|
||||
await component.InitializeAsync();
|
||||
|
||||
// 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;
|
||||
}
|
||||
var restoredState = ServerState.Offline;
|
||||
|
||||
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;
|
||||
ConfigureStateMachine(restoredState);
|
||||
ConfigureStateMachineEvents();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!TaskCancellationSource.IsCancellationRequested)
|
||||
await TaskCancellationSource.CancelAsync();
|
||||
|
||||
foreach (var subSystem in SubSystems.Values)
|
||||
await subSystem.DisposeAsync();
|
||||
foreach (var handler in Handlers)
|
||||
await handler.DisposeAsync();
|
||||
|
||||
ServiceScope.Dispose();
|
||||
foreach (var component in AllComponents)
|
||||
await component.DisposeAsync();
|
||||
}
|
||||
}
|
||||
66
MoonlightServers.Daemon/ServerSystem/ServerFactory.cs
Normal file
66
MoonlightServers.Daemon/ServerSystem/ServerFactory.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using MoonlightServers.Daemon.Models.Cache;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public class ServerFactory
|
||||
{
|
||||
private readonly IServiceProvider ServiceProvider;
|
||||
|
||||
public ServerFactory(IServiceProvider serviceProvider)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task<Server> Create(ServerConfiguration configuration)
|
||||
{
|
||||
var scope = ServiceProvider.CreateAsyncScope();
|
||||
|
||||
var loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger($"Servers.Instance.{configuration.Id}.{nameof(Server)}");
|
||||
|
||||
var context = scope.ServiceProvider.GetRequiredService<ServerContext>();
|
||||
|
||||
context.Identifier = configuration.Id;
|
||||
context.Configuration = configuration;
|
||||
context.ServiceScope = scope;
|
||||
|
||||
// Define all required components
|
||||
|
||||
IConsole console;
|
||||
IFileSystem runtimeFs;
|
||||
IFileSystem installFs;
|
||||
IInstallation installation;
|
||||
IOnlineDetector onlineDetector;
|
||||
IReporter reporter;
|
||||
IRestorer restorer;
|
||||
IRuntime runtime;
|
||||
IStatistics statistics;
|
||||
|
||||
// Resolve the components
|
||||
// TODO: Add a plugin hook for dynamically resolving components and checking if any is unset
|
||||
|
||||
// Resolve server from di
|
||||
var server = new Server(
|
||||
logger,
|
||||
context,
|
||||
// Now all components
|
||||
console,
|
||||
runtimeFs,
|
||||
installFs,
|
||||
installation,
|
||||
onlineDetector,
|
||||
reporter,
|
||||
restorer,
|
||||
runtime,
|
||||
statistics,
|
||||
// And now all the handlers
|
||||
[]
|
||||
);
|
||||
|
||||
context.Server = server;
|
||||
|
||||
return server;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public enum ServerTrigger
|
||||
{
|
||||
Start = 0,
|
||||
Stop = 1,
|
||||
Kill = 2,
|
||||
Install = 3,
|
||||
Exited = 4,
|
||||
OnlineDetected = 5,
|
||||
FailSafe = 6
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
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 Task Attach(string containerId)
|
||||
{
|
||||
// Reading
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// This loop is here to reconnect to the container if for some reason the container
|
||||
// attach stream fails before the server tasks have been canceled i.e. the before the server
|
||||
// goes offline
|
||||
|
||||
while (!Server.TaskCancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
Stream = await DockerClient.Containers.AttachContainerAsync(containerId,
|
||||
true,
|
||||
new ContainerAttachParameters()
|
||||
{
|
||||
Stderr = true,
|
||||
Stdin = true,
|
||||
Stdout = true,
|
||||
Stream = true
|
||||
},
|
||||
Server.TaskCancellation
|
||||
);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
|
||||
try
|
||||
{
|
||||
// Read while server tasks are not canceled
|
||||
while (!Server.TaskCancellation.IsCancellationRequested)
|
||||
{
|
||||
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);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Stream.Dispose();
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("An error occured while attaching to container: {e}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Reset stream so no further inputs will be piped to it
|
||||
Stream = null;
|
||||
|
||||
Logger.LogDebug("Disconnected from container stream");
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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[0;38;2;255;255;255;48;2;124;28;230m 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);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
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 locations exists
|
||||
|
||||
Logger.LogDebug("Ensuring storage");
|
||||
|
||||
var storageSubSystem = Server.GetRequiredSubSystem<StorageSubSystem>();
|
||||
|
||||
if (!await storageSubSystem.RequestRuntimeVolume())
|
||||
{
|
||||
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 = storageSubSystem.RuntimeVolumePath;
|
||||
|
||||
await storageSubSystem.EnsureInstallVolume();
|
||||
var installPath = storageSubSystem.InstallVolumePath;
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
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. Attach to stats
|
||||
// 8. 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.RequestRuntimeVolume())
|
||||
{
|
||||
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 = storageSubSystem.RuntimeVolumePath;
|
||||
|
||||
// 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. Attach stats stream
|
||||
|
||||
var statsSubSystem = Server.GetRequiredSubSystem<StatsSubSystem>();
|
||||
|
||||
await statsSubSystem.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(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
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
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);
|
||||
|
||||
// Update and attach console
|
||||
|
||||
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);
|
||||
|
||||
// Attach stats
|
||||
var statsSubSystem = Server.GetRequiredSubSystem<StatsSubSystem>();
|
||||
await statsSubSystem.Attach(provisionSubSystem.CurrentContainerId);
|
||||
|
||||
// Done :>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
using Docker.DotNet;
|
||||
using Docker.DotNet.Models;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using MoonlightServers.Daemon.Http.Hubs;
|
||||
using MoonlightServers.DaemonShared.DaemonSide.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.SubSystems;
|
||||
|
||||
public class StatsSubSystem : ServerSubSystem
|
||||
{
|
||||
public ServerStats CurrentStats { get; private set; }
|
||||
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly IHubContext<ServerWebSocketHub> HubContext;
|
||||
|
||||
public StatsSubSystem(
|
||||
Server server,
|
||||
ILogger logger,
|
||||
DockerClient dockerClient,
|
||||
IHubContext<ServerWebSocketHub> hubContext
|
||||
) : base(server, logger)
|
||||
{
|
||||
DockerClient = dockerClient;
|
||||
HubContext = hubContext;
|
||||
|
||||
CurrentStats = new();
|
||||
}
|
||||
|
||||
public Task Attach(string containerId)
|
||||
{
|
||||
Logger.LogDebug("Attaching to stats stream");
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (!Server.TaskCancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DockerClient.Containers.GetContainerStatsAsync(
|
||||
containerId,
|
||||
new()
|
||||
{
|
||||
Stream = true
|
||||
},
|
||||
new Progress<ContainerStatsResponse>(async response =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var stats = ConvertToStats(response);
|
||||
|
||||
// Update current stats for usage of other components
|
||||
CurrentStats = stats;
|
||||
|
||||
await HubContext.Clients
|
||||
.Group(Configuration.Id.ToString())
|
||||
.SendAsync("StatsUpdated", stats);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("An error occured handling stats update: {e}", e);
|
||||
}
|
||||
}),
|
||||
Server.TaskCancellation
|
||||
);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("An error occured while loading container stats: {e}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset current stats
|
||||
CurrentStats = new();
|
||||
|
||||
Logger.LogDebug("Stopped fetching container stats");
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private ServerStats ConvertToStats(ContainerStatsResponse response)
|
||||
{
|
||||
var result = new ServerStats();
|
||||
|
||||
#region CPU
|
||||
|
||||
if(response.CPUStats != null && response.PreCPUStats.CPUUsage != null) // Sometimes some values are just null >:/
|
||||
{
|
||||
var cpuDelta = (float)response.CPUStats.CPUUsage.TotalUsage - response.PreCPUStats.CPUUsage.TotalUsage;
|
||||
var cpuSystemDelta = (float)response.CPUStats.SystemUsage - response.PreCPUStats.SystemUsage;
|
||||
|
||||
var cpuCoreCount = (int)response.CPUStats.OnlineCPUs;
|
||||
|
||||
if (cpuCoreCount == 0 && response.CPUStats.CPUUsage.PercpuUsage != null)
|
||||
cpuCoreCount = response.CPUStats.CPUUsage.PercpuUsage.Count;
|
||||
|
||||
var cpuPercent = 0f;
|
||||
|
||||
if (cpuSystemDelta > 0.0f && cpuDelta > 0.0f)
|
||||
{
|
||||
cpuPercent = (cpuDelta / cpuSystemDelta) * 100;
|
||||
|
||||
if (cpuCoreCount > 0)
|
||||
cpuPercent *= cpuCoreCount;
|
||||
}
|
||||
|
||||
result.CpuUsage = Math.Round(cpuPercent * 1000) / 1000;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Memory
|
||||
|
||||
result.MemoryUsage = response.MemoryStats.Usage;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Network
|
||||
|
||||
if (response.Networks != null)
|
||||
{
|
||||
foreach (var network in response.Networks)
|
||||
{
|
||||
result.NetworkRead += network.Value.RxBytes;
|
||||
result.NetworkWrite += network.Value.TxBytes;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IO
|
||||
|
||||
if (response.BlkioStats.IoServiceBytesRecursive != null)
|
||||
{
|
||||
result.IoRead = response.BlkioStats.IoServiceBytesRecursive
|
||||
.FirstOrDefault(x => x.Op == "read")?.Value ?? 0;
|
||||
|
||||
result.IoWrite = response.BlkioStats.IoServiceBytesRecursive
|
||||
.FirstOrDefault(x => x.Op == "write")?.Value ?? 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,464 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using Mono.Unix.Native;
|
||||
using MoonCore.Exceptions;
|
||||
using MoonCore.Helpers;
|
||||
using MoonCore.Unix.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 ConsoleSubSystem ConsoleSubSystem;
|
||||
|
||||
public string RuntimeVolumePath { get; private set; }
|
||||
public string InstallVolumePath { get; private set; }
|
||||
public string VirtualDiskPath { get; private set; }
|
||||
|
||||
public bool IsVirtualDiskMounted { get; private set; }
|
||||
public bool IsInitialized { get; private set; } = false;
|
||||
public bool IsFileSystemAccessorCreated { get; private set; } = false;
|
||||
|
||||
public StorageSubSystem(
|
||||
Server server,
|
||||
ILogger logger,
|
||||
AppConfiguration appConfiguration
|
||||
) : base(server, logger)
|
||||
{
|
||||
AppConfiguration = appConfiguration;
|
||||
|
||||
// Runtime Volume
|
||||
var runtimePath = Path.Combine(AppConfiguration.Storage.Volumes, Configuration.Id.ToString());
|
||||
|
||||
if (!runtimePath.StartsWith('/'))
|
||||
runtimePath = Path.Combine(Directory.GetCurrentDirectory(), runtimePath);
|
||||
|
||||
RuntimeVolumePath = runtimePath;
|
||||
|
||||
// Install Volume
|
||||
var installPath = Path.Combine(AppConfiguration.Storage.Install, Configuration.Id.ToString());
|
||||
|
||||
if (!installPath.StartsWith('/'))
|
||||
installPath = Path.Combine(Directory.GetCurrentDirectory(), installPath);
|
||||
|
||||
InstallVolumePath = installPath;
|
||||
|
||||
// Virtual Disk
|
||||
if (!Configuration.UseVirtualDisk)
|
||||
return;
|
||||
|
||||
var virtualDiskPath = Path.Combine(AppConfiguration.Storage.VirtualDisks, $"{Configuration.Id}.img");
|
||||
|
||||
if (!virtualDiskPath.StartsWith('/'))
|
||||
virtualDiskPath = Path.Combine(Directory.GetCurrentDirectory(), virtualDiskPath);
|
||||
|
||||
VirtualDiskPath = virtualDiskPath;
|
||||
}
|
||||
|
||||
public override async Task Initialize()
|
||||
{
|
||||
ConsoleSubSystem = Server.GetRequiredSubSystem<ConsoleSubSystem>();
|
||||
|
||||
try
|
||||
{
|
||||
await Reinitialize();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await ConsoleSubSystem.WriteMoonlight(
|
||||
"Unable to initialize server file system. Please contact the administrator"
|
||||
);
|
||||
|
||||
Logger.LogError("An unhandled error occured while lazy initializing server file system: {e}", e);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task Delete()
|
||||
{
|
||||
if (Configuration.UseVirtualDisk)
|
||||
await DeleteVirtualDisk();
|
||||
|
||||
await DeleteRuntimeVolume();
|
||||
await DeleteInstallVolume();
|
||||
}
|
||||
|
||||
public async Task Reinitialize()
|
||||
{
|
||||
if (IsInitialized && StateMachine.State != ServerState.Offline)
|
||||
{
|
||||
throw new HttpApiException(
|
||||
"Unable to reinitialize storage sub system while the server is not offline",
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
IsInitialized = false;
|
||||
|
||||
await EnsureRuntimeVolumeCreated();
|
||||
|
||||
if (Configuration.UseVirtualDisk)
|
||||
{
|
||||
// Load the state of a possible mount already existing.
|
||||
// This ensures we are aware of the mount state after a restart.
|
||||
// Without that we would get errors when mounting
|
||||
IsVirtualDiskMounted = await CheckVirtualDiskMounted();
|
||||
|
||||
// Ensure we have the virtual disk created and in the correct size
|
||||
await EnsureVirtualDisk();
|
||||
}
|
||||
|
||||
IsInitialized = true;
|
||||
}
|
||||
|
||||
#region Runtime
|
||||
|
||||
public async Task<ServerFileSystem> GetFileSystem()
|
||||
{
|
||||
if (!await RequestRuntimeVolume(skipPermissions: true))
|
||||
throw new HttpApiException("The file system is still initializing. Please try again later", 503);
|
||||
|
||||
return ServerFileSystem;
|
||||
}
|
||||
|
||||
// This method allows other sub systems to request access to the runtime volume.
|
||||
// The return value specifies if the request to the runtime volume is possible or not
|
||||
public async Task<bool> RequestRuntimeVolume(bool skipPermissions = false)
|
||||
{
|
||||
// If the initialization is still running we don't want to allow access to the runtime volume at all
|
||||
if (!IsInitialized)
|
||||
return false;
|
||||
|
||||
// If we use virtual disks and the disk isn't already mounted, we need to mount it now
|
||||
if (Configuration.UseVirtualDisk && !IsVirtualDiskMounted)
|
||||
await MountVirtualDisk();
|
||||
|
||||
if (!IsFileSystemAccessorCreated)
|
||||
await CreateFileSystemAccessor();
|
||||
|
||||
if (!skipPermissions)
|
||||
await EnsureRuntimePermissions();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Task EnsureRuntimeVolumeCreated()
|
||||
{
|
||||
// Create the volume directory if required
|
||||
if (!Directory.Exists(RuntimeVolumePath))
|
||||
Directory.CreateDirectory(RuntimeVolumePath);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task DeleteRuntimeVolume()
|
||||
{
|
||||
// Already deleted? Then we don't want to care about anything at all
|
||||
if (!Directory.Exists(RuntimeVolumePath))
|
||||
return;
|
||||
|
||||
// If we use a virtual disk there are no files to delete via the
|
||||
// secure file system as the virtual disk is already gone by now
|
||||
if (Configuration.UseVirtualDisk)
|
||||
{
|
||||
Directory.Delete(RuntimeVolumePath, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we still habe a file system accessor, we reuse it :)
|
||||
if (IsFileSystemAccessorCreated)
|
||||
{
|
||||
foreach (var entry in SecureFileSystem.ReadDir("/"))
|
||||
{
|
||||
if (entry.IsFile)
|
||||
SecureFileSystem.Remove(entry.Name);
|
||||
else
|
||||
SecureFileSystem.RemoveAll(entry.Name);
|
||||
}
|
||||
|
||||
await DestroyFileSystemAccessor();
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the file system accessor has already been removed we create a temporary one.
|
||||
// This handles the case when a server was never accessed and as such there is no accessor created yet
|
||||
|
||||
var sfs = new SecureFileSystem(RuntimeVolumePath);
|
||||
|
||||
foreach (var entry in sfs.ReadDir("/"))
|
||||
{
|
||||
if (entry.IsFile)
|
||||
sfs.Remove(entry.Name);
|
||||
else
|
||||
sfs.RemoveAll(entry.Name);
|
||||
}
|
||||
|
||||
sfs.Dispose();
|
||||
}
|
||||
|
||||
Directory.Delete(RuntimeVolumePath, true);
|
||||
}
|
||||
|
||||
private Task EnsureRuntimePermissions()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(SecureFileSystem);
|
||||
|
||||
//TODO: Config
|
||||
var uid = (int)Syscall.getuid();
|
||||
var gid = (int)Syscall.getgid();
|
||||
|
||||
if (uid == 0)
|
||||
{
|
||||
uid = 998;
|
||||
gid = 998;
|
||||
}
|
||||
|
||||
// Chown all content of the runtime volume
|
||||
foreach (var entry in SecureFileSystem.ReadDir("/"))
|
||||
{
|
||||
if (entry.IsFile)
|
||||
SecureFileSystem.Chown(entry.Name, uid, gid);
|
||||
else
|
||||
SecureFileSystem.ChownAll(entry.Name, uid, gid);
|
||||
}
|
||||
|
||||
// Chown also the main path of the volume
|
||||
if (Syscall.chown(RuntimeVolumePath, uid, gid) != 0)
|
||||
{
|
||||
var error = Stdlib.GetLastError();
|
||||
throw new SyscallException(error, "An error occured while chowning runtime volume");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task CreateFileSystemAccessor()
|
||||
{
|
||||
SecureFileSystem = new(RuntimeVolumePath);
|
||||
ServerFileSystem = new(SecureFileSystem);
|
||||
|
||||
IsFileSystemAccessorCreated = true;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task DestroyFileSystemAccessor()
|
||||
{
|
||||
if (!SecureFileSystem.IsDisposed)
|
||||
SecureFileSystem.Dispose();
|
||||
|
||||
IsFileSystemAccessorCreated = false;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Installation
|
||||
|
||||
public Task EnsureInstallVolume()
|
||||
{
|
||||
if (!Directory.Exists(InstallVolumePath))
|
||||
Directory.CreateDirectory(InstallVolumePath);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DeleteInstallVolume()
|
||||
{
|
||||
if (!Directory.Exists(InstallVolumePath))
|
||||
return Task.CompletedTask;
|
||||
|
||||
Directory.Delete(InstallVolumePath, true);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Virtual disks
|
||||
|
||||
private async Task MountVirtualDisk()
|
||||
{
|
||||
await ConsoleSubSystem.WriteMoonlight("Mounting virtual disk");
|
||||
await ExecuteCommand("mount", $"-t auto -o loop {VirtualDiskPath} {RuntimeVolumePath}", true);
|
||||
|
||||
IsVirtualDiskMounted = true;
|
||||
}
|
||||
|
||||
private async Task UnmountVirtualDisk()
|
||||
{
|
||||
await ConsoleSubSystem.WriteMoonlight("Unmounting virtual disk");
|
||||
await ExecuteCommand("umount", RuntimeVolumePath, handleExitCode: true);
|
||||
|
||||
IsVirtualDiskMounted = false;
|
||||
}
|
||||
|
||||
private async Task<bool> CheckVirtualDiskMounted()
|
||||
=> await ExecuteCommand("findmnt", RuntimeVolumePath) == 0;
|
||||
|
||||
private async Task EnsureVirtualDisk()
|
||||
{
|
||||
var existingDiskInfo = new FileInfo(VirtualDiskPath);
|
||||
|
||||
// Check if we need to create the disk or just check for the size
|
||||
if (existingDiskInfo.Exists)
|
||||
{
|
||||
var expectedSize = ByteConverter.FromMegaBytes(Configuration.Disk).Bytes;
|
||||
|
||||
// If the disk size matches, we are done here
|
||||
if (expectedSize == existingDiskInfo.Length)
|
||||
{
|
||||
Logger.LogDebug("Virtual disk size matches expected size");
|
||||
return;
|
||||
}
|
||||
|
||||
// We cant resize while the server is running as this would lead to possible file corruptions
|
||||
// and crashes of the software the server is running
|
||||
if (StateMachine.State != ServerState.Offline)
|
||||
{
|
||||
Logger.LogDebug("Skipping disk resizing while server is not offline");
|
||||
await ConsoleSubSystem.WriteMoonlight("Skipping disk resizing as the server is not offline");
|
||||
return;
|
||||
}
|
||||
|
||||
if (expectedSize > existingDiskInfo.Length)
|
||||
{
|
||||
Logger.LogDebug("Detected smaller disk size as expected. Resizing now");
|
||||
await ConsoleSubSystem.WriteMoonlight("Preparing to resize virtual disk");
|
||||
|
||||
// If the file system accessor is still open we need to destroy it in order to to resize
|
||||
if (IsFileSystemAccessorCreated)
|
||||
await DestroyFileSystemAccessor();
|
||||
|
||||
// If the disk is still mounted we need to unmount it in order to resize
|
||||
if (IsVirtualDiskMounted)
|
||||
await UnmountVirtualDisk();
|
||||
|
||||
// Resize the disk image file
|
||||
Logger.LogDebug("Resizing virtual disk file");
|
||||
|
||||
var fileStream = File.Open(VirtualDiskPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
|
||||
|
||||
fileStream.SetLength(expectedSize);
|
||||
|
||||
await fileStream.FlushAsync();
|
||||
fileStream.Close();
|
||||
await fileStream.DisposeAsync();
|
||||
|
||||
// Now we need to run the file system check on the disk
|
||||
Logger.LogDebug("Checking virtual disk for corruptions using e2fsck");
|
||||
await ConsoleSubSystem.WriteMoonlight("Checking virtual disk for any corruptions");
|
||||
|
||||
await ExecuteCommand(
|
||||
"e2fsck",
|
||||
$"{AppConfiguration.Storage.VirtualDiskOptions.E2FsckParameters} {VirtualDiskPath}",
|
||||
handleExitCode: true
|
||||
);
|
||||
|
||||
// Resize the file system
|
||||
Logger.LogDebug("Resizing filesystem of virtual disk using resize2fs");
|
||||
await ConsoleSubSystem.WriteMoonlight("Resizing virtual disk");
|
||||
|
||||
await ExecuteCommand("resize2fs", VirtualDiskPath, handleExitCode: true);
|
||||
|
||||
// Done :>
|
||||
Logger.LogDebug("Successfully resized virtual disk");
|
||||
await ConsoleSubSystem.WriteMoonlight("Resize of virtual disk completed");
|
||||
}
|
||||
else if (existingDiskInfo.Length > expectedSize)
|
||||
{
|
||||
Logger.LogDebug("Shrink from {expected} to {existing} detected", expectedSize, existingDiskInfo.Length);
|
||||
|
||||
await ConsoleSubSystem.WriteMoonlight(
|
||||
"Unable to shrink virtual disk. Virtual disk will stay unmodified"
|
||||
);
|
||||
|
||||
Logger.LogWarning(
|
||||
"Server disk limit was lower then the size of the virtual disk. Virtual disk wont be resized to prevent loss of files");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create the image file and adjust the size
|
||||
Logger.LogDebug("Creating virtual disk");
|
||||
await ConsoleSubSystem.WriteMoonlight("Creating virtual disk file. Please be patient");
|
||||
|
||||
var fileStream = File.Open(VirtualDiskPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
|
||||
|
||||
fileStream.SetLength(
|
||||
ByteConverter.FromMegaBytes(Configuration.Disk).Bytes
|
||||
);
|
||||
|
||||
await fileStream.FlushAsync();
|
||||
fileStream.Close();
|
||||
await fileStream.DisposeAsync();
|
||||
|
||||
// Now we want to format it
|
||||
Logger.LogDebug("Formatting virtual disk");
|
||||
await ConsoleSubSystem.WriteMoonlight("Formatting virtual disk. This can take a bit");
|
||||
|
||||
await ExecuteCommand(
|
||||
"mkfs",
|
||||
$"-t {AppConfiguration.Storage.VirtualDiskOptions.FileSystemType} {VirtualDiskPath}",
|
||||
handleExitCode: true
|
||||
);
|
||||
|
||||
// Done :)
|
||||
Logger.LogDebug("Successfully created virtual disk");
|
||||
await ConsoleSubSystem.WriteMoonlight("Virtual disk created");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteVirtualDisk()
|
||||
{
|
||||
if (IsFileSystemAccessorCreated)
|
||||
await DestroyFileSystemAccessor();
|
||||
|
||||
if (IsVirtualDiskMounted)
|
||||
await UnmountVirtualDisk();
|
||||
|
||||
File.Delete(VirtualDiskPath);
|
||||
}
|
||||
|
||||
private async Task<int> ExecuteCommand(string command, string arguments, bool handleExitCode = false)
|
||||
{
|
||||
var psi = new ProcessStartInfo()
|
||||
{
|
||||
FileName = command,
|
||||
Arguments = arguments,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true
|
||||
};
|
||||
|
||||
var process = Process.Start(psi);
|
||||
|
||||
if (process == null)
|
||||
throw new AggregateException("The spawned process reference is null");
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
if (process.ExitCode == 0 || !handleExitCode)
|
||||
return process.ExitCode;
|
||||
|
||||
var output = await process.StandardOutput.ReadToEndAsync();
|
||||
output += await process.StandardError.ReadToEndAsync();
|
||||
|
||||
throw new Exception($"The command {command} failed: {output}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
// We check for that just to ensure that we no longer access the file system
|
||||
if (IsFileSystemAccessorCreated)
|
||||
await DestroyFileSystemAccessor();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user