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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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