Deed debug handler. Added installation handler. Improved docker console streaming
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -97,7 +97,8 @@ public class Startup
|
||||
Bandwidth = 0,
|
||||
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.Write("Press enter to install server");
|
||||
Console.ReadLine();
|
||||
|
||||
await s.StateMachine.FireAsync(ServerTrigger.Install);
|
||||
|
||||
Console.ReadLine();
|
||||
|
||||
Console.Write("Press enter to start server");
|
||||
Console.ReadLine();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user