Recreated plugin with new project template. Started implementing server system daemon

This commit is contained in:
2026-03-01 21:09:29 +01:00
parent f6b71f4de6
commit 52dbd13fb5
350 changed files with 2795 additions and 21553 deletions

View File

@@ -0,0 +1,13 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IInstallConsole
{
public event Func<string, Task>? OnOutput;
public Task AttachAsync();
public Task WriteInputAsync(string value);
public Task ClearCacheAsync();
public Task<string[]> GetCacheAsync();
}

View File

@@ -0,0 +1,14 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IInstallEnvironment : IAsyncDisposable
{
public IInstallStatistics Statistics { get; }
public IInstallConsole Console { get; }
public event Func<Task>? OnExited;
public Task<bool> IsRunningAsync();
public Task StartAsync();
public Task KillAsync();
}

View File

@@ -0,0 +1,18 @@
using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IInstallEnvironmentService
{
public Task<IInstallEnvironment?> FindAsync(string id);
public Task<IInstallEnvironment> CreateAsync(
string id,
RuntimeConfiguration runtimeConfiguration,
InstallConfiguration installConfiguration,
IInstallStorage installStorage,
IRuntimeStorage runtimeStorage
);
public Task DeleteAsync(IInstallEnvironment environment);
}

View File

@@ -0,0 +1,11 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IInstallStatistics
{
public event Func<ServerStatistics, Task>? OnStatisticsReceived;
public Task AttachAsync();
public Task ClearCacheAsync();
public Task<ServerStatistics[]> GetCacheAsync();
}

View File

@@ -0,0 +1,6 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IInstallStorage
{
public Task<string> GetHostPathAsync();
}

View File

@@ -0,0 +1,10 @@
using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IInstallStorageService
{
public Task<IInstallStorage?> FindAsync(string id);
public Task<IInstallStorage> CreateAsync(string id, RuntimeConfiguration runtimeConfiguration, InstallConfiguration installConfiguration);
public Task DeleteAsync(IInstallStorage installStorage);
}

View File

@@ -0,0 +1,13 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IRuntimeConsole
{
public event Func<string, Task>? OnOutput;
public Task AttachAsync();
public Task WriteInputAsync(string value);
public Task ClearCacheAsync();
public Task<string[]> GetCacheAsync();
}

View File

@@ -0,0 +1,14 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IRuntimeEnvironment : IAsyncDisposable
{
public IRuntimeStatistics Statistics { get; }
public IRuntimeConsole Console { get; }
public event Func<Task>? OnExited;
public Task<bool> IsRunningAsync();
public Task StartAsync();
public Task KillAsync();
}

View File

@@ -0,0 +1,11 @@
using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IRuntimeEnvironmentService
{
public Task<IRuntimeEnvironment?> FindAsync(string id);
public Task<IRuntimeEnvironment> CreateAsync(string id, RuntimeConfiguration configuration, IRuntimeStorage runtimeStorage);
public Task UpdateAsync(IRuntimeEnvironment environment, RuntimeConfiguration configuration);
public Task DeleteAsync(IRuntimeEnvironment environment);
}

View File

@@ -0,0 +1,11 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IRuntimeStatistics
{
public event Func<ServerStatistics, Task>? OnStatisticsReceived;
public Task AttachAsync();
public Task ClearCacheAsync();
public Task<ServerStatistics[]> GetCacheAsync();
}

View File

@@ -0,0 +1,6 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IRuntimeStorage
{
public Task<string> GetHostPathAsync();
}

View File

@@ -0,0 +1,11 @@
using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IRuntimeStorageService
{
public Task<IRuntimeStorage?> FindAsync(string id);
public Task<IRuntimeStorage> CreateAsync(string id, RuntimeConfiguration configuration);
public Task UpdateAsync(IRuntimeStorage runtimeStorage, RuntimeConfiguration configuration);
public Task DeleteAsync(IRuntimeStorage runtimeStorage);
}

View File

@@ -1,221 +0,0 @@
using System.Text;
using Docker.DotNet;
using MoonCore.Events;
using MoonCore.Helpers;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerConsole : IConsole
{
private readonly EventSource<string> StdOutEventSource = new();
private readonly ConcurrentList<string> StdOutCache = new();
private readonly DockerClient DockerClient;
private readonly ServerContext Context;
private readonly ILogger Logger;
private MultiplexedStream? CurrentStream;
private CancellationTokenSource Cts = new();
public DockerConsole(DockerClient dockerClient, ServerContext context)
{
DockerClient = dockerClient;
Context = context;
Logger = Context.Logger;
}
public Task InitializeAsync()
=> Task.CompletedTask;
public async Task WriteStdInAsync(string content)
{
if (CurrentStream == null)
{
Logger.LogWarning("Unable to write to stdin as no stream is connected");
return;
}
var contextBuffer = Encoding.UTF8.GetBytes(content);
await CurrentStream.WriteAsync(contextBuffer, 0, contextBuffer.Length, Cts.Token);
}
public async Task WriteStdOutAsync(string content)
{
// Add output cache
if (StdOutCache.Count > 250) // TODO: Config
StdOutCache.RemoveRange(0, 100);
StdOutCache.Add(content);
// Fire event
await StdOutEventSource.InvokeAsync(content);
}
public async Task AttachRuntimeAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await AttachToContainerAsync(containerName);
}
public async Task AttachInstallationAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id);
await AttachToContainerAsync(containerName);
}
private async Task AttachToContainerAsync(string containerName)
{
var cts = new CancellationTokenSource();
// Cancels previous active read task if it exists
if (!Cts.IsCancellationRequested)
await Cts.CancelAsync();
// Update the current cancellation token
Cts = cts;
// Start reading task
Task.Run(async () =>
{
// This loop is here to reconnect to the stream when connection is lost.
// This can occur when docker restarts for example
while (!cts.IsCancellationRequested)
{
MultiplexedStream? innerStream = null;
try
{
Logger.LogTrace("Attaching");
innerStream = await DockerClient.Containers.AttachContainerAsync(
containerName,
true,
new()
{
Stderr = true,
Stdin = true,
Stdout = true,
Stream = true
},
cts.Token
);
CurrentStream = innerStream;
var buffer = new byte[1024];
try
{
// Read while server tasks are not canceled
while (!cts.Token.IsCancellationRequested)
{
var readResult = await innerStream.ReadOutputAsync(
buffer,
0,
buffer.Length,
cts.Token
);
if (readResult.EOF)
await cts.CancelAsync();
var decodedText = Encoding.UTF8.GetString(buffer, 0, readResult.Count);
await WriteStdOutAsync(decodedText);
}
Logger.LogTrace("Read loop exited");
}
catch (TaskCanceledException)
{
// Ignored
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception e)
{
Logger.LogWarning(e, "An unhandled error occured while reading from container stream");
}
}
catch (TaskCanceledException)
{
// ignored
}
catch (DockerContainerNotFoundException)
{
// Container got removed. Stop the reconnect loop
Logger.LogDebug("Container '{name}' got removed. Stopping reconnect stream for console", containerName);
await cts.CancelAsync();
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while attaching to container");
}
innerStream?.Dispose();
}
Logger.LogDebug("Disconnected from container stream");
});
}
public async Task FetchRuntimeAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await FetchFromContainerAsync(containerName);
}
public async Task FetchInstallationAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id);
await FetchFromContainerAsync(containerName);
}
private async Task FetchFromContainerAsync(string containerName)
{
var logStream = await DockerClient.Containers.GetContainerLogsAsync(containerName, true, new()
{
Follow = false,
ShowStderr = true,
ShowStdout = true
});
var combinedOutput = await logStream.ReadOutputToEndAsync(Cts.Token);
var contentToAdd = combinedOutput.stdout + combinedOutput.stderr;
await WriteStdOutAsync(contentToAdd);
}
public Task ClearCacheAsync()
{
StdOutCache.Clear();
return Task.CompletedTask;
}
public Task<IEnumerable<string>> GetCacheAsync()
{
return Task.FromResult<IEnumerable<string>>(StdOutCache);
}
public async Task<IAsyncDisposable> SubscribeStdOutAsync(Func<string, ValueTask> callback)
=> await StdOutEventSource.SubscribeAsync(callback);
public async ValueTask DisposeAsync()
{
if (!Cts.IsCancellationRequested)
await Cts.CancelAsync();
if (CurrentStream != null)
CurrentStream.Dispose();
}
}

View File

@@ -1,7 +0,0 @@
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public static class DockerConstants
{
public const string RuntimeNameTemplate = "moonlight-runtime-{0}";
public const string InstallationNameTemplate = "moonlight-installation-{0}";
}

View File

