Improved comments. Started implementing docker components and other base components. Updated dependencies

This commit is contained in:
2025-09-06 21:44:22 +02:00
parent 348e9560ab
commit 282096595d
16 changed files with 672 additions and 52 deletions

View File

@@ -0,0 +1,203 @@
using System.Text;
using Docker.DotNet;
using MoonCore.Events;
using MoonCore.Helpers;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerConsole : IConsole
{
private readonly EventSource<string> StdOutEventSource = new();
private readonly ConcurrentList<string> StdOutCache = new();
private readonly DockerClient DockerClient;
private readonly ServerContext Context;
private readonly ILogger Logger;
private MultiplexedStream? BaseStream;
private CancellationTokenSource Cts = new();
public DockerConsole(DockerClient dockerClient, ServerContext context)
{
DockerClient = dockerClient;
Context = context;
}
public Task InitializeAsync()
=> Task.CompletedTask;
public async Task WriteStdInAsync(string content)
{
if (BaseStream == null)
{
Logger.LogWarning("Unable to write to stdin as no stream is connected");
return;
}
var contextBuffer = Encoding.UTF8.GetBytes(content);
await BaseStream.WriteAsync(contextBuffer, 0, contextBuffer.Length, Cts.Token);
}
public async Task WriteStdOutAsync(string content)
{
// Add output cache
if (StdOutCache.Count > 250) // TODO: Config
StdOutCache.RemoveRange(0, 100);
StdOutCache.Add(content);
// Fire event
await StdOutEventSource.InvokeAsync(content);
}
public async Task AttachRuntimeAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await AttachToContainer(containerName);
}
public async Task AttachInstallationAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id);
await AttachToContainer(containerName);
}
private async Task AttachToContainer(string containerName)
{
// Cancels previous active read task if it exists
if (!Cts.IsCancellationRequested)
await Cts.CancelAsync();
// Reset cancellation token
Cts = new();
// Start reading task
Task.Run(async () =>
{
// This loop is here to reconnect to the stream when connection is lost.
// This can occur when docker restarts for example
while (!Cts.IsCancellationRequested)
{
try
{
using var stream = await DockerClient.Containers.AttachContainerAsync(
containerName,
true,
new()
{
Stderr = true,
Stdin = true,
Stdout = true,
Stream = true
},
Cts.Token
);
BaseStream = stream;
var buffer = new byte[1024];
try
{
// Read while server tasks are not canceled
while (!Cts.Token.IsCancellationRequested)
{
var readResult = await BaseStream.ReadOutputAsync(
buffer,
0,
buffer.Length,
Cts.Token
);
if (readResult.EOF)
break;
var decodedText = Encoding.UTF8.GetString(buffer, 0, readResult.Count);
await WriteStdOutAsync(decodedText);
}
}
catch (TaskCanceledException)
{
// Ignored
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception e)
{
Logger.LogWarning(e, "An unhandled error occured while reading from container stream");
}
}
catch (TaskCanceledException)
{
// ignored
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while attaching to container");
}
}
Logger.LogDebug("Disconnected from container stream");
});
}
public async Task FetchRuntimeAsync()
{
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
await FetchFromContainer(containerName);
}
public async Task FetchInstallationAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id);
await FetchFromContainer(containerName);
}
private async Task FetchFromContainer(string containerName)
{
var logStream = await DockerClient.Containers.GetContainerLogsAsync(containerName, true, new()
{
Follow = false,
ShowStderr = true,
ShowStdout = true
});
var combinedOutput = await logStream.ReadOutputToEndAsync(Cts.Token);
var contentToAdd = combinedOutput.stdout + combinedOutput.stderr;
await WriteStdOutAsync(contentToAdd);
}
public Task ClearCacheAsync()
{
StdOutCache.Clear();
return Task.CompletedTask;
}
public Task<IEnumerable<string>> GetCacheAsync()
{
return Task.FromResult<IEnumerable<string>>(StdOutCache);
}
public async Task<IAsyncDisposable> SubscribeStdOutAsync(Func<string, ValueTask> callback)
=> await StdOutEventSource.SubscribeAsync(callback);
public async ValueTask DisposeAsync()
{
if (!Cts.IsCancellationRequested)
await Cts.CancelAsync();
if (BaseStream != null)
BaseStream.Dispose();
}
}

