diff --git a/.gitignore b/.gitignore index 01ac454..5ad737b 100644 --- a/.gitignore +++ b/.gitignore @@ -428,4 +428,7 @@ core.min.css # Build script for nuget packages finalPackages/ -nupkgs/ \ No newline at end of file +nupkgs/ + +# Local daemon tests +**/data/volumes/** \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Extensions/ServerStateExtensions.cs b/MoonlightServers.ApiServer/Extensions/ServerStateExtensions.cs index c92a050..71b000f 100644 --- a/MoonlightServers.ApiServer/Extensions/ServerStateExtensions.cs +++ b/MoonlightServers.ApiServer/Extensions/ServerStateExtensions.cs @@ -1,20 +1,21 @@ using MoonlightServers.DaemonShared.Enums; using MoonlightServers.Shared.Enums; +using ServerState = MoonlightServers.Shared.Enums.ServerState; namespace MoonlightServers.ApiServer.Extensions; public static class ServerStateExtensions { - public static ServerPowerState ToServerPowerState(this ServerState state) + public static ServerState ToServerPowerState(this DaemonShared.Enums.ServerState state) { return state switch { - ServerState.Installing => ServerPowerState.Installing, - ServerState.Stopping => ServerPowerState.Stopping, - ServerState.Online => ServerPowerState.Online, - ServerState.Starting => ServerPowerState.Starting, - ServerState.Offline => ServerPowerState.Offline, - _ => ServerPowerState.Offline + DaemonShared.Enums.ServerState.Installing => ServerState.Installing, + DaemonShared.Enums.ServerState.Stopping => ServerState.Stopping, + DaemonShared.Enums.ServerState.Online => ServerState.Online, + DaemonShared.Enums.ServerState.Starting => ServerState.Starting, + DaemonShared.Enums.ServerState.Offline => ServerState.Offline, + _ => ServerState.Offline }; } } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Http/Controllers/Users/ServersController.cs b/MoonlightServers.ApiServer/Http/Controllers/Users/ServersController.cs index 18f126f..1d1f3fe 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Users/ServersController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Users/ServersController.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MoonCore.Extended.PermFilter; @@ -28,7 +29,7 @@ public class ServersController : Controller } [HttpGet] - [RequirePermission("meta.authenticated")] + [Authorize] public async Task> GetAll([FromQuery] int page, [FromQuery] int pageSize) { var userIdClaim = User.Claims.First(x => x.Type == "userId"); @@ -69,7 +70,7 @@ public class ServersController : Controller } [HttpGet("{serverId:int}")] - [RequirePermission("meta.authenticated")] + [Authorize] public async Task Get([FromRoute] int serverId) { var server = await GetServerWithPermCheck( @@ -96,7 +97,7 @@ public class ServersController : Controller } [HttpGet("{serverId:int}/status")] - [RequirePermission("meta.authenticated")] + [Authorize] public async Task GetStatus([FromRoute] int serverId) { var server = await GetServerWithPermCheck(serverId); @@ -111,7 +112,7 @@ public class ServersController : Controller return new ServerStatusResponse() { - PowerState = data.State.ToServerPowerState() + State = data.State.ToServerPowerState() }; } catch (HttpRequestException e) @@ -120,9 +121,9 @@ public class ServersController : Controller } } - [HttpGet("{serverId:int}/console")] - [RequirePermission("meta.authenticated")] - public async Task GetConsole([FromRoute] int serverId) + [HttpGet("{serverId:int}/ws")] + [Authorize] + public async Task GetWebSocket([FromRoute] int serverId) { var server = await GetServerWithPermCheck(serverId); @@ -130,7 +131,7 @@ public class ServersController : Controller var accessToken = NodeService.CreateAccessToken(server.Node, parameters => { - parameters.Add("type", "console"); + parameters.Add("type", "websocket"); parameters.Add("serverId", server.Id); }, TimeSpan.FromMinutes(10)); @@ -141,9 +142,9 @@ public class ServersController : Controller else url += "http://"; - url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/api/servers/console"; + url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/api/servers/ws"; - return new ServerConsoleResponse() + return new ServerWebSocketResponse() { Target = url, AccessToken = accessToken @@ -151,7 +152,7 @@ public class ServersController : Controller } [HttpGet("{serverId:int}/logs")] - [RequirePermission("meta.authenticated")] + [Authorize] public async Task GetLogs([FromRoute] int serverId) { var server = await GetServerWithPermCheck(serverId); @@ -174,6 +175,42 @@ public class ServersController : Controller throw new HttpApiException("Unable to access the node the server is running on", 502); } } + + [HttpPost("{serverId:int}/start")] + [Authorize] + public async Task Start([FromRoute] int serverId) + { + var server = await GetServerWithPermCheck(serverId); + + using var apiClient = await NodeService.CreateApiClient(server.Node); + + try + { + await apiClient.Post($"api/servers/{server.Id}/start"); + } + catch (HttpRequestException e) + { + throw new HttpApiException("Unable to access the node the server is running on", 502); + } + } + + [HttpPost("{serverId:int}/stop")] + [Authorize] + public async Task Stop([FromRoute] int serverId) + { + var server = await GetServerWithPermCheck(serverId); + + using var apiClient = await NodeService.CreateApiClient(server.Node); + + try + { + await apiClient.Post($"api/servers/{server.Id}/stop"); + } + catch (HttpRequestException e) + { + throw new HttpApiException("Unable to access the node the server is running on", 502); + } + } private async Task GetServerWithPermCheck(int serverId, Func, IQueryable>? queryModifier = null) diff --git a/MoonlightServers.ApiServer/Services/StarImportExportService.cs b/MoonlightServers.ApiServer/Services/StarImportExportService.cs index eeeac97..c8b375b 100644 --- a/MoonlightServers.ApiServer/Services/StarImportExportService.cs +++ b/MoonlightServers.ApiServer/Services/StarImportExportService.cs @@ -372,11 +372,16 @@ public class StarImportExportService foreach (var pConfigFind in pConfig.Value.GetProperty("find").EnumerateObject()) { - pc.Entries.Add(new ParseConfiguration.ParseConfigurationEntry() + var entry = new ParseConfiguration.ParseConfigurationEntry() { Key = pConfigFind.Name, Value = pConfigFind.Value.GetString() ?? "Parse error" - }); + }; + + // Fix up special variables + entry.Value = entry.Value.Replace("server.allocations.default.port", "SERVER_PORT"); + + pc.Entries.Add(entry); } resultPcs.Add(pc); diff --git a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerConsoleExtensions.cs b/MoonlightServers.Daemon/Abstractions/Server.Console.cs similarity index 54% rename from MoonlightServers.Daemon/Extensions/ServerExtensions/ServerConsoleExtensions.cs rename to MoonlightServers.Daemon/Abstractions/Server.Console.cs index 764ed07..28a6bbe 100644 --- a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerConsoleExtensions.cs +++ b/MoonlightServers.Daemon/Abstractions/Server.Console.cs @@ -1,17 +1,16 @@ using System.Text; using Docker.DotNet; using Docker.DotNet.Models; -using MoonlightServers.Daemon.Models; -namespace MoonlightServers.Daemon.Extensions.ServerExtensions; +namespace MoonlightServers.Daemon.Abstractions; -public static class ServerConsoleExtensions +public partial class Server { - public static async Task Attach(this Server server) + private async Task AttachConsole(string containerId) { - var dockerClient = server.ServiceProvider.GetRequiredService(); + var dockerClient = ServiceProvider.GetRequiredService(); - var stream = await dockerClient.Containers.AttachContainerAsync(server.ContainerId, true, + var stream = await dockerClient.Containers.AttachContainerAsync(containerId, true, new ContainerAttachParameters() { Stderr = true, @@ -19,12 +18,13 @@ public static class ServerConsoleExtensions Stdout = true, Stream = true }, - server.Cancellation.Token + Cancellation.Token ); + // Reading Task.Run(async () => { - while (!server.Cancellation.Token.IsCancellationRequested) + while (!Cancellation.Token.IsCancellationRequested) { try { @@ -34,10 +34,10 @@ public static class ServerConsoleExtensions buffer, 0, buffer.Length, - server.Cancellation.Token + Cancellation.Token ); - - if(readResult.EOF) + + if (readResult.EOF) break; var resizedBuffer = new byte[readResult.Count]; @@ -45,7 +45,7 @@ public static class ServerConsoleExtensions buffer = new byte[buffer.Length]; var decodedText = Encoding.UTF8.GetString(resizedBuffer); - await server.Console.WriteToOutput(decodedText); + await Console.WriteToOutput(decodedText); } catch (TaskCanceledException) { @@ -57,9 +57,24 @@ public static class ServerConsoleExtensions } catch (Exception e) { - server.Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", e); + Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", e); } } }); + + // Writing + Console.OnInput += async content => + { + var contentBuffer = Encoding.UTF8.GetBytes(content); + await stream.WriteAsync(contentBuffer, 0, contentBuffer.Length, Cancellation.Token); + }; } + + private async Task LogToConsole(string message) + { + await Console.WriteToOutput($"\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m {message}\n\r"); + } + + public Task GetConsoleMessages() + => Task.FromResult(Console.Messages); } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Abstractions/Server.Create.cs b/MoonlightServers.Daemon/Abstractions/Server.Create.cs new file mode 100644 index 0000000..050a179 --- /dev/null +++ b/MoonlightServers.Daemon/Abstractions/Server.Create.cs @@ -0,0 +1,37 @@ +using Docker.DotNet; +using MoonlightServers.Daemon.Extensions; +using MoonlightServers.Daemon.Services; + +namespace MoonlightServers.Daemon.Abstractions; + +public partial class Server +{ + private async Task Create() + { + var dockerImageService = ServiceProvider.GetRequiredService(); + + // We call an external service for that, as we want to have a central management point of images + // for analytics and automatic deletion + await dockerImageService.Ensure(Configuration.DockerImage, async message => { await LogToConsole(message); }); + + var hostPath = await EnsureRuntimeVolume(); + + await LogToConsole("Creating container"); + + var dockerClient = ServiceProvider.GetRequiredService(); + + var parameters = Configuration.ToRuntimeCreateParameters( + hostPath: hostPath, + containerName: RuntimeContainerName + ); + + var container = await dockerClient.Containers.CreateContainerAsync(parameters); + RuntimeContainerId = container.ID; + } + + private async Task ReCreate() + { + await Destroy(); + await Create(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Abstractions/Server.Destroy.cs b/MoonlightServers.Daemon/Abstractions/Server.Destroy.cs new file mode 100644 index 0000000..a0b1c53 --- /dev/null +++ b/MoonlightServers.Daemon/Abstractions/Server.Destroy.cs @@ -0,0 +1,50 @@ +using Docker.DotNet; + +namespace MoonlightServers.Daemon.Abstractions; + +public partial class Server +{ + private async Task Destroy() + { + // Note: This only destroys the container, it doesn't delete any data + + var dockerClient = ServiceProvider.GetRequiredService(); + + try + { + var container = await dockerClient.Containers.InspectContainerAsync( + RuntimeContainerName + ); + + if (container.State.Running) + { + // Stop container when running + + await LogToConsole("Stopping container"); + + await dockerClient.Containers.StopContainerAsync(container.ID, new() + { + WaitBeforeKillSeconds = 30 // TODO: Config + }); + } + + await LogToConsole("Removing container"); + await dockerClient.Containers.RemoveContainerAsync(container.ID, new()); + } + catch (DockerContainerNotFoundException){} + + // Canceling server tasks & listeners + await CancelTasks(); + + // and recreating cancellation token + Cancellation = new(); + } + + public async Task CancelTasks() + { + // Note: This will keep the docker container running, it will just cancel the server cancellation token + + if (!Cancellation.IsCancellationRequested) + await Cancellation.CancelAsync(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs b/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs new file mode 100644 index 0000000..05dab76 --- /dev/null +++ b/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs @@ -0,0 +1,166 @@ +using System.Text.RegularExpressions; +using Docker.DotNet.Models; +using MoonlightServers.Daemon.Enums; +using Stateless; + +namespace MoonlightServers.Daemon.Abstractions; + +public partial class Server +{ + // We are expecting a list of running containers, as we don't wont to inspect every possible container just to check if it exists. + // If none are provided, we skip the checks. Use this overload if you are creating a new server which didn't exist before + public async Task Initialize(IList? runningContainers = null) + { + if (runningContainers != null) + { + var reAttachSuccessful = await ReAttach(runningContainers); + + // If we weren't able to reattach with the current running containers, we initialize the + // state machine as offline + if(!reAttachSuccessful) + await InitializeStateMachine(ServerState.Offline); + } + else + await InitializeStateMachine(ServerState.Offline); + + // And at last we initialize all events, so we can react to certain state changes and outputs. + // We need to do this regardless if the server was reattached or not, as it hasn't been initialized yet + await InitializeEvents(); + } + + private Task InitializeStateMachine(ServerState initialState) + { + StateMachine = new StateMachine(initialState); + + // Setup transitions + StateMachine.Configure(ServerState.Offline) + .Permit(ServerTrigger.Start, ServerState.Starting) + .Permit(ServerTrigger.Reinstall, ServerState.Installing); + + StateMachine.Configure(ServerState.Starting) + .Permit(ServerTrigger.NotifyContainerDied, ServerState.Offline) + .Permit(ServerTrigger.NotifyOnline, ServerState.Online) + .Permit(ServerTrigger.Stop, ServerState.Stopping) + .OnEntryAsync(InternalStart); + + StateMachine.Configure(ServerState.Online) + .Permit(ServerTrigger.NotifyContainerDied, ServerState.Offline) + .Permit(ServerTrigger.Stop, ServerState.Stopping); + + StateMachine.Configure(ServerState.Stopping) + .Permit(ServerTrigger.NotifyContainerDied, ServerState.Offline) + .Permit(ServerTrigger.Kill, ServerState.Offline) + .OnEntryAsync(InternalStop); + + StateMachine.Configure(ServerState.Installing) + .Permit(ServerTrigger.NotifyInstallContainerDied, ServerState.Offline); + + return Task.CompletedTask; + } + + private Task InitializeEvents() + { + Console.OnOutput += async content => + { + if (StateMachine.State == ServerState.Starting) + { + if (Regex.Matches(content, Configuration.OnlineDetection).Count > 0) + await StateMachine.FireAsync(ServerTrigger.NotifyOnline); + } + }; + + StateMachine.OnTransitioned(transition => + { + Logger.LogInformation( + "{source} => {destination} ({trigger})", + transition.Source, + transition.Destination, + transition.Trigger + ); + }); + + StateMachine.OnTransitionCompleted(transition => + { + Logger.LogInformation("State: {state}", transition.Destination); + }); + + // Proxy the events so outside subscribes can react to it + StateMachine.OnTransitionCompletedAsync(async transition => + { + if (OnStateChanged != null) + { + await OnStateChanged(transition.Destination); + } + }); + + Console.OnOutput += (async message => + { + if (OnConsoleOutput != null) + { + await OnConsoleOutput(message); + } + }); + + return Task.CompletedTask; + } + + #region Reattaching & reattach strategies + + private async Task ReAttach(IList runningContainers) + { + // Docker container names are starting with a / when returned in the docker container list api endpoint, + // so we trim it from the name when searching + + var existingRuntimeContainer = runningContainers.FirstOrDefault( + x => x.Names.Any(y => y.TrimStart('/') == RuntimeContainerName) + ); + + if (existingRuntimeContainer != null) + { + await ReAttachToRuntime(existingRuntimeContainer); + return true; + } + + var existingInstallContainer = runningContainers.FirstOrDefault( + x => x.Names.Any(y => y.TrimStart('/') == InstallationContainerName) + ); + + if (existingInstallContainer != null) + { + await ReAttachToInstallation(existingInstallContainer); + return true; + } + + return false; + } + + private async Task ReAttachToRuntime(ContainerListResponse runtimeContainer) + { + if (runtimeContainer.State == "running") + { + RuntimeContainerId = runtimeContainer.ID; + + await InitializeStateMachine(ServerState.Online); + + await AttachConsole(runtimeContainer.ID); + } + else + await InitializeStateMachine(ServerState.Offline); + } + + private async Task ReAttachToInstallation(ContainerListResponse installationContainer) + { + if (installationContainer.State == "running") + { + InstallationContainerId = installationContainer.ID; + + await InitializeStateMachine(ServerState.Installing); + + await AttachConsole(installationContainer.ID); + } + else + await InitializeStateMachine(ServerState.Offline); + } + + #endregion +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Abstractions/Server.Notify.cs b/MoonlightServers.Daemon/Abstractions/Server.Notify.cs new file mode 100644 index 0000000..f8f1e43 --- /dev/null +++ b/MoonlightServers.Daemon/Abstractions/Server.Notify.cs @@ -0,0 +1,8 @@ +using MoonlightServers.Daemon.Enums; + +namespace MoonlightServers.Daemon.Abstractions; + +public partial class Server +{ + public async Task NotifyContainerDied() => await StateMachine.FireAsync(ServerTrigger.NotifyContainerDied); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Abstractions/Server.Start.cs b/MoonlightServers.Daemon/Abstractions/Server.Start.cs new file mode 100644 index 0000000..afb98a6 --- /dev/null +++ b/MoonlightServers.Daemon/Abstractions/Server.Start.cs @@ -0,0 +1,23 @@ +using Docker.DotNet; +using MoonlightServers.Daemon.Enums; + +namespace MoonlightServers.Daemon.Abstractions; + +public partial class Server +{ + public async Task Start() => await StateMachine.FireAsync(ServerTrigger.Start); + + private async Task InternalStart() + { + await ReCreate(); + + await LogToConsole("Starting container"); + + // We can disable the null check for the runtime container id, as we set it by calling ReCreate(); + await AttachConsole(RuntimeContainerId!); + + // Start container + var dockerClient = ServiceProvider.GetRequiredService(); + await dockerClient.Containers.StartContainerAsync(RuntimeContainerId, new()); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Abstractions/Server.Stop.cs b/MoonlightServers.Daemon/Abstractions/Server.Stop.cs new file mode 100644 index 0000000..ae3956f --- /dev/null +++ b/MoonlightServers.Daemon/Abstractions/Server.Stop.cs @@ -0,0 +1,13 @@ +using MoonlightServers.Daemon.Enums; + +namespace MoonlightServers.Daemon.Abstractions; + +public partial class Server +{ + public async Task Stop() => await StateMachine.FireAsync(ServerTrigger.Stop); + + private async Task InternalStop() + { + await Console.WriteToInput($"{Configuration.StopCommand}\n\r"); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Abstractions/Server.Storage.cs b/MoonlightServers.Daemon/Abstractions/Server.Storage.cs new file mode 100644 index 0000000..f562706 --- /dev/null +++ b/MoonlightServers.Daemon/Abstractions/Server.Storage.cs @@ -0,0 +1,55 @@ +using MoonCore.Helpers; +using MoonlightServers.Daemon.Configuration; + +namespace MoonlightServers.Daemon.Abstractions; + +public partial class Server +{ + private async Task EnsureRuntimeVolume() + { + var appConfiguration = ServiceProvider.GetRequiredService(); + + var hostPath = PathBuilder.Dir( + appConfiguration.Storage.Volumes, + Configuration.Id.ToString() + ); + + await LogToConsole("Creating storage"); + + // TODO: Virtual disk + + // Create volume if missing + if (!Directory.Exists(hostPath)) + Directory.CreateDirectory(hostPath); + + if (hostPath.StartsWith("/")) + return hostPath; + else + return PathBuilder.JoinPaths(Directory.GetCurrentDirectory(), hostPath); + + return hostPath; + } + + private async Task EnsureInstallationVolume() + { + var appConfiguration = ServiceProvider.GetRequiredService(); + + var hostPath = PathBuilder.Dir( + appConfiguration.Storage.Volumes, + Configuration.Id.ToString() + ); + + await LogToConsole("Creating storage"); + + // Create volume if missing + if (!Directory.Exists(hostPath)) + Directory.CreateDirectory(hostPath); + + if (hostPath.StartsWith("/")) + return hostPath; + else + return PathBuilder.JoinPaths(Directory.GetCurrentDirectory(), hostPath); + + return hostPath; + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Abstractions/Server.cs b/MoonlightServers.Daemon/Abstractions/Server.cs new file mode 100644 index 0000000..9c6eeb3 --- /dev/null +++ b/MoonlightServers.Daemon/Abstractions/Server.cs @@ -0,0 +1,52 @@ +using Docker.DotNet.Models; +using MoonlightServers.Daemon.Enums; +using MoonlightServers.Daemon.Models; +using MoonlightServers.Daemon.Models.Cache; +using Stateless; + +namespace MoonlightServers.Daemon.Abstractions; + +public partial class Server +{ + // Exposed configuration/state values + public int Id => Configuration.Id; + public ServerState State => StateMachine.State; + + // Exposed container names and ids + public string RuntimeContainerName { get; private set; } + public string? RuntimeContainerId { get; private set; } + + public string InstallationContainerName { get; private set; } + public string? InstallationContainerId { get; private set; } + + // Events + public event Func OnStateChanged; + public event Func OnConsoleOutput; + + // Private stuff + + private readonly ILogger Logger; + private readonly IServiceProvider ServiceProvider; + private readonly ServerConsole Console; + + private StateMachine StateMachine; + private ServerConfiguration Configuration; + private CancellationTokenSource Cancellation; + + public Server( + ILogger logger, + IServiceProvider serviceProvider, + ServerConfiguration configuration + ) + { + Logger = logger; + ServiceProvider = serviceProvider; + Configuration = configuration; + + Console = new(); + Cancellation = new(); + + RuntimeContainerName = $"moonlight-runtime-{Configuration.Id}"; + InstallationContainerName = $"moonlight-install-{Configuration.Id}"; + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Enums/ServerState.cs b/MoonlightServers.Daemon/Enums/ServerState.cs new file mode 100644 index 0000000..b2093b5 --- /dev/null +++ b/MoonlightServers.Daemon/Enums/ServerState.cs @@ -0,0 +1,10 @@ +namespace MoonlightServers.Daemon.Enums; + +public enum ServerState +{ + Offline = 0, + Starting = 1, + Online = 2, + Stopping = 3, + Installing = 4 +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Enums/ServerTrigger.cs b/MoonlightServers.Daemon/Enums/ServerTrigger.cs new file mode 100644 index 0000000..9668299 --- /dev/null +++ b/MoonlightServers.Daemon/Enums/ServerTrigger.cs @@ -0,0 +1,13 @@ +namespace MoonlightServers.Daemon.Enums; + +public enum ServerTrigger +{ + Start = 0, + Stop = 1, + Restart = 2, + Kill = 3, + Reinstall = 4, + NotifyOnline = 5, + NotifyContainerDied = 6, + NotifyInstallContainerDied = 7 +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerConfigExtensions.cs b/MoonlightServers.Daemon/Extensions/ServerConfigurationExtensions.cs similarity index 79% rename from MoonlightServers.Daemon/Extensions/ServerExtensions/ServerConfigExtensions.cs rename to MoonlightServers.Daemon/Extensions/ServerConfigurationExtensions.cs index 4c0ccd5..1dfda0f 100644 --- a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerConfigExtensions.cs +++ b/MoonlightServers.Daemon/Extensions/ServerConfigurationExtensions.cs @@ -1,15 +1,15 @@ using Docker.DotNet.Models; using Mono.Unix.Native; using MoonCore.Helpers; -using MoonlightServers.Daemon.Models; +using MoonlightServers.Daemon.Models.Cache; -namespace MoonlightServers.Daemon.Extensions.ServerExtensions; +namespace MoonlightServers.Daemon.Extensions; -public static class ServerConfigExtensions +public static class ServerConfigurationExtensions { - public static CreateContainerParameters GetRuntimeContainerParameters(this Server server) + public static CreateContainerParameters ToRuntimeCreateParameters(this ServerConfiguration configuration, string hostPath, string containerName) { - var parameters = server.GetSharedContainerParameters(); + var parameters = configuration.ToSharedCreateParameters(); #region Security @@ -29,20 +29,20 @@ public static class ServerConfigExtensions #region Name - parameters.Name = server.RuntimeContainerName; - parameters.Hostname = server.RuntimeContainerName; + parameters.Name = containerName; + parameters.Hostname = containerName; #endregion #region Docker Image - parameters.Image = server.Configuration.DockerImage; + parameters.Image = configuration.DockerImage; #endregion #region Environment - parameters.Env = server.ConstructEnv() + parameters.Env = configuration.ToEnvironmentVariables() .Select(x => $"{x.Key}={x.Value}") .ToList(); @@ -56,7 +56,7 @@ public static class ServerConfigExtensions #region User - var userId = Syscall.getuid(); + var userId = Syscall.getuid(); // TODO: Extract to external service? if (userId == 0) { @@ -78,7 +78,7 @@ public static class ServerConfigExtensions parameters.HostConfig.Mounts.Add(new() { - Source = server.RuntimeVolumePath, + Source = hostPath, Target = "/home/container", ReadOnly = false, Type = "bind" @@ -93,7 +93,7 @@ public static class ServerConfigExtensions parameters.ExposedPorts = new Dictionary(); parameters.HostConfig.PortBindings = new Dictionary>(); - foreach (var allocation in server.Configuration.Allocations) + foreach (var allocation in configuration.Allocations) { parameters.ExposedPorts.Add($"{allocation.Port}/tcp", new()); parameters.ExposedPorts.Add($"{allocation.Port}/udp", new()); @@ -122,8 +122,8 @@ public static class ServerConfigExtensions return parameters; } - - public static CreateContainerParameters GetSharedContainerParameters(this Server server) + + private static CreateContainerParameters ToSharedCreateParameters(this ServerConfiguration configuration) { var parameters = new CreateContainerParameters() { @@ -142,7 +142,7 @@ public static class ServerConfigExtensions #region CPU - parameters.HostConfig.CPUQuota = server.Configuration.Cpu * 1000; + parameters.HostConfig.CPUQuota = configuration.Cpu * 1000; parameters.HostConfig.CPUPeriod = 100000; parameters.HostConfig.CPUShares = 1024; @@ -150,7 +150,7 @@ public static class ServerConfigExtensions #region Memory & Swap - var memoryLimit = server.Configuration.Memory; + var memoryLimit = configuration.Memory; // The overhead multiplier gives the container a little bit more memory to prevent crashes var memoryOverhead = memoryLimit + (memoryLimit * 0.05f); // TODO: Config @@ -183,6 +183,8 @@ public static class ServerConfigExtensions #region DNS + // TODO: Read hosts dns settings? + parameters.HostConfig.DNS = /*config.Docker.DnsServers.Any() ? config.Docker.DnsServers :*/ new List() { "1.1.1.1", @@ -215,27 +217,25 @@ public static class ServerConfigExtensions parameters.Labels = new Dictionary(); parameters.Labels.Add("Software", "Moonlight-Panel"); - parameters.Labels.Add("ServerId", server.Configuration.Id.ToString()); + parameters.Labels.Add("ServerId", configuration.Id.ToString()); #endregion return parameters; } - public static Dictionary ConstructEnv(this Server server) + public static Dictionary ToEnvironmentVariables(this ServerConfiguration configuration) { - var config = server.Configuration; - var result = new Dictionary { //TODO: Add timezone, add server ip - { "STARTUP", config.StartupCommand }, - { "SERVER_MEMORY", config.Memory.ToString() } + { "STARTUP", configuration.StartupCommand }, + { "SERVER_MEMORY", configuration.Memory.ToString() } }; - if (config.Allocations.Length > 0) + if (configuration.Allocations.Length > 0) { - var mainAllocation = config.Allocations.First(); + var mainAllocation = configuration.Allocations.First(); result.Add("SERVER_IP", mainAllocation.IpAddress); result.Add("SERVER_PORT", mainAllocation.Port.ToString()); @@ -243,14 +243,14 @@ public static class ServerConfigExtensions // Handle allocation variables var i = 1; - foreach (var allocation in config.Allocations) + foreach (var allocation in configuration.Allocations) { result.Add($"ML_PORT_{i}", allocation.Port.ToString()); i++; } // Copy variables as env vars - foreach (var variable in config.Variables) + foreach (var variable in configuration.Variables) result.Add(variable.Key, variable.Value); return result; diff --git a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerCreateExtensions.cs b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerCreateExtensions.cs deleted file mode 100644 index 0e5b53c..0000000 --- a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerCreateExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index 5e34253..0000000 --- a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerDestroyExtensions.cs +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 3de1b68..0000000 --- a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerImageExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index f5ab185..0000000 --- a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerMetaExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -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); - await server.InvokeTaskAdded(task.ToString()); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerStartExtensions.cs b/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerStartExtensions.cs deleted file mode 100644 index 9e558ed..0000000 --- a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerStartExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 6d29078..0000000 --- a/MoonlightServers.Daemon/Extensions/ServerExtensions/ServerStorageExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index e96de4a..0000000 --- a/MoonlightServers.Daemon/Helpers/ServerActionHelper.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Docker.DotNet; -using Docker.DotNet.Models; -using MoonCore.Helpers; -using MoonlightServers.Daemon.Configuration; -using MoonlightServers.Daemon.Models; -using MoonlightServers.DaemonShared.Enums; - -namespace MoonlightServers.Daemon.Helpers; - -public class ServerActionHelper -{ - public static async Task Start(Server server, IServiceProvider serviceProvider) - { - await EnsureStorage(server, serviceProvider); - await EnsureDockerImage(server, serviceProvider); - await CreateRuntimeContainer(server, serviceProvider); - await StartRuntimeContainer(server, serviceProvider); - } - - private static async Task EnsureStorage(Server server, IServiceProvider serviceProvider) - { - await NotifyTask(server, serviceProvider, ServerTask.CreatingStorage); - - // Build paths - var configuration = serviceProvider.GetRequiredService(); - - var volumePath = PathBuilder.Dir( - configuration.Storage.Volumes, - server.Configuration.Id.ToString() - ); - - // Create volume if missing - if (!Directory.Exists(volumePath)) - Directory.CreateDirectory(volumePath); - - // TODO: Virtual disk - } - - private static async Task EnsureDockerImage(Server server, IServiceProvider serviceProvider) - { - await NotifyTask(server, serviceProvider, ServerTask.PullingDockerImage); - - var dockerClient = serviceProvider.GetRequiredService(); - - await dockerClient.Images.CreateImageAsync(new() - { - FromImage = server.Configuration.DockerImage - }, - new AuthConfig(), - new Progress(async message => - { - //var percentage = (int)(message.Progress.Current / message.Progress.Total); - //await UpdateProgress(server, serviceProvider, percentage); - }) - ); - } - - private static async Task CreateRuntimeContainer(Server server, IServiceProvider serviceProvider) - { - var dockerClient = serviceProvider.GetRequiredService(); - - try - { - var existingContainer = await dockerClient.Containers.InspectContainerAsync( - $"moonlight-runtime-{server.Configuration.Id}" - ); - - await NotifyTask(server, serviceProvider, ServerTask.RemovingContainer); - - if (existingContainer.State.Running) // Stop already running container - { - await dockerClient.Containers.StopContainerAsync(existingContainer.ID, new() - { - WaitBeforeKillSeconds = 30 // TODO: Config - }); - } - - await dockerClient.Containers.RemoveContainerAsync(existingContainer.ID, new()); - } - catch (DockerContainerNotFoundException) - { - } - - await NotifyTask(server, serviceProvider, ServerTask.CreatingContainer); - - // Create a new container - var parameters = new CreateContainerParameters(); - - ServerConfigurationHelper.ApplyRuntimeOptions( - parameters, - server.Configuration, - serviceProvider.GetRequiredService() - ); - - var container = await dockerClient.Containers.CreateContainerAsync(parameters); - server.ContainerId = container.ID; - } - - private static async Task StartRuntimeContainer(Server server, IServiceProvider serviceProvider) - { - await NotifyTask(server, serviceProvider, ServerTask.StartingContainer); - - var dockerClient = serviceProvider.GetRequiredService(); - - await dockerClient.Containers.StartContainerAsync(server.ContainerId, new()); - } - - private static async Task NotifyTask(Server server, IServiceProvider serviceProvider, ServerTask task) - { - var loggerFactory = serviceProvider.GetRequiredService(); - var logger = loggerFactory.CreateLogger($"Server {server.Configuration.Id}"); - - logger.LogInformation("Task: {task}", task); - } - - private static async Task UpdateProgress(Server server, IServiceProvider serviceProvider, int progress) - { - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Helpers/ServerConsoleMonitor.cs b/MoonlightServers.Daemon/Helpers/ServerConsoleMonitor.cs deleted file mode 100644 index 150d9c6..0000000 --- a/MoonlightServers.Daemon/Helpers/ServerConsoleMonitor.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.AspNetCore.SignalR; -using MoonlightServers.Daemon.Models; -using MoonlightServers.DaemonShared.Enums; - -namespace MoonlightServers.Daemon.Helpers; - -public class ServerConsoleMonitor -{ - private readonly Server Server; - private readonly IHubClients Clients; - - public ServerConsoleMonitor(Server server, IHubClients clients) - { - Server = server; - Clients = clients; - } - - public void Initialize() - { - Server.StateMachine.OnTransitioned += OnPowerStateChanged; - Server.OnTaskAdded += OnTaskNotify; - } - - public void Destroy() - { - Server.StateMachine.OnTransitioned -= OnPowerStateChanged; - } - - private async Task OnTaskNotify(string task) - { - await Clients.Group($"server-{Server.Configuration.Id}").SendAsync( - "TaskNotify", - task - ); - } - - private async Task OnPowerStateChanged(ServerState serverState) - { - await Clients.Group($"server-{Server.Configuration.Id}").SendAsync( - "PowerStateChanged", - serverState.ToString() - ); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Helpers/ServerConsoleConnection.cs b/MoonlightServers.Daemon/Helpers/ServerWebSocketConnection.cs similarity index 80% rename from MoonlightServers.Daemon/Helpers/ServerConsoleConnection.cs rename to MoonlightServers.Daemon/Helpers/ServerWebSocketConnection.cs index 16e1574..08900d7 100644 --- a/MoonlightServers.Daemon/Helpers/ServerConsoleConnection.cs +++ b/MoonlightServers.Daemon/Helpers/ServerWebSocketConnection.cs @@ -1,28 +1,29 @@ using Microsoft.AspNetCore.SignalR; +using MoonlightServers.Daemon.Abstractions; +using MoonlightServers.Daemon.Enums; using MoonlightServers.Daemon.Http.Hubs; using MoonlightServers.Daemon.Models; using MoonlightServers.Daemon.Services; -using MoonlightServers.DaemonShared.Enums; namespace MoonlightServers.Daemon.Helpers; -public class ServerConsoleConnection +public class ServerWebSocketConnection { private readonly ServerService ServerService; - private readonly ILogger Logger; + private readonly ILogger Logger; private readonly AccessTokenHelper AccessTokenHelper; - private readonly IHubContext HubContext; + private readonly IHubContext HubContext; private int ServerId = -1; private Server Server; private bool IsInitialized = false; private string ConnectionId; - public ServerConsoleConnection( + public ServerWebSocketConnection( ServerService serverService, - ILogger logger, + ILogger logger, AccessTokenHelper accessTokenHelper, - IHubContext hubContext + IHubContext hubContext ) { ServerService = serverService; @@ -64,7 +65,7 @@ public class ServerConsoleConnection // Validate access token type var type = accessData["type"].GetString()!; - if (type != "console") + if (type != "websocket") { Logger.LogDebug("Received invalid access token: Invalid type '{type}'", type); @@ -78,7 +79,7 @@ public class ServerConsoleConnection var serverId = accessData["serverId"].GetInt32(); - // Check that the access token isn't or another server + // Check that the access token isn't for another server if (ServerId != -1 && ServerId == serverId) { Logger.LogDebug("Received invalid access token: Server id not valid for this session. Current server id: {serverId}", ServerId); @@ -117,31 +118,27 @@ public class ServerConsoleConnection IsInitialized = true; // Setup event handlers - Server.StateMachine.OnTransitioned += HandlePowerStateChange; - Server.OnTaskAdded += HandleTaskAdded; - Server.Console.OnOutput += HandleConsoleOutput; + Server.OnConsoleOutput += HandleConsoleOutput; + Server.OnStateChanged += HandleStateChange; Logger.LogTrace("Authenticated and initialized server console connection '{id}'", context.ConnectionId); } public Task Destroy(HubCallerContext context) { - Server.StateMachine.OnTransitioned -= HandlePowerStateChange; - Server.OnTaskAdded -= HandleTaskAdded; - Logger.LogTrace("Destroyed server console connection '{id}'", context.ConnectionId); + Server.OnConsoleOutput -= HandleConsoleOutput; + Server.OnStateChanged -= HandleStateChange; + return Task.CompletedTask; } #region Event Handlers - - private async Task HandlePowerStateChange(ServerState serverState) - => await HubContext.Clients.Client(ConnectionId).SendAsync("PowerStateChanged", serverState.ToString()); - private async Task HandleTaskAdded(string task) - => await HubContext.Clients.Client(ConnectionId).SendAsync("TaskNotify", task); - + private async Task HandleStateChange(ServerState state) + => await HubContext.Clients.Client(ConnectionId).SendAsync("StateChanged", state.ToString()); + private async Task HandleConsoleOutput(string line) => await HubContext.Clients.Client(ConnectionId).SendAsync("ConsoleOutput", line); diff --git a/MoonlightServers.Daemon/Helpers/StateMachine.cs b/MoonlightServers.Daemon/Helpers/StateMachine.cs deleted file mode 100644 index ccdb7ef..0000000 --- a/MoonlightServers.Daemon/Helpers/StateMachine.cs +++ /dev/null @@ -1,74 +0,0 @@ -namespace MoonlightServers.Daemon.Helpers; - -public class StateMachine where T : struct, Enum -{ - private readonly List Transitions = new(); - private readonly object Lock = new(); - - public T CurrentState { get; private set; } - public event Func OnTransitioned; - public event Action OnError; - - public StateMachine(T initialState) - { - CurrentState = initialState; - } - - public void AddTransition(T from, T to, T? onError, Func? fun) - { - Transitions.Add(new() - { - From = from, - To = to, - OnError = onError, - OnTransitioning = fun - }); - } - - 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) - { - lock (Lock) - { - var transition = Transitions.FirstOrDefault(x => - x.From.Equals(CurrentState) && - x.To.Equals(to) - ); - - if (transition == null) - throw new InvalidOperationException("Unable to transition to the request state: No transition found"); - - try - { - if(transition.OnTransitioning != null) - transition.OnTransitioning.Invoke().Wait(); - - // Successfully executed => update state - CurrentState = transition.To; - } - catch (Exception e) - { - if(OnError != null) - OnError.Invoke(to, e); - - if (transition.OnError.HasValue) - CurrentState = transition.OnError.Value; - else - throw new AggregateException("An error occured while transitioning to a state", e); - } - } - - if(OnTransitioned != null) - await OnTransitioned.Invoke(CurrentState); - } - - public class StateMachineTransition - { - public T From { get; set; } - public T To { get; set; } - public T? OnError { get; set; } - public Func? OnTransitioning { get; set; } - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs new file mode 100644 index 0000000..014b08f --- /dev/null +++ b/MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Mvc; +using MoonCore.Exceptions; +using MoonlightServers.Daemon.Enums; +using MoonlightServers.Daemon.Services; + +namespace MoonlightServers.Daemon.Http.Controllers.Servers; + +[ApiController] +[Route("api/servers")] +public class ServerPowerController : Controller +{ + private readonly ServerService ServerService; + + public ServerPowerController(ServerService serverService) + { + ServerService = serverService; + } + + [HttpPost("{serverId:int}/start")] + public async Task Start(int serverId, [FromQuery] bool runAsync = true) + { + var server = ServerService.GetServer(serverId); + + if (server == null) + throw new HttpApiException("No server with this id found", 404); + + await server.Start(); + } + + [HttpPost("{serverId:int}/stop")] + public async Task Stop(int serverId, [FromQuery] bool runAsync = true) + { + var server = ServerService.GetServer(serverId); + + if (server == null) + throw new HttpApiException("No server with this id found", 404); + + await server.Stop(); + } +} \ 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 1c7366b..c79201b 100644 --- a/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs +++ b/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs @@ -1,6 +1,5 @@ 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; @@ -28,7 +27,7 @@ public class ServersController : Controller return new ServerStatusResponse() { - State = server.State + State = (ServerState)server.State }; } @@ -42,18 +41,7 @@ public class ServersController : Controller return new ServerLogsResponse() { - Messages = server.Console.Messages + Messages = await server.GetConsoleMessages() }; } - - [HttpPost("{serverId:int}/start")] - public async Task Start(int serverId) - { - var server = ServerService.GetServer(serverId); - - if (server == null) - throw new HttpApiException("No server with this id found", 404); - - await server.StateMachine.TransitionTo(ServerState.Starting); - } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Hubs/ServerConsoleHub.cs b/MoonlightServers.Daemon/Http/Hubs/ServerWebSocketHub.cs similarity index 52% rename from MoonlightServers.Daemon/Http/Hubs/ServerConsoleHub.cs rename to MoonlightServers.Daemon/Http/Hubs/ServerWebSocketHub.cs index 8343ac9..ec57757 100644 --- a/MoonlightServers.Daemon/Http/Hubs/ServerConsoleHub.cs +++ b/MoonlightServers.Daemon/Http/Hubs/ServerWebSocketHub.cs @@ -1,29 +1,26 @@ using Microsoft.AspNetCore.SignalR; -using MoonlightServers.Daemon.Helpers; -using MoonlightServers.Daemon.Models; using MoonlightServers.Daemon.Services; -using MoonlightServers.DaemonShared.Enums; namespace MoonlightServers.Daemon.Http.Hubs; -public class ServerConsoleHub : Hub +public class ServerWebSocketHub : Hub { - private readonly ILogger Logger; - private readonly ServerConsoleService ConsoleService; + private readonly ILogger Logger; + private readonly ServerWebSocketService WebSocketService; - public ServerConsoleHub(ILogger logger, ServerConsoleService consoleService) + public ServerWebSocketHub(ILogger logger, ServerWebSocketService webSocketService) { Logger = logger; - ConsoleService = consoleService; + WebSocketService = webSocketService; } #region Connection Handlers public override async Task OnConnectedAsync() - => await ConsoleService.InitializeClient(Context); + => await WebSocketService.InitializeClient(Context); public override async Task OnDisconnectedAsync(Exception? exception) - => await ConsoleService.DestroyClient(Context); + => await WebSocketService.DestroyClient(Context); #endregion @@ -34,7 +31,7 @@ public class ServerConsoleHub : Hub { try { - await ConsoleService.AuthenticateClient(Context, accessToken); + await WebSocketService.AuthenticateClient(Context, accessToken); } catch (Exception e) { diff --git a/MoonlightServers.Daemon/Models/Server.cs b/MoonlightServers.Daemon/Models/Server.cs deleted file mode 100644 index ce9814e..0000000 --- a/MoonlightServers.Daemon/Models/Server.cs +++ /dev/null @@ -1,75 +0,0 @@ -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; } - public event Func OnTaskAdded; - - // 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 - - #region Event invokers - - public async Task InvokeTaskAdded(string task) - { - if(OnTaskAdded == null) - return; - - await OnTaskAdded.Invoke(task).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); - } - - #endregion -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj index f782a5c..5fbb39f 100644 --- a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj +++ b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj @@ -9,9 +9,10 @@ - - + + + @@ -24,13 +25,19 @@ - <_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 deleted file mode 100644 index 75c5add..0000000 --- a/MoonlightServers.Daemon/Services/ApplicationStateService.cs +++ /dev/null @@ -1,42 +0,0 @@ -using MoonCore.Attributes; - -namespace MoonlightServers.Daemon.Services; - -[Singleton] -public class ApplicationStateService : IHostedLifecycleService -{ - private readonly ServerService ServerService; - private readonly ILogger Logger; - - public ApplicationStateService(ServerService serverService, ILogger logger) - { - ServerService = serverService; - Logger = logger; - } - - public Task StartAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - public Task StopAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - public async Task StartedAsync(CancellationToken cancellationToken) - { - Logger.LogInformation("Performing initialization"); - - await ServerService.Initialize(); - } - - public Task StartingAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - public Task StoppedAsync(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/DockerImageService.cs b/MoonlightServers.Daemon/Services/DockerImageService.cs new file mode 100644 index 0000000..46b95e0 --- /dev/null +++ b/MoonlightServers.Daemon/Services/DockerImageService.cs @@ -0,0 +1,40 @@ +using Docker.DotNet; +using Docker.DotNet.Models; +using MoonCore.Attributes; + +namespace MoonlightServers.Daemon.Services; + +[Singleton] +public class DockerImageService +{ + private readonly DockerClient DockerClient; + private readonly ILogger Logger; + + public DockerImageService(DockerClient dockerClient, ILogger logger) + { + DockerClient = dockerClient; + Logger = logger; + } + + public async Task Ensure(string name, Action? onProgressUpdated) + { + await DockerClient.Images.CreateImageAsync(new() + { + FromImage = name + }, + new AuthConfig(), // TODO: Config for custom registries + new Progress(async message => + { + if (message.Progress == null) + return; + + var line = $"[{message.ID}] {message.ProgressMessage}"; + + Logger.LogInformation("{line}", line); + + if(onProgressUpdated != null) + onProgressUpdated.Invoke(line); + }) + ); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Services/ServerService.cs b/MoonlightServers.Daemon/Services/ServerService.cs index 5137f39..73e59a6 100644 --- a/MoonlightServers.Daemon/Services/ServerService.cs +++ b/MoonlightServers.Daemon/Services/ServerService.cs @@ -2,29 +2,30 @@ using Docker.DotNet; using Docker.DotNet.Models; using MoonCore.Attributes; using MoonCore.Models; -using MoonlightServers.Daemon.Extensions.ServerExtensions; -using MoonlightServers.Daemon.Models; +using MoonlightServers.Daemon.Abstractions; using MoonlightServers.Daemon.Models.Cache; -using MoonlightServers.DaemonShared.Enums; using MoonlightServers.DaemonShared.PanelSide.Http.Responses; namespace MoonlightServers.Daemon.Services; [Singleton] -public class ServerService +public class ServerService : IHostedLifecycleService { private readonly List Servers = new(); private readonly ILogger Logger; private readonly RemoteService RemoteService; private readonly IServiceProvider ServiceProvider; + private readonly ILoggerFactory LoggerFactory; private bool IsInitialized = false; private CancellationTokenSource Cancellation = new(); - public ServerService(RemoteService remoteService, ILogger logger, IServiceProvider serviceProvider) + public ServerService(RemoteService remoteService, ILogger logger, IServiceProvider serviceProvider, + ILoggerFactory loggerFactory) { RemoteService = remoteService; Logger = logger; ServiceProvider = serviceProvider; + LoggerFactory = loggerFactory; } public async Task Initialize() //TODO: Add initialize call from panel @@ -39,7 +40,7 @@ public class ServerService // Loading models and converting them Logger.LogInformation("Fetching servers from panel"); - var apiClient = await RemoteService.CreateHttpClient(); + using var apiClient = await RemoteService.CreateHttpClient(); var servers = await PagedData.All(async (page, pageSize) => await apiClient.GetJson>( @@ -69,9 +70,8 @@ public class ServerService Logger.LogInformation("Initializing {count} servers", servers.Length); - foreach (var configuration in configurations) - await InitializeServer(configuration); - + await InitializeServerRange(configurations); // TODO: Initialize them multi threaded (maybe) + // Attach to docker events await AttachToDockerEvents(); } @@ -83,11 +83,13 @@ public class ServerService lock (Servers) servers = Servers.ToArray(); - Logger.LogTrace("Canceling server sub tasks"); - + // + Logger.LogTrace("Canceling server tasks"); + foreach (var server in servers) - await server.Cancellation.CancelAsync(); - + await server.CancelTasks(); + + // Logger.LogTrace("Canceling own tasks"); await Cancellation.CancelAsync(); } @@ -104,27 +106,29 @@ public class ServerService try { Logger.LogTrace("Attached to docker events"); - + await dockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(), new Progress(async message => { - if(message.Action != "die") + if (message.Action != "die") return; Server? server; lock (Servers) - server = Servers.FirstOrDefault(x => x.ContainerId == message.ID); + server = Servers.FirstOrDefault(x => x.RuntimeContainerId == message.ID); // TODO: Maybe implement a lookup for containers which id isn't set in the cache - - if(server == null) + + if (server == null) return; - await server.StateMachine.TransitionTo(ServerState.Offline); + await server.NotifyContainerDied(); }), Cancellation.Token); } - catch(TaskCanceledException){} // Can be ignored + catch (TaskCanceledException) + { + } // Can be ignored catch (Exception e) { Logger.LogError("An unhandled error occured while attaching to docker events: {e}", e); @@ -132,45 +136,48 @@ public class ServerService } }); } - - private async Task InitializeServer(ServerConfiguration configuration) + + private async Task InitializeServerRange(ServerConfiguration[] serverConfigurations) { - Logger.LogTrace("Initializing server '{id}'", configuration.Id); + var dockerClient = ServiceProvider.GetRequiredService(); - var loggerFactory = ServiceProvider.GetRequiredService(); - - var server = new Server() + var existingContainers = await dockerClient.Containers.ListContainersAsync(new() { - Configuration = configuration, - StateMachine = new(ServerState.Offline), - ServiceProvider = ServiceProvider, - Logger = loggerFactory.CreateLogger($"Server {configuration.Id}"), - Console = new(), - Cancellation = new() - }; + All = true, + Limit = null, + Filters = new Dictionary>() + { + { + "label", + new Dictionary() + { + { + "Software=Moonlight-Panel", + true + } + } + } + } + }); - server.StateMachine.OnError += (state, exception) => - { - server.Logger.LogError("Encountered an unhandled error while transitioning to {state}: {e}", - state, - exception - ); - }; + foreach (var configuration in serverConfigurations) + await InitializeServer(configuration, existingContainers); + } - server.StateMachine.OnTransitioned += state => - { - server.Logger.LogInformation("State: {state}", state); - return Task.CompletedTask; - }; + private async Task InitializeServer( + ServerConfiguration serverConfiguration, + IList existingContainers + ) + { + Logger.LogTrace("Initializing server '{id}'", serverConfiguration.Id); - server.StateMachine.AddTransition(ServerState.Offline, ServerState.Starting, ServerState.Offline, async () => - await server.StateMachineHandler_Start() + var server = new Server( + LoggerFactory.CreateLogger($"Server {serverConfiguration.Id}"), + ServiceProvider, + serverConfiguration ); - - 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); + + await server.Initialize(existingContainers); lock (Servers) Servers.Add(server); @@ -179,6 +186,46 @@ public class ServerService public Server? GetServer(int id) { lock (Servers) - return Servers.FirstOrDefault(x => x.Configuration.Id == id); + return Servers.FirstOrDefault(x => x.Id == id); } + + #region Lifecycle + + public Task StartAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public async Task StartedAsync(CancellationToken cancellationToken) + { + try + { + await Initialize(); + } + catch (Exception e) + { + Logger.LogCritical("Unable to initialize servers. Is the panel online? Error: {e}", e); + } + } + + public Task StartingAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public Task StoppedAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public async Task StoppingAsync(CancellationToken cancellationToken) + { + try + { + await Stop(); + } + catch (Exception e) + { + Logger.LogCritical("Unable to stop server handling: {e}", e); + } + } + + #endregion } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Services/ServerConsoleService.cs b/MoonlightServers.Daemon/Services/ServerWebSocketService.cs similarity index 74% rename from MoonlightServers.Daemon/Services/ServerConsoleService.cs rename to MoonlightServers.Daemon/Services/ServerWebSocketService.cs index 9318a8d..f27436f 100644 --- a/MoonlightServers.Daemon/Services/ServerConsoleService.cs +++ b/MoonlightServers.Daemon/Services/ServerWebSocketService.cs @@ -6,15 +6,15 @@ using MoonlightServers.Daemon.Http.Hubs; namespace MoonlightServers.Daemon.Services; [Singleton] -public class ServerConsoleService +public class ServerWebSocketService { - private readonly ILogger Logger; + private readonly ILogger Logger; private readonly IServiceProvider ServiceProvider; - private readonly Dictionary Connections = new(); + private readonly Dictionary Connections = new(); - public ServerConsoleService( - ILogger logger, + public ServerWebSocketService( + ILogger logger, IServiceProvider serviceProvider ) { @@ -24,11 +24,11 @@ public class ServerConsoleService public async Task InitializeClient(HubCallerContext context) { - var connection = new ServerConsoleConnection( + var connection = new ServerWebSocketConnection( ServiceProvider.GetRequiredService(), - ServiceProvider.GetRequiredService>(), + ServiceProvider.GetRequiredService>(), ServiceProvider.GetRequiredService(), - ServiceProvider.GetRequiredService>() + ServiceProvider.GetRequiredService>() ); lock (Connections) @@ -39,7 +39,7 @@ public class ServerConsoleService public async Task AuthenticateClient(HubCallerContext context, string accessToken) { - ServerConsoleConnection? connection; + ServerWebSocketConnection? connection; lock (Connections) connection = Connections.GetValueOrDefault(context.ConnectionId); @@ -52,7 +52,7 @@ public class ServerConsoleService public async Task DestroyClient(HubCallerContext context) { - ServerConsoleConnection? connection; + ServerWebSocketConnection? connection; lock (Connections) connection = Connections.GetValueOrDefault(context.ConnectionId); diff --git a/MoonlightServers.Daemon/Startup.cs b/MoonlightServers.Daemon/Startup.cs index 1da9b70..38378fb 100644 --- a/MoonlightServers.Daemon/Startup.cs +++ b/MoonlightServers.Daemon/Startup.cs @@ -249,8 +249,8 @@ public class Startup private Task RegisterServers() { - WebApplicationBuilder.Services.AddHostedService( - sp => sp.GetRequiredService() + WebApplicationBuilder.Services.AddHostedService( + sp => sp.GetRequiredService() ); return Task.CompletedTask; @@ -263,7 +263,7 @@ public class Startup #endregion - #region Maps + #region Hubs private Task RegisterSignalR() { @@ -273,7 +273,7 @@ public class Startup private Task MapHubs() { - WebApplication.MapHub("api/servers/console"); + WebApplication.MapHub("api/servers/ws"); return Task.CompletedTask; } @@ -286,7 +286,7 @@ public class Startup { //TODO: IMPORTANT: CHANGE !!! WebApplicationBuilder.Services.AddCors(x => - x.AddDefaultPolicy(builder => + x.AddDefaultPolicy(builder => builder.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod().Build() ) ); diff --git a/MoonlightServers.DaemonShared/Enums/ServerTask.cs b/MoonlightServers.DaemonShared/Enums/ServerTask.cs deleted file mode 100644 index 5939098..0000000 --- a/MoonlightServers.DaemonShared/Enums/ServerTask.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MoonlightServers.DaemonShared.Enums; - -public enum ServerTask -{ - None = 0, - CreatingStorage = 1, - PullingDockerImage = 2, - RemovingContainer = 3, - CreatingContainer = 4, - StartingContainer = 5, - StoppingContainer = 6 -} \ No newline at end of file diff --git a/MoonlightServers.Frontend/Styles/package-lock.json b/MoonlightServers.Frontend/Styles/package-lock.json index 4d4ebe1..6a0b8fb 100644 --- a/MoonlightServers.Frontend/Styles/package-lock.json +++ b/MoonlightServers.Frontend/Styles/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "Styles", "dependencies": { "@tailwindcss/forms": "^0.5.9" }, @@ -610,9 +611,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", diff --git a/MoonlightServers.Frontend/UI/Components/Nodes/Modals/CreateMultipleAllocationModal.razor b/MoonlightServers.Frontend/UI/Components/Nodes/Modals/CreateMultipleAllocationModal.razor index 7386f22..5e3e297 100644 --- a/MoonlightServers.Frontend/UI/Components/Nodes/Modals/CreateMultipleAllocationModal.razor +++ b/MoonlightServers.Frontend/UI/Components/Nodes/Modals/CreateMultipleAllocationModal.razor @@ -41,7 +41,9 @@ { Form = new() { - IpAddress = "0.0.0.0" + IpAddress = "0.0.0.0", + Start = 2000, + End = 3000 }; } diff --git a/MoonlightServers.Frontend/UI/Components/Servers/ServerCard.razor b/MoonlightServers.Frontend/UI/Components/Servers/ServerCard.razor index 22390d6..9cd8af0 100644 --- a/MoonlightServers.Frontend/UI/Components/Servers/ServerCard.razor +++ b/MoonlightServers.Frontend/UI/Components/Servers/ServerCard.razor @@ -11,23 +11,23 @@ if (IsLoaded && !IsFailed) { - gradient = Status.PowerState switch + gradient = Status.State switch { - ServerPowerState.Installing => "from-primary-600/20", - ServerPowerState.Offline => "from-danger-600/20", - ServerPowerState.Starting => "from-warning-600/20", - ServerPowerState.Stopping => "from-warning-600/20", - ServerPowerState.Online => "from-success-600/20", + ServerState.Installing => "from-primary-600/20", + ServerState.Offline => "from-danger-600/20", + ServerState.Starting => "from-warning-600/20", + ServerState.Stopping => "from-warning-600/20", + ServerState.Online => "from-success-600/20", _ => "from-gray-600/20" }; - border = Status.PowerState switch + border = Status.State switch { - ServerPowerState.Installing => "border-primary-600", - ServerPowerState.Offline => "border-danger-600", - ServerPowerState.Starting => "border-warning-600", - ServerPowerState.Stopping => "border-warning-600", - ServerPowerState.Online => "border-success-600", + ServerState.Installing => "border-primary-600", + ServerState.Offline => "border-danger-600", + ServerState.Starting => "border-warning-600", + ServerState.Stopping => "border-warning-600", + ServerState.Online => "border-success-600", _ => "border-gray-600" }; } @@ -49,7 +49,7 @@ @if ( IsLoaded && !IsFailed && - Status.PowerState is ServerPowerState.Starting or ServerPowerState.Stopping or ServerPowerState.Online + Status.State is ServerState.Starting or ServerState.Stopping or ServerState.Online ) {
@@ -98,7 +98,7 @@
Unreachable
} - else if (IsLoaded && !IsFailed && Status.PowerState is ServerPowerState.Offline) + else if (IsLoaded && !IsFailed && Status.State is ServerState.Offline) {
@@ -108,7 +108,7 @@
Offline
} - else if (IsLoaded && !IsFailed && Status.PowerState is ServerPowerState.Installing) + else if (IsLoaded && !IsFailed && Status.State is ServerState.Installing) {
diff --git a/MoonlightServers.Frontend/UI/Views/User/Manage.razor b/MoonlightServers.Frontend/UI/Views/User/Manage.razor index 33bd1cf..e6e00e2 100644 --- a/MoonlightServers.Frontend/UI/Views/User/Manage.razor +++ b/MoonlightServers.Frontend/UI/Views/User/Manage.razor @@ -15,9 +15,11 @@ @if (NotFound) { -
Not found illustration -

Server not found

+
+ Not found illustration +

+ Server not found +

The server you requested does not exist

@@ -28,17 +30,17 @@
@{ - var bgColor = PowerState switch + var bgColor = State switch { - ServerPowerState.Installing => "bg-primary-500", - ServerPowerState.Offline => "bg-danger-500", - ServerPowerState.Starting => "bg-warning-500", - ServerPowerState.Stopping => "bg-warning-500", - ServerPowerState.Online => "bg-success-500", + ServerState.Installing => "bg-primary-500", + ServerState.Offline => "bg-danger-500", + ServerState.Starting => "bg-warning-500", + ServerState.Stopping => "bg-warning-500", + ServerState.Online => "bg-success-500", _ => "bg-gray-500" }; } - +
@@ -57,29 +59,39 @@
- @if (!string.IsNullOrEmpty(CurrentTask)) - { - - } -
- + @if (State == ServerState.Offline) + { + + + Start + + } + else + { + + } - + @if (State == ServerState.Starting || State == ServerState.Online) + { + + + Stop + + } + else + { + + }
@@ -87,31 +99,45 @@ - +
- +
} @@ -122,13 +148,12 @@ private ServerDetailResponse Server; private bool NotFound = false; - private ServerPowerState PowerState; + private ServerState State; private string InitialConsoleMessage; // TODO: When moving to a single component, fail safe when failed to load - private string CurrentTask = ""; private XtermConsole? XtermConsole; - - private HubConnection ConsoleConnection; + + private HubConnection WebSocketConnection; private async Task Load(LazyLoader _) { @@ -144,7 +169,7 @@ $"api/servers/{ServerId}/status" ); - PowerState = status.PowerState; + State = status.State; // Load initial messages var initialLogs = await ApiClient.GetJson( @@ -155,45 +180,40 @@ foreach (var message in initialLogs.Messages) InitialConsoleMessage += message; - - // Load console meta - var consoleDetails = await ApiClient.GetJson( - $"api/servers/{ServerId}/console" + + // Load websocket meta + var websocketDetails = await ApiClient.GetJson( + $"api/servers/{ServerId}/ws" ); // Build signal r - ConsoleConnection = new HubConnectionBuilder() - .WithUrl(consoleDetails.Target) + WebSocketConnection = new HubConnectionBuilder() + .WithUrl(websocketDetails.Target) .Build(); // Define handlers - ConsoleConnection.On("PowerStateChanged", async powerStateStr => + WebSocketConnection.On("StateChanged", async stateStr => { - if(!Enum.TryParse(powerStateStr, out ServerPowerState receivedState)) + if (!Enum.TryParse(stateStr, out ServerState receivedState)) return; - PowerState = receivedState; + State = receivedState; await InvokeAsync(StateHasChanged); }); - - ConsoleConnection.On("TaskNotify", async task => - { - await AddTask(Formatter.ConvertCamelCaseToSpaces(task)); - }); - ConsoleConnection.On("ConsoleOutput", async content => + WebSocketConnection.On("ConsoleOutput", async content => { if (XtermConsole != null) await XtermConsole.Write(content); - + await InvokeAsync(StateHasChanged); }); // Connect - await ConsoleConnection.StartAsync(); - + await WebSocketConnection.StartAsync(); + // Authenticate - await ConsoleConnection.SendAsync("Authenticate", consoleDetails.AccessToken); + await WebSocketConnection.SendAsync("Authenticate", websocketDetails.AccessToken); } catch (HttpApiException e) { @@ -203,34 +223,27 @@ throw; } } - + private async Task OnAfterConsoleInitialized() { await XtermConsole!.Write(InitialConsoleMessage); } - private async Task AddTask(string message) + private async Task Start() { - CurrentTask = message; - await InvokeAsync(StateHasChanged); + await ApiClient.Post($"api/servers/{Server.Id}/start"); + } - Task.Run(async () => - { - await Task.Delay(3000); - - if (CurrentTask != message) - return; - - CurrentTask = ""; - await InvokeAsync(StateHasChanged); - }); + private async Task Stop() + { + await ApiClient.Post($"api/servers/{Server.Id}/stop"); } public async ValueTask DisposeAsync() { - if (ConsoleConnection.State == HubConnectionState.Connected) - await ConsoleConnection.StopAsync(); - - await ConsoleConnection.DisposeAsync(); + if (WebSocketConnection.State == HubConnectionState.Connected) + await WebSocketConnection.StopAsync(); + + await WebSocketConnection.DisposeAsync(); } } diff --git a/MoonlightServers.Shared/Enums/ServerPowerState.cs b/MoonlightServers.Shared/Enums/ServerState.cs similarity index 82% rename from MoonlightServers.Shared/Enums/ServerPowerState.cs rename to MoonlightServers.Shared/Enums/ServerState.cs index 112d2de..ecb9f59 100644 --- a/MoonlightServers.Shared/Enums/ServerPowerState.cs +++ b/MoonlightServers.Shared/Enums/ServerState.cs @@ -1,6 +1,6 @@ namespace MoonlightServers.Shared.Enums; -public enum ServerPowerState +public enum ServerState { Offline = 0, Starting = 1, diff --git a/MoonlightServers.Shared/Http/Responses/Users/Servers/ServerStatusResponse.cs b/MoonlightServers.Shared/Http/Responses/Users/Servers/ServerStatusResponse.cs index 513451c..0299bc9 100644 --- a/MoonlightServers.Shared/Http/Responses/Users/Servers/ServerStatusResponse.cs +++ b/MoonlightServers.Shared/Http/Responses/Users/Servers/ServerStatusResponse.cs @@ -4,5 +4,5 @@ namespace MoonlightServers.Shared.Http.Responses.Users.Servers; public class ServerStatusResponse { - public ServerPowerState PowerState { get; set; } + public ServerState State { get; set; } } \ No newline at end of file diff --git a/MoonlightServers.Shared/Http/Responses/Users/Servers/ServerConsoleResponse.cs b/MoonlightServers.Shared/Http/Responses/Users/Servers/ServerWebSocketResponse.cs similarity index 80% rename from MoonlightServers.Shared/Http/Responses/Users/Servers/ServerConsoleResponse.cs rename to MoonlightServers.Shared/Http/Responses/Users/Servers/ServerWebSocketResponse.cs index 4445b09..26165cb 100644 --- a/MoonlightServers.Shared/Http/Responses/Users/Servers/ServerConsoleResponse.cs +++ b/MoonlightServers.Shared/Http/Responses/Users/Servers/ServerWebSocketResponse.cs @@ -1,6 +1,6 @@ namespace MoonlightServers.Shared.Http.Responses.Users.Servers; -public class ServerConsoleResponse +public class ServerWebSocketResponse { public string Target { get; set; } public string AccessToken { get; set; }