diff --git a/MoonlightServers.Daemon/Configuration/DockerOptions.cs b/MoonlightServers.Daemon/Configuration/DockerOptions.cs new file mode 100644 index 0000000..e527ccd --- /dev/null +++ b/MoonlightServers.Daemon/Configuration/DockerOptions.cs @@ -0,0 +1,6 @@ +namespace MoonlightServers.Daemon.Configuration; + +public class DockerOptions +{ + public string SocketUri { get; set; } = "unix:///var/run/docker.sock"; +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Configuration/LocalStorageOptions.cs b/MoonlightServers.Daemon/Configuration/LocalStorageOptions.cs new file mode 100644 index 0000000..a27c4f9 --- /dev/null +++ b/MoonlightServers.Daemon/Configuration/LocalStorageOptions.cs @@ -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"; +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj index c55d216..aa8139d 100644 --- a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj +++ b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj @@ -23,6 +23,9 @@ Server.cs + + Server.cs + diff --git a/MoonlightServers.Daemon/Program.cs b/MoonlightServers.Daemon/Program.cs index 3267770..b068d23 100644 --- a/MoonlightServers.Daemon/Program.cs +++ b/MoonlightServers.Daemon/Program.cs @@ -14,6 +14,7 @@ builder.Logging.AddConsoleFormatter(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddDockerServices(); builder.Services.AddLocalServices(); @@ -31,23 +32,26 @@ Task.Run(async () => try { - var factory = app.Services.GetRequiredService(); - var server = await factory.CreateAsync("a0e3ddb4-2c72-4f4c-bc49-35650a4bc5c0"); + var serverService = app.Services.GetRequiredService(); - 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.ReadLine(); - if (server.State == ServerState.Offline) await server.StartAsync(); else await server.StopAsync(); Console.ReadLine(); - - await server.DisposeAsync(); + + await serverService.DeleteAsync("a0e3ddb4-2c72-4f4c-bc49-35650a4bc5c0"); } catch (Exception e) { diff --git a/MoonlightServers.Daemon/ServerSystem/Abstractions/IInstallStatistics.cs b/MoonlightServers.Daemon/ServerSystem/Abstractions/IInstallStatistics.cs index efc52f4..c564bfb 100644 --- a/MoonlightServers.Daemon/ServerSystem/Abstractions/IInstallStatistics.cs +++ b/MoonlightServers.Daemon/ServerSystem/Abstractions/IInstallStatistics.cs @@ -1,6 +1,6 @@ namespace MoonlightServers.Daemon.ServerSystem.Abstractions; -public interface IInstallStatistics +public interface IInstallStatistics : IAsyncDisposable { public event Func? OnStatisticsReceived; diff --git a/MoonlightServers.Daemon/ServerSystem/Abstractions/IInstallStorage.cs b/MoonlightServers.Daemon/ServerSystem/Abstractions/IInstallStorage.cs index 7fb07ac..e20ad74 100644 --- a/MoonlightServers.Daemon/ServerSystem/Abstractions/IInstallStorage.cs +++ b/MoonlightServers.Daemon/ServerSystem/Abstractions/IInstallStorage.cs @@ -2,5 +2,5 @@ namespace MoonlightServers.Daemon.ServerSystem.Abstractions; public interface IInstallStorage { - public Task GetHostPathAsync(); + public Task GetBindPathAsync(); } \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Abstractions/IRuntimeStatistics.cs b/MoonlightServers.Daemon/ServerSystem/Abstractions/IRuntimeStatistics.cs index ab65549..acdb020 100644 --- a/MoonlightServers.Daemon/ServerSystem/Abstractions/IRuntimeStatistics.cs +++ b/MoonlightServers.Daemon/ServerSystem/Abstractions/IRuntimeStatistics.cs @@ -1,6 +1,6 @@ namespace MoonlightServers.Daemon.ServerSystem.Abstractions; -public interface IRuntimeStatistics +public interface IRuntimeStatistics : IAsyncDisposable { public event Func? OnStatisticsReceived; diff --git a/MoonlightServers.Daemon/ServerSystem/Abstractions/IRuntimeStorage.cs b/MoonlightServers.Daemon/ServerSystem/Abstractions/IRuntimeStorage.cs index d532125..399c8e6 100644 --- a/MoonlightServers.Daemon/ServerSystem/Abstractions/IRuntimeStorage.cs +++ b/MoonlightServers.Daemon/ServerSystem/Abstractions/IRuntimeStorage.cs @@ -2,5 +2,5 @@ namespace MoonlightServers.Daemon.ServerSystem.Abstractions; public interface IRuntimeStorage { - public Task GetHostPathAsync(); + public Task GetBindPathAsync(); } \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/ConfigMapper.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/ContainerConfigMapper.cs similarity index 96% rename from MoonlightServers.Daemon/ServerSystem/Implementations/Docker/ConfigMapper.cs rename to MoonlightServers.Daemon/ServerSystem/Implementations/Docker/ContainerConfigMapper.cs index aae853c..7497ee5 100644 --- a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/ConfigMapper.cs +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/ContainerConfigMapper.cs @@ -1,12 +1,11 @@ -using System.ComponentModel; using Docker.DotNet.Models; using MoonlightServers.Daemon.Models; namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker; -public static class ConfigMapper +public class ContainerConfigMapper { - public static CreateContainerParameters GetRuntimeConfig( + public CreateContainerParameters GetRuntimeConfig( string uuid, string name, RuntimeConfiguration configuration, @@ -138,7 +137,7 @@ public static class ConfigMapper return parameters; } - public static CreateContainerParameters GetInstallConfig( + public CreateContainerParameters GetInstallConfig( string uuid, string name, RuntimeConfiguration runtimeConfiguration, @@ -201,7 +200,7 @@ public static class ConfigMapper return parameters; } - private static void ApplySharedOptions( + private void ApplySharedOptions( CreateContainerParameters parameters, RuntimeConfiguration configuration ) diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerConsole.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerConsole.cs index 087f69a..1e6c11f 100644 --- a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerConsole.cs +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerConsole.cs @@ -31,14 +31,15 @@ public class DockerConsole : IRuntimeConsole, IInstallConsole, IAsyncDisposable Logger = logger; } + public async Task AttachAsync() { - // Fetch initial logs - Logger.LogTrace("Fetching pre-existing logs from container"); - - var logResponse = await DockerClient.Containers.GetContainerLogsAsync( + // Fetch initial container logs + Logger.LogTrace("Fetching initial container logs"); + + using var logStream = await DockerClient.Containers.GetContainerLogsAsync( ContainerId, - new() + new ContainerLogsParameters() { Follow = false, ShowStderr = true, @@ -46,94 +47,50 @@ public class DockerConsole : IRuntimeConsole, IInstallConsole, IAsyncDisposable } ); - // Append to cache - var logs = await logResponse.ReadOutputToEndAsync(Cts.Token); + // and process it + await ProcessStreamAsync(logStream, 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"); + // After that we can actually start streaming the new logs + Logger.LogTrace("Attaching to container"); + Stream = await DockerClient.Containers.AttachContainerAsync( + ContainerId, + new ContainerAttachParameters() + { + Stderr = true, + Stdin = true, + Stdout = true, + Stream = true + }, + Cts.Token + ); + Task.Run(async () => { - var capturedCt = Cts.Token; + Logger.LogTrace("Entered streaming loop"); - Logger.LogTrace("Starting attach loop"); - - while (!capturedCt.IsCancellationRequested) + while (!Cts.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.Shared.Rent(bufferSize); - - while (!capturedCt.IsCancellationRequested) + if (Stream == null) // Triggers when e.g. a connection issue occurs cause the catch clause resets the stream { - try - { - var readResult = await stream.ReadOutputAsync(buffer, 0, bufferSize, capturedCt); + Logger.LogTrace("Reattaching to container"); - if (readResult.Count > 0) + Stream = await DockerClient.Containers.AttachContainerAsync( + ContainerId, + new ContainerAttachParameters() { - 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"); - } + Stderr = true, + Stdin = true, + Stdout = true, + Stream = true + }, + Cts.Token + ); } - ArrayPool.Shared.Return(buffer); + await ProcessStreamAsync(Stream, Cts.Token); } catch (OperationCanceledException) { @@ -141,14 +98,57 @@ public class DockerConsole : IRuntimeConsole, IInstallConsole, IAsyncDisposable } 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.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.Shared.Return(buffer); + } + public async Task WriteInputAsync(string value) { if (Stream == null) @@ -188,6 +188,8 @@ public class DockerConsole : IRuntimeConsole, IInstallConsole, IAsyncDisposable public async ValueTask DisposeAsync() { + Logger.LogTrace("Disposing"); + await Cts.CancelAsync(); Stream?.Dispose(); CacheLock.Dispose(); diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerInstallEnv.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerInstallEnv.cs index 6620b6b..009aa35 100644 --- a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerInstallEnv.cs +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerInstallEnv.cs @@ -27,7 +27,7 @@ public class DockerInstallEnv : IInstallEnvironment Logger = logger; EventService = eventService; - InnerStatistics = new DockerStatistics(); + InnerStatistics = new DockerStatistics(containerId, dockerClient, logger); InnerConsole = new DockerConsole(containerId, dockerClient, logger); EventService.OnContainerDied += HandleDieEventAsync; @@ -59,5 +59,6 @@ public class DockerInstallEnv : IInstallEnvironment EventService.OnContainerDied -= HandleDieEventAsync; await InnerConsole.DisposeAsync(); + await InnerStatistics.DisposeAsync(); } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerInstallEnvService.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerInstallEnvService.cs index 13262d9..fc860cb 100644 --- a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerInstallEnvService.cs +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerInstallEnvService.cs @@ -9,15 +9,21 @@ public class DockerInstallEnvService : IInstallEnvironmentService private readonly DockerClient DockerClient; private readonly ILoggerFactory LoggerFactory; private readonly DockerEventService DockerEventService; + private readonly ContainerConfigMapper ConfigMapper; private const string NameTemplate = "ml-install-{0}"; - public DockerInstallEnvService(DockerClient dockerClient, ILoggerFactory loggerFactory, - DockerEventService dockerEventService) + public DockerInstallEnvService( + DockerClient dockerClient, + ILoggerFactory loggerFactory, + DockerEventService dockerEventService, + ContainerConfigMapper configMapper + ) { DockerClient = dockerClient; LoggerFactory = loggerFactory; DockerEventService = dockerEventService; + ConfigMapper = configMapper; } public async Task FindAsync(string id) @@ -28,8 +34,7 @@ public class DockerInstallEnvService : IInstallEnvironmentService string.Format(NameTemplate, id) ); - var logger = - LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})"); + var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})"); return new DockerInstallEnv(dockerInspect.ID, DockerClient, logger, DockerEventService); } @@ -63,8 +68,8 @@ public class DockerInstallEnvService : IInstallEnvironmentService // Ignored } - var runtimeStoragePath = await runtimeStorage.GetHostPathAsync(); - var installStoragePath = await installStorage.GetHostPathAsync(); + var runtimeStoragePath = await runtimeStorage.GetBindPathAsync(); + var installStoragePath = await installStorage.GetBindPathAsync(); var parameters = ConfigMapper.GetInstallConfig( id, @@ -74,7 +79,7 @@ public class DockerInstallEnvService : IInstallEnvironmentService runtimeStoragePath, installStoragePath ); - + var container = await DockerClient.Containers.CreateContainerAsync(parameters); var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})"); diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerRuntimeEnv.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerRuntimeEnv.cs index 7638926..8e85d91 100644 --- a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerRuntimeEnv.cs +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerRuntimeEnv.cs @@ -25,7 +25,7 @@ public class DockerRuntimeEnv : IRuntimeEnvironment DockerClient = dockerClient; EventService = eventService; - InnerStatistics = new DockerStatistics(); + InnerStatistics = new DockerStatistics(containerId, dockerClient, logger); InnerConsole = new DockerConsole(containerId, dockerClient, logger); EventService.OnContainerDied += HandleDieEventAsync; @@ -57,5 +57,6 @@ public class DockerRuntimeEnv : IRuntimeEnvironment EventService.OnContainerDied -= HandleDieEventAsync; await InnerConsole.DisposeAsync(); + await InnerStatistics.DisposeAsync(); } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerRuntimeEnvService.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerRuntimeEnvService.cs index 7d1875a..8360991 100644 --- a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerRuntimeEnvService.cs +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerRuntimeEnvService.cs @@ -9,15 +9,21 @@ public class DockerRuntimeEnvService : IRuntimeEnvironmentService private readonly DockerClient DockerClient; private readonly ILoggerFactory LoggerFactory; private readonly DockerEventService DockerEventService; + private readonly ContainerConfigMapper ConfigMapper; private const string NameTemplate = "ml-runtime-{0}"; - public DockerRuntimeEnvService(DockerClient dockerClient, ILoggerFactory loggerFactory, - DockerEventService dockerEventService) + public DockerRuntimeEnvService( + DockerClient dockerClient, + ILoggerFactory loggerFactory, + DockerEventService dockerEventService, + ContainerConfigMapper configMapper + ) { DockerClient = dockerClient; LoggerFactory = loggerFactory; DockerEventService = dockerEventService; + ConfigMapper = configMapper; } public async Task FindAsync(string id) @@ -28,8 +34,7 @@ public class DockerRuntimeEnvService : IRuntimeEnvironmentService string.Format(NameTemplate, id) ); - var logger = - LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})"); + var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})"); return new DockerRuntimeEnv(dockerInspect.ID, DockerClient, logger, DockerEventService); } @@ -61,7 +66,7 @@ public class DockerRuntimeEnvService : IRuntimeEnvironmentService // Ignored } - var storagePath = await storage.GetHostPathAsync(); + var storagePath = await storage.GetBindPathAsync(); var parameters = ConfigMapper.GetRuntimeConfig( id, @@ -78,9 +83,7 @@ public class DockerRuntimeEnvService : IRuntimeEnvironmentService } public Task UpdateAsync(IRuntimeEnvironment environment, RuntimeConfiguration configuration) - { - throw new NotImplementedException(); - } + => Task.CompletedTask; public async Task DeleteAsync(IRuntimeEnvironment environment) { diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerStatistics.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerStatistics.cs index 0740887..918a135 100644 --- a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerStatistics.cs +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/DockerStatistics.cs @@ -1,3 +1,5 @@ +using Docker.DotNet; +using Docker.DotNet.Models; using MoonlightServers.Daemon.ServerSystem.Abstractions; namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker; @@ -6,9 +8,186 @@ public class DockerStatistics : IRuntimeStatistics, IInstallStatistics { public event Func? 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 Cache = new(102); + private readonly SemaphoreSlim CacheLock = new(1, 1); + private readonly CancellationTokenSource Cts = new(); - public Task GetCacheAsync() => Task.FromResult([]); + 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(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 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(); + } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/Extensions.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/Extensions.cs index 992f685..3e663a7 100644 --- a/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/Extensions.cs +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/Docker/Extensions.cs @@ -1,4 +1,6 @@ using Docker.DotNet; +using Microsoft.Extensions.Options; +using MoonlightServers.Daemon.Configuration; using MoonlightServers.Daemon.ServerSystem.Abstractions; namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker; @@ -7,16 +9,23 @@ 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.AddOptions().BindConfiguration("Moonlight:Docker"); - collection.AddSingleton(client); + collection.AddSingleton(sp => + { + var options = sp.GetRequiredService>(); + + return new DockerClientBuilder() + .WithEndpoint(new Uri(options.Value.SocketUri)) + .Build(); + }); collection.AddSingleton(); collection.AddHostedService(sp => sp.GetRequiredService()); collection.AddSingleton(); collection.AddSingleton(); + + collection.AddSingleton(); } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/Local/Extensions.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/Local/Extensions.cs index 8cc1e54..8f12fb8 100644 --- a/MoonlightServers.Daemon/ServerSystem/Implementations/Local/Extensions.cs +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/Local/Extensions.cs @@ -1,3 +1,4 @@ +using MoonlightServers.Daemon.Configuration; using MoonlightServers.Daemon.ServerSystem.Abstractions; namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local; @@ -8,5 +9,7 @@ public static class Extensions { services.AddSingleton(); services.AddSingleton(); + + services.AddOptions().BindConfiguration("Moonlight:LocalStorage"); } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalInstallStorage.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalInstallStorage.cs index f016ee7..b8f2e7e 100644 --- a/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalInstallStorage.cs +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalInstallStorage.cs @@ -4,12 +4,12 @@ namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local; 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 GetHostPathAsync() => Task.FromResult(HostPath); + public Task GetBindPathAsync() => Task.FromResult(BindPath); } \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalInstallStorageService.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalInstallStorageService.cs index df0f555..ced8f3e 100644 --- a/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalInstallStorageService.cs +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalInstallStorageService.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Options; +using MoonlightServers.Daemon.Configuration; using MoonlightServers.Daemon.Models; using MoonlightServers.Daemon.ServerSystem.Abstractions; @@ -5,11 +7,16 @@ namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local; public class LocalInstallStorageService : IInstallStorageService { - private const string HostPathTemplate = "./mldaemon/install/{0}"; - + private readonly IOptions Options; + + public LocalInstallStorageService(IOptions options) + { + Options = options; + } + public Task FindAsync(string id) { - var path = string.Format(HostPathTemplate, id); + var path = Path.Combine(Options.Value.InstallPath, id); if (!Directory.Exists(path)) return Task.FromResult(null); @@ -19,7 +26,7 @@ public class LocalInstallStorageService : IInstallStorageService public Task CreateAsync(string id, RuntimeConfiguration runtimeConfiguration, InstallConfiguration installConfiguration) { - var path = string.Format(HostPathTemplate, id); + var path = Path.Combine(Options.Value.InstallPath, id); Directory.CreateDirectory(path); @@ -35,8 +42,8 @@ public class LocalInstallStorageService : IInstallStorageService ); } - if(Directory.Exists(localInstallStorage.HostPath)) - Directory.Delete(localInstallStorage.HostPath, true); + if(Directory.Exists(localInstallStorage.BindPath)) + Directory.Delete(localInstallStorage.BindPath, true); return Task.CompletedTask; } diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalRuntimeStorage.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalRuntimeStorage.cs index f780e94..ef0679d 100644 --- a/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalRuntimeStorage.cs +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalRuntimeStorage.cs @@ -4,12 +4,12 @@ namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local; 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 GetHostPathAsync() => Task.FromResult(HostPath); + public Task GetBindPathAsync() => Task.FromResult(BindPath); } \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalRuntimeStorageService.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalRuntimeStorageService.cs index 53943fd..484bafd 100644 --- a/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalRuntimeStorageService.cs +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/Local/LocalRuntimeStorageService.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Options; +using MoonlightServers.Daemon.Configuration; using MoonlightServers.Daemon.Models; using MoonlightServers.Daemon.ServerSystem.Abstractions; @@ -5,11 +7,16 @@ namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local; public class LocalRuntimeStorageService : IRuntimeStorageService { - private const string HostPathTemplate = "./mldaemon/runtime/{0}"; + private readonly IOptions Options; + + public LocalRuntimeStorageService(IOptions options) + { + Options = options; + } public Task FindAsync(string id) { - var path = string.Format(HostPathTemplate, id); + var path = Path.Combine(Options.Value.RuntimePath, id); if (!Directory.Exists(path)) return Task.FromResult(null); @@ -19,7 +26,7 @@ public class LocalRuntimeStorageService : IRuntimeStorageService public Task CreateAsync(string id, RuntimeConfiguration configuration) { - var path = string.Format(HostPathTemplate, id); + var path = Path.Combine(Options.Value.RuntimePath, id); Directory.CreateDirectory(path); @@ -38,8 +45,8 @@ public class LocalRuntimeStorageService : IRuntimeStorageService ); } - if(Directory.Exists(localRuntimeStorage.HostPath)) - Directory.Delete(localRuntimeStorage.HostPath, true); + if(Directory.Exists(localRuntimeStorage.BindPath)) + Directory.Delete(localRuntimeStorage.BindPath, true); return Task.CompletedTask; } diff --git a/MoonlightServers.Daemon/ServerSystem/Server.Install.cs b/MoonlightServers.Daemon/ServerSystem/Server.Install.cs index aaa31f2..607eabf 100644 --- a/MoonlightServers.Daemon/ServerSystem/Server.Install.cs +++ b/MoonlightServers.Daemon/ServerSystem/Server.Install.cs @@ -74,7 +74,7 @@ public partial class Server InstallStorage = await InstallStorageService.CreateAsync(Uuid, RuntimeConfiguration, InstallConfiguration); // Write install script - var installStoragePath = await InstallStorage.GetHostPathAsync(); + var installStoragePath = await InstallStorage.GetBindPathAsync(); await File.WriteAllTextAsync( Path.Combine(installStoragePath, "install.sh"), diff --git a/MoonlightServers.Daemon/ServerSystem/Server.Update.cs b/MoonlightServers.Daemon/ServerSystem/Server.Update.cs new file mode 100644 index 0000000..537f4bb --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Server.Update.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Server.cs b/MoonlightServers.Daemon/ServerSystem/Server.cs index 87b7ec8..600f040 100644 --- a/MoonlightServers.Daemon/ServerSystem/Server.cs +++ b/MoonlightServers.Daemon/ServerSystem/Server.cs @@ -67,11 +67,12 @@ public partial class Server : IAsyncDisposable private async Task OnConsoleMessageAsync(string message) { - Console.WriteLine($"Console: {message}"); + Console.Write($"Console: {message}"); } 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) @@ -88,7 +89,7 @@ public partial class Server : IAsyncDisposable if (RuntimeEnvironment != null) { - Logger.LogTrace("Detaching and disposing runtime environment"); + Logger.LogTrace("Detaching events and disposing runtime environment"); RuntimeEnvironment.Console.OnOutput -= OnConsoleMessageAsync; RuntimeEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync; @@ -99,7 +100,7 @@ public partial class Server : IAsyncDisposable if (InstallEnvironment != null) { - Logger.LogTrace("Detaching and disposing install environment"); + Logger.LogTrace("Detaching events and disposing install environment"); InstallEnvironment.Console.OnOutput -= OnConsoleMessageAsync; InstallEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync; diff --git a/MoonlightServers.Daemon/ServerSystem/ServerStatistics.cs b/MoonlightServers.Daemon/ServerSystem/ServerStatistics.cs index a513812..0a4bd7b 100644 --- a/MoonlightServers.Daemon/ServerSystem/ServerStatistics.cs +++ b/MoonlightServers.Daemon/ServerSystem/ServerStatistics.cs @@ -1,3 +1,11 @@ namespace MoonlightServers.Daemon.ServerSystem; -public record ServerStatistics(); \ No newline at end of file +public record ServerStatistics( + double CpuUsage, + ulong UsedMemory, + ulong TotalMemory, + ulong OutgoingNetwork, + ulong IngoingNetwork, + ulong WriteDisk, + ulong ReadDisk +); \ No newline at end of file diff --git a/MoonlightServers.Daemon/Services/ServerConfigurationService.cs b/MoonlightServers.Daemon/Services/ServerConfigurationService.cs index 02af6c5..3c79dff 100644 --- a/MoonlightServers.Daemon/Services/ServerConfigurationService.cs +++ b/MoonlightServers.Daemon/Services/ServerConfigurationService.cs @@ -44,7 +44,7 @@ public class ServerConfigurationService return new InstallConfiguration( "bash", "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" ); } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Services/ServerService.cs b/MoonlightServers.Daemon/Services/ServerService.cs new file mode 100644 index 0000000..a7afea1 --- /dev/null +++ b/MoonlightServers.Daemon/Services/ServerService.cs @@ -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 Servers = new(); + private readonly ILogger Logger; + + public ServerService(ServerFactory serverFactory, ILogger 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 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(); + } +} \ No newline at end of file