Implemented restorer, runtime and dummy statistics. Added service registering and fixed server factory. Moved logger to server context

This commit is contained in:
2025-09-07 23:15:48 +02:00
parent 282096595d
commit b90100d250
18 changed files with 385 additions and 65 deletions

View File

@@ -17,7 +17,6 @@
<ItemGroup>
<PackageReference Include="Moonlight.ApiServer" Version="2.1.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7"/>
<PackageReference Include="Riok.Mapperly" Version="4.3.0-next.2" />
</ItemGroup>
<ItemGroup>

View File

@@ -57,6 +57,13 @@
<_ContentIncludedByDefault Remove="volumes\3\version_history.json" />
<_ContentIncludedByDefault Remove="volumes\3\whitelist.json" />
<_ContentIncludedByDefault Remove="storage\volumes\69\plugins\spark\config.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\banned-ips.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\banned-players.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\ops.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\plugins\spark\config.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\usercache.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\version_history.json" />
<_ContentIncludedByDefault Remove="storage\volumes\6\whitelist.json" />
</ItemGroup>
</Project>

View File

@@ -22,6 +22,7 @@ public class DockerConsole : IConsole
{
DockerClient = dockerClient;
Context = context;
Logger = Context.Logger;
}
public Task InitializeAsync()

View File

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

View File

@@ -16,7 +16,7 @@ public class DockerInstallation : IInstallation
private readonly DockerImageService ImageService;
private readonly ServerContext ServerContext;
private readonly DockerClient DockerClient;
private readonly IReporter Reporter;
private IReporter Reporter => ServerContext.Server.Reporter;
private readonly EventSource<int> ExitEventSource = new();
@@ -28,7 +28,6 @@ public class DockerInstallation : IInstallation
ServerContext serverContext,
ServerConfigurationMapper mapper,
DockerImageService imageService,
IReporter reporter,
DockerEventService dockerEventService
)
{
@@ -36,7 +35,6 @@ public class DockerInstallation : IInstallation
ServerContext = serverContext;
Mapper = mapper;
ImageService = imageService;
Reporter = reporter;
DockerEventService = dockerEventService;
}
@@ -119,7 +117,8 @@ public class DockerInstallation : IInstallation
//
await DockerClient.Containers.CreateContainerAsync(parameters);
var response = await DockerClient.Containers.CreateContainerAsync(parameters);
ContainerId = response.ID;
await Reporter.StatusAsync("Created container");
}

View File

@@ -0,0 +1,59 @@
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