@@ -1,184 +0,0 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonCore.Events;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerInstallation : IInstallation
{
private readonly DockerEventService DockerEventService;
private readonly ServerConfigurationMapper Mapper;
private readonly DockerImageService ImageService;
private readonly ServerContext ServerContext;
private readonly DockerClient DockerClient;
private IReporter Reporter => ServerContext.Server.Reporter;
private readonly EventSource<int> ExitEventSource = new();
private IAsyncDisposable ContainerEventSubscription;
private string ContainerId;
public DockerInstallation(
DockerClient dockerClient,
ServerContext serverContext,
ServerConfigurationMapper mapper,
DockerImageService imageService,
DockerEventService dockerEventService
)
{
DockerClient = dockerClient;
ServerContext = serverContext;
Mapper = mapper;
ImageService = imageService;
DockerEventService = dockerEventService;
}
public async Task InitializeAsync()
{
ContainerEventSubscription = await DockerEventService.SubscribeContainerAsync(OnContainerEvent);
}
private async ValueTask OnContainerEvent(Message message)
{
// Only handle events for our own container
if (message.ID != ContainerId)
return;
// Only handle die events
if (message.Action != "die")
return;
int exitCode;
if (message.Actor.Attributes.TryGetValue("exitCode", out var exitCodeStr))
{
if (!int.TryParse(exitCodeStr, out exitCode))
exitCode = 0;
}
else
exitCode = 0;
await ExitEventSource.InvokeAsync(exitCode);
}
public async Task<bool> CheckExistsAsync()
{
try
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
await DockerClient.Containers.InspectContainerAsync(
containerName
);
return true;
}
catch (DockerContainerNotFoundException)
{
return false;
}
}
public async Task CreateAsync(
string runtimePath,
string hostPath,
ServerInstallDataResponse data
)
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
var parameters = Mapper.ToInstallParameters(
ServerContext.Configuration,
data,
runtimePath,
hostPath,
containerName
);
// Docker image
await Reporter.StatusAsync("Downloading docker image");
await ImageService.DownloadAsync(data.DockerImage, async status => { await Reporter.StatusAsync(status); });
await Reporter.StatusAsync("Downloaded docker image");
// Write install script to install fs
await File.WriteAllTextAsync(
Path.Combine(hostPath, "install.sh"),
data.Script
);
//
var response = await DockerClient.Containers.CreateContainerAsync(parameters);
ContainerId = response.ID;
await Reporter.StatusAsync("Created container");
}
public async Task StartAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
await DockerClient.Containers.StartContainerAsync(containerName, new());
}
public async Task KillAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
await DockerClient.Containers.KillContainerAsync(containerName, new());
}
public async Task DestroyAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
try
{
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
if (container.State.Running)
await DockerClient.Containers.KillContainerAsync(containerName, new());
await DockerClient.Containers.RemoveContainerAsync(containerName, new()
{
Force = true
});
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
}
public async Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> callback)
=> await ExitEventSource.SubscribeAsync(callback);
public async Task RestoreAsync()
{
try
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
ContainerId = container.ID;
}
catch (DockerContainerNotFoundException)
{
// Ignore
}
}
public async ValueTask DisposeAsync()
{
await ContainerEventSubscription.DisposeAsync();
}
}

View File

@@ -1,59 +0,0 @@
using Docker.DotNet;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerRestorer : IRestorer
{
private readonly DockerClient DockerClient;
private readonly ServerContext Context;
public DockerRestorer(DockerClient dockerClient, ServerContext context)
{
DockerClient = dockerClient;
Context = context;
}
public Task InitializeAsync()
=> Task.CompletedTask;
public async Task<bool> HandleRuntimeAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
try
{
var container = await DockerClient.Containers.InspectContainerAsync(
containerName
);
return container.State.Running;
}
catch (DockerContainerNotFoundException)
{
return false;
}
}
public async Task<bool> HandleInstallationAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id);
try
{
var container = await DockerClient.Containers.InspectContainerAsync(
containerName
);
return container.State.Running;
}
catch (DockerContainerNotFoundException)
{
return false;
}
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -1,177 +0,0 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonCore.Events;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerRuntime : IRuntime
{
private readonly DockerClient DockerClient;
private readonly ServerContext Context;
private readonly ServerConfigurationMapper Mapper;
private readonly DockerEventService DockerEventService;
private readonly DockerImageService ImageService;
private readonly EventSource<int> ExitEventSource = new();
private IReporter Reporter => Context.Server.Reporter;
private IAsyncDisposable ContainerEventSubscription;
private string ContainerId;
public DockerRuntime(
DockerClient dockerClient,
ServerContext context,
ServerConfigurationMapper mapper,
DockerEventService dockerEventService,
DockerImageService imageService
)
{
DockerClient = dockerClient;
Context = context;
Mapper = mapper;
DockerEventService = dockerEventService;
ImageService = imageService;
}
public async Task InitializeAsync()
{
ContainerEventSubscription = await DockerEventService.SubscribeContainerAsync(OnContainerEvent);
}
private async ValueTask OnContainerEvent(Message message)
{
// Only handle events for our own container
if (message.ID != ContainerId)
return;
// Only handle die events
if (message.Action != "die")
return;
int exitCode;
if (message.Actor.Attributes.TryGetValue("exitCode", out var exitCodeStr))
{
if (!int.TryParse(exitCodeStr, out exitCode))
exitCode = 0;
}
else
exitCode = 0;
await ExitEventSource.InvokeAsync(exitCode);
}
public async Task<bool> CheckExistsAsync()
{
try
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await DockerClient.Containers.InspectContainerAsync(
containerName
);
return true;
}
catch (DockerContainerNotFoundException)
{
return false;
}
}
public async Task CreateAsync(string path)
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
var parameters = Mapper.ToRuntimeParameters(
Context.Configuration,
path,
containerName
);
// Docker image
await Reporter.StatusAsync("Downloading docker image");
await ImageService.DownloadAsync(
Context.Configuration.DockerImage,
async status => { await Reporter.StatusAsync(status); }
);
await Reporter.StatusAsync("Downloaded docker image");
//
var response = await DockerClient.Containers.CreateContainerAsync(parameters);
ContainerId = response.ID;
await Reporter.StatusAsync("Created container");
}
public async Task StartAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await DockerClient.Containers.StartContainerAsync(containerName, new());
}
public Task UpdateAsync()
{
return Task.CompletedTask;
}
public async Task KillAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await DockerClient.Containers.KillContainerAsync(containerName, new());
}
public async Task DestroyAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
try
{
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
if (container.State.Running)
await DockerClient.Containers.KillContainerAsync(containerName, new());
await DockerClient.Containers.RemoveContainerAsync(containerName, new()
{
Force = true
});
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
}
public async Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> callback)
=> await ExitEventSource.SubscribeAsync(callback);
public async Task RestoreAsync()
{
try
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
ContainerId = container.ID;
}
catch (DockerContainerNotFoundException)
{
// Ignore
}
}
public async ValueTask DisposeAsync()
{
await ContainerEventSubscription.DisposeAsync();
}
}

View File

