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

View File

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

View File

@@ -97,7 +97,8 @@ public class Startup
Bandwidth = 0, Bandwidth = 0,
Variables = new Dictionary<string, string>() Variables = new Dictionary<string, string>()
{ {
{ "SERVER_JARFILE", "server.jar" } { "SERVER_JARFILE", "server.jar" },
{ "MINECRAFT_VERSION", "latest" },
} }
}; };
@@ -115,6 +116,13 @@ public class Startup
Console.WriteLine(transition.Destination); Console.WriteLine(transition.Destination);
}); });
Console.Write("Press enter to install server");
Console.ReadLine();
await s.StateMachine.FireAsync(ServerTrigger.Install);
Console.ReadLine();
Console.Write("Press enter to start server"); Console.Write("Press enter to start server");
Console.ReadLine(); Console.ReadLine();