diff --git a/MoonlightServers.ApiServer/Http/Controllers/Remote/Servers/RemoteServersController.cs b/MoonlightServers.ApiServer/Http/Controllers/Remote/Servers/RemoteServersController.cs index 97a930e..38ee362 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Remote/Servers/RemoteServersController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Remote/Servers/RemoteServersController.cs @@ -56,7 +56,7 @@ public class RemoteServersController : Controller } if (dockerImage == null) - dockerImage = server.Star.DockerImages.FirstOrDefault(); + dockerImage = server.Star.DockerImages.LastOrDefault(); if (dockerImage == null) { diff --git a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerConfigExtensions.cs b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerConfigExtensions.cs new file mode 100644 index 0000000..4c0ccd5 --- /dev/null +++ b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerConfigExtensions.cs @@ -0,0 +1,258 @@ +using Docker.DotNet.Models; +using Mono.Unix.Native; +using MoonCore.Helpers; +using MoonlightServers.Daemon.Models; + +namespace MoonlightServers.Daemon.Extensions.ServerExtensions; + +public static class ServerConfigExtensions +{ + public static CreateContainerParameters GetRuntimeContainerParameters(this Server server) + { + var parameters = server.GetSharedContainerParameters(); + + #region Security + + parameters.HostConfig.CapDrop = new List() + { + "setpcap", "mknod", "audit_write", "net_raw", "dac_override", + "fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap" + }; + + parameters.HostConfig.ReadonlyRootfs = true; + parameters.HostConfig.SecurityOpt = new List() + { + "no-new-privileges" + }; + + #endregion + + #region Name + + parameters.Name = server.RuntimeContainerName; + parameters.Hostname = server.RuntimeContainerName; + + #endregion + + #region Docker Image + + parameters.Image = server.Configuration.DockerImage; + + #endregion + + #region Environment + + parameters.Env = server.ConstructEnv() + .Select(x => $"{x.Key}={x.Value}") + .ToList(); + + #endregion + + #region Working Dir + + parameters.WorkingDir = "/home/container"; + + #endregion + + #region User + + var userId = Syscall.getuid(); + + if (userId == 0) + { + // We are running as root, so we need to run the container as another user and chown the files when we make changes + parameters.User = $"998:998"; + } + else + { + // We are not running as root, so we start the container as the same user, + // as we are not able to chown the container content to a different user + parameters.User = $"{userId}:{userId}"; + } + + #endregion + + #region Mounts + + parameters.HostConfig.Mounts = new List(); + + parameters.HostConfig.Mounts.Add(new() + { + Source = server.RuntimeVolumePath, + Target = "/home/container", + ReadOnly = false, + Type = "bind" + }); + + #endregion + + #region Port Bindings + + if (true) // TODO: Add network toggle + { + parameters.ExposedPorts = new Dictionary(); + parameters.HostConfig.PortBindings = new Dictionary>(); + + foreach (var allocation in server.Configuration.Allocations) + { + parameters.ExposedPorts.Add($"{allocation.Port}/tcp", new()); + parameters.ExposedPorts.Add($"{allocation.Port}/udp", new()); + + parameters.HostConfig.PortBindings.Add($"{allocation.Port}/tcp", new List + { + new() + { + HostPort = allocation.Port.ToString(), + HostIP = allocation.IpAddress + } + }); + + parameters.HostConfig.PortBindings.Add($"{allocation.Port}/udp", new List + { + new() + { + HostPort = allocation.Port.ToString(), + HostIP = allocation.IpAddress + } + }); + } + } + + #endregion + + return parameters; + } + + public static CreateContainerParameters GetSharedContainerParameters(this Server server) + { + var parameters = new CreateContainerParameters() + { + HostConfig = new() + }; + + #region Input, output & error streams and tty + + parameters.Tty = true; + parameters.AttachStderr = true; + parameters.AttachStdin = true; + parameters.AttachStdout = true; + parameters.OpenStdin = true; + + #endregion + + #region CPU + + parameters.HostConfig.CPUQuota = server.Configuration.Cpu * 1000; + parameters.HostConfig.CPUPeriod = 100000; + parameters.HostConfig.CPUShares = 1024; + + #endregion + + #region Memory & Swap + + var memoryLimit = server.Configuration.Memory; + + // The overhead multiplier gives the container a little bit more memory to prevent crashes + var memoryOverhead = memoryLimit + (memoryLimit * 0.05f); // TODO: Config + + long swapLimit = -1; + + /* + + // If swap is enabled globally and not disabled on this server, set swap + if (!configuration.Limits.DisableSwap && config.Server.EnableSwap) + swapLimit = (long)(memoryOverhead + memoryOverhead * config.Server.SwapMultiplier); + co + */ + + // Finalize limits by converting and updating the host config + parameters.HostConfig.Memory = ByteConverter.FromMegaBytes((long)memoryOverhead, 1000).Bytes; + parameters.HostConfig.MemoryReservation = ByteConverter.FromMegaBytes(memoryLimit, 1000).Bytes; + parameters.HostConfig.MemorySwap = swapLimit == -1 ? swapLimit : ByteConverter.FromMegaBytes(swapLimit, 1000).Bytes; + + #endregion + + #region Misc Limits + + // -- Other limits + parameters.HostConfig.BlkioWeight = 100; + //container.HostConfig.PidsLimit = configuration.Limits.PidsLimit; + parameters.HostConfig.OomKillDisable = true; //!configuration.Limits.EnableOomKill; + + #endregion + + #region DNS + + parameters.HostConfig.DNS = /*config.Docker.DnsServers.Any() ? config.Docker.DnsServers :*/ new List() + { + "1.1.1.1", + "9.9.9.9" + }; + + #endregion + + #region Tmpfs + + parameters.HostConfig.Tmpfs = new Dictionary() + { + { "/tmp", $"rw,exec,nosuid,size=100M" } // TODO: Config + }; + + #endregion + + #region Logging + + parameters.HostConfig.LogConfig = new() + { + Type = "json-file", // We need to use this provider, as the GetLogs endpoint needs it + Config = new Dictionary() + }; + + #endregion + + #region Labels + + parameters.Labels = new Dictionary(); + + parameters.Labels.Add("Software", "Moonlight-Panel"); + parameters.Labels.Add("ServerId", server.Configuration.Id.ToString()); + + #endregion + + return parameters; + } + + public static Dictionary ConstructEnv(this Server server) + { + var config = server.Configuration; + + var result = new Dictionary + { + //TODO: Add timezone, add server ip + { "STARTUP", config.StartupCommand }, + { "SERVER_MEMORY", config.Memory.ToString() } + }; + + if (config.Allocations.Length > 0) + { + var mainAllocation = config.Allocations.First(); + + result.Add("SERVER_IP", mainAllocation.IpAddress); + result.Add("SERVER_PORT", mainAllocation.Port.ToString()); + } + + // Handle allocation variables + var i = 1; + foreach (var allocation in config.Allocations) + { + result.Add($"ML_PORT_{i}", allocation.Port.ToString()); + i++; + } + + // Copy variables as env vars + foreach (var variable in config.Variables) + result.Add(variable.Key, variable.Value); + + return result; + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerConsoleExtensions.cs b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerConsoleExtensions.cs new file mode 100644 index 0000000..7111246 --- /dev/null +++ b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerConsoleExtensions.cs @@ -0,0 +1,61 @@ +using System.Text; +using Docker.DotNet; +using Docker.DotNet.Models; +using MoonlightServers.Daemon.Models; + +namespace MoonlightServers.Daemon.Extensions.ServerExtensions; + +public static class ServerConsoleExtensions +{ + public static async Task Attach(this Server server) + { + var dockerClient = server.ServiceProvider.GetRequiredService(); + + var stream = await dockerClient.Containers.AttachContainerAsync(server.ContainerId, true, + new ContainerAttachParameters() + { + Stderr = true, + Stdin = true, + Stdout = true, + Stream = true + }, + server.Cancellation.Token + ); + + Task.Run(async () => + { + while (!server.Cancellation.Token.IsCancellationRequested) + { + try + { + var buffer = new byte[1024]; + + var readResult = await stream.ReadOutputAsync( + buffer, + 0, + buffer.Length, + server.Cancellation.Token + ); + + if(readResult.EOF) + break; + + var resizedBuffer = new byte[readResult.Count]; + Array.Copy(buffer, resizedBuffer, readResult.Count); + buffer = new byte[buffer.Length]; + + var decodedText = Encoding.UTF8.GetString(resizedBuffer); + await server.Console.WriteToOutput(decodedText); + } + catch (TaskCanceledException) + { + // Ignored + } + catch (Exception e) + { + server.Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", e); + } + } + }); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerCreateExtensions.cs b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerCreateExtensions.cs new file mode 100644 index 0000000..0e5b53c --- /dev/null +++ b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerCreateExtensions.cs @@ -0,0 +1,36 @@ +using Docker.DotNet; +using MoonlightServers.Daemon.Models; +using MoonlightServers.DaemonShared.Enums; + +namespace MoonlightServers.Daemon.Extensions.ServerExtensions; + +public static class ServerCreateExtensions +{ + public static async Task Create(this Server server) + { + var dockerClient = server.ServiceProvider.GetRequiredService(); + + // Ensure image is pulled + await server.EnsureDockerImage(); + + // Ensure runtime storage is created + await server.EnsureRuntimeStorage(); + + // Creating container + await server.NotifyTask(ServerTask.CreatingContainer); + + var parameters = server.GetRuntimeContainerParameters(); + var container = await dockerClient.Containers.CreateContainerAsync(parameters); + + server.ContainerId = container.ID; + + // Attach console + await server.Attach(); + } + + public static async Task ReCreate(this Server server) + { + await server.Destroy(); + await server.Create(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerDestroyExtensions.cs b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerDestroyExtensions.cs new file mode 100644 index 0000000..5e34253 --- /dev/null +++ b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerDestroyExtensions.cs @@ -0,0 +1,44 @@ +using Docker.DotNet; +using MoonlightServers.Daemon.Models; +using MoonlightServers.DaemonShared.Enums; + +namespace MoonlightServers.Daemon.Extensions.ServerExtensions; + +public static class ServerDestroyExtensions +{ + public static async Task Destroy(this Server server) + { + // Note: This only destroys the container, it doesn't delete any data + + var dockerClient = server.ServiceProvider.GetRequiredService(); + + try + { + var container = await dockerClient.Containers.InspectContainerAsync( + server.RuntimeContainerName + ); + + if (container.State.Running) + { + // Stop container when running + + await server.NotifyTask(ServerTask.StoppingContainer); + + await dockerClient.Containers.StopContainerAsync(container.ID, new() + { + WaitBeforeKillSeconds = 30 // TODO: Config + }); + } + + await server.NotifyTask(ServerTask.RemovingContainer); + await dockerClient.Containers.RemoveContainerAsync(container.ID, new()); + } + catch (DockerContainerNotFoundException){} + + // Canceling server sub-tasks and recreating cancellation token + if (!server.Cancellation.IsCancellationRequested) + await server.Cancellation.CancelAsync(); + + server.Cancellation = new(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerImageExtensions.cs b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerImageExtensions.cs new file mode 100644 index 0000000..3de1b68 --- /dev/null +++ b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerImageExtensions.cs @@ -0,0 +1,41 @@ +using Docker.DotNet; +using Docker.DotNet.Models; +using MoonlightServers.Daemon.Models; +using MoonlightServers.DaemonShared.Enums; + +namespace MoonlightServers.Daemon.Extensions.ServerExtensions; + +public static class ServerImageExtensions +{ + public static async Task EnsureDockerImage(this Server server) + { + await server.NotifyTask(ServerTask.PullingDockerImage); + + var dockerClient = server.ServiceProvider.GetRequiredService(); + + await dockerClient.Images.CreateImageAsync(new() + { + FromImage = server.Configuration.DockerImage + }, + new AuthConfig(), + new Progress(async message => + { + if (message.Progress == null) + return; + + var percentage = message.Progress.Total > 0 + ? Math.Round((float)message.Progress.Current / message.Progress.Total * 100f, 2) + : 0d; + + server.Logger.LogInformation( + "Docker Image: [{id}] {status} - {percent}", + message.ID, + message.Status, + percentage + ); + + //await UpdateProgress(server, serviceProvider, percentage); + }) + ); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerMetaExtensions.cs b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerMetaExtensions.cs new file mode 100644 index 0000000..1b2e764 --- /dev/null +++ b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerMetaExtensions.cs @@ -0,0 +1,12 @@ +using MoonlightServers.Daemon.Models; +using MoonlightServers.DaemonShared.Enums; + +namespace MoonlightServers.Daemon.Extensions.ServerExtensions; + +public static class ServerMetaExtensions +{ + public static async Task NotifyTask(this Server server, ServerTask task) + { + server.Logger.LogInformation("Task: {task}", task); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerStartExtensions.cs b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerStartExtensions.cs new file mode 100644 index 0000000..9e558ed --- /dev/null +++ b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerStartExtensions.cs @@ -0,0 +1,18 @@ +using Docker.DotNet; +using MoonlightServers.Daemon.Models; +using MoonlightServers.DaemonShared.Enums; + +namespace MoonlightServers.Daemon.Extensions.ServerExtensions; + +public static class ServerStartExtensions +{ + public static async Task StateMachineHandler_Start(this Server server) + { + await server.ReCreate(); + + await server.NotifyTask(ServerTask.StartingContainer); + var dockerClient = server.ServiceProvider.GetRequiredService(); + + await dockerClient.Containers.StartContainerAsync(server.ContainerId, new()); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerStorageExtensions.cs b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerStorageExtensions.cs new file mode 100644 index 0000000..6d29078 --- /dev/null +++ b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerStorageExtensions.cs @@ -0,0 +1,20 @@ +using MoonlightServers.Daemon.Models; +using MoonlightServers.DaemonShared.Enums; + +namespace MoonlightServers.Daemon.Extensions.ServerExtensions; + +public static class ServerStorageExtensions +{ + public static async Task EnsureRuntimeStorage(this Server server) + { + // TODO: Add virtual disk + await server.NotifyTask(ServerTask.CreatingStorage); + + // Create volume if missing + if (!Directory.Exists(server.RuntimeVolumePath)) + Directory.CreateDirectory(server.RuntimeVolumePath); + + // TODO: Chown + //Syscall.chown() + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Helpers/ServerActionHelper.cs b/MoonlightServers.Daemon/Helpers/ServerActionHelper.cs index f5b48e8..e96de4a 100644 --- a/MoonlightServers.Daemon/Helpers/ServerActionHelper.cs +++ b/MoonlightServers.Daemon/Helpers/ServerActionHelper.cs @@ -3,6 +3,7 @@ using Docker.DotNet.Models; using MoonCore.Helpers; using MoonlightServers.Daemon.Configuration; using MoonlightServers.Daemon.Models; +using MoonlightServers.DaemonShared.Enums; namespace MoonlightServers.Daemon.Helpers; diff --git a/MoonlightServers.Daemon/Helpers/ServerConfigurationHelper.cs b/MoonlightServers.Daemon/Helpers/ServerConfigurationHelper.cs index c353789..03c4db0 100644 --- a/MoonlightServers.Daemon/Helpers/ServerConfigurationHelper.cs +++ b/MoonlightServers.Daemon/Helpers/ServerConfigurationHelper.cs @@ -1,4 +1,5 @@ using Docker.DotNet.Models; +using Mono.Unix.Native; using MoonCore.Helpers; using MoonlightServers.Daemon.Configuration; using MoonlightServers.Daemon.Models.Cache; @@ -42,8 +43,20 @@ public static class ServerConfigurationHelper parameters.WorkingDir = "/home/container"; // - User - //TODO: use config - parameters.User = $"998:998"; + var userId = Syscall.getuid(); + + if (userId == 0) + { + // We are running as root, so we need to run the container as another user and chown the files when we make changes + parameters.User = $"998:998"; + } + else + { + // We are not running as root, so we start the container as the same user, + // as we are not able to chown the container content to a different user + parameters.User = $"{userId}:{userId}"; + } + // -- Mounts parameters.HostConfig.Mounts = new List(); diff --git a/MoonlightServers.Daemon/Helpers/StateMachine.cs b/MoonlightServers.Daemon/Helpers/StateMachine.cs index d3bfbcc..ccdb7ef 100644 --- a/MoonlightServers.Daemon/Helpers/StateMachine.cs +++ b/MoonlightServers.Daemon/Helpers/StateMachine.cs @@ -14,7 +14,7 @@ public class StateMachine where T : struct, Enum CurrentState = initialState; } - public void AddTransition(T from, T to, T? onError, Func fun) + public void AddTransition(T from, T to, T? onError, Func? fun) { Transitions.Add(new() { @@ -26,6 +26,7 @@ public class StateMachine where T : struct, Enum } public void AddTransition(T from, T to, Func fun) => AddTransition(from, to, null, fun); + public void AddTransition(T from, T to) => AddTransition(from, to, null, null); public async Task TransitionTo(T to) { @@ -41,7 +42,11 @@ public class StateMachine where T : struct, Enum try { - transition.OnTransitioning.Invoke().Wait(); + if(transition.OnTransitioning != null) + transition.OnTransitioning.Invoke().Wait(); + + // Successfully executed => update state + CurrentState = transition.To; } catch (Exception e) { @@ -64,6 +69,6 @@ public class StateMachine where T : struct, Enum public T From { get; set; } public T To { get; set; } public T? OnError { get; set; } - public Func OnTransitioning { get; set; } + public Func? OnTransitioning { get; set; } } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs index 80dda6b..be7fc8a 100644 --- a/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs +++ b/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs @@ -2,6 +2,8 @@ using Microsoft.AspNetCore.Mvc; using MoonCore.Exceptions; using MoonlightServers.Daemon.Models; using MoonlightServers.Daemon.Services; +using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers; +using MoonlightServers.DaemonShared.Enums; namespace MoonlightServers.Daemon.Http.Controllers.Servers; @@ -16,7 +18,21 @@ public class ServersController : Controller ServerService = serverService; } - [HttpPost("{serverId}/start")] + [HttpGet("{serverId:int}/status")] + public async Task GetStatus(int serverId) + { + var server = ServerService.GetServer(serverId); + + if (server == null) + throw new HttpApiException("No server with this id found", 404); + + return new ServerStatusResponse() + { + State = server.State + }; + } + + [HttpPost("{serverId:int}/start")] public async Task Start(int serverId) { var server = ServerService.GetServer(serverId); diff --git a/MoonlightServers.Daemon/Models/Server.cs b/MoonlightServers.Daemon/Models/Server.cs index 3081ee5..e80346f 100644 --- a/MoonlightServers.Daemon/Models/Server.cs +++ b/MoonlightServers.Daemon/Models/Server.cs @@ -1,12 +1,62 @@ +using MoonCore.Helpers; +using MoonlightServers.Daemon.Configuration; using MoonlightServers.Daemon.Helpers; using MoonlightServers.Daemon.Models.Cache; +using MoonlightServers.DaemonShared.Enums; namespace MoonlightServers.Daemon.Models; public class Server { + public ILogger Logger { get; set; } + public ServerConsole Console { get; set; } + public IServiceProvider ServiceProvider { get; set; } public ServerState State => StateMachine.CurrentState; public StateMachine StateMachine { get; set; } public ServerConfiguration Configuration { get; set; } public string? ContainerId { get; set; } + + // This can be used to stop streaming when the server gets destroyed or something + public CancellationTokenSource Cancellation { get; set; } + + #region Small helpers + + public string RuntimeContainerName => $"moonlight-runtime-{Configuration.Id}"; + public string InstallContainerName => $"moonlight-install-{Configuration.Id}"; + + public string RuntimeVolumePath + { + get + { + var appConfig = ServiceProvider.GetRequiredService(); + + var localPath = PathBuilder.Dir( + appConfig.Storage.Volumes, + Configuration.Id.ToString() + ); + + var absolutePath = Path.GetFullPath(localPath); + + return absolutePath; + } + } + + public string InstallVolumePath + { + get + { + var appConfig = ServiceProvider.GetRequiredService(); + + var localPath = PathBuilder.Dir( + appConfig.Storage.Install, + Configuration.Id.ToString() + ); + + var absolutePath = Path.GetFullPath(localPath); + + return absolutePath; + } + } + + #endregion } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Models/ServerConsole.cs b/MoonlightServers.Daemon/Models/ServerConsole.cs new file mode 100644 index 0000000..8e5e928 --- /dev/null +++ b/MoonlightServers.Daemon/Models/ServerConsole.cs @@ -0,0 +1,41 @@ +namespace MoonlightServers.Daemon.Models; + +public class ServerConsole +{ + public event Func OnOutput; + public event Func OnInput; + + public string[] Messages => GetMessages(); + private readonly Queue MessageCache = new(); + private const int MaxMessagesInCache = 250; //TODO: Config + + public async Task WriteToOutput(string content) + { + lock (MessageCache) + { + MessageCache.Enqueue(content); + + if (MessageCache.Count > MaxMessagesInCache) + MessageCache.Dequeue(); + } + + if (OnOutput != null) + { + await OnOutput + .Invoke(content) + .ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + } + + public async Task WriteToInput(string content) + { + if (OnInput != null) + await OnInput.Invoke(content); + } + + private string[] GetMessages() + { + lock (MessageCache) + return MessageCache.ToArray(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj index d055152..dd90d61 100644 --- a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj +++ b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj @@ -10,11 +10,11 @@ + - @@ -22,4 +22,14 @@ + + <_ContentIncludedByDefault Remove="storage\volumes\11\banned-ips.json" /> + <_ContentIncludedByDefault Remove="storage\volumes\11\banned-players.json" /> + <_ContentIncludedByDefault Remove="storage\volumes\11\ops.json" /> + <_ContentIncludedByDefault Remove="storage\volumes\11\plugins\spark\config.json" /> + <_ContentIncludedByDefault Remove="storage\volumes\11\usercache.json" /> + <_ContentIncludedByDefault Remove="storage\volumes\11\version_history.json" /> + <_ContentIncludedByDefault Remove="storage\volumes\11\whitelist.json" /> + + diff --git a/MoonlightServers.Daemon/Services/ApplicationStateService.cs b/MoonlightServers.Daemon/Services/ApplicationStateService.cs index e3fe587..75c5add 100644 --- a/MoonlightServers.Daemon/Services/ApplicationStateService.cs +++ b/MoonlightServers.Daemon/Services/ApplicationStateService.cs @@ -33,6 +33,10 @@ public class ApplicationStateService : IHostedLifecycleService public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; - public Task StoppingAsync(CancellationToken cancellationToken) - => Task.CompletedTask; + public async Task StoppingAsync(CancellationToken cancellationToken) + { + Logger.LogInformation("Stopping services"); + + await ServerService.Stop(); + } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Services/ServerService.cs b/MoonlightServers.Daemon/Services/ServerService.cs index a9898a6..5137f39 100644 --- a/MoonlightServers.Daemon/Services/ServerService.cs +++ b/MoonlightServers.Daemon/Services/ServerService.cs @@ -1,9 +1,11 @@ +using Docker.DotNet; +using Docker.DotNet.Models; using MoonCore.Attributes; -using MoonCore.Helpers; using MoonCore.Models; -using MoonlightServers.Daemon.Helpers; +using MoonlightServers.Daemon.Extensions.ServerExtensions; using MoonlightServers.Daemon.Models; using MoonlightServers.Daemon.Models.Cache; +using MoonlightServers.DaemonShared.Enums; using MoonlightServers.DaemonShared.PanelSide.Http.Responses; namespace MoonlightServers.Daemon.Services; @@ -16,6 +18,7 @@ public class ServerService private readonly RemoteService RemoteService; private readonly IServiceProvider ServiceProvider; private bool IsInitialized = false; + private CancellationTokenSource Cancellation = new(); public ServerService(RemoteService remoteService, ILogger logger, IServiceProvider serviceProvider) { @@ -68,30 +71,106 @@ public class ServerService foreach (var configuration in configurations) await InitializeServer(configuration); + + // Attach to docker events + await AttachToDockerEvents(); } + public async Task Stop() + { + Server[] servers; + + lock (Servers) + servers = Servers.ToArray(); + + Logger.LogTrace("Canceling server sub tasks"); + + foreach (var server in servers) + await server.Cancellation.CancelAsync(); + + Logger.LogTrace("Canceling own tasks"); + await Cancellation.CancelAsync(); + } + + private async Task AttachToDockerEvents() + { + var dockerClient = ServiceProvider.GetRequiredService(); + + Task.Run(async () => + { + // This lets the event monitor restart + while (!Cancellation.Token.IsCancellationRequested) + { + try + { + Logger.LogTrace("Attached to docker events"); + + await dockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(), + new Progress(async message => + { + if(message.Action != "die") + return; + + Server? server; + + lock (Servers) + server = Servers.FirstOrDefault(x => x.ContainerId == message.ID); + + // TODO: Maybe implement a lookup for containers which id isn't set in the cache + + if(server == null) + return; + + await server.StateMachine.TransitionTo(ServerState.Offline); + }), Cancellation.Token); + } + catch(TaskCanceledException){} // Can be ignored + catch (Exception e) + { + Logger.LogError("An unhandled error occured while attaching to docker events: {e}", e); + } + } + }); + } + private async Task InitializeServer(ServerConfiguration configuration) { - Logger.LogInformation("Initializing server '{id}'", configuration.Id); + Logger.LogTrace("Initializing server '{id}'", configuration.Id); + var loggerFactory = ServiceProvider.GetRequiredService(); + var server = new Server() { Configuration = configuration, - StateMachine = new(ServerState.Offline) + StateMachine = new(ServerState.Offline), + ServiceProvider = ServiceProvider, + Logger = loggerFactory.CreateLogger($"Server {configuration.Id}"), + Console = new(), + Cancellation = new() }; server.StateMachine.OnError += (state, exception) => { - Logger.LogError("Server {id} encountered an unhandled error while transitioning to {state}: {e}", - server.Configuration.Id, + server.Logger.LogError("Encountered an unhandled error while transitioning to {state}: {e}", state, exception ); }; + server.StateMachine.OnTransitioned += state => + { + server.Logger.LogInformation("State: {state}", state); + return Task.CompletedTask; + }; + server.StateMachine.AddTransition(ServerState.Offline, ServerState.Starting, ServerState.Offline, async () => - await ServerActionHelper.Start(server, ServiceProvider) + await server.StateMachineHandler_Start() ); + + server.StateMachine.AddTransition(ServerState.Starting, ServerState.Offline); + server.StateMachine.AddTransition(ServerState.Online, ServerState.Offline); + server.StateMachine.AddTransition(ServerState.Stopping, ServerState.Offline); + server.StateMachine.AddTransition(ServerState.Installing, ServerState.Offline); lock (Servers) Servers.Add(server); diff --git a/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Servers/ServerStatusResponse.cs b/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Servers/ServerStatusResponse.cs new file mode 100644 index 0000000..c546752 --- /dev/null +++ b/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Servers/ServerStatusResponse.cs @@ -0,0 +1,8 @@ +using MoonlightServers.DaemonShared.Enums; + +namespace MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers; + +public class ServerStatusResponse +{ + public ServerState State { get; set; } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Models/ServerState.cs b/MoonlightServers.DaemonShared/Enums/ServerState.cs similarity index 71% rename from MoonlightServers.Daemon/Models/ServerState.cs rename to MoonlightServers.DaemonShared/Enums/ServerState.cs index 0646952..f2a3072 100644 --- a/MoonlightServers.Daemon/Models/ServerState.cs +++ b/MoonlightServers.DaemonShared/Enums/ServerState.cs @@ -1,4 +1,4 @@ -namespace MoonlightServers.Daemon.Models; +namespace MoonlightServers.DaemonShared.Enums; public enum ServerState { diff --git a/MoonlightServers.Daemon/Models/ServerTask.cs b/MoonlightServers.DaemonShared/Enums/ServerTask.cs similarity index 59% rename from MoonlightServers.Daemon/Models/ServerTask.cs rename to MoonlightServers.DaemonShared/Enums/ServerTask.cs index ba3ffe0..5939098 100644 --- a/MoonlightServers.Daemon/Models/ServerTask.cs +++ b/MoonlightServers.DaemonShared/Enums/ServerTask.cs @@ -1,4 +1,4 @@ -namespace MoonlightServers.Daemon.Models; +namespace MoonlightServers.DaemonShared.Enums; public enum ServerTask { @@ -7,5 +7,6 @@ public enum ServerTask PullingDockerImage = 2, RemovingContainer = 3, CreatingContainer = 4, - StartingContainer = 5 + StartingContainer = 5, + StoppingContainer = 6 } \ No newline at end of file