@@ -1,25 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerStatistics : IStatistics
{
public Task InitializeAsync()
=> Task.CompletedTask;
public Task AttachRuntimeAsync()
=> Task.CompletedTask;
public Task AttachInstallationAsync()
=> Task.CompletedTask;
public Task ClearCacheAsync()
=> Task.CompletedTask;
public Task<IEnumerable<StatisticsData>> GetCacheAsync()
=> Task.FromResult<IEnumerable<StatisticsData>>([]);
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

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

View File

@@ -1,58 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.FileSystems;
public class RawInstallationFs : IFileSystem
{
private readonly string BaseDirectory;
public RawInstallationFs(ServerContext context)
{
BaseDirectory = Path.Combine(
Directory.GetCurrentDirectory(),
"storage",
"install",
context.Configuration.Id.ToString()
);
}
public Task InitializeAsync()
=> Task.CompletedTask;
public Task<string> GetPathAsync()
=> Task.FromResult(BaseDirectory);
public Task<bool> CheckExistsAsync()
{
var exists = Directory.Exists(BaseDirectory);
return Task.FromResult(exists);
}
public Task<bool> CheckMountedAsync()
=> Task.FromResult(true);
public Task CreateAsync()
{
Directory.CreateDirectory(BaseDirectory);
return Task.CompletedTask;
}
public Task PerformChecksAsync()
=> Task.CompletedTask;
public Task MountAsync()
=> Task.CompletedTask;
public Task UnmountAsync()
=> Task.CompletedTask;
public Task DestroyAsync()
{
Directory.Delete(BaseDirectory, true);
return Task.CompletedTask;
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -1,58 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.FileSystems;
public class RawRuntimeFs : IFileSystem
{
private readonly string BaseDirectory;
public RawRuntimeFs(ServerContext context)
{
BaseDirectory = Path.Combine(
Directory.GetCurrentDirectory(),
"storage",
"volumes",
context.Configuration.Id.ToString()
);
}
public Task InitializeAsync()
=> Task.CompletedTask;
public Task<string> GetPathAsync()
=> Task.FromResult(BaseDirectory);
public Task<bool> CheckExistsAsync()
{
var exists = Directory.Exists(BaseDirectory);
return Task.FromResult(exists);
}
public Task<bool> CheckMountedAsync()
=> Task.FromResult(true);
public Task CreateAsync()
{
Directory.CreateDirectory(BaseDirectory);
return Task.CompletedTask;
}
public Task PerformChecksAsync()
=> Task.CompletedTask;
public Task MountAsync()
=> Task.CompletedTask;
public Task UnmountAsync()
=> Task.CompletedTask;
public Task DestroyAsync()
{
Directory.Delete(BaseDirectory, true);
return Task.CompletedTask;
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -1,35 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class DebugHandler : IServerStateHandler
{
private readonly ServerContext Context;
private IAsyncDisposable? StdOutSubscription;
public DebugHandler(ServerContext context)
{
Context = context;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
if(StdOutSubscription != null)
return;
StdOutSubscription = await Context.Server.Console.SubscribeStdOutAsync(line =>
{
Console.WriteLine($"STD OUT: {line}");
return ValueTask.CompletedTask;
});
}
public async ValueTask DisposeAsync()
{
if (StdOutSubscription != null)
await StdOutSubscription.DisposeAsync();
}
}

View File

@@ -1,125 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class InstallationHandler : IServerStateHandler
{
private readonly ServerContext Context;
private Server Server => Context.Server;
private IAsyncDisposable? ExitSubscription;
public InstallationHandler(ServerContext context)
{
Context = context;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
if (transition is
{ Source: ServerState.Offline, Destination: ServerState.Installing, Trigger: ServerTrigger.Install })
{
await StartAsync();
}
else if (transition is
{ Source: ServerState.Installing, Destination: ServerState.Offline, Trigger: ServerTrigger.Exited })
{
await CompleteAsync();
}
}
private async Task StartAsync()
{
// Plan:
// 1. Fetch latest configuration
// 2. Check if both file systems exists
// 3. Check if both file systems are mounted
// 4. Run file system checks
// 5. Create installation container
// 6. Attach console
// 7. Start installation container
// 1. Fetch latest configuration
var installData = new ServerInstallDataResponse()
{
Script = await File.ReadAllTextAsync(Path.Combine("storage", "install.sh")),
Shell = "/bin/ash",
DockerImage = "ghcr.io/parkervcp/installers:alpine"
};
// 2. Check if file system exists
if (!await Server.RuntimeFileSystem.CheckExistsAsync())
await Server.RuntimeFileSystem.CreateAsync();
if (!await Server.InstallationFileSystem.CheckExistsAsync())
await Server.InstallationFileSystem.CreateAsync();
// 3. Check if both file systems are mounted
if (!await Server.RuntimeFileSystem.CheckMountedAsync())
await Server.RuntimeFileSystem.MountAsync();
if (!await Server.InstallationFileSystem.CheckMountedAsync())
await Server.InstallationFileSystem.MountAsync();
// 4. Run file system checks
await Server.RuntimeFileSystem.PerformChecksAsync();
await Server.InstallationFileSystem.PerformChecksAsync();
// 5. Create installation
var runtimePath = await Server.RuntimeFileSystem.GetPathAsync();
var installationPath = await Server.InstallationFileSystem.GetPathAsync();
if (await Server.Installation.CheckExistsAsync())
await Server.Installation.DestroyAsync();
await Server.Installation.CreateAsync(runtimePath, installationPath, installData);
if (ExitSubscription == null)
ExitSubscription = await Server.Installation.SubscribeExitedAsync(OnInstallationExited);
// 6. Attach console
await Server.Console.AttachInstallationAsync();
// 7. Start installation container
await Server.Installation.StartAsync();
}
private async ValueTask OnInstallationExited(int exitCode)
{
// TODO: Notify the crash handler component of the exit code
await Server.StateMachine.FireAsync(ServerTrigger.Exited);
}
private async Task CompleteAsync()
{
// Plan:
// 1. Handle possible crash
// 2. Remove installation container
// 3. Remove installation file system
// 1. Handle possible crash
// TODO
// 2. Remove installation container
await Server.Installation.DestroyAsync();
// 3. Remove installation file system
await Server.InstallationFileSystem.UnmountAsync();
await Server.InstallationFileSystem.DestroyAsync();
Context.Logger.LogDebug("Completed installation");
}
public async ValueTask DisposeAsync()
{
if (ExitSubscription != null)
await ExitSubscription.DisposeAsync();
}
}

View File

@@ -1,85 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class OnlineDetectionHandler : IServerStateHandler
{
private readonly ServerContext Context;
private IOnlineDetector OnlineDetector => Context.Server.OnlineDetector;
private ILogger Logger => Context.Logger;
private IAsyncDisposable? ConsoleSubscription;
private bool IsActive = false;
public OnlineDetectionHandler(ServerContext context)
{
Context = context;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
if (
transition is
{ Source: ServerState.Offline, Destination: ServerState.Starting, Trigger: ServerTrigger.Start } && !IsActive
)
{
await StartAsync();
}
else if (transition is { Source: not ServerState.Installing, Destination: ServerState.Offline } && IsActive)
{
await StopAsync();
}
}
private async Task StartAsync()
{
IsActive = true;
await OnlineDetector.CreateAsync();
ConsoleSubscription = await Context.Server.Console.SubscribeStdOutAsync(OnHandleOutput);
Logger.LogTrace("Created online detector. Created console subscription");
}
private async ValueTask OnHandleOutput(string line)
{
if(!IsActive)
return;
if(!await OnlineDetector.HandleOutputAsync(line))
return;
if(!Context.Server.StateMachine.CanFire(ServerTrigger.DetectOnline))
return;
Logger.LogTrace("Detected server as online. Destroying online detector");
await Context.Server.StateMachine.FireAsync(ServerTrigger.DetectOnline);
await StopAsync();
}
private async Task StopAsync()
{
IsActive = false;
if (ConsoleSubscription != null)
{
await ConsoleSubscription.DisposeAsync();
ConsoleSubscription = null;
}
await OnlineDetector.DestroyAsync();
Logger.LogTrace("Destroyed online detector. Revoked console subscription");
}
public async ValueTask DisposeAsync()
{
if (ConsoleSubscription != null)
await ConsoleSubscription.DisposeAsync();
}
}

View File

@@ -1,42 +0,0 @@
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 not
{
Destination: ServerState.Offline,
Source: not ServerState.Installing,
Trigger: ServerTrigger.Exited // We don't want to handle the fail event here
})
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;
}

View File

@@ -1,84 +0,0 @@
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. Start runtime
// 1. Fetch latest configuration
// TODO
// Consider moving it out of the startup handler, as other handlers might need
// the updated config as well or add sorting into the handler registration to ensure they are executing in the correct order.
// Sort when building server, not when executing handlers
// 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();
if (await Server.Runtime.CheckExistsAsync())
await Server.Runtime.DestroyAsync();
await Server.Runtime.CreateAsync(hostPath);
if (ExitSubscription == null)
ExitSubscription = await Server.Runtime.SubscribeExitedAsync(OnRuntimeExited);
// 6. Attach console
await Server.Console.AttachRuntimeAsync();
// 7. Start runtime
await Server.Runtime.StartAsync();
}
private async ValueTask 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();
}
}

View File

@@ -1,39 +0,0 @@
using Microsoft.AspNetCore.SignalR;
using MoonlightServers.Daemon.Http.Hubs;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Implementations;
public class ConsoleSignalRComponent : IServerComponent
{
private readonly IHubContext<ServerWebSocketHub> Hub;
private readonly ServerContext Context;
private IAsyncDisposable? StdOutSubscription;
private string HubGroup;
public ConsoleSignalRComponent(IHubContext<ServerWebSocketHub> hub, ServerContext context)
{
Hub = hub;
Context = context;
}
public async Task InitializeAsync()
{
HubGroup = Context.Configuration.Id.ToString();
StdOutSubscription = await Context.Server.Console.SubscribeStdOutAsync(OnStdOut);
}
private async ValueTask OnStdOut(string output)
{
await Hub.Clients.Group(HubGroup).SendAsync("ConsoleOutput", output);
}
public async ValueTask DisposeAsync()
{
if (StdOutSubscription != null)
await StdOutSubscription.DisposeAsync();
}
}

View File

