diff --git a/MoonlightServers.Daemon/ServerSystem/Docker/DockerConsole.cs b/MoonlightServers.Daemon/ServerSystem/Docker/DockerConsole.cs index d3d8bd2..39acf5a 100644 --- a/MoonlightServers.Daemon/ServerSystem/Docker/DockerConsole.cs +++ b/MoonlightServers.Daemon/ServerSystem/Docker/DockerConsole.cs @@ -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(); } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Handlers/DebugHandler.cs b/MoonlightServers.Daemon/ServerSystem/Handlers/DebugHandler.cs new file mode 100644 index 0000000..cb32684 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Handlers/DebugHandler.cs @@ -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.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(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Handlers/InstallationHandler.cs b/MoonlightServers.Daemon/ServerSystem/Handlers/InstallationHandler.cs new file mode 100644 index 0000000..8c38b8d --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Handlers/InstallationHandler.cs @@ -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.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(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Handlers/StartupHandler.cs b/MoonlightServers.Daemon/ServerSystem/Handlers/StartupHandler.cs index 2097a80..7e151ad 100644 --- a/MoonlightServers.Daemon/ServerSystem/Handlers/StartupHandler.cs +++ b/MoonlightServers.Daemon/ServerSystem/Handlers/StartupHandler.cs @@ -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 diff --git a/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs b/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs index 17be4d6..6810b78 100644 --- a/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs +++ b/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs @@ -61,6 +61,8 @@ public class ServerFactory handlers.Add(ActivatorUtilities.CreateInstance(scope.ServiceProvider)); handlers.Add(ActivatorUtilities.CreateInstance(scope.ServiceProvider)); handlers.Add(ActivatorUtilities.CreateInstance(scope.ServiceProvider)); + handlers.Add(ActivatorUtilities.CreateInstance(scope.ServiceProvider)); + handlers.Add(ActivatorUtilities.CreateInstance(scope.ServiceProvider)); // TODO: Add a plugin hook for dynamically resolving components and checking if any is unset diff --git a/MoonlightServers.Daemon/Startup.cs b/MoonlightServers.Daemon/Startup.cs index 657ab6f..253a111 100644 --- a/MoonlightServers.Daemon/Startup.cs +++ b/MoonlightServers.Daemon/Startup.cs @@ -97,7 +97,8 @@ public class Startup Bandwidth = 0, Variables = new Dictionary() { - { "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();