Implemented statistics. Refactored storage abstractions. Added config options for docker and local storage. Added server service and server updating.
This commit is contained in:
6
MoonlightServers.Daemon/Configuration/DockerOptions.cs
Normal file
6
MoonlightServers.Daemon/Configuration/DockerOptions.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace MoonlightServers.Daemon.Configuration;
|
||||
|
||||
public class DockerOptions
|
||||
{
|
||||
public string SocketUri { get; set; } = "unix:///var/run/docker.sock";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -23,6 +23,9 @@
|
||||
<Compile Update="ServerSystem\Server.Delete.cs">
|
||||
<DependentUpon>Server.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="ServerSystem\Server.Update.cs">
|
||||
<DependentUpon>Server.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -14,6 +14,7 @@ builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions
|
||||
|
||||
builder.Services.AddSingleton<ServerConfigurationService>();
|
||||
builder.Services.AddSingleton<ServerFactory>();
|
||||
builder.Services.AddSingleton<ServerService>();
|
||||
builder.Services.AddDockerServices();
|
||||
builder.Services.AddLocalServices();
|
||||
|
||||
@@ -31,23 +32,26 @@ Task.Run(async () =>
|
||||
|
||||
try
|
||||
{
|
||||
var factory = app.Services.GetRequiredService<ServerFactory>();
|
||||
var server = await factory.CreateAsync("a0e3ddb4-2c72-4f4c-bc49-35650a4bc5c0");
|
||||
var serverService = app.Services.GetRequiredService<ServerService>();
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IInstallStatistics
|
||||
public interface IInstallStatistics : IAsyncDisposable
|
||||
{
|
||||
public event Func<ServerStatistics, Task>? OnStatisticsReceived;
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@ namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IInstallStorage
|
||||
{
|
||||
public Task<string> GetHostPathAsync();
|
||||
public Task<string> GetBindPathAsync();
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IRuntimeStatistics
|
||||
public interface IRuntimeStatistics : IAsyncDisposable
|
||||
{
|
||||
public event Func<ServerStatistics, Task>? OnStatisticsReceived;
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@ namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IRuntimeStorage
|
||||
{
|
||||
public Task<string> GetHostPathAsync();
|
||||
public Task<string> GetBindPathAsync();
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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<byte>.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<byte>.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<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)
|
||||
{
|
||||
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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<IInstallEnvironment?> 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})");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<IRuntimeEnvironment?> 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)
|
||||
{
|
||||
|
||||
@@ -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<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();
|
||||
}
|
||||
}
|
||||
@@ -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<DockerOptions>().BindConfiguration("Moonlight:Docker");
|
||||
|
||||
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.AddHostedService(sp => sp.GetRequiredService<DockerEventService>());
|
||||
|
||||
collection.AddSingleton<IRuntimeEnvironmentService, DockerRuntimeEnvService>();
|
||||
collection.AddSingleton<IInstallEnvironmentService, DockerInstallEnvService>();
|
||||
|
||||
collection.AddSingleton<ContainerConfigMapper>();
|
||||
}
|
||||
}
|
||||
@@ -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<IRuntimeStorageService, LocalRuntimeStorageService>();
|
||||
services.AddSingleton<IInstallStorageService, LocalInstallStorageService>();
|
||||
|
||||
services.AddOptions<LocalStorageOptions>().BindConfiguration("Moonlight:LocalStorage");
|
||||
}
|
||||
}
|
||||
@@ -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<string> GetHostPathAsync() => Task.FromResult(HostPath);
|
||||
public Task<string> GetBindPathAsync() => Task.FromResult(BindPath);
|
||||
}
|
||||
@@ -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<LocalStorageOptions> Options;
|
||||
|
||||
public LocalInstallStorageService(IOptions<LocalStorageOptions> options)
|
||||
{
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public Task<IInstallStorage?> FindAsync(string id)
|
||||
{
|
||||
var path = string.Format(HostPathTemplate, id);
|
||||
var path = Path.Combine(Options.Value.InstallPath, id);
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
return Task.FromResult<IInstallStorage?>(null);
|
||||
@@ -19,7 +26,7 @@ public class LocalInstallStorageService : IInstallStorageService
|
||||
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string> GetHostPathAsync() => Task.FromResult(HostPath);
|
||||
public Task<string> GetBindPathAsync() => Task.FromResult(BindPath);
|
||||
}
|
||||
@@ -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<LocalStorageOptions> Options;
|
||||
|
||||
public LocalRuntimeStorageService(IOptions<LocalStorageOptions> options)
|
||||
{
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public Task<IRuntimeStorage?> FindAsync(string id)
|
||||
{
|
||||
var path = string.Format(HostPathTemplate, id);
|
||||
var path = Path.Combine(Options.Value.RuntimePath, id);
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
return Task.FromResult<IRuntimeStorage?>(null);
|
||||
@@ -19,7 +26,7 @@ public class LocalRuntimeStorageService : IRuntimeStorageService
|
||||
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
34
MoonlightServers.Daemon/ServerSystem/Server.Update.cs
Normal file
34
MoonlightServers.Daemon/ServerSystem/Server.Update.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public record ServerStatistics();
|
||||
public record ServerStatistics(
|
||||
double CpuUsage,
|
||||
ulong UsedMemory,
|
||||
ulong TotalMemory,
|
||||
ulong OutgoingNetwork,
|
||||
ulong IngoingNetwork,
|
||||
ulong WriteDisk,
|
||||
ulong ReadDisk
|
||||
);
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
67
MoonlightServers.Daemon/Services/ServerService.cs
Normal file
67
MoonlightServers.Daemon/Services/ServerService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user