@@ -0,0 +1,254 @@
using System.ComponentModel;
using Docker.DotNet.Models;
using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public static class ConfigMapper
{
public static CreateContainerParameters GetRuntimeConfig(
string uuid,
string name,
RuntimeConfiguration configuration,
string runtimeStoragePath
)
{
var parameters = new CreateContainerParameters()
{
HostConfig = new()
};
ApplySharedOptions(parameters, configuration);
// Limits
if (configuration.Limits.CpuPercent.HasValue)
{
parameters.HostConfig.CPUQuota = configuration.Limits.CpuPercent.Value * 1000;
parameters.HostConfig.CPUPeriod = 100000;
parameters.HostConfig.CPUShares = 1024;
}
if (configuration.Limits.MemoryMb.HasValue)
{
var memoryLimit = configuration.Limits.MemoryMb.Value;
// The overhead multiplier gives the container a little bit more memory to prevent crashes
var memoryOverhead = memoryLimit + memoryLimit * 0.05f;
parameters.HostConfig.Memory = (long)memoryOverhead * 1024L * 1024L;
parameters.HostConfig.MemoryReservation = (long)memoryLimit * 1024L * 1024L;
if (configuration.Limits.SwapMb.HasValue)
{
var rawSwap = configuration.Limits.SwapMb.Value * 1024L * 1024L;
parameters.HostConfig.MemorySwap = rawSwap + (long)memoryOverhead;
}
}
parameters.HostConfig.BlkioWeight = 100;
parameters.HostConfig.OomKillDisable = true;
// Storage
parameters.HostConfig.Tmpfs = new Dictionary<string, string>()
{
{ "/tmp", "rw,exec,nosuid,size=100M" } // TODO: Config
};
parameters.WorkingDir = "/home/container";
parameters.HostConfig.Mounts = new List<Mount>();
parameters.HostConfig.Mounts.Add(new Mount()
{
Source = runtimeStoragePath,
Target = "/home/container",
Type = "bind",
ReadOnly = false
});
// Labels
parameters.Labels = new Dictionary<string, string>()
{
{ "dev.moonlightpanel", "true" },
{ "dev.moonlightpanel.id", uuid }
};
foreach (var label in configuration.Environment.Labels)
parameters.Labels.Add(label.Key, label.Value);
// Security
parameters.HostConfig.CapDrop = new List<string>()
{
"setpcap", "mknod", "audit_write", "net_raw", "dac_override",
"fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap"
};
parameters.HostConfig.ReadonlyRootfs = true;
parameters.HostConfig.SecurityOpt = new List<string>()
{
"no-new-privileges"
};
// Name
parameters.Name = name;
// Docker Image
parameters.Image = configuration.Template.DockerImage;
// Networking
if (configuration.Network.Ports.Length > 0 && !string.IsNullOrWhiteSpace(configuration.Network.FriendlyName))
parameters.Hostname = configuration.Network.FriendlyName;
parameters.ExposedPorts = new Dictionary<string, EmptyStruct>();
parameters.HostConfig.PortBindings = new Dictionary<string, IList<PortBinding>>();
foreach (var port in configuration.Network.Ports)
{
parameters.ExposedPorts.Add($"{port.Port}/tcp", new());
parameters.ExposedPorts.Add($"{port.Port}/udp", new());
parameters.HostConfig.PortBindings.Add($"{port.Port}/tcp", new List<PortBinding>
{
new()
{
HostPort = port.Port.ToString(),
HostIP = port.IpAddress
}
});
parameters.HostConfig.PortBindings.Add($"{port.Port}/udp", new List<PortBinding>
{
new()
{
HostPort = port.Port.ToString(),
HostIP = port.IpAddress
}
});
}
// TODO: Force outgoing ip stuff
// User
parameters.User = "1000:1000";
return parameters;
}
public static CreateContainerParameters GetInstallConfig(
string uuid,
string name,
RuntimeConfiguration runtimeConfiguration,
InstallConfiguration installConfiguration,
string runtimeStoragePath,
string installStoragePath
)
{
var parameters = new CreateContainerParameters()
{
HostConfig = new()
};
ApplySharedOptions(parameters, runtimeConfiguration);
// Labels
parameters.Labels = new Dictionary<string, string>()
{
{ "dev.moonlightpanel", "true" },
{ "dev.moonlightpanel.id", uuid }
};
foreach (var label in runtimeConfiguration.Environment.Labels)
parameters.Labels.Add(label.Key, label.Value);
// Name
parameters.Name = name;
// Docker Image
parameters.Image = installConfiguration.DockerImage;
// User
parameters.User = "1000:1000";
// Storage
parameters.WorkingDir = "/mnt/server";
parameters.HostConfig.Mounts = new List<Mount>();
parameters.HostConfig.Mounts.Add(new Mount()
{
Source = runtimeStoragePath,
Target = "/mnt/server",
ReadOnly = false,
Type = "bind"
});
parameters.HostConfig.Mounts.Add(new Mount()
{
Source = installStoragePath,
Target = "/mnt/install",
ReadOnly = false,
Type = "bind"
});
// Command
parameters.Cmd = [installConfiguration.Shell, "/mnt/install/install.sh"];
return parameters;
}
private static void ApplySharedOptions(
CreateContainerParameters parameters,
RuntimeConfiguration configuration
)
{
// Input, output & error streams and TTY
parameters.Tty = true;
parameters.AttachStderr = true;
parameters.AttachStdin = true;
parameters.AttachStdout = true;
parameters.OpenStdin = true;
// Logging
parameters.HostConfig.LogConfig = new()
{
Type = "json-file", // We need to use this provider, as the GetLogs endpoint needs it
Config = new Dictionary<string, string>()
};
// Environment variables
parameters.Env = new List<string>()
{
$"STARTUP={configuration.Template.StartupCommand}",
//TODO: Add timezone, add server ip
};
if (configuration.Limits.MemoryMb.HasValue)
parameters.Env.Add($"SERVER_MEMORY={configuration.Limits.MemoryMb.Value}");
if (configuration.Network.MainPort != null)
{
parameters.Env.Add($"SERVER_IP={configuration.Network.MainPort.IpAddress}");
parameters.Env.Add($"SERVER_PORT={configuration.Network.MainPort.Port}");
}
// Handle port variables
var i = 1;
foreach (var port in configuration.Network.Ports)
{
parameters.Env.Add($"ML_PORT_{i}={port.Port}");
i++;
}
// Copy variables as env vars
foreach (var variable in configuration.Environment.Variables)
parameters.Env.Add($"{variable.Key}={variable.Value}");
}
}

View File

@@ -0,0 +1,195 @@
using System.Buffers;
using System.Text;
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public class DockerConsole : IRuntimeConsole, IInstallConsole, IAsyncDisposable
{
public event Func<string, Task>? OnOutput;
private MultiplexedStream? Stream;
private readonly string ContainerId;
private readonly DockerClient DockerClient;
private readonly ILogger Logger;
private readonly List<string> Cache = new(302);
private readonly SemaphoreSlim CacheLock = new(1, 1);
private readonly CancellationTokenSource Cts = new();
public DockerConsole(
string containerId,
DockerClient dockerClient,
ILogger logger
)
{
ContainerId = containerId;
DockerClient = dockerClient;
Logger = logger;
}
public async Task AttachAsync()
{
// Fetch initial logs
Logger.LogTrace("Fetching pre-existing logs from container");
var logResponse = await DockerClient.Containers.GetContainerLogsAsync(
ContainerId,
new()
{
Follow = false,
ShowStderr = true,
ShowStdout = true
}
);
// Append to cache
var logs = await logResponse.ReadOutputToEndAsync(Cts.Token);
await CacheLock.WaitAsync(Cts.Token);
try
{
Cache.Add(logs.stdout);
Cache.Add(logs.stderr);
}
finally
{
CacheLock.Release();
}
// Stream new logs
Logger.LogTrace("Starting log streaming");
Task.Run(async () =>
{
var capturedCt = Cts.Token;
Logger.LogTrace("Starting attach loop");
while (!capturedCt.IsCancellationRequested)
{
try
{
using var stream = await DockerClient.Containers.AttachContainerAsync(
ContainerId,
new ContainerAttachParameters()
{
Stderr = true,
Stdin = true,
Stdout = true,
Stream = true
},
capturedCt
);
// Make stream accessible from the outside
Stream = stream;
const int bufferSize = 1024;
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
while (!capturedCt.IsCancellationRequested)
{
try
{
var readResult = await stream.ReadOutputAsync(buffer, 0, bufferSize, capturedCt);
if (readResult.Count > 0)
{
var decodedBuffer = Encoding.UTF8.GetString(buffer, 0, readResult.Count);
await CacheLock.WaitAsync(capturedCt);
try
{
if (Cache.Count > 300)
Cache.RemoveRange(0, 50);
Cache.Add(decodedBuffer);
}
finally
{
CacheLock.Release();
}
if (OnOutput != null)
await OnOutput.Invoke(decodedBuffer);
}
if (readResult.EOF)
break;
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception e)
{
Logger.LogError(e, "An unhandled error occured while processing container stream");
}
}
ArrayPool<byte>.Shared.Return(buffer);
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception e)
{
Logger.LogError(e, "An unhandled error occured while handling container attaching");
}
}
Logger.LogTrace("Attach loop exited");
});
}
public async Task WriteInputAsync(string value)
{
if (Stream == null)
throw new AggregateException("Stream is not available. Container might not be attached");
var buffer = Encoding.UTF8.GetBytes(value);
await Stream.WriteAsync(buffer, 0, buffer.Length, Cts.Token);
}
public async Task ClearCacheAsync()
{
await CacheLock.WaitAsync(Cts.Token);
try
{
Cache.Clear();
}
finally
{
CacheLock.Release();
}
}
public async Task<string[]> GetCacheAsync()
{
await CacheLock.WaitAsync();
try
{
return Cache.ToArray();
}
finally
{
CacheLock.Release();
}
}
public async ValueTask DisposeAsync()
{
await Cts.CancelAsync();
Stream?.Dispose();
CacheLock.Dispose();
}
}

View File

@@ -0,0 +1,93 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public class DockerEventService : BackgroundService
{
public event Func<ContainerDieEvent, Task>? OnContainerDied;
private readonly ILogger<DockerEventService> Logger;
private readonly DockerClient DockerClient;
public DockerEventService(
ILogger<DockerEventService> logger,
DockerClient dockerClient
)
{
Logger = logger;
DockerClient = dockerClient;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Logger.LogTrace("Starting up docker event monitor");
while (!stoppingToken.IsCancellationRequested)
{
try
{
Logger.LogTrace("Monitoring events");
await DockerClient.System.MonitorEventsAsync(
new ContainerEventsParameters(),
new Progress<Message>(OnEventAsync),
stoppingToken
);
}
catch (OperationCanceledException)
{
// ignored
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while processing container event monitoring");
}
}
Logger.LogTrace("Closed docker event monitor");
}
private async void OnEventAsync(Message message)
{
try
{
switch (message.Type)
{
case "container":
var containerId = message.Actor.ID;
switch (message.Action)
{
case "die":
if (
!message.Actor.Attributes.TryGetValue("exitCode", out var exitCodeStr) ||
!int.TryParse(exitCodeStr, out var exitCode)
)
{
return;
}
if (OnContainerDied != null)
await OnContainerDied.Invoke(new ContainerDieEvent(containerId, exitCode));
return;
}
break;
}
}
catch (Exception e)
{
Logger.LogError(
e,
"An error occured while handling event {type} for {action}",
message.Type,
message.Action
);
}
}
}

View File

