Deed debug handler. Added installation handler. Improved docker console streaming

This commit is contained in:
2025-09-13 20:53:03 +02:00
parent 160446eed0
commit 32f447d268
6 changed files with 204 additions and 19 deletions

View File

@@ -15,7 +15,7 @@ public class DockerConsole : IConsole
private readonly ServerContext Context;
private readonly ILogger Logger;
private MultiplexedStream? BaseStream;
private MultiplexedStream? CurrentStream;
private CancellationTokenSource Cts = new();
public DockerConsole(DockerClient dockerClient, ServerContext context)
@@ -30,7 +30,7 @@ public class DockerConsole : IConsole
public async Task WriteStdInAsync(string content)
{
if (BaseStream == null)
if (CurrentStream == null)
{
Logger.LogWarning("Unable to write to stdin as no stream is connected");
return;
@@ -38,7 +38,7 @@ public class DockerConsole : IConsole
var contextBuffer = Encoding.UTF8.GetBytes(content);
await BaseStream.WriteAsync(contextBuffer, 0, contextBuffer.Length, Cts.Token);
await CurrentStream.WriteAsync(contextBuffer, 0, contextBuffer.Length, Cts.Token);
}
public async Task WriteStdOutAsync(string content)
@@ -69,12 +69,14 @@ public class DockerConsole : IConsole
private async Task AttachToContainer(string containerName)
{
var cts = new CancellationTokenSource();
// Cancels previous active read task if it exists
if (!Cts.IsCancellationRequested)
await Cts.CancelAsync();
// Reset cancellation token
Cts = new();
// Update the current cancellation token
Cts = cts;
// Start reading task
Task.Run(async () =>
@@ -82,11 +84,15 @@ public class DockerConsole : IConsole
// This loop is here to reconnect to the stream when connection is lost.
// This can occur when docker restarts for example
while (!Cts.IsCancellationRequested)
while (!cts.IsCancellationRequested)
{
MultiplexedStream? innerStream = null;
try
{
using var stream = await DockerClient.Containers.AttachContainerAsync(
Logger.LogTrace("Attaching");
innerStream = await DockerClient.Containers.AttachContainerAsync(
containerName,
true,
new()
@@ -96,32 +102,34 @@ public class DockerConsole : IConsole
Stdout = true,
Stream = true
},
Cts.Token
cts.Token
);
BaseStream = stream;
CurrentStream = innerStream;
var buffer = new byte[1024];
try
{
// Read while server tasks are not canceled
while (!Cts.Token.IsCancellationRequested)
while (!cts.Token.IsCancellationRequested)
{
var readResult = await BaseStream.ReadOutputAsync(
var readResult = await innerStream.ReadOutputAsync(
buffer,
0,
buffer.Length,
Cts.Token
cts.Token
);
if (readResult.EOF)
break;
await cts.CancelAsync();
var decodedText = Encoding.UTF8.GetString(buffer, 0, readResult.Count);
await WriteStdOutAsync(decodedText);
}
Logger.LogTrace("Read loop exited");
}
catch (TaskCanceledException)
{
@@ -140,10 +148,19 @@ public class DockerConsole : IConsole
{
// ignored
}
catch (DockerContainerNotFoundException)
{
// Container got removed. Stop the reconnect loop
Logger.LogDebug("Container '{name}' got removed. Stopping reconnect stream for console", containerName);
await cts.CancelAsync();
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while attaching to container");
}
innerStream?.Dispose();
}
Logger.LogDebug("Disconnected from container stream");
@@ -198,7 +215,7 @@ public class DockerConsole : IConsole
if (!Cts.IsCancellationRequested)
await Cts.CancelAsync();
if (BaseStream != null)
BaseStream.Dispose();
if (CurrentStream != null)
CurrentStream.Dispose();
}
}

View File

@@ -0,0 +1,35 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class DebugHandler : IServerStateHandler
{
private readonly ServerContext Context;
private IAsyncDisposable? StdOutSubscription;
public DebugHandler(ServerContext context)
{
Context = context;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
if(StdOutSubscription != null)
return;
StdOutSubscription = await Context.Server.Console.SubscribeStdOutAsync(line =>
{
Console.WriteLine($"STD OUT: {line}");
return ValueTask.CompletedTask;
});
}
public async ValueTask DisposeAsync()
{
if (StdOutSubscription != null)
await StdOutSubscription.DisposeAsync();
}
}

View File

@@ -0,0 +1,125 @@
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
public class InstallationHandler : IServerStateHandler
{
private readonly ServerContext Context;
private Server Server => Context.Server;
private IAsyncDisposable? ExitSubscription;
public InstallationHandler(ServerContext context)
{
Context = context;
}
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
{
if (transition is
{ Source: ServerState.Offline, Destination: ServerState.Installing, Trigger: ServerTrigger.Install })
{
await StartAsync();
}
else if (transition is
{ Source: ServerState.Installing, Destination: ServerState.Offline, Trigger: ServerTrigger.Exited })
{
await CompleteAsync();
}
}
private async Task StartAsync()
{
// Plan:
// 1. Fetch latest configuration
// 2. Check if both file systems exists
// 3. Check if both file systems are mounted
// 4. Run file system checks
// 5. Create installation container
// 6. Attach console
// 7. Start installation container
// 1. Fetch latest configuration
var installData = new ServerInstallDataResponse()
{
Script = await File.ReadAllTextAsync(Path.Combine("storage", "install.sh")),
Shell = "/bin/ash",
DockerImage = "ghcr.io/parkervcp/installers:alpine"
};
// 2. Check if file system exists
if (!await Server.RuntimeFileSystem.CheckExistsAsync())
await Server.RuntimeFileSystem.CreateAsync();
if (!await Server.InstallationFileSystem.CheckExistsAsync())
await Server.InstallationFileSystem.CreateAsync();
// 3. Check if both file systems are mounted
if (!await Server.RuntimeFileSystem.CheckMountedAsync())
await Server.RuntimeFileSystem.MountAsync();
if (!await Server.InstallationFileSystem.CheckMountedAsync())
await Server.InstallationFileSystem.MountAsync();
// 4. Run file system checks
await Server.RuntimeFileSystem.PerformChecksAsync();
await Server.InstallationFileSystem.PerformChecksAsync();
// 5. Create installation
var runtimePath = await Server.RuntimeFileSystem.GetPathAsync();
var installationPath = await Server.InstallationFileSystem.GetPathAsync();
if (await Server.Installation.CheckExistsAsync())
await Server.Installation.DestroyAsync();
await Server.Installation.CreateAsync(runtimePath, installationPath, installData);
if (ExitSubscription == null)
ExitSubscription = await Server.Installation.SubscribeExited(OnInstallationExited);
// 6. Attach console
await Server.Console.AttachInstallationAsync();
// 7. Start installation container
await Server.Installation.StartAsync();
}
private async ValueTask OnInstallationExited(int exitCode)
{
// TODO: Notify the crash handler component of the exit code
await Server.StateMachine.FireAsync(ServerTrigger.Exited);
}
private async Task CompleteAsync()
{
// Plan:
// 1. Handle possible crash
// 2. Remove installation container
// 3. Remove installation file system
// 1. Handle possible crash
// TODO
// 2. Remove installation container
await Server.Installation.DestroyAsync();
// 3. Remove installation file system
await Server.InstallationFileSystem.UnmountAsync();
await Server.InstallationFileSystem.DestroyAsync();
Context.Logger.LogDebug("Completed installation");
}
public async ValueTask DisposeAsync()
{
if (ExitSubscription != null)
await ExitSubscription.DisposeAsync();
}
}

View File

@@ -58,9 +58,7 @@ public class StartupHandler : IServerStateHandler
await Server.Runtime.CreateAsync(hostPath);
if (ExitSubscription == null)
{
ExitSubscription = await Server.Runtime.SubscribeExited(OnRuntimeExited);
}
// 6. Attach console

View File

@@ -61,6 +61,8 @@ public class ServerFactory
handlers.Add(ActivatorUtilities.CreateInstance<OnlineDetectionHandler>(scope.ServiceProvider));
handlers.Add(ActivatorUtilities.CreateInstance<StartupHandler>(scope.ServiceProvider));
handlers.Add(ActivatorUtilities.CreateInstance<ShutdownHandler>(scope.ServiceProvider));
handlers.Add(ActivatorUtilities.CreateInstance<InstallationHandler>(scope.ServiceProvider));
handlers.Add(ActivatorUtilities.CreateInstance<DebugHandler>(scope.ServiceProvider));
// TODO: Add a plugin hook for dynamically resolving components and checking if any is unset