Implemented factory pattern for server abstraction creation. Implemented raw fs and docker provisioner. Implemented docker event service with observer pattern

This commit is contained in:
2025-07-26 19:14:02 +02:00
parent 0bef60dbc8
commit 84b3d1caf6
13 changed files with 956 additions and 51 deletions

View File

@@ -44,10 +44,10 @@ public class AppConfiguration
public class StorageData public class StorageData
{ {
public string Volumes { get; set; } = PathBuilder.Dir("volumes"); public string Volumes { get; set; } = Path.Combine("storage", "volumes");
public string VirtualDisks { get; set; } = PathBuilder.Dir("virtualDisks"); public string VirtualDisks { get; set; } = Path.Combine("storage", "virtualDisks");
public string Backups { get; set; } = PathBuilder.Dir("backups"); public string Backups { get; set; } = Path.Combine("storage", "backups");
public string Install { get; set; } = PathBuilder.Dir("install"); public string Install { get; set; } =Path.Combine("storage", "install");
public VirtualDiskData VirtualDiskOptions { get; set; } = new(); public VirtualDiskData VirtualDiskOptions { get; set; } = new();
} }

View File

@@ -0,0 +1,24 @@
namespace MoonlightServers.Daemon.Helpers;
public class CompositeServiceProvider : IServiceProvider
{
private readonly List<IServiceProvider> ServiceProviders;
public CompositeServiceProvider(params IServiceProvider[] serviceProviders)
{
ServiceProviders = new List<IServiceProvider>(serviceProviders);
}
public object? GetService(Type serviceType)
{
foreach (var provider in ServiceProviders)
{
var service = provider.GetService(serviceType);
if (service != null)
return service;
}
return null;
}
}

View File