@@ -0,0 +1,63 @@
using Docker.DotNet;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public class DockerInstallEnv : IInstallEnvironment
{
public IInstallStatistics Statistics => InnerStatistics;
public IInstallConsole Console => InnerConsole;
public event Func<Task>? OnExited;
public string ContainerId { get; }
private readonly DockerClient DockerClient;
private readonly ILogger Logger;
private readonly DockerEventService EventService;
private readonly DockerStatistics InnerStatistics;
private readonly DockerConsole InnerConsole;
public DockerInstallEnv(string containerId, DockerClient dockerClient, ILogger logger, DockerEventService eventService)
{
ContainerId = containerId;
DockerClient = dockerClient;
Logger = logger;
EventService = eventService;
InnerStatistics = new DockerStatistics();
InnerConsole = new DockerConsole(containerId, dockerClient, logger);
EventService.OnContainerDied += HandleDieEventAsync;
}
public async Task<bool> IsRunningAsync()
{
var container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
return container.State.Running;
}
public async Task StartAsync()
=> await DockerClient.Containers.StartContainerAsync(ContainerId, new());
public async Task KillAsync()
=> await DockerClient.Containers.KillContainerAsync(ContainerId, new());
private async Task HandleDieEventAsync(ContainerDieEvent dieEvent)
{
if(dieEvent.ContainerId != ContainerId)
return;
if(OnExited != null)
await OnExited.Invoke();
}
public async ValueTask DisposeAsync()
{
EventService.OnContainerDied -= HandleDieEventAsync;
await InnerConsole.DisposeAsync();
}
}

View File

@@ -0,0 +1,109 @@
using Docker.DotNet;
using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public class DockerInstallEnvService : IInstallEnvironmentService
{
private readonly DockerClient DockerClient;
private readonly ILoggerFactory LoggerFactory;
private readonly DockerEventService DockerEventService;
private const string NameTemplate = "ml-install-{0}";
public DockerInstallEnvService(DockerClient dockerClient, ILoggerFactory loggerFactory,
DockerEventService dockerEventService)
{
DockerClient = dockerClient;
LoggerFactory = loggerFactory;
DockerEventService = dockerEventService;
}
public async Task<IInstallEnvironment?> FindAsync(string id)
{
try
{
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
string.Format(NameTemplate, id)
);
var logger =
LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
return new DockerInstallEnv(dockerInspect.ID, DockerClient, logger, DockerEventService);
}
catch (DockerContainerNotFoundException)
{
return null;
}
}
public async Task<IInstallEnvironment> CreateAsync(
string id,
RuntimeConfiguration runtimeConfiguration,
InstallConfiguration installConfiguration,
IInstallStorage installStorage,
IRuntimeStorage runtimeStorage
)
{
try
{
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
string.Format(NameTemplate, id)
);
if (dockerInspect.State.Running)
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
var runtimeStoragePath = await runtimeStorage.GetHostPathAsync();
var installStoragePath = await installStorage.GetHostPathAsync();
var parameters = ConfigMapper.GetInstallConfig(
id,
string.Format(NameTemplate, id),
runtimeConfiguration,
installConfiguration,
runtimeStoragePath,
installStoragePath
);
var container = await DockerClient.Containers.CreateContainerAsync(parameters);
var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
return new DockerInstallEnv(container.ID, DockerClient, logger, DockerEventService);
}
public async Task DeleteAsync(IInstallEnvironment environment)
{
if (environment is not DockerInstallEnv dockerInstallEnv)
throw new ArgumentException(
$"You cannot delete runtime environments which haven't been created by {nameof(DockerInstallEnv)}");
await dockerInstallEnv.DisposeAsync();
try
{
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
dockerInstallEnv.ContainerId
);
if (dockerInspect.State.Running)
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
}
}

View File

@@ -0,0 +1,61 @@
using Docker.DotNet;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public class DockerRuntimeEnv : IRuntimeEnvironment
{
public IRuntimeStatistics Statistics => InnerStatistics;
public IRuntimeConsole Console => InnerConsole;
public string ContainerId { get; }
public event Func<Task>? OnExited;
private readonly DockerClient DockerClient;
private readonly DockerEventService EventService;
private readonly DockerConsole InnerConsole;
private readonly DockerStatistics InnerStatistics;
public DockerRuntimeEnv(string containerId, DockerClient dockerClient, ILogger logger, DockerEventService eventService)
{
ContainerId = containerId;
DockerClient = dockerClient;
EventService = eventService;
InnerStatistics = new DockerStatistics();
InnerConsole = new DockerConsole(containerId, dockerClient, logger);
EventService.OnContainerDied += HandleDieEventAsync;
}
public async Task<bool> IsRunningAsync()
{
var container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
return container.State.Running;
}
public async Task StartAsync()
=> await DockerClient.Containers.StartContainerAsync(ContainerId, new());
public async Task KillAsync()
=> await DockerClient.Containers.KillContainerAsync(ContainerId, new());
private async Task HandleDieEventAsync(ContainerDieEvent dieEvent)
{
if(dieEvent.ContainerId != ContainerId)
return;
if(OnExited != null)
await OnExited.Invoke();
}
public async ValueTask DisposeAsync()
{
EventService.OnContainerDied -= HandleDieEventAsync;
await InnerConsole.DisposeAsync();
}
}

View File

@@ -0,0 +1,112 @@
using Docker.DotNet;
using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public class DockerRuntimeEnvService : IRuntimeEnvironmentService
{
private readonly DockerClient DockerClient;
private readonly ILoggerFactory LoggerFactory;
private readonly DockerEventService DockerEventService;
private const string NameTemplate = "ml-runtime-{0}";
public DockerRuntimeEnvService(DockerClient dockerClient, ILoggerFactory loggerFactory,
DockerEventService dockerEventService)
{
DockerClient = dockerClient;
LoggerFactory = loggerFactory;
DockerEventService = dockerEventService;
}
public async Task<IRuntimeEnvironment?> FindAsync(string id)
{
try
{
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
string.Format(NameTemplate, id)
);
var logger =
LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
return new DockerRuntimeEnv(dockerInspect.ID, DockerClient, logger, DockerEventService);
}
catch (DockerContainerNotFoundException)
{
return null;
}
}
public async Task<IRuntimeEnvironment> CreateAsync(
string id,
RuntimeConfiguration configuration,
IRuntimeStorage storage
)
{
try
{
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
string.Format(NameTemplate, id)
);
if (dockerInspect.State.Running)
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
var storagePath = await storage.GetHostPathAsync();
var parameters = ConfigMapper.GetRuntimeConfig(
id,
string.Format(NameTemplate, id),
configuration,
storagePath
);
var container = await DockerClient.Containers.CreateContainerAsync(parameters);
var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
return new DockerRuntimeEnv(container.ID, DockerClient, logger, DockerEventService);
}
public Task UpdateAsync(IRuntimeEnvironment environment, RuntimeConfiguration configuration)
{
throw new NotImplementedException();
}
public async Task DeleteAsync(IRuntimeEnvironment environment)
{
if (environment is not DockerRuntimeEnv dockerRuntimeEnv)
{
throw new ArgumentException(
$"You cannot delete runtime environments which haven't been created by {nameof(DockerRuntimeEnvService)}"
);
}
await dockerRuntimeEnv.DisposeAsync();
try
{
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
dockerRuntimeEnv.ContainerId
);
if (dockerInspect.State.Running)
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
}
}

View File

@@ -0,0 +1,14 @@
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public class DockerStatistics : IRuntimeStatistics, IInstallStatistics
{
public event Func<ServerStatistics, Task>? OnStatisticsReceived;
public Task AttachAsync() => Task.CompletedTask;
public Task ClearCacheAsync() => Task.CompletedTask;
public Task<ServerStatistics[]> GetCacheAsync() => Task.FromResult<ServerStatistics[]>([]);
}

View File

@@ -0,0 +1,3 @@
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
public record ContainerDieEvent(string ContainerId, int ExitCode);

View File

@@ -0,0 +1,22 @@
using Docker.DotNet;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public static class Extensions
{
public static void AddDockerServices(this IServiceCollection collection)
{
var client = new DockerClientBuilder()
.WithEndpoint(new Uri("unix:///var/run/docker.sock"))
.Build();
collection.AddSingleton(client);
collection.AddSingleton<DockerEventService>();
collection.AddHostedService(sp => sp.GetRequiredService<DockerEventService>());
collection.AddSingleton<IRuntimeEnvironmentService, DockerRuntimeEnvService>();
collection.AddSingleton<IInstallEnvironmentService, DockerInstallEnvService>();
}
}

View File

@@ -0,0 +1,12 @@
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
public static class Extensions
{
public static void AddLocalServices(this IServiceCollection services)
{
services.AddSingleton<IRuntimeStorageService, LocalRuntimeStorageService>();
services.AddSingleton<IInstallStorageService, LocalInstallStorageService>();
}
}

View File

@@ -0,0 +1,15 @@
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
public class LocalInstallStorage : IInstallStorage
{
public string HostPath { get; }
public LocalInstallStorage(string hostPath)
{
HostPath = hostPath;
}
public Task<string> GetHostPathAsync() => Task.FromResult(HostPath);
}

View File

