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 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user