Implemented statistics. Refactored storage abstractions. Added config options for docker and local storage. Added server service and server updating.

This commit is contained in:
2026-03-02 15:51:05 +00:00
parent 52dbd13fb5
commit 2d1b48b0d4
27 changed files with 493 additions and 147 deletions

View File

@@ -0,0 +1,6 @@
namespace MoonlightServers.Daemon.Configuration;
public class DockerOptions
{
public string SocketUri { get; set; } = "unix:///var/run/docker.sock";
}

View File

@@ -0,0 +1,7 @@
namespace MoonlightServers.Daemon.Configuration;
public class LocalStorageOptions
{
public string InstallPath { get; set; } = "/data/install";
public string RuntimePath { get; set; } = "/data/runtime";
}

View File

@@ -23,6 +23,9 @@
<Compile Update="ServerSystem\Server.Delete.cs"> <Compile Update="ServerSystem\Server.Delete.cs">
<DependentUpon>Server.cs</DependentUpon> <DependentUpon>Server.cs</DependentUpon>
</Compile> </Compile>
<Compile Update="ServerSystem\Server.Update.cs">
<DependentUpon>Server.cs</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -14,6 +14,7 @@ builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions
builder.Services.AddSingleton<ServerConfigurationService>(); builder.Services.AddSingleton<ServerConfigurationService>();
builder.Services.AddSingleton<ServerFactory>(); builder.Services.AddSingleton<ServerFactory>();
builder.Services.AddSingleton<ServerService>();
builder.Services.AddDockerServices(); builder.Services.AddDockerServices();
builder.Services.AddLocalServices(); builder.Services.AddLocalServices();
@@ -31,15 +32,18 @@ Task.Run(async () =>
try try
{ {
var factory = app.Services.GetRequiredService<ServerFactory>(); var serverService = app.Services.GetRequiredService<ServerService>();
var server = await factory.CreateAsync("a0e3ddb4-2c72-4f4c-bc49-35650a4bc5c0");
await server.InitializeAsync(); await serverService.SyncAsync("a0e3ddb4-2c72-4f4c-bc49-35650a4bc5c0");
await serverService.SyncAsync("a0e3ddb4-2c72-4f4c-bc49-35650a4bc5c0");
var server = await serverService.GetAsync("a0e3ddb4-2c72-4f4c-bc49-35650a4bc5c0");
if(server == null)
return;
Console.WriteLine($"Server: {server.State}"); Console.WriteLine($"Server: {server.State}");
Console.ReadLine();
if (server.State == ServerState.Offline) if (server.State == ServerState.Offline)
await server.StartAsync(); await server.StartAsync();
else else
@@ -47,7 +51,7 @@ Task.Run(async () =>
Console.ReadLine(); Console.ReadLine();
await server.DisposeAsync(); await serverService.DeleteAsync("a0e3ddb4-2c72-4f4c-bc49-35650a4bc5c0");
} }
catch (Exception e) catch (Exception e)
{ {

View File

@@ -1,6 +1,6 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions; namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IInstallStatistics public interface IInstallStatistics : IAsyncDisposable
{ {
public event Func<ServerStatistics, Task>? OnStatisticsReceived; public event Func<ServerStatistics, Task>? OnStatisticsReceived;

View File

@@ -2,5 +2,5 @@ namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IInstallStorage public interface IInstallStorage
{ {
public Task<string> GetHostPathAsync(); public Task<string> GetBindPathAsync();
} }

View File

@@ -1,6 +1,6 @@
namespace MoonlightServers.Daemon.ServerSystem.Abstractions; namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IRuntimeStatistics public interface IRuntimeStatistics : IAsyncDisposable
{ {
public event Func<ServerStatistics, Task>? OnStatisticsReceived; public event Func<ServerStatistics, Task>? OnStatisticsReceived;

View File

@@ -2,5 +2,5 @@ namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
public interface IRuntimeStorage public interface IRuntimeStorage
{ {
public Task<string> GetHostPathAsync(); public Task<string> GetBindPathAsync();
} }

View File

@@ -1,12 +1,11 @@
using System.ComponentModel;
using Docker.DotNet.Models; using Docker.DotNet.Models;
using MoonlightServers.Daemon.Models; using MoonlightServers.Daemon.Models;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker; namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
public static class ConfigMapper public class ContainerConfigMapper
{ {
public static CreateContainerParameters GetRuntimeConfig( public CreateContainerParameters GetRuntimeConfig(
string uuid, string uuid,
string name, string name,
RuntimeConfiguration configuration, RuntimeConfiguration configuration,
@@ -138,7 +137,7 @@ public static class ConfigMapper
return parameters; return parameters;
} }
public static CreateContainerParameters GetInstallConfig( public CreateContainerParameters GetInstallConfig(
string uuid, string uuid,
string name, string name,
RuntimeConfiguration runtimeConfiguration, RuntimeConfiguration runtimeConfiguration,
@@ -201,7 +200,7 @@ public static class ConfigMapper
return parameters; return parameters;
} }
private static void ApplySharedOptions( private void ApplySharedOptions(
CreateContainerParameters parameters, CreateContainerParameters parameters,
RuntimeConfiguration configuration RuntimeConfiguration configuration
) )

View File

@@ -31,14 +31,15 @@ public class DockerConsole : IRuntimeConsole, IInstallConsole, IAsyncDisposable
Logger = logger; Logger = logger;
} }
public async Task AttachAsync() public async Task AttachAsync()
{ {
// Fetch initial logs // Fetch initial container logs
Logger.LogTrace("Fetching pre-existing logs from container"); Logger.LogTrace("Fetching initial container logs");
var logResponse = await DockerClient.Containers.GetContainerLogsAsync( using var logStream = await DockerClient.Containers.GetContainerLogsAsync(
ContainerId, ContainerId,
new() new ContainerLogsParameters()
{ {
Follow = false, Follow = false,
ShowStderr = true, ShowStderr = true,
@@ -46,94 +47,50 @@ public class DockerConsole : IRuntimeConsole, IInstallConsole, IAsyncDisposable
} }
); );
// Append to cache // and process it
var logs = await logResponse.ReadOutputToEndAsync(Cts.Token); await ProcessStreamAsync(logStream, Cts.Token);
await CacheLock.WaitAsync(Cts.Token); // After that we can actually start streaming the new logs
Logger.LogTrace("Attaching to container");
try Stream = await DockerClient.Containers.AttachContainerAsync(
{ ContainerId,
Cache.Add(logs.stdout); new ContainerAttachParameters()
Cache.Add(logs.stderr); {
} Stderr = true,
finally Stdin = true,
{ Stdout = true,
CacheLock.Release(); Stream = true
} },
Cts.Token
// Stream new logs );
Logger.LogTrace("Starting log streaming");
Task.Run(async () => Task.Run(async () =>
{ {
var capturedCt = Cts.Token; Logger.LogTrace("Entered streaming loop");
Logger.LogTrace("Starting attach loop"); while (!Cts.IsCancellationRequested)
while (!capturedCt.IsCancellationRequested)
{ {
try try
{ {
using var stream = await DockerClient.Containers.AttachContainerAsync( if (Stream == null) // Triggers when e.g. a connection issue occurs cause the catch clause resets the stream
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 Logger.LogTrace("Reattaching to container");
{
var readResult = await stream.ReadOutputAsync(buffer, 0, bufferSize, capturedCt);
if (readResult.Count > 0) Stream = await DockerClient.Containers.AttachContainerAsync(
ContainerId,
new ContainerAttachParameters()
{ {
var decodedBuffer = Encoding.UTF8.GetString(buffer, 0, readResult.Count); Stderr = true,
Stdin = true,
await CacheLock.WaitAsync(capturedCt); Stdout = true,
Stream = true
try },
{ Cts.Token
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); await ProcessStreamAsync(Stream, Cts.Token);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -141,14 +98,57 @@ public class DockerConsole : IRuntimeConsole, IInstallConsole, IAsyncDisposable
} }
catch (Exception e) catch (Exception e)
{ {
Logger.LogError(e, "An unhandled error occured while handling container attaching"); Logger.LogError(e, "An unhandled error occured while processing container stream");
}
finally
{
Stream?.Dispose();
Stream = null;
} }
} }
Logger.LogTrace("Attach loop exited"); Logger.LogTrace("Exited streaming loop");
}); });
} }
private async Task ProcessStreamAsync(MultiplexedStream stream, CancellationToken cancellationToken)
{
const int bufferSize = 1024;
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
while (!cancellationToken.IsCancellationRequested)
{
var readResult = await stream.ReadOutputAsync(buffer, 0, bufferSize, cancellationToken);
if (readResult.Count > 0)
{
var decodedBuffer = Encoding.UTF8.GetString(buffer, 0, readResult.Count);
await CacheLock.WaitAsync(cancellationToken);
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;
}
ArrayPool<byte>.Shared.Return(buffer);
}
public async Task WriteInputAsync(string value) public async Task WriteInputAsync(string value)
{ {
if (Stream == null) if (Stream == null)
@@ -188,6 +188,8 @@ public class DockerConsole : IRuntimeConsole, IInstallConsole, IAsyncDisposable
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
Logger.LogTrace("Disposing");
await Cts.CancelAsync(); await Cts.CancelAsync();
Stream?.Dispose(); Stream?.Dispose();
CacheLock.Dispose(); CacheLock.Dispose();

View File

@@ -27,7 +27,7 @@ public class DockerInstallEnv : IInstallEnvironment
Logger = logger; Logger = logger;
EventService = eventService; EventService = eventService;
InnerStatistics = new DockerStatistics(); InnerStatistics = new DockerStatistics(containerId, dockerClient, logger);
InnerConsole = new DockerConsole(containerId, dockerClient, logger); InnerConsole = new DockerConsole(containerId, dockerClient, logger);
EventService.OnContainerDied += HandleDieEventAsync; EventService.OnContainerDied += HandleDieEventAsync;
@@ -59,5 +59,6 @@ public class DockerInstallEnv : IInstallEnvironment
EventService.OnContainerDied -= HandleDieEventAsync; EventService.OnContainerDied -= HandleDieEventAsync;
await InnerConsole.DisposeAsync(); await InnerConsole.DisposeAsync();
await InnerStatistics.DisposeAsync();
} }
} }

View File

@@ -9,15 +9,21 @@ public class DockerInstallEnvService : IInstallEnvironmentService
private readonly DockerClient DockerClient; private readonly DockerClient DockerClient;
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
private readonly DockerEventService DockerEventService; private readonly DockerEventService DockerEventService;
private readonly ContainerConfigMapper ConfigMapper;
private const string NameTemplate = "ml-install-{0}"; private const string NameTemplate = "ml-install-{0}";
public DockerInstallEnvService(DockerClient dockerClient, ILoggerFactory loggerFactory, public DockerInstallEnvService(
DockerEventService dockerEventService) DockerClient dockerClient,
ILoggerFactory loggerFactory,
DockerEventService dockerEventService,
ContainerConfigMapper configMapper
)
{ {
DockerClient = dockerClient; DockerClient = dockerClient;
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
DockerEventService = dockerEventService; DockerEventService = dockerEventService;
ConfigMapper = configMapper;
} }
public async Task<IInstallEnvironment?> FindAsync(string id) public async Task<IInstallEnvironment?> FindAsync(string id)
@@ -28,8 +34,7 @@ public class DockerInstallEnvService : IInstallEnvironmentService
string.Format(NameTemplate, id) string.Format(NameTemplate, id)
); );
var logger = var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
return new DockerInstallEnv(dockerInspect.ID, DockerClient, logger, DockerEventService); return new DockerInstallEnv(dockerInspect.ID, DockerClient, logger, DockerEventService);
} }
@@ -63,8 +68,8 @@ public class DockerInstallEnvService : IInstallEnvironmentService
// Ignored // Ignored
} }
var runtimeStoragePath = await runtimeStorage.GetHostPathAsync(); var runtimeStoragePath = await runtimeStorage.GetBindPathAsync();
var installStoragePath = await installStorage.GetHostPathAsync(); var installStoragePath = await installStorage.GetBindPathAsync();
var parameters = ConfigMapper.GetInstallConfig( var parameters = ConfigMapper.GetInstallConfig(
id, id,

View File

@@ -25,7 +25,7 @@ public class DockerRuntimeEnv : IRuntimeEnvironment
DockerClient = dockerClient; DockerClient = dockerClient;
EventService = eventService; EventService = eventService;
InnerStatistics = new DockerStatistics(); InnerStatistics = new DockerStatistics(containerId, dockerClient, logger);
InnerConsole = new DockerConsole(containerId, dockerClient, logger); InnerConsole = new DockerConsole(containerId, dockerClient, logger);
EventService.OnContainerDied += HandleDieEventAsync; EventService.OnContainerDied += HandleDieEventAsync;
@@ -57,5 +57,6 @@ public class DockerRuntimeEnv : IRuntimeEnvironment
EventService.OnContainerDied -= HandleDieEventAsync; EventService.OnContainerDied -= HandleDieEventAsync;
await InnerConsole.DisposeAsync(); await InnerConsole.DisposeAsync();
await InnerStatistics.DisposeAsync();
} }
} }

View File

@@ -9,15 +9,21 @@ public class DockerRuntimeEnvService : IRuntimeEnvironmentService
private readonly DockerClient DockerClient; private readonly DockerClient DockerClient;
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
private readonly DockerEventService DockerEventService; private readonly DockerEventService DockerEventService;
private readonly ContainerConfigMapper ConfigMapper;
private const string NameTemplate = "ml-runtime-{0}"; private const string NameTemplate = "ml-runtime-{0}";
public DockerRuntimeEnvService(DockerClient dockerClient, ILoggerFactory loggerFactory, public DockerRuntimeEnvService(
DockerEventService dockerEventService) DockerClient dockerClient,
ILoggerFactory loggerFactory,
DockerEventService dockerEventService,
ContainerConfigMapper configMapper
)
{ {
DockerClient = dockerClient; DockerClient = dockerClient;
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
DockerEventService = dockerEventService; DockerEventService = dockerEventService;
ConfigMapper = configMapper;
} }
public async Task<IRuntimeEnvironment?> FindAsync(string id) public async Task<IRuntimeEnvironment?> FindAsync(string id)
@@ -28,8 +34,7 @@ public class DockerRuntimeEnvService : IRuntimeEnvironmentService
string.Format(NameTemplate, id) string.Format(NameTemplate, id)
); );
var logger = var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
return new DockerRuntimeEnv(dockerInspect.ID, DockerClient, logger, DockerEventService); return new DockerRuntimeEnv(dockerInspect.ID, DockerClient, logger, DockerEventService);
} }
@@ -61,7 +66,7 @@ public class DockerRuntimeEnvService : IRuntimeEnvironmentService
// Ignored // Ignored
} }
var storagePath = await storage.GetHostPathAsync(); var storagePath = await storage.GetBindPathAsync();
var parameters = ConfigMapper.GetRuntimeConfig( var parameters = ConfigMapper.GetRuntimeConfig(
id, id,
@@ -78,9 +83,7 @@ public class DockerRuntimeEnvService : IRuntimeEnvironmentService
} }
public Task UpdateAsync(IRuntimeEnvironment environment, RuntimeConfiguration configuration) public Task UpdateAsync(IRuntimeEnvironment environment, RuntimeConfiguration configuration)
{ => Task.CompletedTask;
throw new NotImplementedException();
}
public async Task DeleteAsync(IRuntimeEnvironment environment) public async Task DeleteAsync(IRuntimeEnvironment environment)
{ {

View File

@@ -1,3 +1,5 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions; using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker; namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
@@ -6,9 +8,186 @@ public class DockerStatistics : IRuntimeStatistics, IInstallStatistics
{ {
public event Func<ServerStatistics, Task>? OnStatisticsReceived; public event Func<ServerStatistics, Task>? OnStatisticsReceived;
public Task AttachAsync() => Task.CompletedTask; private readonly string ContainerId;
private readonly DockerClient DockerClient;
private readonly ILogger Logger;
public Task ClearCacheAsync() => Task.CompletedTask; private readonly List<ServerStatistics> Cache = new(102);
private readonly SemaphoreSlim CacheLock = new(1, 1);
private readonly CancellationTokenSource Cts = new();
public Task<ServerStatistics[]> GetCacheAsync() => Task.FromResult<ServerStatistics[]>([]); public DockerStatistics(string containerId, DockerClient dockerClient, ILogger logger)
{
ContainerId = containerId;
DockerClient = dockerClient;
Logger = logger;
}
public Task AttachAsync()
{
Task.Run(async () =>
{
Logger.LogTrace("Streaming loop entered");
while (!Cts.IsCancellationRequested)
{
try
{
await DockerClient.Containers.GetContainerStatsAsync(
ContainerId,
new ContainerStatsParameters()
{
Stream = true
},
new Progress<ContainerStatsResponse>(HandleStatsAsync),
Cts.Token
);
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception e)
{
Logger.LogError(e, "An unhandled error occured while processing container stats");
}
}
Logger.LogTrace("Streaming loop exited");
});
return Task.CompletedTask;
}
private async void HandleStatsAsync(ContainerStatsResponse statsResponse)
{
try
{
var convertedStats = ToServerStatistics(statsResponse);
await CacheLock.WaitAsync();
try
{
if(Cache.Count > 100)
Cache.RemoveRange(0, 30);
Cache.Add(convertedStats);
}
finally
{
CacheLock.Release();
}
if (OnStatisticsReceived != null)
await OnStatisticsReceived.Invoke(convertedStats);
}
catch (Exception e)
{
Logger.LogError(e, "An unhandled error occured while handling container stats");
}
}
public async Task ClearCacheAsync()
{
await CacheLock.WaitAsync();
try
{
Cache.Clear();
}
finally
{
CacheLock.Release();
}
}
public async Task<ServerStatistics[]> GetCacheAsync()
{
await CacheLock.WaitAsync();
try
{
return Cache.ToArray();
}
finally
{
CacheLock.Release();
}
}
// Helpers
public static ServerStatistics ToServerStatistics(ContainerStatsResponse stats)
{
double cpuUsage = CalculateCpuUsage(stats);
ulong usedMemory = stats.MemoryStats?.Usage ?? 0;
ulong totalMemory = stats.MemoryStats?.Limit ?? 0;
ulong outgoingNetwork = 0;
ulong ingoingNetwork = 0;
if (stats.Networks != null)
{
foreach (var network in stats.Networks.Values)
{
if (network == null)
continue;
outgoingNetwork += network.TxBytes;
ingoingNetwork += network.RxBytes;
}
}
ulong writeDisk = stats.StorageStats?.WriteSizeBytes ?? 0;
ulong readDisk = stats.StorageStats?.ReadSizeBytes ?? 0;
return new ServerStatistics(
CpuUsage: cpuUsage,
UsedMemory: usedMemory,
TotalMemory: totalMemory,
OutgoingNetwork: outgoingNetwork,
IngoingNetwork: ingoingNetwork,
WriteDisk: writeDisk,
ReadDisk: readDisk
);
}
private static double CalculateCpuUsage(ContainerStatsResponse stats)
{
var cpuStats = stats.CPUStats;
var preCpuStats = stats.PreCPUStats;
if (cpuStats == null || preCpuStats == null)
return 0;
var cpuUsage = cpuStats.CPUUsage;
var preCpuUsage = preCpuStats.CPUUsage;
if (cpuUsage == null || preCpuUsage == null)
return 0;
ulong cpuDelta = cpuUsage.TotalUsage - preCpuUsage.TotalUsage;
ulong systemDelta = cpuStats.SystemUsage - preCpuStats.SystemUsage;
if (systemDelta == 0 || cpuDelta == 0)
return 0;
uint onlineCpus = cpuStats.OnlineCPUs;
if (onlineCpus == 0 && cpuUsage.PercpuUsage != null)
{
onlineCpus = (uint)cpuUsage.PercpuUsage.Count;
}
if (onlineCpus == 0)
onlineCpus = 1;
return (double)cpuDelta / systemDelta * onlineCpus * 100.0;
}
public async ValueTask DisposeAsync()
{
await Cts.CancelAsync();
}
} }

View File

@@ -1,4 +1,6 @@
using Docker.DotNet; using Docker.DotNet;
using Microsoft.Extensions.Options;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.ServerSystem.Abstractions; using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker; namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
@@ -7,16 +9,23 @@ public static class Extensions
{ {
public static void AddDockerServices(this IServiceCollection collection) public static void AddDockerServices(this IServiceCollection collection)
{ {
var client = new DockerClientBuilder() collection.AddOptions<DockerOptions>().BindConfiguration("Moonlight:Docker");
.WithEndpoint(new Uri("unix:///var/run/docker.sock"))
.Build();
collection.AddSingleton(client); collection.AddSingleton(sp =>
{
var options = sp.GetRequiredService<IOptions<DockerOptions>>();
return new DockerClientBuilder()
.WithEndpoint(new Uri(options.Value.SocketUri))
.Build();
});
collection.AddSingleton<DockerEventService>(); collection.AddSingleton<DockerEventService>();
collection.AddHostedService(sp => sp.GetRequiredService<DockerEventService>()); collection.AddHostedService(sp => sp.GetRequiredService<DockerEventService>());
collection.AddSingleton<IRuntimeEnvironmentService, DockerRuntimeEnvService>(); collection.AddSingleton<IRuntimeEnvironmentService, DockerRuntimeEnvService>();
collection.AddSingleton<IInstallEnvironmentService, DockerInstallEnvService>(); collection.AddSingleton<IInstallEnvironmentService, DockerInstallEnvService>();
collection.AddSingleton<ContainerConfigMapper>();
} }
} }

View File

@@ -1,3 +1,4 @@
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.ServerSystem.Abstractions; using MoonlightServers.Daemon.ServerSystem.Abstractions;
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local; namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
@@ -8,5 +9,7 @@ public static class Extensions
{ {
services.AddSingleton<IRuntimeStorageService, LocalRuntimeStorageService>(); services.AddSingleton<IRuntimeStorageService, LocalRuntimeStorageService>();
services.AddSingleton<IInstallStorageService, LocalInstallStorageService>(); services.AddSingleton<IInstallStorageService, LocalInstallStorageService>();
services.AddOptions<LocalStorageOptions>().BindConfiguration("Moonlight:LocalStorage");
} }
} }