@@ -0,0 +1,43 @@
using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
public class LocalInstallStorageService : IInstallStorageService
{
private const string HostPathTemplate = "./mldaemon/install/{0}";
public Task<IInstallStorage?> FindAsync(string id)
{
var path = string.Format(HostPathTemplate, id);
if (!Directory.Exists(path))
return Task.FromResult<IInstallStorage?>(null);
return Task.FromResult<IInstallStorage?>(new LocalInstallStorage(path));
}
public Task<IInstallStorage> CreateAsync(string id, RuntimeConfiguration runtimeConfiguration, InstallConfiguration installConfiguration)
{
var path = string.Format(HostPathTemplate, id);
Directory.CreateDirectory(path);
return Task.FromResult<IInstallStorage>(new LocalInstallStorage(path));
}
public Task DeleteAsync(IInstallStorage installStorage)
{
if (installStorage is not LocalInstallStorage localInstallStorage)
{
throw new ArgumentException(
$"You cannot delete install storages which haven't been created by {nameof(LocalInstallStorageService)}"
);
}
if(Directory.Exists(localInstallStorage.HostPath))
Directory.Delete(localInstallStorage.HostPath, true);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,15 @@
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
public class LocalRuntimeStorage : IRuntimeStorage
{
public string HostPath { get; }
public LocalRuntimeStorage(string hostPath)
{
HostPath = hostPath;
}
public Task<string> GetHostPathAsync() => Task.FromResult(HostPath);
}

View File

@@ -0,0 +1,46 @@
using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
public class LocalRuntimeStorageService : IRuntimeStorageService
{
private const string HostPathTemplate = "./mldaemon/runtime/{0}";
public Task<IRuntimeStorage?> FindAsync(string id)
{
var path = string.Format(HostPathTemplate, id);
if (!Directory.Exists(path))
return Task.FromResult<IRuntimeStorage?>(null);
return Task.FromResult<IRuntimeStorage?>(new LocalRuntimeStorage(path));
}
public Task<IRuntimeStorage> CreateAsync(string id, RuntimeConfiguration configuration)
{
var path = string.Format(HostPathTemplate, id);
Directory.CreateDirectory(path);
return Task.FromResult<IRuntimeStorage>(new LocalRuntimeStorage(path));
}
public Task UpdateAsync(IRuntimeStorage runtimeStorage, RuntimeConfiguration configuration)
=> Task.CompletedTask;
public Task DeleteAsync(IRuntimeStorage runtimeStorage)
{
if (runtimeStorage is not LocalRuntimeStorage localRuntimeStorage)
{
throw new ArgumentException(
$"You cannot delete runtime storages which haven't been created by {nameof(LocalRuntimeStorageService)}"
);
}
if(Directory.Exists(localRuntimeStorage.HostPath))
Directory.Delete(localRuntimeStorage.HostPath, true);
return Task.CompletedTask;
}
}

View File

@@ -1,52 +0,0 @@
using System.Text.RegularExpressions;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Implementations;
public class RegexOnlineDetector : IOnlineDetector
{
private readonly ServerContext Context;
private Regex? Expression;
public RegexOnlineDetector(ServerContext context)
{
Context = context;
}
public Task InitializeAsync()
=> Task.CompletedTask;
public Task CreateAsync()
{
if(string.IsNullOrEmpty(Context.Configuration.OnlineDetection))
return Task.CompletedTask;
Expression = new Regex(Context.Configuration.OnlineDetection, RegexOptions.Compiled);
return Task.CompletedTask;
}
public Task<bool> HandleOutputAsync(string line)
{
if (Expression == null)
return Task.FromResult(false);
var result = Expression.Matches(line).Count > 0;
return Task.FromResult(result);
}
public Task DestroyAsync()
{
Expression = null;
return Task.CompletedTask;
}
public ValueTask DisposeAsync()
{
Expression = null;
return ValueTask.CompletedTask;
}
}

View File

@@ -1,44 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Implementations;
public class ServerReporter : IReporter
{
private readonly ServerContext Context;
private const string StatusTemplate =
"\x1b[1;38;2;200;90;200mM\x1b[1;38;2;204;110;230mo\x1b[1;38;2;170;130;245mo\x1b[1;38;2;140;150;255mn\x1b[1;38;2;110;180;255ml\x1b[1;38;2;100;200;255mi\x1b[1;38;2;100;220;255mg\x1b[1;38;2;120;235;255mh\x1b[1;38;2;140;250;255mt\x1b[0m \x1b[3;38;2;200;200;200m{0}\x1b[0m\n\r";
private const string ErrorTemplate =
"\x1b[1;38;2;200;90;200mM\x1b[1;38;2;204;110;230mo\x1b[1;38;2;170;130;245mo\x1b[1;38;2;140;150;255mn\x1b[1;38;2;110;180;255ml\x1b[1;38;2;100;200;255mi\x1b[1;38;2;100;220;255mg\x1b[1;38;2;120;235;255mh\x1b[1;38;2;140;250;255mt\x1b[0m \x1b[1;38;2;255;0;0m{0}\x1b[0m\n\r";
public ServerReporter(ServerContext context)
{
Context = context;
}
public Task InitializeAsync()
=> Task.CompletedTask;
public async Task StatusAsync(string message)
{
Context.Logger.LogInformation("Status: {message}", message);
await Context.Server.Console.WriteStdOutAsync(
string.Format(StatusTemplate, message)
);
}
public async Task ErrorAsync(string message)
{
Context.Logger.LogError("Error: {message}", message);
await Context.Server.Console.WriteStdOutAsync(
string.Format(ErrorTemplate, message)
);
}
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -1,64 +0,0 @@
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">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">Content to write</param>
/// <returns></returns>
public Task WriteStdOutAsync(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>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, ValueTask> callback);
}

View File

@@ -1,54 +0,0 @@
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();
}

View File

@@ -1,53 +0,0 @@
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
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">Host path of the runtime storage location</param>
/// <param name="hostPath">Host path of the installation file system</param>
/// <param name="data">Installation data for the server</param>
/// <returns></returns>
public Task CreateAsync(string runtimePath, string hostPath, ServerInstallDataResponse data);
/// <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">Callback to invoke whenever the installation exists</param>
/// <returns>Subscription disposable to unsubscribe from the event</returns>
public Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> 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();
}

View File

@@ -1,23 +0,0 @@
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">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();
}

View File

@@ -1,18 +0,0 @@
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">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">Message to write</param>
/// <returns></returns>
public Task ErrorAsync(string message);
}

View File

@@ -1,16 +0,0 @@
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();
}

View File

@@ -1,55 +0,0 @@
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">Path where the server files are located</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">Callback gets invoked whenever the runtime exites</param>
/// <returns>Subscription disposable to unsubscribe from the event</returns>
public Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> 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();
}

View File

@@ -1,10 +0,0 @@
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IServerComponent : IAsyncDisposable
{
/// <summary>
/// Initializes the server component
/// </summary>
/// <returns></returns>
public Task InitializeAsync();
}

View File

@@ -1,9 +0,0 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
public interface IServerStateHandler : IAsyncDisposable
{
public Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition);
}

View File

@@ -1,30 +0,0 @@
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();
}

View File

@@ -1,12 +0,0 @@
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; }
public ILogger Logger { get; set; }
}

View File

@@ -1,6 +0,0 @@
namespace MoonlightServers.Daemon.ServerSystem.Models;
public class StatisticsData
{
}

View File

@@ -0,0 +1,37 @@
namespace MoonlightServers.Daemon.ServerSystem;
public partial class Server
{
public async Task DeleteAsync()
{
await Lock.WaitAsync();
try
{
if(State != ServerState.Offline)
throw new InvalidOperationException("Server is not offline");
Logger.LogTrace("Deleting");
InstallStorage ??= await InstallStorageService.FindAsync(Uuid);
if (InstallStorage != null)
{
Logger.LogTrace("Deleting install storage");
await InstallStorageService.DeleteAsync(InstallStorage);
}
RuntimeStorage ??= await RuntimeStorageService.FindAsync(Uuid);
if (RuntimeStorage != null)
{
Logger.LogTrace("Deleting runtime storage");
await RuntimeStorageService.DeleteAsync(RuntimeStorage);
}
}
finally
{
Lock.Release();
}
}
}

View File