View File

@@ -0,0 +1,7 @@
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public static class DockerConstants
{
public const string RuntimeNameTemplate = "monnlight-runtime-{0}";
public const string InstallationNameTemplate = "monnlight-installation-{0}";
}

View File

@@ -0,0 +1,185 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonCore.Events;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.ServerSystem.Docker;
public class DockerInstallation : IInstallation
{
private readonly DockerEventService DockerEventService;
private readonly ServerConfigurationMapper Mapper;
private readonly DockerImageService ImageService;
private readonly ServerContext ServerContext;
private readonly DockerClient DockerClient;
private readonly IReporter Reporter;
private readonly EventSource<int> ExitEventSource = new();
private IAsyncDisposable ContainerEventSubscription;
private string ContainerId;
public DockerInstallation(
DockerClient dockerClient,
ServerContext serverContext,
ServerConfigurationMapper mapper,
DockerImageService imageService,
IReporter reporter,
DockerEventService dockerEventService
)
{
DockerClient = dockerClient;
ServerContext = serverContext;
Mapper = mapper;
ImageService = imageService;
Reporter = reporter;
DockerEventService = dockerEventService;
}
public async Task InitializeAsync()
{
ContainerEventSubscription = await DockerEventService.SubscribeContainerAsync(OnContainerEvent);
}
private async ValueTask OnContainerEvent(Message message)
{
// Only handle events for our own container
if (message.ID != ContainerId)
return;
// Only handle die events
if (message.Action != "die")
return;
int exitCode;
if (message.Actor.Attributes.TryGetValue("exitCode", out var exitCodeStr))
{
if (!int.TryParse(exitCodeStr, out exitCode))
exitCode = 0;
}
else
exitCode = 0;
await ExitEventSource.InvokeAsync(exitCode);
}
public async Task<bool> CheckExistsAsync()
{
try
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
await DockerClient.Containers.InspectContainerAsync(
containerName
);
return true;
}
catch (DockerContainerNotFoundException)
{
return false;
}
}
public async Task CreateAsync(
string runtimePath,
string hostPath,
ServerInstallDataResponse data
)
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
var parameters = Mapper.ToInstallParameters(
ServerContext.Configuration,
data,
runtimePath,
hostPath,
containerName
);
// Docker image
await Reporter.StatusAsync("Downloading docker image");
await ImageService.Download(data.DockerImage, async status => { await Reporter.StatusAsync(status); });
await Reporter.StatusAsync("Downloaded docker image");
// Write install script to install fs
await File.WriteAllTextAsync(
Path.Combine(hostPath, "install.sh"),
data.Script
);
//
await DockerClient.Containers.CreateContainerAsync(parameters);
await Reporter.StatusAsync("Created container");
}
public async Task StartAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
await DockerClient.Containers.StartContainerAsync(containerName, new());
}
public async Task KillAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
await DockerClient.Containers.KillContainerAsync(containerName, new());
}
public async Task DestroyAsync()
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
try
{
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
if (container.State.Running)
await DockerClient.Containers.KillContainerAsync(containerName, new());
await DockerClient.Containers.RemoveContainerAsync(containerName, new()
{
Force = true
});
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
}
public async Task<IAsyncDisposable> SubscribeExited(Func<int, ValueTask> callback)
=> await ExitEventSource.SubscribeAsync(callback);
public async Task RestoreAsync()
{
try
{
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
ContainerId = container.ID;
}
catch (DockerContainerNotFoundException)
{
// Ignore
}
}
public async ValueTask DisposeAsync()
{
await ContainerEventSubscription.DisposeAsync();
}
}