Improved comments. Started implementing docker components and other base components. Updated dependencies
This commit is contained in:
203
MoonlightServers.Daemon/ServerSystem/Docker/DockerConsole.cs
Normal file
203
MoonlightServers.Daemon/ServerSystem/Docker/DockerConsole.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user