@@ -0,0 +1,157 @@
namespace MoonlightServers.Daemon.ServerSystem;
public partial class Server
{
public async Task InstallAsync()
{
await Lock.WaitAsync();
try
{
if (State != ServerState.Offline)
throw new InvalidOperationException("Server is not offline");
// Check if any pre-existing install env exists, if we don't have a reference to it already
InstallEnvironment ??= await InstallEnvironmentService.FindAsync(Uuid);
// Check if storages exist
InstallStorage ??= await InstallStorageService.FindAsync(Uuid);
RuntimeStorage ??= await RuntimeStorageService.FindAsync(Uuid);
// Remove any pre-existing installation env
if (InstallEnvironment != null)
{
Logger.LogTrace("Destroying pre-existing install environment");
if (await InstallEnvironment.IsRunningAsync())
{
Logger.LogTrace("Pre-existing install environment is still running, killing it");
await InstallEnvironment.KillAsync();
}
// Remove any event handlers if existing
InstallEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
InstallEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
InstallEnvironment.OnExited -= OnInstallExitedAsync;
// Now remove it
// Finally remove it
await InstallEnvironmentService.DeleteAsync(InstallEnvironment);
InstallEnvironment = null;
Logger.LogTrace("Pre-existing install environment destroyed");
}
// Remove pre-existing installation storage
if (InstallStorage != null)
{
Logger.LogTrace("Destroying pre-existing installation storage");
await InstallStorageService.DeleteAsync(InstallStorage);
InstallStorage = null;
}
// Fetch the latest configuration
Logger.LogTrace("Fetching latest configuration");
RuntimeConfiguration = await ConfigurationService.GetRuntimeConfigurationAsync(Uuid);
InstallConfiguration = await ConfigurationService.GetInstallConfigurationAsync(Uuid);
// Ensure runtime storage
if (RuntimeStorage == null)
{
Logger.LogTrace("Creating runtime storage");
RuntimeStorage = await RuntimeStorageService.CreateAsync(Uuid, RuntimeConfiguration);
}
else
{
Logger.LogTrace("Updating runtime storage");
await RuntimeStorageService.UpdateAsync(RuntimeStorage, RuntimeConfiguration);
}
// Create installation storage
Logger.LogTrace("Creating installation storage");
InstallStorage = await InstallStorageService.CreateAsync(Uuid, RuntimeConfiguration, InstallConfiguration);
// Write install script
var installStoragePath = await InstallStorage.GetHostPathAsync();
await File.WriteAllTextAsync(
Path.Combine(installStoragePath, "install.sh"),
InstallConfiguration.Script
);
// Create env
Logger.LogTrace("Creating install environment");
InstallEnvironment = await InstallEnvironmentService.CreateAsync(
Uuid,
RuntimeConfiguration,
InstallConfiguration,
InstallStorage,
RuntimeStorage
);
// Add event handlers
Logger.LogTrace("Attaching to install environment");
InstallEnvironment.Console.OnOutput += OnConsoleMessageAsync;
InstallEnvironment.Statistics.OnStatisticsReceived += OnStatisticsReceivedAsync;
InstallEnvironment.OnExited += OnInstallExitedAsync;
// Attach console and statistics
await InstallEnvironment.Console.AttachAsync();
await InstallEnvironment.Statistics.AttachAsync();
// Finally start the env
Logger.LogTrace("Starting install environment");
await InstallEnvironment.StartAsync();
await ChangeStateAsync(ServerState.Installing);
}
finally
{
Lock.Release();
}
}
private async Task OnInstallExitedAsync()
{
Logger.LogTrace("Install environment exited, checking result and cleaning up");
await Lock.WaitAsync();
try
{
// TODO: Handle crash
if (InstallEnvironment == null)
throw new InvalidOperationException("Install environment is not set");
// Make sure no event handler is there
InstallEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
InstallEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
InstallEnvironment.OnExited -= OnInstallExitedAsync;
// Remove env
await InstallEnvironmentService.DeleteAsync(InstallEnvironment);
InstallEnvironment = null;
Logger.LogTrace("Install environment cleaned up");
if(InstallStorage == null)
throw new InvalidOperationException("Install storage is not set");
Logger.LogTrace("Cleaned up install storage");
await InstallStorageService.DeleteAsync(InstallStorage);
InstallStorage = null;
}
finally
{
Lock.Release();
}
await ChangeStateAsync(ServerState.Offline);
}
}

View File

@@ -0,0 +1,163 @@
namespace MoonlightServers.Daemon.ServerSystem;
public partial class Server
{
public async Task StartAsync()
{
await Lock.WaitAsync();
try
{
if (State != ServerState.Offline)
throw new InvalidOperationException("Server is not offline");
// Check for any pre-existing runtime environment, if we don't have a reference already
RuntimeEnvironment ??= await RuntimeEnvironmentService.FindAsync(Uuid);
RuntimeStorage ??= await RuntimeStorageService.FindAsync(Uuid);
// Remove any pre-existing environment
if (RuntimeEnvironment != null)
{
Logger.LogTrace("Destroying pre-existing runtime environment");
if (await RuntimeEnvironment.IsRunningAsync())
{
Logger.LogTrace("Pre-existing runtime environment is still running, killing it");
await RuntimeEnvironment.KillAsync();
}
// Make sure no event handler is there anymore
RuntimeEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
RuntimeEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
RuntimeEnvironment.OnExited -= OnRuntimeExitedAsync;
// Finally remove it
await RuntimeEnvironmentService.DeleteAsync(RuntimeEnvironment);
RuntimeEnvironment = null;
Logger.LogTrace("Pre-existing runtime environment destroyed");
}
// Fetch the latest config
Logger.LogTrace("Fetching latest configuration");
RuntimeConfiguration = await ConfigurationService.GetRuntimeConfigurationAsync(Uuid);
// Ensure runtime storage
if (RuntimeStorage == null)
{
Logger.LogTrace("Creating runtime storage");
RuntimeStorage = await RuntimeStorageService.CreateAsync(Uuid, RuntimeConfiguration);
}
else
{
Logger.LogTrace("Updating runtime storage");
await RuntimeStorageService.UpdateAsync(RuntimeStorage, RuntimeConfiguration);
}
// Create the environment
Logger.LogTrace("Creating runtime environment");
RuntimeEnvironment = await RuntimeEnvironmentService.CreateAsync(Uuid, RuntimeConfiguration, RuntimeStorage);
// Set event handlers
Logger.LogTrace("Attaching to runtime environment");
RuntimeEnvironment.Console.OnOutput += OnConsoleMessageAsync;
RuntimeEnvironment.Statistics.OnStatisticsReceived += OnStatisticsReceivedAsync;
RuntimeEnvironment.OnExited += OnRuntimeExitedAsync;
// Attach console & statistics
await RuntimeEnvironment.Console.AttachAsync();
await RuntimeEnvironment.Statistics.AttachAsync();
// Start up
Logger.LogTrace("Starting runtime environment");
await RuntimeEnvironment.StartAsync();
await ChangeStateAsync(ServerState.Starting);
}
finally
{
Lock.Release();
}
}
public async Task StopAsync()
{
await Lock.WaitAsync();
try
{
if (State is not (ServerState.Starting or ServerState.Online))
throw new InvalidOperationException("Server is not starting or online");
if (RuntimeEnvironment == null)
throw new InvalidOperationException("Runtime environment is not set");
Logger.LogTrace("Sending stop command to runtime environment");
await RuntimeEnvironment.Console.WriteInputAsync("stop\n\r");
await ChangeStateAsync(ServerState.Stopping);
}
finally
{
Lock.Release();
}
}
public async Task KillAsync()
{
await Lock.WaitAsync();
try
{
if (State is not (ServerState.Starting or ServerState.Online or ServerState.Stopping))
throw new InvalidOperationException("Server is not starting, stopping or online");
if (RuntimeEnvironment == null)
throw new InvalidOperationException("Runtime environment is not set");
Logger.LogTrace("Killing runtime environment");
await RuntimeEnvironment.KillAsync();
await ChangeStateAsync(ServerState.Stopping);
}
finally
{
Lock.Release();
}
}
private async Task OnRuntimeExitedAsync()
{
Logger.LogTrace("Runtime environment exited, checking result and cleaning up");
await Lock.WaitAsync();
try
{
// TODO: Handle crash
if (RuntimeEnvironment == null)
throw new InvalidOperationException("Runtime environment is not set");
// Make sure no event handler is there anymore
RuntimeEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
RuntimeEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
RuntimeEnvironment.OnExited -= OnRuntimeExitedAsync;
// Finally remove it
await RuntimeEnvironmentService.DeleteAsync(RuntimeEnvironment);
RuntimeEnvironment = null;
Logger.LogTrace("Runtime environment cleaned up");
}
finally
{
Lock.Release();
}
await ChangeStateAsync(ServerState.Offline);
}
}

View File

@@ -0,0 +1,69 @@
namespace MoonlightServers.Daemon.ServerSystem;
public partial class Server
{
// Attempts to reattach to any running install or runtime environment that survived a daemon restart.
// Returns the appropriate state based on what was found, or Offline if nothing is running.
private async Task<ServerState> RestoreAsync()
{
// Install
Logger.LogTrace("Checking for existing install environment");
InstallEnvironment = await InstallEnvironmentService.FindAsync(Uuid);
InstallStorage = await InstallStorageService.FindAsync(Uuid);
if (InstallEnvironment != null)
{
var isRunning = await InstallEnvironment.IsRunningAsync();
if (isRunning)
{
Logger.LogTrace("Found running install environment, reattaching");
InstallEnvironment.Console.OnOutput += OnConsoleMessageAsync;
InstallEnvironment.Statistics.OnStatisticsReceived += OnStatisticsReceivedAsync;
InstallEnvironment.OnExited += OnInstallExitedAsync;
await InstallEnvironment.Console.AttachAsync();
await InstallEnvironment.Statistics.AttachAsync();
return ServerState.Installing;
}
Logger.LogTrace("Install environment exists but is not running, ignoring");
}
// Runtime
Logger.LogTrace("Checking for existing runtime environment");
RuntimeEnvironment = await RuntimeEnvironmentService.FindAsync(Uuid);
RuntimeStorage = await RuntimeStorageService.FindAsync(Uuid);
if (RuntimeEnvironment != null)
{
var isRunning = await RuntimeEnvironment.IsRunningAsync();
if (isRunning)
{
Logger.LogTrace("Found running runtime environment, reattaching");
RuntimeEnvironment.Console.OnOutput += OnConsoleMessageAsync;
RuntimeEnvironment.Statistics.OnStatisticsReceived += OnStatisticsReceivedAsync;
RuntimeEnvironment.OnExited += OnRuntimeExitedAsync;
await RuntimeEnvironment.Console.AttachAsync();
await RuntimeEnvironment.Statistics.AttachAsync();
// TODO: Use string online check here
return ServerState.Online;
}
Logger.LogTrace("Runtime environment exists but is not running, ignoring");
}
Logger.LogTrace("No running environments found");
return ServerState.Offline;
}
}