@@ -0,0 +1,284 @@
using Docker.DotNet.Models;
using Mono.Unix.Native;
using MoonCore.Helpers;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Models.Cache;
namespace MoonlightServers.Daemon.Mappers;
public class ServerConfigurationMapper
{
private readonly AppConfiguration AppConfiguration;
public ServerConfigurationMapper(AppConfiguration appConfiguration)
{
AppConfiguration = appConfiguration;
}
public CreateContainerParameters ToRuntimeParameters(
ServerConfiguration serverConfiguration,
string hostPath,
string containerName
)
{
var parameters = ToSharedParameters(serverConfiguration);
#region 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"
};
#endregion
#region Name
parameters.Name = containerName;
parameters.Hostname = containerName;
#endregion
#region Docker Image
parameters.Image = serverConfiguration.DockerImage;
#endregion
#region Working Dir
parameters.WorkingDir = "/home/container";
#endregion
#region User
// TODO: Extract this to an external service with config options and return a userspace user id and a install user id
// in order to know which permissions are required in order to run the container with the correct permissions
var userId = Syscall.getuid();
if (userId == 0)
userId = 998;
parameters.User = $"{userId}:{userId}";
/*
if (userId == 0)
{
// We are running as root, so we need to run the container as another user and chown the files when we make changes
parameters.User = $"998:998";
}
else
{
// We are not running as root, so we start the container as the same user,
// as we are not able to chown the container content to a different user
parameters.User = $"{userId}:{userId}";
}*/
#endregion
#region Mounts
parameters.HostConfig.Mounts = new List<Mount>();
parameters.HostConfig.Mounts.Add(new()
{
Source = hostPath,
Target = "/home/container",
ReadOnly = false,
Type = "bind"
});
#endregion
#region Port Bindings
if (true) // TODO: Add network toggle
{
parameters.ExposedPorts = new Dictionary<string, EmptyStruct>();
parameters.HostConfig.PortBindings = new Dictionary<string, IList<PortBinding>>();
foreach (var allocation in serverConfiguration.Allocations)
{
parameters.ExposedPorts.Add($"{allocation.Port}/tcp", new());
parameters.ExposedPorts.Add($"{allocation.Port}/udp", new());
parameters.HostConfig.PortBindings.Add($"{allocation.Port}/tcp", new List<PortBinding>
{
new()
{
HostPort = allocation.Port.ToString(),
HostIP = allocation.IpAddress
}
});
parameters.HostConfig.PortBindings.Add($"{allocation.Port}/udp", new List<PortBinding>
{
new()
{
HostPort = allocation.Port.ToString(),
HostIP = allocation.IpAddress
}
});
}
}
#endregion
// TODO: Implement a way to directly startup a server without using the entrypoint.sh and parsing the startup command here
// in the daemon instead of letting it the entrypoint do. iirc pelican wants to do that as well so we need to do that
// sooner or later in order to stay compatible to pelican
// Possible flag name: LegacyEntrypointMode
return parameters;
}
public CreateContainerParameters ToSharedParameters(ServerConfiguration serverConfiguration)
{
var parameters = new CreateContainerParameters()
{
HostConfig = new()
};
#region Input, output & error streams and tty
parameters.Tty = true;
parameters.AttachStderr = true;
parameters.AttachStdin = true;
parameters.AttachStdout = true;
parameters.OpenStdin = true;
#endregion
#region CPU
parameters.HostConfig.CPUQuota = serverConfiguration.Cpu * 1000;
parameters.HostConfig.CPUPeriod = 100000;
parameters.HostConfig.CPUShares = 1024;
#endregion
#region Memory & Swap
var memoryLimit = serverConfiguration.Memory;
// The overhead multiplier gives the container a little bit more memory to prevent crashes
var memoryOverhead = memoryLimit + (memoryLimit * AppConfiguration.Server.MemoryOverheadMultiplier);
long swapLimit = -1;
/*
// If swap is enabled globally and not disabled on this server, set swap
if (!configuration.Limits.DisableSwap && config.Server.EnableSwap)
swapLimit = (long)(memoryOverhead + memoryOverhead * config.Server.SwapMultiplier);
co
*/
// Finalize limits by converting and updating the host config
parameters.HostConfig.Memory = ByteConverter.FromMegaBytes((long)memoryOverhead, 1000).Bytes;
parameters.HostConfig.MemoryReservation = ByteConverter.FromMegaBytes(memoryLimit, 1000).Bytes;
parameters.HostConfig.MemorySwap =
swapLimit == -1 ? swapLimit : ByteConverter.FromMegaBytes(swapLimit, 1000).Bytes;
#endregion
#region Misc Limits
// -- Other limits
parameters.HostConfig.BlkioWeight = 100;
//container.HostConfig.PidsLimit = configuration.Limits.PidsLimit;
parameters.HostConfig.OomKillDisable = true; //!configuration.Limits.EnableOomKill;
#endregion
#region DNS
// TODO: Read hosts dns settings?
parameters.HostConfig.DNS = /*config.Docker.DnsServers.Any() ? config.Docker.DnsServers :*/ new List<string>()
{
"1.1.1.1",
"9.9.9.9"
};
#endregion
#region Tmpfs
parameters.HostConfig.Tmpfs = new Dictionary<string, string>()
{
{ "/tmp", $"rw,exec,nosuid,size={AppConfiguration.Server.TmpFsSize}M" }
};
#endregion
#region 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>()
};
#endregion
#region Labels
parameters.Labels = new Dictionary<string, string>();
parameters.Labels.Add("Software", "Moonlight-Panel");
parameters.Labels.Add("ServerId", serverConfiguration.Id.ToString());
#endregion
#region Environment
parameters.Env = CreateEnvironmentVariables(serverConfiguration);
#endregion
return parameters;
}
private List<string> CreateEnvironmentVariables(ServerConfiguration serverConfiguration)
{
var result = new Dictionary<string, string>
{
//TODO: Add timezone, add server ip
{ "STARTUP", serverConfiguration.StartupCommand },
{ "SERVER_MEMORY", serverConfiguration.Memory.ToString() }
};
if (serverConfiguration.Allocations.Length > 0)
{
for (var i = 0; i < serverConfiguration.Allocations.Length; i++)
{
var allocation = serverConfiguration.Allocations[i];
result.Add($"ML_PORT_{i}", allocation.Port.ToString());
if (i == 0) // TODO: Implement a way to set the default/main allocation
{
result.Add("SERVER_IP", allocation.IpAddress);
result.Add("SERVER_PORT", allocation.Port.ToString());
}
}
}
// Copy variables as env vars
foreach (var variable in serverConfiguration.Variables)
result.Add(variable.Key, variable.Value);
// Convert to the format of the docker library
return result.Select(variable => $"{variable.Key}={variable.Value}").ToList();
}
}

