From 91fb15a03e39e1134cd138aec041827a1dddaed1 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Mon, 15 Sep 2025 21:47:07 +0200 Subject: [PATCH] Started implementing server service and daemon controllers --- .../Admin/Servers/ServersController.cs | 3 +- .../Controllers/Servers/PowerController.cs | 77 ++++++++++++ .../Controllers/Servers/ServersController.cs | 68 +++++++++++ .../Mappers/ServerConfigurationMapper.cs | 2 +- .../MoonlightServers.Daemon.csproj | 1 - .../ConsoleSignalRComponent.cs | 39 ++++++ .../ServerSystem/Server.cs | 10 +- .../ServerSystem/ServerFactory.cs | 8 +- .../Services/ServerService.cs | 112 ++++++++++++++++++ MoonlightServers.Daemon/Startup.cs | 3 + .../Enums/ServerState.cs | 3 +- 11 files changed, 318 insertions(+), 8 deletions(-) create mode 100644 MoonlightServers.Daemon/Http/Controllers/Servers/PowerController.cs create mode 100644 MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Implementations/ConsoleSignalRComponent.cs create mode 100644 MoonlightServers.Daemon/Services/ServerService.cs diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Servers/ServersController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Servers/ServersController.cs index 7d3e27d..b3c8388 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Admin/Servers/ServersController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Servers/ServersController.cs @@ -92,8 +92,9 @@ public class ServersController : Controller .Include(x => x.Variables) .Include(x => x.Star) .AsNoTracking() + .Where(x => x.Id == id) .ProjectToAdminResponse() - .FirstOrDefaultAsync(x => x.Id == id); + .FirstOrDefaultAsync(); if (server == null) return Problem("No server with that id found", statusCode: 404); diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/PowerController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/PowerController.cs new file mode 100644 index 0000000..e7185bf --- /dev/null +++ b/MoonlightServers.Daemon/Http/Controllers/Servers/PowerController.cs @@ -0,0 +1,77 @@ +using Microsoft.AspNetCore.Mvc; +using MoonlightServers.Daemon.ServerSystem.Enums; +using MoonlightServers.Daemon.Services; + +namespace MoonlightServers.Daemon.Http.Controllers.Servers; + +[ApiController] +[Route("api/servers/{id:int}")] +public class PowerController : Controller +{ + private readonly ServerService ServerService; + + public PowerController(ServerService serverService) + { + ServerService = serverService; + } + + [HttpPost("start")] + public async Task Start([FromRoute] int id) + { + var server = ServerService.GetById(id); + + if (server == null) + return Problem("No server with this id found", statusCode: 404); + + if (!server.StateMachine.CanFire(ServerTrigger.Start)) + return Problem("Cannot fire start trigger in this state"); + + await server.StateMachine.FireAsync(ServerTrigger.Start); + return NoContent(); + } + + [HttpPost("stop")] + public async Task Stop([FromRoute] int id) + { + var server = ServerService.GetById(id); + + if (server == null) + return Problem("No server with this id found", statusCode: 404); + + if (!server.StateMachine.CanFire(ServerTrigger.Stop)) + return Problem("Cannot fire stop trigger in this state"); + + await server.StateMachine.FireAsync(ServerTrigger.Stop); + return NoContent(); + } + + [HttpPost("kill")] + public async Task Kill([FromRoute] int id) + { + var server = ServerService.GetById(id); + + if (server == null) + return Problem("No server with this id found", statusCode: 404); + + if (!server.StateMachine.CanFire(ServerTrigger.Kill)) + return Problem("Cannot fire kill trigger in this state"); + + await server.StateMachine.FireAsync(ServerTrigger.Kill); + return NoContent(); + } + + [HttpPost("install")] + public async Task Install([FromRoute] int id) + { + var server = ServerService.GetById(id); + + if (server == null) + return Problem("No server with this id found", statusCode: 404); + + if (!server.StateMachine.CanFire(ServerTrigger.Install)) + return Problem("Cannot fire install trigger in this state"); + + await server.StateMachine.FireAsync(ServerTrigger.Install); + return NoContent(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs new file mode 100644 index 0000000..da794ab --- /dev/null +++ b/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Mvc; +using MoonlightServers.Daemon.Mappers; +using MoonlightServers.Daemon.Services; +using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers; +using MoonlightServers.DaemonShared.Enums; +using MoonlightServers.DaemonShared.PanelSide.Http.Responses; + +namespace MoonlightServers.Daemon.Http.Controllers.Servers; + +[ApiController] +[Route("api/servers/{id:int}")] +public class ServersController : Controller +{ + private readonly ServerService ServerService; + private readonly ServerConfigurationMapper ConfigurationMapper; + + public ServersController(ServerService serverService, ServerConfigurationMapper configurationMapper) + { + ServerService = serverService; + ConfigurationMapper = configurationMapper; + } + + [HttpPost("sync")] + public async Task Sync([FromRoute] int id) + { + await ServerService.InitializeById(id); + return NoContent(); + } + + [HttpGet("status")] + public async Task> Status([FromRoute] int id) + { + var server = ServerService.GetById(id); + + if (server == null) + return Problem("No server with this id found", statusCode: 404); + + return new ServerStatusResponse() + { + State = (ServerState)server.StateMachine.State + }; + } + + [HttpGet("logs")] + public async Task> Logs([FromRoute] int id) + { + var server = ServerService.GetById(id); + + if (server == null) + return Problem("No server with this id found", statusCode: 404); + + var messages = await server.Console.GetCacheAsync(); + + return new ServerLogsResponse() + { + Messages = messages.ToArray() + }; + } + + [HttpGet("stats")] + public async Task GetStats([FromRoute] int id) + { + return new ServerStatsResponse() + { + + }; + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Mappers/ServerConfigurationMapper.cs b/MoonlightServers.Daemon/Mappers/ServerConfigurationMapper.cs index 5256d13..78c2d03 100644 --- a/MoonlightServers.Daemon/Mappers/ServerConfigurationMapper.cs +++ b/MoonlightServers.Daemon/Mappers/ServerConfigurationMapper.cs @@ -35,7 +35,7 @@ public class ServerConfigurationMapper Cpu = response.Cpu, Disk = response.Disk, Memory = response.Memory, - StopCommand = response.StopCommand + StopCommand = response.StopCommand, }; } diff --git a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj index a1d584f..40f85be 100644 --- a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj +++ b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj @@ -17,7 +17,6 @@ - diff --git a/MoonlightServers.Daemon/ServerSystem/Implementations/ConsoleSignalRComponent.cs b/MoonlightServers.Daemon/ServerSystem/Implementations/ConsoleSignalRComponent.cs new file mode 100644 index 0000000..0cc292f --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Implementations/ConsoleSignalRComponent.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.SignalR; +using MoonlightServers.Daemon.Http.Hubs; +using MoonlightServers.Daemon.ServerSystem.Interfaces; +using MoonlightServers.Daemon.ServerSystem.Models; + +namespace MoonlightServers.Daemon.ServerSystem.Implementations; + +public class ConsoleSignalRComponent : IServerComponent +{ + private readonly IHubContext Hub; + private readonly ServerContext Context; + + private IAsyncDisposable? StdOutSubscription; + private string HubGroup; + + public ConsoleSignalRComponent(IHubContext hub, ServerContext context) + { + Hub = hub; + Context = context; + } + + public async Task InitializeAsync() + { + HubGroup = Context.Configuration.Id.ToString(); + + StdOutSubscription = await Context.Server.Console.SubscribeStdOutAsync(OnStdOut); + } + + private async ValueTask OnStdOut(string output) + { + await Hub.Clients.Group(HubGroup).SendAsync("ConsoleOutput", output); + } + + public async ValueTask DisposeAsync() + { + if (StdOutSubscription != null) + await StdOutSubscription.DisposeAsync(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Server.cs b/MoonlightServers.Daemon/ServerSystem/Server.cs index 6e8b5c2..8b7848d 100644 --- a/MoonlightServers.Daemon/ServerSystem/Server.cs +++ b/MoonlightServers.Daemon/ServerSystem/Server.cs @@ -1,3 +1,4 @@ +using System.Collections; using MoonlightServers.Daemon.ServerSystem.Enums; using MoonlightServers.Daemon.ServerSystem.Interfaces; using MoonlightServers.Daemon.ServerSystem.Models; @@ -39,7 +40,8 @@ public partial class Server : IAsyncDisposable IRestorer restorer, IRuntime runtime, IStatistics statistics, - IServerStateHandler[] handlers + IEnumerable handlers, + IEnumerable additionalComponents ) { Logger = logger; @@ -54,13 +56,15 @@ public partial class Server : IAsyncDisposable Runtime = runtime; Statistics = statistics; - AllComponents = + IEnumerable defaultComponents = [ Console, RuntimeFileSystem, InstallationFileSystem, Installation, OnlineDetector, Reporter, Restorer, Runtime, Statistics ]; - Handlers = handlers; + AllComponents = defaultComponents.Concat(additionalComponents).ToArray(); + + Handlers = handlers.ToArray(); } private void ConfigureStateMachine(ServerState initialState) diff --git a/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs b/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs index 6810b78..53f8cb2 100644 --- a/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs +++ b/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs @@ -63,6 +63,11 @@ public class ServerFactory handlers.Add(ActivatorUtilities.CreateInstance(scope.ServiceProvider)); handlers.Add(ActivatorUtilities.CreateInstance(scope.ServiceProvider)); handlers.Add(ActivatorUtilities.CreateInstance(scope.ServiceProvider)); + + // Resolve additional components + var components = new List(); + + components.Add(ActivatorUtilities.CreateInstance(scope.ServiceProvider)); // TODO: Add a plugin hook for dynamically resolving components and checking if any is unset @@ -81,7 +86,8 @@ public class ServerFactory runtime, statistics, // And now all the handlers - handlers.ToArray() + handlers, + components ); context.Server = server; diff --git a/MoonlightServers.Daemon/Services/ServerService.cs b/MoonlightServers.Daemon/Services/ServerService.cs new file mode 100644 index 0000000..985210b --- /dev/null +++ b/MoonlightServers.Daemon/Services/ServerService.cs @@ -0,0 +1,112 @@ +using System.Collections.Concurrent; +using MoonCore.Helpers; +using MoonCore.Models; +using MoonlightServers.Daemon.Mappers; +using MoonlightServers.Daemon.Models.Cache; +using MoonlightServers.Daemon.ServerSystem; +using MoonlightServers.DaemonShared.PanelSide.Http.Responses; + +namespace MoonlightServers.Daemon.Services; + +public class ServerService : IHostedLifecycleService +{ + private readonly ConcurrentDictionary Servers = new(); + private readonly ILogger Logger; + private readonly ServerFactory ServerFactory; + private readonly RemoteService RemoteService; + private readonly ServerConfigurationMapper ConfigurationMapper; + + public ServerService( + ILogger logger, + ServerFactory serverFactory, + RemoteService remoteService, + ServerConfigurationMapper configurationMapper + ) + { + Logger = logger; + ServerFactory = serverFactory; + RemoteService = remoteService; + ConfigurationMapper = configurationMapper; + } + + public Server? GetById(int id) + => Servers.GetValueOrDefault(id); + + public async Task Initialize(ServerConfiguration configuration) + { + var existingServer = Servers.GetValueOrDefault(configuration.Id); + + if (existingServer != null) + { + existingServer.Context.Configuration = configuration; + // TODO: Implement a way for components to get notified + } + else + { + var server = await ServerFactory.CreateAsync(configuration); + Servers[configuration.Id] = server; + + await server.InitializeAsync(); + } + } + + public async Task InitializeById(int id) + { + var serverData = await RemoteService.GetServer(id); + var config = ConfigurationMapper.FromServerDataResponse(serverData); + + await Initialize(config); + } + + private async Task InitializeAll() + { + Logger.LogDebug("Initialing servers from panel"); + + var servers = await PagedData.All(async (page, pageSize) => + await RemoteService.GetServers(page, pageSize) + ); + + foreach (var serverData in servers) + { + try + { + var config = ConfigurationMapper.FromServerDataResponse(serverData); + + await Initialize(config); + } + catch (Exception e) + { + Logger.LogError(e, "An error occured while initializing server {id}", serverData.Id); + } + } + } + + #region Lifetime handlers + + public Task StartAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public async Task StartedAsync(CancellationToken cancellationToken) + { + await InitializeAll(); + } + + public Task StartingAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public Task StoppedAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public async Task StoppingAsync(CancellationToken cancellationToken) + { + Logger.LogDebug("Stopping server service. Disposing servers"); + + foreach (var server in Servers.Values) + await server.DisposeAsync(); + } + + #endregion +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Startup.cs b/MoonlightServers.Daemon/Startup.cs index 253a111..b631e04 100644 --- a/MoonlightServers.Daemon/Startup.cs +++ b/MoonlightServers.Daemon/Startup.cs @@ -328,6 +328,9 @@ public class Startup WebApplicationBuilder.Services.AddScoped(); WebApplicationBuilder.Services.AddSingleton(); WebApplicationBuilder.Services.AddSingleton(); + + WebApplicationBuilder.Services.AddSingleton(); + WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService()); return Task.CompletedTask; } diff --git a/MoonlightServers.DaemonShared/Enums/ServerState.cs b/MoonlightServers.DaemonShared/Enums/ServerState.cs index f2a3072..ee77f92 100644 --- a/MoonlightServers.DaemonShared/Enums/ServerState.cs +++ b/MoonlightServers.DaemonShared/Enums/ServerState.cs @@ -6,5 +6,6 @@ public enum ServerState Starting = 1, Online = 2, Stopping = 3, - Installing = 4 + Installing = 4, + Locked = 5 } \ No newline at end of file