View File

@@ -1,161 +1,111 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using Stateless;
using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.ServerSystem;
public partial class Server : IAsyncDisposable
{
public int Identifier => InnerContext.Identifier;
public ServerContext Context => InnerContext;
public ServerState State { get; private set; }
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 IRuntimeEnvironment? RuntimeEnvironment;
private RuntimeConfiguration RuntimeConfiguration;
private IRuntimeStorage? RuntimeStorage;
private readonly IServerStateHandler[] Handlers;
private IInstallEnvironment? InstallEnvironment;
private InstallConfiguration InstallConfiguration;
private IInstallStorage? InstallStorage;
private readonly IServerComponent[] AllComponents;
private readonly ServerContext InnerContext;
private readonly IRuntimeEnvironmentService RuntimeEnvironmentService;
private readonly IInstallEnvironmentService InstallEnvironmentService;
private readonly IRuntimeStorageService RuntimeStorageService;
private readonly IInstallStorageService InstallStorageService;
private readonly ServerConfigurationService ConfigurationService;
private readonly string Uuid;
private readonly ILogger Logger;
private readonly SemaphoreSlim Lock = new(1, 1);
public Server(
ILogger logger,
ServerContext context,
IConsole console,
IFileSystem runtimeFileSystem,
IFileSystem installationFileSystem,
IInstallation installation,
IOnlineDetector onlineDetector,
IReporter reporter,
IRestorer restorer,
IRuntime runtime,
IStatistics statistics,
IEnumerable<IServerStateHandler> handlers,
IEnumerable<IServerComponent> additionalComponents
string uuid,
IRuntimeEnvironmentService runtimeEnvironmentService,
IInstallEnvironmentService installEnvironmentService,
IRuntimeStorageService runtimeStorageService,
IInstallStorageService installStorageService,
ServerConfigurationService configurationService,
ILogger logger
)
{
Uuid = uuid;
RuntimeEnvironmentService = runtimeEnvironmentService;
InstallEnvironmentService = installEnvironmentService;
RuntimeStorageService = runtimeStorageService;
InstallStorageService = installStorageService;
ConfigurationService = configurationService;
Logger = logger;
InnerContext = context;
Console = console;
RuntimeFileSystem = runtimeFileSystem;
InstallationFileSystem = installationFileSystem;
Installation = installation;
OnlineDetector = onlineDetector;
Reporter = reporter;
Restorer = restorer;
Runtime = runtime;
Statistics = statistics;
IEnumerable<IServerComponent> defaultComponents =
[
Console, RuntimeFileSystem, InstallationFileSystem, Installation, OnlineDetector, Reporter, Restorer,
Runtime, Statistics
];
AllComponents = defaultComponents.Concat(additionalComponents).ToArray();
Handlers = handlers.ToArray();
}
private void ConfigureStateMachine(ServerState initialState)
{
StateMachine = new StateMachine<ServerState, ServerTrigger>(
initialState, FiringMode.Queued
);
StateMachine.Configure(ServerState.Offline)
.Permit(ServerTrigger.Start, ServerState.Starting)
.Permit(ServerTrigger.Install, ServerState.Installing)
.PermitReentry(ServerTrigger.Fail);
StateMachine.Configure(ServerState.Starting)
.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);
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.Fail)
.PermitReentry(ServerTrigger.Kill)
.Permit(ServerTrigger.Exited, ServerState.Offline);
StateMachine.Configure(ServerState.Installing)
.Permit(ServerTrigger.Fail, ServerState.Offline) // TODO: Add kill
.Permit(ServerTrigger.Exited, ServerState.Offline);
}
private void ConfigureStateMachineEvents()
{
// Configure the calling of the handlers
StateMachine.OnTransitionedAsync(async transition =>
{
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 InitializeAsync()
{
foreach (var component in AllComponents)
await component.InitializeAsync();
Logger.LogTrace("Initializing");
var restoredState = ServerState.Offline;
await Lock.WaitAsync();
ConfigureStateMachine(restoredState);
ConfigureStateMachineEvents();
try
{
// Restore state
State = await RestoreAsync();
Logger.LogTrace("Initialization complete, restored to state {State}", State);
}
finally
{
Lock.Release();
}
}
private async Task OnConsoleMessageAsync(string message)
{
Console.WriteLine($"Console: {message}");
}
private async Task OnStatisticsReceivedAsync(ServerStatistics statistics)
{
}
private Task ChangeStateAsync(ServerState newState)
{
Logger.LogTrace("State changed from {OldState} to {NewState}", State, newState);
State = newState;
return Task.CompletedTask;
}
public async ValueTask DisposeAsync()
{
foreach (var handler in Handlers)
await handler.DisposeAsync();
foreach (var component in AllComponents)
await component.DisposeAsync();
Logger.LogTrace("Disposing");
if (RuntimeEnvironment != null)
{
Logger.LogTrace("Detaching and disposing runtime environment");
RuntimeEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
RuntimeEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
RuntimeEnvironment.OnExited -= OnRuntimeExitedAsync;
await RuntimeEnvironment.DisposeAsync();
}
if (InstallEnvironment != null)
{
Logger.LogTrace("Detaching and disposing install environment");
InstallEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
InstallEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
InstallEnvironment.OnExited -= OnInstallExitedAsync;
await InstallEnvironment.DisposeAsync();
}
}
}

View File

@@ -1,97 +1,46 @@
using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.Daemon.ServerSystem.Docker;
using MoonlightServers.Daemon.ServerSystem.FileSystems;
using MoonlightServers.Daemon.ServerSystem.Handlers;
using MoonlightServers.Daemon.ServerSystem.Implementations;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.ServerSystem;
public class ServerFactory
{
private readonly IServiceProvider ServiceProvider;
private readonly IRuntimeEnvironmentService RuntimeEnvironmentService;
private readonly IInstallEnvironmentService InstallEnvironmentService;
private readonly IRuntimeStorageService RuntimeStorageService;
private readonly IInstallStorageService InstallStorageService;
private readonly ServerConfigurationService ConfigurationService;
private readonly ILoggerFactory LoggerFactory;
public ServerFactory(IServiceProvider serviceProvider)
public ServerFactory(
IRuntimeEnvironmentService runtimeEnvironmentService,
IInstallEnvironmentService installEnvironmentService,
IRuntimeStorageService runtimeStorageService,
IInstallStorageService installStorageService,
ServerConfigurationService configurationService,
ILoggerFactory loggerFactory
)
{
ServiceProvider = serviceProvider;
RuntimeEnvironmentService = runtimeEnvironmentService;
InstallEnvironmentService = installEnvironmentService;
RuntimeStorageService = runtimeStorageService;
InstallStorageService = installStorageService;
ConfigurationService = configurationService;
LoggerFactory = loggerFactory;
}
public async Task<Server> CreateAsync(ServerConfiguration configuration)
public async Task<Server> CreateAsync(string uuid)
{
var scope = ServiceProvider.CreateAsyncScope();
var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Server({uuid})");
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;
context.Logger = logger;
// 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
console = ActivatorUtilities.CreateInstance<DockerConsole>(scope.ServiceProvider);
reporter = ActivatorUtilities.CreateInstance<ServerReporter>(scope.ServiceProvider);
runtimeFs = ActivatorUtilities.CreateInstance<RawRuntimeFs>(scope.ServiceProvider);
installFs = ActivatorUtilities.CreateInstance<RawInstallationFs>(scope.ServiceProvider);
installation = ActivatorUtilities.CreateInstance<DockerInstallation>(scope.ServiceProvider);
onlineDetector = ActivatorUtilities.CreateInstance<RegexOnlineDetector>(scope.ServiceProvider);
restorer = ActivatorUtilities.CreateInstance<DockerRestorer>(scope.ServiceProvider);
runtime = ActivatorUtilities.CreateInstance<DockerRuntime>(scope.ServiceProvider);
statistics = ActivatorUtilities.CreateInstance<DockerStatistics>(scope.ServiceProvider);
// Resolve handlers
var handlers = new List<IServerStateHandler>();
handlers.Add(ActivatorUtilities.CreateInstance<OnlineDetectionHandler>(scope.ServiceProvider));
handlers.Add(ActivatorUtilities.CreateInstance<StartupHandler>(scope.ServiceProvider));
handlers.Add(ActivatorUtilities.CreateInstance<ShutdownHandler>(scope.ServiceProvider));
handlers.Add(ActivatorUtilities.CreateInstance<InstallationHandler>(scope.ServiceProvider));
handlers.Add(ActivatorUtilities.CreateInstance<DebugHandler>(scope.ServiceProvider));
// Resolve additional components
var components = new List<IServerComponent>();
components.Add(ActivatorUtilities.CreateInstance<ConsoleSignalRComponent>(scope.ServiceProvider));
// 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
handlers,
components
return new Server(
uuid,
RuntimeEnvironmentService,
InstallEnvironmentService,
RuntimeStorageService,
InstallStorageService,
ConfigurationService,
logger
);
context.Server = server;
return server;
}
}

View File

@@ -1,4 +1,4 @@
namespace MoonlightServers.Daemon.ServerSystem.Enums;
namespace MoonlightServers.Daemon.ServerSystem;
public enum ServerState
{

View File

@@ -0,0 +1,3 @@
namespace MoonlightServers.Daemon.ServerSystem;
public record ServerStatistics();