View File

@@ -10,6 +10,7 @@ public interface IConsole : IServerComponent
public Task WriteToOutput(string content); public Task WriteToOutput(string content);
public Task WriteToInput(string content); public Task WriteToInput(string content);
public Task WriteToMoonlight(string content);
public Task ClearOutput(); public Task ClearOutput();
public string[] GetOutput(); public string[] GetOutput();

View File

@@ -4,10 +4,11 @@ public interface IProvisioner : IServerComponent
{ {
public IAsyncObservable<object> OnExited { get; set; } public IAsyncObservable<object> OnExited { get; set; }
public Task Provision();
public Task Start(); public Task Start();
public Task Stop(); public Task Stop();
public Task Kill(); public Task Kill();
public Task Cleanup(); public Task Deprovision();
public Task<ServerCrash?> SearchForCrash(); public Task<ServerCrash?> SearchForCrash();
} }

View File

@@ -119,8 +119,59 @@ public class Server : IAsyncDisposable
// Handle transitions // Handle transitions
StateMachine.Configure(ServerState.Starting)
.OnActivateAsync(HandleStart);
} }
#region State machine handlers
private async Task HandleStart()
{
try
{
// Plan for starting the server:
// 1. Fetch latest configuration from panel (maybe: and perform sync)
// 2. Ensure that the file system exists
// 3. Mount the file system
// 4. Provision the container
// 5. Attach console to container
// 6. Start the container
// 1. Fetch latest configuration from panel
// TODO: Implement
// 2. Ensure that the file system exists
if (!FileSystem.Exists)
{
await Console.WriteToMoonlight("Creating storage");
await FileSystem.Create();
}
// 3. Mount the file system
if (!FileSystem.IsMounted)
{
await Console.WriteToMoonlight("Mounting storage");
await FileSystem.Mount();
}
// 4. Provision the container
await Console.WriteToMoonlight("Provisioning runtime");
await Provisioner.Provision();
// 5. Attach console to container
await Console.AttachToRuntime();
// 6. Start the container
await Provisioner.Start();
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while starting the server");
}
}
#endregion
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
if (ProvisionExitSubscription != null) if (ProvisionExitSubscription != null)

View File

@@ -0,0 +1,10 @@
using MoonlightServers.Daemon.Models.Cache;
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
public record ServerMeta
{
public ServerConfiguration Configuration { get; set; }
public IServiceCollection ServiceCollection { get; set; }
public IServiceProvider ServiceProvider { get; set; }
}

View File