View File

@@ -4,12 +4,12 @@ namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
public class LocalInstallStorage : IInstallStorage public class LocalInstallStorage : IInstallStorage
{ {
public string HostPath { get; } public string BindPath { get; }
public LocalInstallStorage(string hostPath) public LocalInstallStorage(string bindPath)
{ {
HostPath = hostPath; BindPath = bindPath;
} }
public Task<string> GetHostPathAsync() => Task.FromResult(HostPath); public Task<string> GetBindPathAsync() => Task.FromResult(BindPath);
} }

View File

@@ -1,3 +1,5 @@
using Microsoft.Extensions.Options;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Models; using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions; using MoonlightServers.Daemon.ServerSystem.Abstractions;
@@ -5,11 +7,16 @@ namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
public class LocalInstallStorageService : IInstallStorageService public class LocalInstallStorageService : IInstallStorageService
{ {
private const string HostPathTemplate = "./mldaemon/install/{0}"; private readonly IOptions<LocalStorageOptions> Options;
public LocalInstallStorageService(IOptions<LocalStorageOptions> options)
{
Options = options;
}
public Task<IInstallStorage?> FindAsync(string id) public Task<IInstallStorage?> FindAsync(string id)
{ {
var path = string.Format(HostPathTemplate, id); var path = Path.Combine(Options.Value.InstallPath, id);
if (!Directory.Exists(path)) if (!Directory.Exists(path))
return Task.FromResult<IInstallStorage?>(null); return Task.FromResult<IInstallStorage?>(null);
@@ -19,7 +26,7 @@ public class LocalInstallStorageService : IInstallStorageService
public Task<IInstallStorage> CreateAsync(string id, RuntimeConfiguration runtimeConfiguration, InstallConfiguration installConfiguration) public Task<IInstallStorage> CreateAsync(string id, RuntimeConfiguration runtimeConfiguration, InstallConfiguration installConfiguration)
{ {
var path = string.Format(HostPathTemplate, id); var path = Path.Combine(Options.Value.InstallPath, id);
Directory.CreateDirectory(path); Directory.CreateDirectory(path);
@@ -35,8 +42,8 @@ public class LocalInstallStorageService : IInstallStorageService
); );
} }
if(Directory.Exists(localInstallStorage.HostPath)) if(Directory.Exists(localInstallStorage.BindPath))
Directory.Delete(localInstallStorage.HostPath, true); Directory.Delete(localInstallStorage.BindPath, true);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -4,12 +4,12 @@ namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
public class LocalRuntimeStorage : IRuntimeStorage public class LocalRuntimeStorage : IRuntimeStorage
{ {
public string HostPath { get; } public string BindPath { get; }
public LocalRuntimeStorage(string hostPath) public LocalRuntimeStorage(string bindPath)
{ {
HostPath = hostPath; BindPath = bindPath;
} }
public Task<string> GetHostPathAsync() => Task.FromResult(HostPath); public Task<string> GetBindPathAsync() => Task.FromResult(BindPath);
} }

View File

@@ -1,3 +1,5 @@
using Microsoft.Extensions.Options;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Models; using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.ServerSystem.Abstractions; using MoonlightServers.Daemon.ServerSystem.Abstractions;
@@ -5,11 +7,16 @@ namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
public class LocalRuntimeStorageService : IRuntimeStorageService public class LocalRuntimeStorageService : IRuntimeStorageService
{ {
private const string HostPathTemplate = "./mldaemon/runtime/{0}"; private readonly IOptions<LocalStorageOptions> Options;
public LocalRuntimeStorageService(IOptions<LocalStorageOptions> options)
{
Options = options;
}
public Task<IRuntimeStorage?> FindAsync(string id) public Task<IRuntimeStorage?> FindAsync(string id)
{ {
var path = string.Format(HostPathTemplate, id); var path = Path.Combine(Options.Value.RuntimePath, id);
if (!Directory.Exists(path)) if (!Directory.Exists(path))
return Task.FromResult<IRuntimeStorage?>(null); return Task.FromResult<IRuntimeStorage?>(null);
@@ -19,7 +26,7 @@ public class LocalRuntimeStorageService : IRuntimeStorageService
public Task<IRuntimeStorage> CreateAsync(string id, RuntimeConfiguration configuration) public Task<IRuntimeStorage> CreateAsync(string id, RuntimeConfiguration configuration)
{ {
var path = string.Format(HostPathTemplate, id); var path = Path.Combine(Options.Value.RuntimePath, id);
Directory.CreateDirectory(path); Directory.CreateDirectory(path);
@@ -38,8 +45,8 @@ public class LocalRuntimeStorageService : IRuntimeStorageService
); );
} }
if(Directory.Exists(localRuntimeStorage.HostPath)) if(Directory.Exists(localRuntimeStorage.BindPath))
Directory.Delete(localRuntimeStorage.HostPath, true); Directory.Delete(localRuntimeStorage.BindPath, true);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -74,7 +74,7 @@ public partial class Server
InstallStorage = await InstallStorageService.CreateAsync(Uuid, RuntimeConfiguration, InstallConfiguration); InstallStorage = await InstallStorageService.CreateAsync(Uuid, RuntimeConfiguration, InstallConfiguration);
// Write install script // Write install script
var installStoragePath = await InstallStorage.GetHostPathAsync(); var installStoragePath = await InstallStorage.GetBindPathAsync();
await File.WriteAllTextAsync( await File.WriteAllTextAsync(
Path.Combine(installStoragePath, "install.sh"), Path.Combine(installStoragePath, "install.sh"),

View File

@@ -0,0 +1,34 @@
namespace MoonlightServers.Daemon.ServerSystem;
public partial class Server
{
public async Task UpdateAsync()
{
await Lock.WaitAsync();
try
{
if (State is ServerState.Online or ServerState.Starting or ServerState.Stopping)
{
Logger.LogTrace("Fetching latest runtime configuration");
RuntimeConfiguration = await ConfigurationService.GetRuntimeConfigurationAsync(Uuid);
if (RuntimeEnvironment != null)
{
Logger.LogTrace("Updating runtime environment");
await RuntimeEnvironmentService.UpdateAsync(RuntimeEnvironment, RuntimeConfiguration);
}
if (RuntimeStorage != null)
{
Logger.LogTrace("Updating runtime storage");
await RuntimeStorageService.UpdateAsync(RuntimeStorage, RuntimeConfiguration);
}
}
}
finally
{
Lock.Release();
}
}
}

View File

@@ -67,11 +67,12 @@ public partial class Server : IAsyncDisposable
private async Task OnConsoleMessageAsync(string message) private async Task OnConsoleMessageAsync(string message)
{ {
Console.WriteLine($"Console: {message}"); Console.Write($"Console: {message}");
} }
private async Task OnStatisticsReceivedAsync(ServerStatistics statistics) private async Task OnStatisticsReceivedAsync(ServerStatistics statistics)
{ {
Logger.LogTrace("{cpu:F} {used:F} {total:F}", statistics.CpuUsage, statistics.UsedMemory / 1024 / 1024, statistics.TotalMemory / 1024 / 1024);
} }
private Task ChangeStateAsync(ServerState newState) private Task ChangeStateAsync(ServerState newState)
@@ -88,7 +89,7 @@ public partial class Server : IAsyncDisposable
if (RuntimeEnvironment != null) if (RuntimeEnvironment != null)
{ {
Logger.LogTrace("Detaching and disposing runtime environment"); Logger.LogTrace("Detaching events and disposing runtime environment");
RuntimeEnvironment.Console.OnOutput -= OnConsoleMessageAsync; RuntimeEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
RuntimeEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync; RuntimeEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
@@ -99,7 +100,7 @@ public partial class Server : IAsyncDisposable
if (InstallEnvironment != null) if (InstallEnvironment != null)
{ {
Logger.LogTrace("Detaching and disposing install environment"); Logger.LogTrace("Detaching events and disposing install environment");
InstallEnvironment.Console.OnOutput -= OnConsoleMessageAsync; InstallEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
InstallEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync; InstallEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;

View File

@@ -1,3 +1,11 @@
namespace MoonlightServers.Daemon.ServerSystem; namespace MoonlightServers.Daemon.ServerSystem;
public record ServerStatistics(); public record ServerStatistics(
double CpuUsage,
ulong UsedMemory,
ulong TotalMemory,
ulong OutgoingNetwork,
ulong IngoingNetwork,
ulong WriteDisk,
ulong ReadDisk
);

View File

@@ -44,7 +44,7 @@ public class ServerConfigurationService
return new InstallConfiguration( return new InstallConfiguration(
"bash", "bash",
"installer", "installer",
await File.ReadAllTextAsync("/home/chiara/Documents/daemonScripts/install.sh") "#!/bin/bash\n# Paper Installation Script\n#\necho -e \"Started Installation\"\n\n# Server Files: /mnt/server\nPROJECT=paper\n\nif [ \"${MINECRAFT_VERSION}\" == \"latest\" ]; then\n LATEST_VERSION=$(curl -s https://api.papermc.io/v2/projects/${PROJECT} | jq -r '.versions[-1]')\n MINECRAFT_VERSION=${LATEST_VERSION}\nfi\n\nGET_BUILD=$(curl -s https://api.papermc.io/v2/projects/${PROJECT}/versions/${MINECRAFT_VERSION} | jq -r '.builds[-1]')\nBUILD_NUMBER=${GET_BUILD}\n\n\nJAR_NAME=${PROJECT}-${MINECRAFT_VERSION}-${BUILD_NUMBER}.jar\n\necho \"Version being downloaded\"\necho -e \"MC Version: ${MINECRAFT_VERSION}\"\necho -e \"Build: ${BUILD_NUMBER}\"\necho -e \"JAR Name of Build: ${JAR_NAME}\"\nDOWNLOAD_URL=https://api.papermc.io/v2/projects/${PROJECT}/versions/${MINECRAFT_VERSION}/builds/${BUILD_NUMBER}/downloads/${JAR_NAME}\n\ncd /mnt/server\n\necho -e \"Running curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\"\n\nif [ -f ${SERVER_JARFILE} ]; then\n mv ${SERVER_JARFILE} ${SERVER_JARFILE}.old\nfi\n\ncurl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\n"
); );
} }
} }

View File

@@ -0,0 +1,67 @@
using System.Collections.Concurrent;
using MoonlightServers.Daemon.ServerSystem;
namespace MoonlightServers.Daemon.Services;
public class ServerService : IAsyncDisposable
{
private readonly ServerFactory ServerFactory;
private readonly ConcurrentDictionary<string, Server> Servers = new();
private readonly ILogger<ServerService> Logger;
public ServerService(ServerFactory serverFactory, ILogger<ServerService> logger)
{
ServerFactory = serverFactory;
Logger = logger;
}
public async Task SyncAsync(string uuid)
{
if (Servers.TryGetValue(uuid, out var server))
{
Logger.LogTrace("Updating existing server {uuid}", uuid);
await server.UpdateAsync();
}
else
{
Logger.LogTrace("Creating new server instance {uuid}", uuid);
var newServer = await ServerFactory.CreateAsync(uuid);
await newServer.InitializeAsync();
Servers[uuid] = newServer;
}
}
public Task<Server?> GetAsync(string uuid)
{
var server = Servers.GetValueOrDefault(uuid);
return Task.FromResult(server);
}
public async Task DeleteAsync(string uuid)
{
Logger.LogTrace("Deleting server {uuid}", uuid);
var server = Servers.GetValueOrDefault(uuid);
if(server == null)
return;
// Trigger internal deletion mechanism
await server.DeleteAsync();
// Dispose any left over resources
await server.DisposeAsync();
// Remove server from cache
Servers.TryRemove(uuid, out _);
}
public async ValueTask DisposeAsync()
{
foreach (var server in Servers)
await server.Value.DisposeAsync();
}
}