@@ -0,0 +1,177 @@
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.Download(
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> SubscribeExited(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

@@ -0,0 +1,25 @@
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

@@ -12,7 +12,7 @@ public class RawInstallationFs : IFileSystem
BaseDirectory = Path.Combine(
Directory.GetCurrentDirectory(),
"storage",
"volumes",
"install",
context.Configuration.Id.ToString()
);
}

View File

@@ -17,7 +17,7 @@ public class ShutdownHandler : IServerStateHandler
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
if (transition is not
{
Destination: ServerState.Offline,
Source: not ServerState.Installing,

View File

@@ -79,7 +79,7 @@ public class StartupHandler : IServerStateHandler
await Server.Runtime.StartAsync();
}
private async Task OnRuntimeExited(int exitCode)
private async ValueTask OnRuntimeExited(int exitCode)
{
// TODO: Notify the crash handler component of the exit code

View File

@@ -7,14 +7,12 @@ namespace MoonlightServers.Daemon.ServerSystem.Implementations;
public class RegexOnlineDetector : IOnlineDetector
{
private readonly ServerContext Context;
private readonly ILogger Logger;
private Regex? Expression;
public RegexOnlineDetector(ServerContext context, ILogger logger)
public RegexOnlineDetector(ServerContext context)
{
Context = context;
Logger = logger;
}
public Task InitializeAsync()
@@ -25,14 +23,7 @@ public class RegexOnlineDetector : IOnlineDetector
if(string.IsNullOrEmpty(Context.Configuration.OnlineDetection))
return Task.CompletedTask;
try
{
Expression = new Regex(Context.Configuration.OnlineDetection, RegexOptions.Compiled);
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while creating regex. Check the regex expression");
}
Expression = new Regex(Context.Configuration.OnlineDetection, RegexOptions.Compiled);
return Task.CompletedTask;
}

View File

@@ -1,11 +1,11 @@
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Implementations;
public class ServerReporter : IReporter
{
private readonly IConsole Console;
private readonly ILogger Logger;
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";
@@ -13,10 +13,9 @@ public class ServerReporter : IReporter
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(IConsole console, ILogger logger)
public ServerReporter(ServerContext context)
{
Console = console;
Logger = logger;
Context = context;
}
public Task InitializeAsync()
@@ -24,18 +23,18 @@ public class ServerReporter : IReporter
public async Task StatusAsync(string message)
{
Logger.LogInformation("Status: {message}", message);
Context.Logger.LogInformation("Status: {message}", message);
await Console.WriteStdOutAsync(
await Context.Server.Console.WriteStdOutAsync(
string.Format(StatusTemplate, message)
);
}
public async Task ErrorAsync(string message)
{
Logger.LogError("Error: {message}", message);
Context.Logger.LogError("Error: {message}", message);
await Console.WriteStdOutAsync(
await Context.Server.Console.WriteStdOutAsync(
string.Format(ErrorTemplate, message)
);
}

View File

@@ -44,7 +44,7 @@ public interface IRuntime : IServerComponent
/// </summary>
/// <param name="callback">Callback gets invoked whenever the runtime exites</param>
/// <returns>Subscription disposable to unsubscribe from the event</returns>
public Task<IAsyncDisposable> SubscribeExited(Func<int, Task> callback);
public Task<IAsyncDisposable> SubscribeExited(Func<int, ValueTask> callback);
/// <summary>
/// Connects an existing runtime to this abstraction in order to restore it.

View File

@@ -1,4 +1,5 @@
using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
namespace MoonlightServers.Daemon.ServerSystem.Models;
@@ -8,4 +9,5 @@ public class ServerContext
public int Identifier { get; set; }
public AsyncServiceScope ServiceScope { get; set; }
public Server Server { get; set; }
public ILogger Logger { get; set; }
}

View File

@@ -136,32 +136,6 @@ public partial class Server : IAsyncDisposable
});
}
private async Task HandleSaveAsync(Func<Task> callback)
{
try
{
await callback.Invoke();
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while handling");
await StateMachine.FireAsync(ServerTrigger.Fail);
}
}
private async Task HandleIgnoredAsync(Func<Task> callback)
{
try
{
await callback.Invoke();
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while handling");
}
}
public async Task InitializeAsync()
{
foreach (var component in AllComponents)

View File

@@ -1,6 +1,7 @@
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;
@@ -16,7 +17,7 @@ public class ServerFactory
ServiceProvider = serviceProvider;
}
public async Task<Server> Create(ServerConfiguration configuration)
public async Task<Server> CreateAsync(ServerConfiguration configuration)
{
var scope = ServiceProvider.CreateAsyncScope();
@@ -28,6 +29,7 @@ public class ServerFactory
context.Identifier = configuration.Id;
context.Configuration = configuration;
context.ServiceScope = scope;
context.Logger = logger;
// Define all required components
@@ -43,12 +45,21 @@ public class ServerFactory
// Resolve the components
console = ActivatorUtilities.CreateInstance<DockerConsole>(scope.ServiceProvider, logger);
reporter = ActivatorUtilities.CreateInstance<ServerReporter>(scope.ServiceProvider, console, logger);
runtimeFs = ActivatorUtilities.CreateInstance<RawRuntimeFs>(scope.ServiceProvider, logger, reporter);
installFs = ActivatorUtilities.CreateInstance<RawInstallationFs>(scope.ServiceProvider, logger, reporter);
installation = ActivatorUtilities.CreateInstance<DockerInstallation>(scope.ServiceProvider, logger, reporter);
onlineDetector = ActivatorUtilities.CreateInstance<RegexOnlineDetector>(scope.ServiceProvider, logger, reporter);
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<StartupHandler>(scope.ServiceProvider));
handlers.Add(ActivatorUtilities.CreateInstance<ShutdownHandler>(scope.ServiceProvider));
// TODO: Add a plugin hook for dynamically resolving components and checking if any is unset
@@ -67,7 +78,7 @@ public class ServerFactory
runtime,
statistics,
// And now all the handlers
[]
handlers.ToArray()
);
context.Server = server;

View File

@@ -11,6 +11,16 @@ using MoonCore.Logging;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.Daemon.Http.Hubs;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.Daemon.ServerSystem;
using MoonlightServers.Daemon.ServerSystem.Docker;
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.FileSystems;
using MoonlightServers.Daemon.ServerSystem.Handlers;
using MoonlightServers.Daemon.ServerSystem.Implementations;
using MoonlightServers.Daemon.ServerSystem.Models;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon;
@@ -60,6 +70,64 @@ public class Startup
await MapBase();
await MapHubs();
Task.Run(async () =>
{
try
{
var serverConfig = new ServerConfiguration()
{
Id = 69,
Allocations =
[
new ServerConfiguration.AllocationConfiguration()
{
IpAddress = "0.0.0.0",
Port = 25565
}
],
Cpu = 400,
Disk = 10240,
Memory = 4096,
OnlineDetection = "\\! For help, type ",
StartupCommand =
"java -Xms128M -Xmx{{SERVER_MEMORY}}M -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}",
DockerImage = "ghcr.io/nexocrew-hq/moonlightdockerimages:java21",
StopCommand = "stop",
UseVirtualDisk = false,
Bandwidth = 0,
Variables = new Dictionary<string, string>()
{
{ "SERVER_JARFILE", "server.jar" }
}
};
var factory = WebApplication.Services.GetRequiredService<ServerFactory>();
Console.Write("Press enter to create and init server");
Console.ReadLine();
var s = await factory.CreateAsync(serverConfig);
await s.InitializeAsync();
s.StateMachine.OnTransitionCompleted(transition =>
{
Console.WriteLine(transition.Destination);
});
Console.Write("Press enter to start server");
Console.ReadLine();
await s.StateMachine.FireAsync(ServerTrigger.Start);
Console.ReadLine();
}
catch (Exception e)
{
Console.WriteLine(e);
}
});
await WebApplication.RunAsync();
}
@@ -125,6 +193,10 @@ public class Startup
).CreateClient();
WebApplicationBuilder.Services.AddSingleton(dockerClient);
WebApplicationBuilder.Services.AddScoped<DockerImageService>();
WebApplicationBuilder.Services.AddSingleton<DockerEventService>();
WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<DockerEventService>());
return Task.CompletedTask;
}
@@ -245,6 +317,10 @@ public class Startup
private Task RegisterServers()
{
WebApplicationBuilder.Services.AddScoped<ServerContext>();
WebApplicationBuilder.Services.AddSingleton<ServerFactory>();
WebApplicationBuilder.Services.AddSingleton<ServerConfigurationMapper>();
return Task.CompletedTask;
}