@@ -1,6 +1,9 @@
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Text;
using Docker.DotNet; using Docker.DotNet;
using Docker.DotNet.Models;
using Microsoft.Extensions.Options;
using MoonCore.Helpers; using MoonCore.Helpers;
using MoonlightServers.Daemon.ServerSys.Abstractions; using MoonlightServers.Daemon.ServerSys.Abstractions;
@@ -15,12 +18,18 @@ public class DockerConsole : IConsole
private readonly AsyncSubject<string> OnInputSubject = new(); private readonly AsyncSubject<string> OnInputSubject = new();
private readonly ConcurrentList<string> OutputCache = new(); private readonly ConcurrentList<string> OutputCache = new();
private readonly DockerClient DockerClient; private readonly DockerClient DockerClient;
private readonly ILogger<DockerConsole> Logger; private readonly ILogger<DockerConsole> Logger;
private readonly ServerMeta Meta;
private MultiplexedStream? CurrentStream; private MultiplexedStream? CurrentStream;
private CancellationTokenSource Cts = new(); private CancellationTokenSource Cts = new();
public DockerConsole(ServerMeta meta)
{
Meta = meta;
}
public Task Initialize() public Task Initialize()
=> Task.CompletedTask; => Task.CompletedTask;
@@ -29,12 +38,101 @@ public class DockerConsole : IConsole
public async Task AttachToRuntime() public async Task AttachToRuntime()
{ {
throw new NotImplementedException(); var containerName = $"moonlight-runtime-{Meta.Configuration.Id}";
await AttachStream(containerName);
} }
public async Task AttachToInstallation() public async Task AttachToInstallation()
{ {
throw new NotImplementedException(); var containerName = $"moonlight-install-{Meta.Configuration.Id}";
await AttachStream(containerName);
}
private Task AttachStream(string containerName)
{
Task.Run(async () =>
{
// This loop is here to reconnect to the container if for some reason the container
// attach stream fails before the server tasks have been canceled i.e. the before the server
// goes offline
while (!Cts.Token.IsCancellationRequested)
{
try
{
CurrentStream = await DockerClient.Containers.AttachContainerAsync(
containerName,
true,
new ContainerAttachParameters()
{
Stderr = true,
Stdin = true,
Stdout = true,
Stream = true
},
Cts.Token
);
var buffer = new byte[1024];
try
{
// Read while server tasks are not canceled
while (!Cts.Token.IsCancellationRequested)
{
var readResult = await CurrentStream.ReadOutputAsync(
buffer,
0,
buffer.Length,
Cts.Token
);
if (readResult.EOF)
break;
var resizedBuffer = new byte[readResult.Count];
Array.Copy(buffer, resizedBuffer, readResult.Count);
buffer = new byte[buffer.Length];
var decodedText = Encoding.UTF8.GetString(resizedBuffer);
await WriteToOutput(decodedText);
}
}
catch (TaskCanceledException)
{
// Ignored
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception e)
{
Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", e);
}
finally
{
CurrentStream.Dispose();
}
}
catch (TaskCanceledException)
{
// ignored
}
catch (Exception e)
{
Logger.LogError("An error occured while attaching to container: {e}", e);
}
}
// Reset stream so no further inputs will be piped to it
CurrentStream = null;
Logger.LogDebug("Disconnected from container stream");
}, Cts.Token);
return Task.CompletedTask;
} }
public Task WriteToOutput(string content) public Task WriteToOutput(string content)
@@ -56,9 +154,22 @@ public class DockerConsole : IConsole
public async Task WriteToInput(string content) public async Task WriteToInput(string content)
{ {
throw new NotImplementedException(); if (CurrentStream == null)
return;
var contentBuffer = Encoding.UTF8.GetBytes(content);
await CurrentStream.WriteAsync(
contentBuffer,
0,
contentBuffer.Length,
Cts.Token
);
} }
public async Task WriteToMoonlight(string content)
=> await WriteToOutput($"\x1b[0;38;2;255;255;255;48;2;124;28;230m Moonlight \x1b[0m\x1b[38;5;250m\x1b[3m {content}\x1b[0m\n\r");
public Task ClearOutput() public Task ClearOutput()
{ {
OutputCache.Clear(); OutputCache.Clear();

View File

@@ -0,0 +1,246 @@
using System.Reactive.Subjects;
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.ServerSys.Abstractions;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.ServerSys.Implementations;
public class DockerProvisioner : IProvisioner
{
public IAsyncObservable<object> OnExited { get; set; }
private readonly DockerClient DockerClient;
private readonly ILogger<DockerProvisioner> Logger;
private readonly DockerEventService EventService;
private readonly ServerMeta Meta;
private readonly IConsole Console;
private readonly DockerImageService ImageService;
private readonly ServerConfigurationMapper Mapper;
private readonly IFileSystem FileSystem;
private AsyncSubject<object> OnExitedSubject = new();
private string? ContainerId;
private string ContainerName;
private IAsyncDisposable? ContainerEventSubscription;
public DockerProvisioner(
DockerClient dockerClient,
ILogger<DockerProvisioner> logger,
DockerEventService eventService,
ServerMeta meta,
IConsole console,
DockerImageService imageService,
ServerConfigurationMapper mapper,
IFileSystem fileSystem
)
{
DockerClient = dockerClient;
Logger = logger;
EventService = eventService;
Meta = meta;
Console = console;
ImageService = imageService;
Mapper = mapper;
FileSystem = fileSystem;
}
public async Task Initialize()
{
ContainerName = $"moonlight-runtime-{Meta.Configuration.Id}";
ContainerEventSubscription = await EventService
.OnContainerEvent
.SubscribeAsync(HandleContainerEvent);
// Check for any already existing runtime container
// TODO: Implement a way for restoring the state
// Needs to be able to be consumed by the restorer
}
private ValueTask HandleContainerEvent(Message message)
{
// Only handle events for our own container
if (message.ID != ContainerId)
return ValueTask.CompletedTask;
// Only handle die events
if (message.Action != "die")
return ValueTask.CompletedTask;
OnExitedSubject.OnNext(message);
return ValueTask.CompletedTask;
}
public Task Sync()
{
return Task.CompletedTask; // TODO: Implement
}
public async Task Provision()
{
// Plan of action:
// 1. Ensure no other container with that name exist
// 2. Ensure the docker image has been downloaded
// 3. Create the container from the configuration in the meta
// 1. Ensure no other container with that name exist
try
{
Logger.LogDebug("Searching for orphan container");
var possibleContainer = await DockerClient.Containers.InspectContainerAsync(ContainerName);
Logger.LogDebug("Orphan container found. Removing it");
await Console.WriteToMoonlight("Found orphan container. Removing it");
await EnsureContainerOffline(possibleContainer);
Logger.LogInformation("Removing orphan container");
await DockerClient.Containers.RemoveContainerAsync(ContainerName, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
// 2. Ensure the docker image has been downloaded
await Console.WriteToMoonlight("Downloading docker image");
await ImageService.Download(Meta.Configuration.DockerImage, async message =>
{
try
{
await Console.WriteToMoonlight(message);
}
catch (Exception)
{
// Ignored. Not handling it here could cause an application wide crash afaik
}
});
// 3. Create the container from the configuration in the meta
var hostFsPath = FileSystem.GetExternalPath();
var parameters = Mapper.ToRuntimeParameters(
Meta.Configuration,
hostFsPath,
ContainerName
);
var createdContainer = await DockerClient.Containers.CreateContainerAsync(parameters);
ContainerId = createdContainer.ID;
Logger.LogInformation("Created container");
await Console.WriteToMoonlight("Created container");
}
public async Task Start()
{
if(string.IsNullOrEmpty(ContainerId))
throw new ArgumentNullException(nameof(ContainerId), "Container id of runtime is unknown");
await Console.WriteToMoonlight("Starting container");
await DockerClient.Containers.StartContainerAsync(ContainerId, new());
}
public async Task Stop()
{
if (Meta.Configuration.StopCommand.StartsWith('^'))
{
await DockerClient.Containers.KillContainerAsync(ContainerId, new()
{
Signal = Meta.Configuration.StopCommand.Substring(1)
});
}
else
await Console.WriteToInput(Meta.Configuration.StopCommand);
}
public async Task Kill()
{
await EnsureContainerOffline();
}
public async Task Deprovision()
{
// Plan of action:
// 1. Search for the container by id or name
// 2. Ensure container is offline
// 3. Remove the container
// 1. Search for the container by id or name
ContainerInspectResponse? container = null;
try
{
if (string.IsNullOrEmpty(ContainerId))
container = await DockerClient.Containers.InspectContainerAsync(ContainerName);
else
container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
}
catch (DockerContainerNotFoundException)
{
// Ignored
Logger.LogDebug("Runtime container could not be found. Reporting deprovision success");
}
// No container found? We are done here then
if (container == null)
return;
// 2. Ensure container is offline
await EnsureContainerOffline(container);
// 3. Remove the container
Logger.LogInformation("Removing container");
await Console.WriteToMoonlight("Removing container");
await DockerClient.Containers.RemoveContainerAsync(container.ID, new());
}
private async Task EnsureContainerOffline(ContainerInspectResponse? container = null)
{
try
{
if (string.IsNullOrEmpty(ContainerId))
container = await DockerClient.Containers.InspectContainerAsync(ContainerName);
else
container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
// No container found? We are done here then
if (container == null)
return;
// Check if container is running
if (!container.State.Running)
return;
await Console.WriteToMoonlight("Killing container");
await DockerClient.Containers.KillContainerAsync(ContainerId, new());
}
public Task<ServerCrash?> SearchForCrash()
{
throw new NotImplementedException();
}
public async ValueTask DisposeAsync()
{
OnExitedSubject.Dispose();
if (ContainerEventSubscription != null)
await ContainerEventSubscription.DisposeAsync();
}
}

View File

@@ -0,0 +1,55 @@
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.ServerSys.Abstractions;
namespace MoonlightServers.Daemon.ServerSys.Implementations;
public class RawFileSystem : IFileSystem
{
public bool IsMounted { get; private set; }
public bool Exists { get; private set; }
private readonly ServerMeta Meta;
private readonly AppConfiguration Configuration;
private string HostPath => Path.Combine(Configuration.Storage.Volumes, Meta.Configuration.Id.ToString());
public RawFileSystem(ServerMeta meta, AppConfiguration configuration)
{
Meta = meta;
Configuration = configuration;
}
public Task Initialize()
=> Task.CompletedTask;
public Task Sync()
=> Task.CompletedTask;
public Task Create()
{
return Task.CompletedTask;
}
public Task Mount()
{
IsMounted = true;
return Task.CompletedTask;
}
public Task Unmount()
{
IsMounted = false;
return Task.CompletedTask;
}
public Task Delete()
{
Directory.Delete(HostPath, true);
return Task.CompletedTask;
}
public string GetExternalPath()
=> HostPath;
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}

View File

@@ -0,0 +1,39 @@
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.Daemon.ServerSys.Abstractions;
using MoonlightServers.Daemon.ServerSys.Implementations;
namespace MoonlightServers.Daemon.ServerSys;
public class ServerFactory
{
private readonly IServiceProvider ServiceProvider;
public ServerFactory(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
public Server CreateServer(ServerConfiguration configuration)
{
var serverMeta = new ServerMeta();
serverMeta.Configuration = configuration;
// Build service collection
serverMeta.ServiceCollection = new ServiceCollection();
// Configure service pipeline for the server components
serverMeta.ServiceCollection.AddSingleton<IConsole, DockerConsole>();
serverMeta.ServiceCollection.AddSingleton<IFileSystem, RawFileSystem>();
// TODO: Handle implementation configurations (e.g. virtual disk) here
// Combine both app service provider and our server instance specific one
serverMeta.ServiceProvider = new CompositeServiceProvider([
serverMeta.ServiceCollection.BuildServiceProvider(),
ServiceProvider
]);
return serverMeta.ServiceProvider.GetRequiredService<Server>();
}
}

View File

@@ -0,0 +1,81 @@
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Docker.DotNet;
using Docker.DotNet.Models;
namespace MoonlightServers.Daemon.Services;
public class DockerEventService : BackgroundService
{
private readonly ILogger<DockerEventService> Logger;
private readonly DockerClient DockerClient;
public IAsyncObservable<Message> OnContainerEvent => OnContainerSubject.ToAsyncObservable();
public IAsyncObservable<Message> OnImageEvent => OnImageSubject.ToAsyncObservable();
public IAsyncObservable<Message> OnNetworkEvent => OnNetworkSubject.ToAsyncObservable();
private readonly AsyncSubject<Message> OnContainerSubject = new();
private readonly AsyncSubject<Message> OnImageSubject = new();
private readonly AsyncSubject<Message> OnNetworkSubject = new();
public DockerEventService(
ILogger<DockerEventService> logger,
DockerClient dockerClient
)
{
Logger = logger;
DockerClient = dockerClient;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Logger.LogInformation("Starting docker event service");
while (!stoppingToken.IsCancellationRequested)
{
try
{
await DockerClient.System.MonitorEventsAsync(
new ContainerEventsParameters(),
new Progress<Message>(message =>
{
switch (message.Type)
{
case "container":
OnContainerSubject.OnNext(message);
break;
case "image":
OnImageSubject.OnNext(message);
break;
case "network":
OnNetworkSubject.OnNext(message);
break;
}
}),
stoppingToken
);
}
catch (TaskCanceledException)
{
// ignored
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while listening for docker events: {message}", e.Message);
}
}
Logger.LogInformation("Stopping docker event service");
}
public override void Dispose()
{
base.Dispose();
OnContainerSubject.Dispose();
OnImageSubject.Dispose();
OnNetworkSubject.Dispose();
}
}

View File

@@ -8,9 +8,12 @@ using MoonCore.EnvConfiguration;
using MoonCore.Extended.Extensions; using MoonCore.Extended.Extensions;
using MoonCore.Extensions; using MoonCore.Extensions;
using MoonCore.Helpers; using MoonCore.Helpers;
using MoonCore.Logging;
using MoonlightServers.Daemon.Configuration; using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Helpers; using MoonlightServers.Daemon.Helpers;
using MoonlightServers.Daemon.Http.Hubs; using MoonlightServers.Daemon.Http.Hubs;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.ServerSys;
using MoonlightServers.Daemon.ServerSystem; using MoonlightServers.Daemon.ServerSystem;
using MoonlightServers.Daemon.Services; using MoonlightServers.Daemon.Services;
@@ -68,8 +71,8 @@ public class Startup
private Task SetupStorage() private Task SetupStorage()
{ {
Directory.CreateDirectory("storage"); Directory.CreateDirectory("storage");
Directory.CreateDirectory(PathBuilder.Dir("storage", "logs")); Directory.CreateDirectory(Path.Combine("storage", "logs"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins")); Directory.CreateDirectory(Path.Combine("storage", "plugins"));
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -100,7 +103,7 @@ public class Startup
private Task UseBase() private Task UseBase()
{ {
WebApplication.UseRouting(); WebApplication.UseRouting();
WebApplication.UseApiExceptionHandler(); WebApplication.UseExceptionHandler();
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -141,7 +144,7 @@ public class Startup
var configurationBuilder = new ConfigurationBuilder(); var configurationBuilder = new ConfigurationBuilder();
// Ensure configuration file exists // Ensure configuration file exists
var jsonFilePath = PathBuilder.File(Directory.GetCurrentDirectory(), "storage", "app.json"); var jsonFilePath = Path.Combine(Directory.GetCurrentDirectory(), "storage", "app.json");
if (!File.Exists(jsonFilePath)) if (!File.Exists(jsonFilePath))
await File.WriteAllTextAsync(jsonFilePath, JsonSerializer.Serialize(new AppConfiguration())); await File.WriteAllTextAsync(jsonFilePath, JsonSerializer.Serialize(new AppConfiguration()));
@@ -187,20 +190,8 @@ public class Startup
private Task SetupLogging() private Task SetupLogging()
{ {
LoggerProviders = LoggerBuildHelper.BuildFromConfiguration(configuration =>
{
configuration.Console.Enable = true;
configuration.Console.EnableAnsiMode = true;
configuration.FileLogging.Enable = true;
configuration.FileLogging.EnableLogRotation = true;
configuration.FileLogging.Path = PathBuilder.File("storage", "logs", "WebAppTemplate.log");
configuration.FileLogging.RotateLogNameTemplate =
PathBuilder.File("storage", "logs", "WebAppTemplate.log.{0}");
});
LoggerFactory = new LoggerFactory(); LoggerFactory = new LoggerFactory();
LoggerFactory.AddProviders(LoggerProviders); LoggerFactory.AddAnsiConsole();
Logger = LoggerFactory.CreateLogger<Startup>(); Logger = LoggerFactory.CreateLogger<Startup>();
@@ -211,30 +202,35 @@ public class Startup
{ {
// Configure application logging // Configure application logging
WebApplicationBuilder.Logging.ClearProviders(); WebApplicationBuilder.Logging.ClearProviders();
WebApplicationBuilder.Logging.AddProviders(LoggerProviders); WebApplicationBuilder.Logging.AddAnsiConsole();
WebApplicationBuilder.Logging.AddFile(
Path.Combine(Directory.GetCurrentDirectory(), "storage", "logs", "MoonlightServer.Daemon.log")
);
// Logging levels // Logging levels
var logConfigPath = PathBuilder.File("storage", "logConfig.json"); var logConfigPath = Path.Combine("storage", "logConfig.json");
// Ensure logging config, add a default one is missing // Ensure logging config, add a default one is missing
if (!File.Exists(logConfigPath)) if (!File.Exists(logConfigPath))
{ {
var logLevels = new Dictionary<string, string> var defaultLogLevels = new Dictionary<string, string>
{ {
{ "Default", "Information" }, { "Default", "Information" },
{ "Microsoft.AspNetCore", "Warning" }, { "Microsoft.AspNetCore", "Warning" },
{ "System.Net.Http.HttpClient", "Warning" } { "System.Net.Http.HttpClient", "Warning" }
}; };
var logLevelsJson = JsonSerializer.Serialize(logLevels); var json = JsonSerializer.Serialize(defaultLogLevels);
var logConfig = "{\"LogLevel\":" + logLevelsJson + "}"; await File.WriteAllTextAsync(logConfigPath, json);
await File.WriteAllTextAsync(logConfigPath, logConfig);
} }
// Configure logging configuration var logLevels = JsonSerializer.Deserialize<Dictionary<string, string>>(
WebApplicationBuilder.Logging.AddConfiguration(
await File.ReadAllTextAsync(logConfigPath) await File.ReadAllTextAsync(logConfigPath)
); )!;
// Configure logging configuration
foreach (var logLevel in logLevels)
WebApplicationBuilder.Logging.AddFilter(logLevel.Key, Enum.Parse<LogLevel>(logLevel.Value));
// Mute exception handler middleware // Mute exception handler middleware
// https://github.com/dotnet/aspnetcore/issues/19740 // https://github.com/dotnet/aspnetcore/issues/19740
@@ -255,10 +251,16 @@ public class Startup
private Task RegisterServers() private Task RegisterServers()
{ {
WebApplicationBuilder.Services.AddHostedService( WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<ServerService>()
sp => sp.GetRequiredService<ServerService>()
); );
WebApplicationBuilder.Services.AddSingleton<DockerEventService>();
WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<DockerEventService>());
WebApplicationBuilder.Services.AddSingleton<ServerConfigurationMapper>();
WebApplicationBuilder.Services.AddSingleton<ServerFactory>();
return Task.CompletedTask; return Task.CompletedTask;
} }