From 348e9560ab86b069cc8520b48944988e93df6f04 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Sat, 6 Sep 2025 15:34:35 +0200 Subject: [PATCH] Cleaned up interfaces. Extracted server state machine trigger handler to seperated classes. Removed legacy code --- .../Admin/Nodes/NodeAllocationsController.cs | 108 ++-- .../Admin/Nodes/NodeStatusController.cs | 18 +- .../Admin/Nodes/NodesController.cs | 59 +-- .../Admin/Nodes/StatisticsController.cs | 24 +- .../Servers/ServerVariablesController.cs | 41 +- .../Admin/Servers/ServersController.cs | 67 ++- .../Admin/Stars/StarDockerImagesController.cs | 61 +-- .../Admin/Stars/StarImportExportController.cs | 9 +- .../Admin/Stars/StarVariablesController.cs | 36 +- .../Admin/Stars/StarsController.cs | 47 +- .../Controllers/Client/FilesController.cs | 86 +++- .../Controllers/Client/PowerController.cs | 43 +- .../Controllers/Client/ServersController.cs | 99 ++-- .../Controllers/Client/SettingsController.cs | 17 +- .../Controllers/Client/SharesController.cs | 68 ++- .../Controllers/Client/VariablesController.cs | 51 +- .../ServerAuthFilters/OwnerAuthFilter.cs | 2 +- .../Mappers/AllocationMapper.cs | 4 + .../Mappers/DockerImageMapper.cs | 6 +- .../Mappers/NodeMapper.cs | 4 + .../Mappers/ServerMapper.cs | 6 + .../Mappers/ServerVariableMapper.cs | 4 + .../Mappers/StarMapper.cs | 6 +- .../Mappers/StarVariableMapper.cs | 6 +- .../Controllers/Servers/DownloadController.cs | 44 -- .../Servers/ServerFileSystemController.cs | 99 ---- .../Servers/ServerPowerController.cs | 65 --- .../Controllers/Servers/ServersController.cs | 109 ---- .../Controllers/Servers/UploadController.cs | 85 ---- .../MoonlightServers.Daemon.csproj | 2 + .../ServerSys/Abstractions/IConsole.cs | 26 - .../ServerSys/Abstractions/IFileSystem.cs | 14 - .../ServerSys/Abstractions/IInstaller.cs | 14 - .../Abstractions/IOnlineDetection.cs | 6 - .../ServerSys/Abstractions/IProvisioner.cs | 15 - .../ServerSys/Abstractions/IRestorer.cs | 8 - .../Abstractions/IServerComponent.cs | 7 - .../ServerSys/Abstractions/IStatistics.cs | 11 - .../ServerSys/Abstractions/Server.cs | 348 ------------- .../ServerSys/Abstractions/ServerContext.cs | 12 - .../ServerSys/Abstractions/ServerCrash.cs | 3 - .../ServerSys/Abstractions/ServerStats.cs | 3 - .../Implementations/DefaultRestorer.cs | 68 --- .../Implementations/DockerConsole.cs | 230 --------- .../Implementations/DockerInstaller.cs | 278 ----------- .../Implementations/DockerProvisioner.cs | 261 ---------- .../Implementations/DockerStatistics.cs | 34 -- .../Implementations/RawFileSystem.cs | 60 --- .../Implementations/RegexOnlineDetection.cs | 90 ---- .../ServerSys/ServerFactory.cs | 30 -- .../ServerSystem/{ => Enums}/ServerState.cs | 5 +- .../ServerSystem/Enums/ServerTrigger.cs | 12 + .../ServerSystem/Handlers/ShutdownHandler.cs | 42 ++ .../ServerSystem/Handlers/StartupHandler.cs | 91 ++++ .../ServerSystem/Interfaces/IConsole.cs | 72 +++ .../ServerSystem/Interfaces/IFileSystem.cs | 54 ++ .../ServerSystem/Interfaces/IInstallation.cs | 50 ++ .../Interfaces/IOnlineDetector.cs | 23 + .../ServerSystem/Interfaces/IReporter.cs | 18 + .../ServerSystem/Interfaces/IRestorer.cs | 16 + .../ServerSystem/Interfaces/IRuntime.cs | 55 +++ .../Interfaces/IServerComponent.cs | 10 + .../Interfaces/IServerStateHandler.cs | 9 + .../ServerSystem/Interfaces/IStatistics.cs | 30 ++ .../ServerSystem/Models/ServerContext.cs | 11 + .../ServerSystem/Models/StatisticsData.cs | 6 + .../ServerSystem/Server.cs | 247 +++++----- .../ServerSystem/ServerFactory.cs | 66 +++ .../ServerSystem/ServerSubSystem.cs | 27 - .../ServerSystem/ServerTrigger.cs | 12 - .../SubSystems/ConsoleSubSystem.cs | 178 ------- .../ServerSystem/SubSystems/DebugSubSystem.cs | 19 - .../SubSystems/InstallationSubSystem.cs | 239 --------- .../SubSystems/OnlineDetectionService.cs | 45 -- .../SubSystems/ProvisionSubSystem.cs | 226 --------- .../SubSystems/RestoreSubSystem.cs | 117 ----- .../SubSystems/ShutdownSubSystem.cs | 85 ---- .../ServerSystem/SubSystems/StatsSubSystem.cs | 150 ------ .../SubSystems/StorageSubSystem.cs | 464 ------------------ .../Services/NewServerService.cs | 183 ------- .../Services/ServerService.cs | 364 -------------- MoonlightServers.Daemon/Startup.cs | 108 ---- .../Servers/CreatePartials/General.razor | 8 +- .../Servers/CreatePartials/Variables.razor | 8 +- .../Servers/UpdatePartials/Variables.razor | 6 +- .../Stars/Modals/UpdateDockerImageModal.razor | 2 +- .../Stars/Modals/UpdateVariableModal.razor | 2 +- .../Stars/UpdatePartials/DockerImage.razor | 10 +- .../Stars/UpdatePartials/Misc.razor | 6 +- .../Stars/UpdatePartials/Variables.razor | 10 +- .../UI/Views/Admin/All/Create.razor | 2 +- .../UI/Views/Admin/All/Index.razor | 4 +- .../UI/Views/Admin/Stars/Index.razor | 30 +- .../UI/Views/Admin/Stars/Update.razor | 4 +- ...Response.cs => StarDockerImageResponse.cs} | 2 +- ...ailResponse.cs => StarVariableResponse.cs} | 2 +- ...{StarDetailResponse.cs => StarResponse.cs} | 2 +- 97 files changed, 1256 insertions(+), 4670 deletions(-) delete mode 100644 MoonlightServers.Daemon/Http/Controllers/Servers/DownloadController.cs delete mode 100644 MoonlightServers.Daemon/Http/Controllers/Servers/ServerFileSystemController.cs delete mode 100644 MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs delete mode 100644 MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs delete mode 100644 MoonlightServers.Daemon/Http/Controllers/Servers/UploadController.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IConsole.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IFileSystem.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IInstaller.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IOnlineDetection.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IProvisioner.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IRestorer.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IServerComponent.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/IStatistics.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/Server.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/ServerContext.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/ServerCrash.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Abstractions/ServerStats.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Implementations/DefaultRestorer.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Implementations/DockerConsole.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Implementations/DockerInstaller.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Implementations/DockerProvisioner.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Implementations/DockerStatistics.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Implementations/RawFileSystem.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/Implementations/RegexOnlineDetection.cs delete mode 100644 MoonlightServers.Daemon/ServerSys/ServerFactory.cs rename MoonlightServers.Daemon/ServerSystem/{ => Enums}/ServerState.cs (52%) create mode 100644 MoonlightServers.Daemon/ServerSystem/Enums/ServerTrigger.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Handlers/ShutdownHandler.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Handlers/StartupHandler.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Interfaces/IConsole.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Interfaces/IFileSystem.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Interfaces/IInstallation.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Interfaces/IOnlineDetector.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Interfaces/IReporter.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Interfaces/IRestorer.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Interfaces/IRuntime.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Interfaces/IServerComponent.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Interfaces/IServerStateHandler.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Interfaces/IStatistics.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Models/ServerContext.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/Models/StatisticsData.cs create mode 100644 MoonlightServers.Daemon/ServerSystem/ServerFactory.cs delete mode 100644 MoonlightServers.Daemon/ServerSystem/ServerSubSystem.cs delete mode 100644 MoonlightServers.Daemon/ServerSystem/ServerTrigger.cs delete mode 100644 MoonlightServers.Daemon/ServerSystem/SubSystems/ConsoleSubSystem.cs delete mode 100644 MoonlightServers.Daemon/ServerSystem/SubSystems/DebugSubSystem.cs delete mode 100644 MoonlightServers.Daemon/ServerSystem/SubSystems/InstallationSubSystem.cs delete mode 100644 MoonlightServers.Daemon/ServerSystem/SubSystems/OnlineDetectionService.cs delete mode 100644 MoonlightServers.Daemon/ServerSystem/SubSystems/ProvisionSubSystem.cs delete mode 100644 MoonlightServers.Daemon/ServerSystem/SubSystems/RestoreSubSystem.cs delete mode 100644 MoonlightServers.Daemon/ServerSystem/SubSystems/ShutdownSubSystem.cs delete mode 100644 MoonlightServers.Daemon/ServerSystem/SubSystems/StatsSubSystem.cs delete mode 100644 MoonlightServers.Daemon/ServerSystem/SubSystems/StorageSubSystem.cs delete mode 100644 MoonlightServers.Daemon/Services/NewServerService.cs delete mode 100644 MoonlightServers.Daemon/Services/ServerService.cs rename MoonlightServers.Shared/Http/Responses/Admin/StarDockerImages/{StarDockerImageDetailResponse.cs => StarDockerImageResponse.cs} (84%) rename MoonlightServers.Shared/Http/Responses/Admin/StarVariables/{StarVariableDetailResponse.cs => StarVariableResponse.cs} (92%) rename MoonlightServers.Shared/Http/Responses/Admin/Stars/{StarDetailResponse.cs => StarResponse.cs} (96%) diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodeAllocationsController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodeAllocationsController.cs index a1a6a93..098ece0 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodeAllocationsController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodeAllocationsController.cs @@ -1,9 +1,9 @@ -using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; +using MoonCore.Extended.Models; using MoonCore.Models; using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Mappers; @@ -28,56 +28,57 @@ public class NodeAllocationsController : Controller AllocationRepository = allocationRepository; } - [HttpGet("")] + [HttpGet] [Authorize(Policy = "permissions:admin.servers.nodes.get")] - public async Task> Get( + public async Task>> Get( [FromRoute] int nodeId, - [FromQuery] [Range(0, int.MaxValue)] int page, - [FromQuery] [Range(1, 100)] int pageSize + [FromQuery] PagedOptions options ) { - var count = await AllocationRepository.Get().CountAsync(x => x.Node.Id == nodeId); + var count = await AllocationRepository + .Get() + .CountAsync(x => x.Node.Id == nodeId); var allocations = await AllocationRepository .Get() .OrderBy(x => x.Id) - .Skip(page * pageSize) - .Take(pageSize) + .Skip(options.Page * options.PageSize) + .Take(options.PageSize) .Where(x => x.Node.Id == nodeId) + .AsNoTracking() + .ProjectToAdminResponse() .ToArrayAsync(); - var mappedAllocations = allocations - .Select(AllocationMapper.ToNodeAllocation) - .ToArray(); - return new PagedData() { - Items = mappedAllocations, - CurrentPage = page, - PageSize = pageSize, + Items = allocations, + CurrentPage = options.Page, + PageSize = options.PageSize, TotalItems = count, - TotalPages = count == 0 ? 0 : (count - 1) / pageSize + TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize) }; } [HttpGet("{id:int}")] [Authorize(Policy = "permissions:admin.servers.nodes.get")] - public async Task GetSingle([FromRoute] int nodeId, [FromRoute] int id) + public async Task> GetSingle([FromRoute] int nodeId, [FromRoute] int id) { var allocation = await AllocationRepository .Get() .Where(x => x.Node.Id == nodeId) + .AsNoTracking() + .ProjectToAdminResponse() .FirstOrDefaultAsync(x => x.Id == id); if (allocation == null) - throw new HttpApiException("No allocation with that id found", 404); + return Problem("No allocation with that id found", statusCode: 400); - return AllocationMapper.ToNodeAllocation(allocation); + return allocation; } - [HttpPost("")] + [HttpPost] [Authorize(Policy = "permissions:admin.servers.nodes.create")] - public async Task Create( + public async Task> Create( [FromRoute] int nodeId, [FromBody] CreateNodeAllocationRequest request ) @@ -87,7 +88,7 @@ public class NodeAllocationsController : Controller .FirstOrDefaultAsync(x => x.Id == nodeId); if (node == null) - throw new HttpApiException("No node with that id found", 404); + return Problem("No node with that id found", statusCode: 404); var allocation = AllocationMapper.ToAllocation(request); @@ -97,7 +98,7 @@ public class NodeAllocationsController : Controller } [HttpPatch("{id:int}")] - public async Task Update( + public async Task> Update( [FromRoute] int nodeId, [FromRoute] int id, [FromBody] UpdateNodeAllocationRequest request @@ -109,7 +110,7 @@ public class NodeAllocationsController : Controller .FirstOrDefaultAsync(x => x.Id == id); if (allocation == null) - throw new HttpApiException("No allocation with that id found", 404); + return Problem("No allocation with that id found", statusCode: 404); AllocationMapper.Merge(request, allocation); await AllocationRepository.Update(allocation); @@ -118,7 +119,7 @@ public class NodeAllocationsController : Controller } [HttpDelete("{id:int}")] - public async Task Delete([FromRoute] int nodeId, [FromRoute] int id) + public async Task Delete([FromRoute] int nodeId, [FromRoute] int id) { var allocation = await AllocationRepository .Get() @@ -126,32 +127,44 @@ public class NodeAllocationsController : Controller .FirstOrDefaultAsync(x => x.Id == id); if (allocation == null) - throw new HttpApiException("No allocation with that id found", 404); + return Problem("No allocation with that id found", statusCode: 404); await AllocationRepository.Remove(allocation); + return NoContent(); } [HttpPost("range")] - public async Task CreateRange([FromRoute] int nodeId, [FromBody] CreateNodeAllocationRangeRequest rangeRequest) + public async Task CreateRange( + [FromRoute] int nodeId, + [FromBody] CreateNodeAllocationRangeRequest request + ) { + if (request.Start > request.End) + return Problem("Invalid start and end specified", statusCode: 400); + + if (request.End - request.Start == 0) + return Problem("Empty range specified", statusCode: 400); + var node = await NodeRepository .Get() .FirstOrDefaultAsync(x => x.Id == nodeId); if (node == null) - throw new HttpApiException("No node with that id found", 404); + return Problem("No node with that id found", statusCode: 404); - var existingAllocations = AllocationRepository + var existingAllocations = await AllocationRepository .Get() - .Where(x => x.Node.Id == nodeId) - .ToArray(); + .Where(x => x.Port >= request.Start && x.Port <= request.End && + x.IpAddress == request.IpAddress) + .AsNoTracking() + .ToArrayAsync(); var ports = new List(); - for (var i = rangeRequest.Start; i < rangeRequest.End; i++) + for (var i = request.Start; i < request.End; i++) { // Skip existing allocations - if (existingAllocations.Any(x => x.Port == i && x.IpAddress == rangeRequest.IpAddress)) + if (existingAllocations.Any(x => x.Port == i)) continue; ports.Add(i); @@ -160,17 +173,18 @@ public class NodeAllocationsController : Controller var allocations = ports .Select(port => new Allocation() { - IpAddress = rangeRequest.IpAddress, + IpAddress = request.IpAddress, Port = port, Node = node }) .ToArray(); - + await AllocationRepository.RunTransaction(async set => { await set.AddRangeAsync(allocations); }); + return NoContent(); } [HttpDelete("all")] - public async Task DeleteAll([FromRoute] int nodeId) + public async Task DeleteAll([FromRoute] int nodeId) { var allocations = AllocationRepository .Get() @@ -178,14 +192,14 @@ public class NodeAllocationsController : Controller .ToArray(); await AllocationRepository.RunTransaction(set => { set.RemoveRange(allocations); }); + return NoContent(); } [HttpGet("free")] [Authorize(Policy = "permissions:admin.servers.nodes.get")] public async Task> GetFree( [FromRoute] int nodeId, - [FromQuery] int page, - [FromQuery] [Range(1, 100)] int pageSize, + [FromQuery] PagedOptions options, [FromQuery] int serverId = -1 ) { @@ -202,19 +216,21 @@ public class NodeAllocationsController : Controller .Where(x => x.Server == null || x.Server.Id == serverId); var count = await freeAllocationsQuery.CountAsync(); - var allocations = await freeAllocationsQuery.ToArrayAsync(); - var mappedAllocations = allocations - .Select(AllocationMapper.ToNodeAllocation) - .ToArray(); + var allocations = await freeAllocationsQuery + .Skip(options.Page * options.PageSize) + .Take(options.PageSize) + .AsNoTracking() + .ProjectToAdminResponse() + .ToArrayAsync(); return new PagedData() { - Items = mappedAllocations, - CurrentPage = page, - PageSize = pageSize, + Items = allocations, + CurrentPage = options.Page, + PageSize = options.PageSize, TotalItems = count, - TotalPages = count == 0 ? 0 : (count - 1) / pageSize + TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize) }; } } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodeStatusController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodeStatusController.cs index 4237bd3..74f55a4 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodeStatusController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodeStatusController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Services; using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys; @@ -24,10 +25,13 @@ public class NodeStatusController : Controller [HttpGet("{nodeId:int}/system/status")] [Authorize(Policy = "permissions:admin.servers.nodes.status")] - public async Task GetStatus([FromRoute] int nodeId) + public async Task> GetStatus([FromRoute] int nodeId) { - var node = GetNode(nodeId); + var node = await GetNode(nodeId); + if (node.Value == null) + return node.Result ?? Problem("Unable to retrieve node"); + NodeSystemStatusResponse response; var sw = new Stopwatch(); @@ -35,7 +39,7 @@ public class NodeStatusController : Controller try { - var statusResponse = await NodeService.GetSystemStatus(node); + var statusResponse = await NodeService.GetSystemStatus(node.Value); sw.Stop(); @@ -65,14 +69,14 @@ public class NodeStatusController : Controller return response; } - private Node GetNode(int nodeId) + private async Task> GetNode(int nodeId) { - var result = NodeRepository + var result = await NodeRepository .Get() - .FirstOrDefault(x => x.Id == nodeId); + .FirstOrDefaultAsync(x => x.Id == nodeId); if (result == null) - throw new HttpApiException("A node with this id could not be found", 404); + return Problem("A node with this id could not be found", statusCode: 404); return result; } diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodesController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodesController.cs index affb20b..b17b958 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodesController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodesController.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using MoonCore.Extended.Abstractions; using Microsoft.AspNetCore.Authorization; using MoonCore.Exceptions; +using MoonCore.Extended.Models; using MoonCore.Helpers; using MoonCore.Models; using MoonlightServers.ApiServer.Database.Entities; @@ -19,62 +20,55 @@ public class NodesController : Controller { private readonly DatabaseRepository NodeRepository; - public NodesController( - DatabaseRepository nodeRepository - ) + public NodesController(DatabaseRepository nodeRepository) { NodeRepository = nodeRepository; } [HttpGet] [Authorize(Policy = "permissions:admin.servers.nodes.get")] - public async Task> Get( - [FromQuery] [Range(0, int.MaxValue)] int page, - [FromQuery] [Range(1, 100)] int pageSize - ) + public async Task> Get([FromQuery] PagedOptions options) { - var query = NodeRepository - .Get(); + var count = await NodeRepository.Get().CountAsync(); - var count = await query.CountAsync(); - - var items = await query + var items = await NodeRepository + .Get() .OrderBy(x => x.Id) - .Skip(page * pageSize) - .Take(pageSize) + .Skip(options.Page * options.PageSize) + .Take(options.PageSize) + .AsNoTracking() + .ProjectToAdminResponse() .ToArrayAsync(); - var mappedItems = items - .Select(NodeMapper.ToAdminNodeResponse) - .ToArray(); - return new PagedData() { - Items = mappedItems, - CurrentPage = page, - PageSize = pageSize, + Items = items, + CurrentPage = options.Page, + PageSize = options.PageSize, TotalItems = count, - TotalPages = count == 0 ? 0 : count / pageSize + TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize) }; } [HttpGet("{id:int}")] [Authorize(Policy = "permissions:admin.servers.nodes.get")] - public async Task GetSingle([FromRoute] int id) + public async Task> GetSingle([FromRoute] int id) { var node = await NodeRepository .Get() + .AsNoTracking() + .ProjectToAdminResponse() .FirstOrDefaultAsync(x => x.Id == id); if (node == null) - throw new HttpApiException("No node with this id found", 404); - - return NodeMapper.ToAdminNodeResponse(node); + return Problem("No node with this id found", statusCode: 404); + + return node; } [HttpPost] [Authorize(Policy = "permissions:admin.servers.nodes.create")] - public async Task Create([FromBody] CreateNodeRequest request) + public async Task> Create([FromBody] CreateNodeRequest request) { var node = NodeMapper.ToNode(request); @@ -88,14 +82,14 @@ public class NodesController : Controller [HttpPatch("{id:int}")] [Authorize(Policy = "permissions:admin.servers.nodes.update")] - public async Task Update([FromRoute] int id, [FromBody] UpdateNodeRequest request) + public async Task> Update([FromRoute] int id, [FromBody] UpdateNodeRequest request) { var node = await NodeRepository .Get() .FirstOrDefaultAsync(x => x.Id == id); if (node == null) - throw new HttpApiException("No node with this id found", 404); + return Problem("No node with this id found", statusCode: 404); NodeMapper.Merge(request, node); await NodeRepository.Update(node); @@ -105,15 +99,16 @@ public class NodesController : Controller [HttpDelete("{id:int}")] [Authorize(Policy = "permissions:admin.servers.nodes.delete")] - public async Task Delete([FromRoute] int id) + public async Task Delete([FromRoute] int id) { var node = await NodeRepository .Get() .FirstOrDefaultAsync(x => x.Id == id); if (node == null) - throw new HttpApiException("No node with this id found", 404); - + return Problem("No node with this id found", statusCode: 404); + await NodeRepository.Remove(node); + return Ok(); } } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/StatisticsController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/StatisticsController.cs index 38d146a..18d869f 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/StatisticsController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/StatisticsController.cs @@ -27,12 +27,16 @@ public class StatisticsController : Controller [HttpGet] [SuppressMessage("ReSharper.DPA", "DPA0011: High execution time of MVC action", MessageId = "time: 1142ms", Justification = "The daemon has an artificial delay of one second to calculate accurate cpu usage values")] - public async Task Get([FromRoute] int nodeId) + public async Task> Get([FromRoute] int nodeId) { var node = await GetNode(nodeId); - var statistics = await NodeService.GetStatistics(node); - return new() + if (node.Value == null) + return node.Result ?? Problem("Unable to retrieve node"); + + var statistics = await NodeService.GetStatistics(node.Value); + + return new StatisticsResponse() { Cpu = new() { @@ -62,12 +66,16 @@ public class StatisticsController : Controller } [HttpGet("docker")] - public async Task GetDocker([FromRoute] int nodeId) + public async Task> GetDocker([FromRoute] int nodeId) { var node = await GetNode(nodeId); - var statistics = await NodeService.GetDockerStatistics(node); - return new() + if (node.Value == null) + return node.Result ?? Problem("Unable to retrieve node"); + + var statistics = await NodeService.GetDockerStatistics(node.Value); + + return new DockerStatisticsResponse() { BuildCacheReclaimable = statistics.BuildCacheReclaimable, BuildCacheUsed = statistics.BuildCacheUsed, @@ -79,14 +87,14 @@ public class StatisticsController : Controller }; } - private async Task GetNode(int nodeId) + private async Task> GetNode(int nodeId) { var result = await NodeRepository .Get() .FirstOrDefaultAsync(x => x.Id == nodeId); if (result == null) - throw new HttpApiException("A node with this id could not be found", 404); + return Problem("A node with this id could not be found", statusCode: 404); return result; } diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Servers/ServerVariablesController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Servers/ServerVariablesController.cs index 53e40ba..f91ea79 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Admin/Servers/ServerVariablesController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Servers/ServerVariablesController.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authorization; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; +using MoonCore.Extended.Models; using MoonCore.Models; using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Mappers; @@ -18,8 +19,10 @@ public class ServerVariablesController : Controller private readonly DatabaseRepository VariableRepository; private readonly DatabaseRepository ServerRepository; - public ServerVariablesController(DatabaseRepository variableRepository, - DatabaseRepository serverRepository) + public ServerVariablesController( + DatabaseRepository variableRepository, + DatabaseRepository serverRepository + ) { VariableRepository = variableRepository; ServerRepository = serverRepository; @@ -27,10 +30,9 @@ public class ServerVariablesController : Controller [HttpGet("{serverId:int}/variables")] [Authorize(Policy = "permissions:admin.servers.read")] - public async Task> Get( + public async Task>> Get( [FromRoute] int serverId, - [FromQuery] [Range(0, int.MaxValue)] int page, - [FromQuery] [Range(1, 100)] int pageSize + [FromQuery] PagedOptions options ) { var serverExists = await ServerRepository @@ -38,20 +40,29 @@ public class ServerVariablesController : Controller .AnyAsync(x => x.Id == serverId); if (!serverExists) - throw new HttpApiException("No server with this id found", 404); + return Problem("No server with this id found", statusCode: 404); - var variables = await VariableRepository + var query = VariableRepository .Get() - .Where(x => x.Server.Id == serverId) + .Where(x => x.Server.Id == serverId); + + var count = await query.CountAsync(); + + var variables = await query .OrderBy(x => x.Id) - .Skip(page * pageSize) - .Take(pageSize) + .Skip(options.Page * options.PageSize) + .Take(options.PageSize) + .AsNoTracking() + .ProjectToAdminResponse() .ToArrayAsync(); - var castedVariables = variables - .Select(ServerVariableMapper.ToAdminResponse) - .ToArray(); - - return PagedData.Create(castedVariables, page, pageSize); + return new PagedData() + { + Items = variables, + CurrentPage = options.Page, + PageSize = options.PageSize, + TotalItems = count, + TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize) + }; } } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Servers/ServersController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Servers/ServersController.cs index bfc2276..7d3e27d 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Admin/Servers/ServersController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Servers/ServersController.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Helpers; +using MoonCore.Extended.Models; using MoonCore.Helpers; using MoonCore.Models; using Moonlight.ApiServer.Database.Entities; @@ -53,41 +54,36 @@ public class ServersController : Controller [HttpGet] [Authorize(Policy = "permissions:admin.servers.read")] - public async Task> Get( - [FromQuery] [Range(0, int.MaxValue)] int page, - [FromQuery] [Range(1, 100)] int pageSize - ) + public async Task>> Get([FromQuery] PagedOptions options) { var count = await ServerRepository.Get().CountAsync(); - var items = await ServerRepository + var servers = await ServerRepository .Get() .Include(x => x.Node) .Include(x => x.Allocations) .Include(x => x.Variables) .Include(x => x.Star) .OrderBy(x => x.Id) - .Skip(page * pageSize) - .Take(pageSize) + .Skip(options.Page * options.PageSize) + .Take(options.PageSize) + .AsNoTracking() + .ProjectToAdminResponse() .ToArrayAsync(); - var mappedItems = items - .Select(ServerMapper.ToAdminServerResponse) - .ToArray(); - return new PagedData() { - Items = mappedItems, - CurrentPage = page, - PageSize = pageSize, + Items = servers, + CurrentPage = options.Page, + PageSize = options.PageSize, TotalItems = count, - TotalPages = count == 0 ? 0 : count / pageSize + TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize) }; } [HttpGet("{id:int}")] [Authorize(Policy = "permissions:admin.servers.read")] - public async Task GetSingle([FromRoute] int id) + public async Task> GetSingle([FromRoute] int id) { var server = await ServerRepository .Get() @@ -95,21 +91,23 @@ public class ServersController : Controller .Include(x => x.Allocations) .Include(x => x.Variables) .Include(x => x.Star) + .AsNoTracking() + .ProjectToAdminResponse() .FirstOrDefaultAsync(x => x.Id == id); if (server == null) - throw new HttpApiException("No server with that id found", 404); - - return ServerMapper.ToAdminServerResponse(server); + return Problem("No server with that id found", statusCode: 404); + + return server; } [HttpPost] [Authorize(Policy = "permissions:admin.servers.write")] - public async Task Create([FromBody] CreateServerRequest request) + public async Task> Create([FromBody] CreateServerRequest request) { // Check if owner user exist if (UserRepository.Get().All(x => x.Id != request.OwnerId)) - throw new HttpApiException("No user with this id found", 400); + return Problem("No user with this id found", statusCode: 400); // Check if the star exists var star = await StarRepository @@ -119,14 +117,14 @@ public class ServersController : Controller .FirstOrDefaultAsync(x => x.Id == request.StarId); if (star == null) - throw new HttpApiException("No star with this id found", 400); + return Problem("No star with this id found", statusCode: 400); var node = await NodeRepository .Get() .FirstOrDefaultAsync(x => x.Id == request.NodeId); if (node == null) - throw new HttpApiException("No node with this id found", 400); + return Problem("No node with this id found", statusCode: 400); var allocations = new List(); @@ -161,13 +159,13 @@ public class ServersController : Controller if (allocations.Count < star.RequiredAllocations) { - throw new HttpApiException( + return Problem( $"Unable to find enough free allocations. Found: {allocations.Count}, Required: {star.RequiredAllocations}", - 400 + statusCode: 400 ); } } - + var server = ServerMapper.ToServer(request); // Set allocations @@ -204,7 +202,7 @@ public class ServersController : Controller Logger.LogError("Unable to sync server to node the server is assigned to: {e}", e); // We are deleting the server from the database after the creation has failed - // to ensure we won't have a bugged server in the database which doesnt exist on the node + // to ensure we won't have a bugged server in the database which doesn't exist on the node await ServerRepository.Remove(finalServer); throw; @@ -215,7 +213,7 @@ public class ServersController : Controller [HttpPatch("{id:int}")] [Authorize(Policy = "permissions:admin.servers.write")] - public async Task Update([FromRoute] int id, [FromBody] UpdateServerRequest request) + public async Task> Update([FromRoute] int id, [FromBody] UpdateServerRequest request) { //TODO: Handle shrinking virtual disk @@ -228,7 +226,7 @@ public class ServersController : Controller .FirstOrDefaultAsync(x => x.Id == id); if (server == null) - throw new HttpApiException("No server with that id found", 404); + return Problem("No server with that id found", statusCode: 404); ServerMapper.Merge(request, server); @@ -255,9 +253,9 @@ public class ServersController : Controller // Check if the specified allocations are enough for the star if (allocations.Count < server.Star.RequiredAllocations) { - throw new HttpApiException( + return Problem( $"You need to specify at least {server.Star.RequiredAllocations} allocation(s)", - 400 + statusCode: 400 ); } @@ -287,7 +285,7 @@ public class ServersController : Controller } [HttpDelete("{id:int}")] - public async Task Delete([FromRoute] int id, [FromQuery] bool force = false) + public async Task Delete([FromRoute] int id, [FromQuery] bool force = false) { var server = await ServerRepository .Get() @@ -299,12 +297,12 @@ public class ServersController : Controller .FirstOrDefaultAsync(x => x.Id == id); if (server == null) - throw new HttpApiException("No server with that id found", 404); + return Problem("No server with that id found", statusCode: 404); server.Variables.Clear(); server.Backups.Clear(); server.Allocations.Clear(); - + try { // If the sync fails on the node and we aren't forcing the deletion, @@ -325,5 +323,6 @@ public class ServersController : Controller } await ServerRepository.Remove(server); + return NoContent(); } } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarDockerImagesController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarDockerImagesController.cs index 5d2daa8..06440a3 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarDockerImagesController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarDockerImagesController.cs @@ -5,6 +5,7 @@ using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Helpers; using Microsoft.AspNetCore.Authorization; +using MoonCore.Extended.Models; using MoonCore.Helpers; using MoonCore.Models; using MoonlightServers.ApiServer.Database.Entities; @@ -32,10 +33,9 @@ public class StarDockerImagesController : Controller [HttpGet] [Authorize(Policy = "permissions:admin.servers.stars.get")] - public async Task> Get( + public async Task>> Get( [FromRoute] int starId, - [FromQuery] [Range(0, int.MaxValue)] int page, - [FromQuery] [Range(1, 100)] int pageSize + [FromQuery] PagedOptions options ) { var starExists = StarRepository @@ -43,7 +43,7 @@ public class StarDockerImagesController : Controller .Any(x => x.Id == starId); if (!starExists) - throw new HttpApiException("No star with this id found", 404); + return Problem("No star with this id found", statusCode: 404); var query = DockerImageRepository .Get() @@ -51,50 +51,50 @@ public class StarDockerImagesController : Controller var count = await query.CountAsync(); - var items = await query + var dockerImages = await query .OrderBy(x => x.Id) - .Skip(page * pageSize) - .Take(pageSize) + .Skip(options.Page * options.PageSize) + .Take(options.PageSize) + .AsNoTracking() + .ProjectToAdminResponse() .ToArrayAsync(); - var mappedItems = items - .Select(DockerImageMapper.ToAdminResponse) - .ToArray(); - - return new PagedData() + return new PagedData() { - Items = mappedItems, - CurrentPage = page, - PageSize = pageSize, + Items = dockerImages, + CurrentPage = options.Page, + PageSize = options.PageSize, TotalItems = count, - TotalPages = count == 0 ? 0 : count / pageSize + TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize) }; } [HttpGet("{id:int}")] [Authorize(Policy = "permissions:admin.servers.stars.read")] - public async Task GetSingle([FromRoute] int starId, [FromRoute] int id) + public async Task> GetSingle([FromRoute] int starId, [FromRoute] int id) { var starExists = StarRepository .Get() .Any(x => x.Id == starId); if (!starExists) - throw new HttpApiException("No star with this id found", 404); + return Problem("No star with this id found", statusCode: 404); var dockerImage = await DockerImageRepository .Get() - .FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId); + .Where(x => x.Id == id && x.Star.Id == starId) + .ProjectToAdminResponse() + .FirstOrDefaultAsync(); if (dockerImage == null) - throw new HttpApiException("No star docker image with this id found", 404); + return Problem("No star docker image with this id found", statusCode: 404); - return DockerImageMapper.ToAdminResponse(dockerImage); + return dockerImage; } - [HttpPost("")] + [HttpPost] [Authorize(Policy = "permissions:admin.servers.stars.write")] - public async Task Create( + public async Task> Create( [FromRoute] int starId, [FromBody] CreateStarDockerImageRequest request ) @@ -104,7 +104,7 @@ public class StarDockerImagesController : Controller .FirstOrDefaultAsync(x => x.Id == starId); if (star == null) - throw new HttpApiException("No star with this id found", 404); + return Problem("No star with this id found", statusCode: 404); var dockerImage = DockerImageMapper.ToDockerImage(request); dockerImage.Star = star; @@ -116,7 +116,7 @@ public class StarDockerImagesController : Controller [HttpPatch("{id:int}")] [Authorize(Policy = "permissions:admin.servers.stars.write")] - public async Task Update( + public async Task> Update( [FromRoute] int starId, [FromRoute] int id, [FromBody] UpdateStarDockerImageRequest request @@ -127,14 +127,14 @@ public class StarDockerImagesController : Controller .Any(x => x.Id == starId); if (!starExists) - throw new HttpApiException("No star with this id found", 404); + return Problem("No star with this id found", statusCode: 404); var dockerImage = await DockerImageRepository .Get() .FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId); if (dockerImage == null) - throw new HttpApiException("No star docker image with this id found", 404); + return Problem("No star docker image with this id found", statusCode: 404); DockerImageMapper.Merge(request, dockerImage); await DockerImageRepository.Update(dockerImage); @@ -144,22 +144,23 @@ public class StarDockerImagesController : Controller [HttpDelete("{id:int}")] [Authorize(Policy = "permissions:admin.servers.stars.write")] - public async Task Delete([FromRoute] int starId, [FromRoute] int id) + public async Task Delete([FromRoute] int starId, [FromRoute] int id) { var starExists = StarRepository .Get() .Any(x => x.Id == starId); if (!starExists) - throw new HttpApiException("No star with this id found", 404); + return Problem("No star with this id found", statusCode: 404); var dockerImage = await DockerImageRepository .Get() .FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId); if (dockerImage == null) - throw new HttpApiException("No star docker image with this id found", 404); + return Problem("No star docker image with this id found", statusCode: 404); await DockerImageRepository.Remove(dockerImage); + return NoContent(); } } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarImportExportController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarImportExportController.cs index 37f7cb4..e64c458 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarImportExportController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarImportExportController.cs @@ -23,18 +23,15 @@ public class StarImportExportController : Controller [HttpGet("{starId:int}/export")] [Authorize(Policy = "permissions:admin.servers.stars.get")] - public async Task Export([FromRoute] int starId) + public async Task Export([FromRoute] int starId) { var exportedStar = await ImportExportService.Export(starId); - - Response.StatusCode = 200; - Response.ContentType = "application/json"; - await Response.WriteAsync(exportedStar); + return Content(exportedStar, "application/json"); } [HttpPost("import")] [Authorize(Policy = "permissions:admin.servers.stars.create")] - public async Task Import() + public async Task Import() { if (Request.Form.Files.Count == 0) throw new HttpApiException("No file to import provided", 400); diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarVariablesController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarVariablesController.cs index 7a3d8e7..f8815e2 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarVariablesController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarVariablesController.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authorization; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; +using MoonCore.Extended.Models; using MoonCore.Models; using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Mappers; @@ -29,10 +30,9 @@ public class StarVariablesController : Controller [HttpGet] [Authorize(Policy = "permissions:admin.servers.stars.get")] - public async Task> Get( + public async Task>> Get( [FromRoute] int starId, - [FromQuery] [Range(0, int.MaxValue)] int page, - [FromQuery] [Range(1, 100)] int pageSize + [FromQuery] PagedOptions options ) { var starExists = StarRepository @@ -40,7 +40,7 @@ public class StarVariablesController : Controller .Any(x => x.Id == starId); if (!starExists) - throw new HttpApiException("No star with this id found", 404); + return Problem("No star with this id found", statusCode: 404); var query = VariableRepository .Get() @@ -48,29 +48,27 @@ public class StarVariablesController : Controller var count = await query.CountAsync(); - var items = await query + var variables = await query .OrderBy(x => x.Id) - .Skip(page * pageSize) - .Take(pageSize) + .Skip(options.Page * options.PageSize) + .Take(options.PageSize) + .AsNoTracking() + .ProjectToAdminResponse() .ToArrayAsync(); - var mappedItems = items - .Select(StarVariableMapper.ToAdminResponse) - .ToArray(); - - return new PagedData() + return new PagedData() { - Items = mappedItems, - CurrentPage = page, - PageSize = pageSize, + Items = variables, + CurrentPage = options.Page, + PageSize = options.PageSize, TotalItems = count, - TotalPages = count == 0 ? 0 : count / pageSize + TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize) }; } [HttpGet("{id:int}")] [Authorize(Policy = "permissions:admin.servers.stars.get")] - public async Task GetSingle( + public async Task GetSingle( [FromRoute] int starId, [FromRoute] int id ) @@ -94,7 +92,7 @@ public class StarVariablesController : Controller [HttpPost("")] [Authorize(Policy = "permissions:admin.servers.stars.create")] - public async Task Create([FromRoute] int starId, + public async Task Create([FromRoute] int starId, [FromBody] CreateStarVariableRequest request) { var star = StarRepository @@ -114,7 +112,7 @@ public class StarVariablesController : Controller [HttpPatch("{id:int}")] [Authorize(Policy = "permissions:admin.servers.stars.update")] - public async Task Update( + public async Task Update( [FromRoute] int starId, [FromRoute] int id, [FromBody] UpdateStarVariableRequest request diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarsController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarsController.cs index 193e5c6..d72f143 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarsController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarsController.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authorization; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; +using MoonCore.Extended.Models; using MoonCore.Models; using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Mappers; @@ -25,51 +26,48 @@ public class StarsController : Controller [HttpGet] [Authorize(Policy = "permissions:admin.servers.stars.read")] - public async Task> Get( - [FromQuery] [Range(0, int.MaxValue)] int page, - [FromQuery] [Range(1, 100)] int pageSize - ) + public async Task>> Get([FromQuery] PagedOptions options) { var count = await StarRepository.Get().CountAsync(); - var items = await StarRepository + var stars = await StarRepository .Get() .OrderBy(x => x.Id) - .Skip(page * pageSize) - .Take(pageSize) + .Skip(options.Page * options.PageSize) + .Take(options.PageSize) + .AsNoTracking() + .ProjectToAdminResponse() .ToArrayAsync(); - var mappedItems = items - .Select(StarMapper.ToAdminResponse) - .ToArray(); - - return new PagedData() + return new PagedData() { - CurrentPage = page, - Items = mappedItems, - PageSize = pageSize, + CurrentPage = options.Page, + Items = stars, + PageSize = options.PageSize, TotalItems = count, - TotalPages = count == 0 ? 0 : count / pageSize + TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize) }; } [HttpGet("{id:int}")] [Authorize(Policy = "permissions:admin.servers.stars.read")] - public async Task GetSingle([FromRoute] int id) + public async Task> GetSingle([FromRoute] int id) { var star = await StarRepository .Get() + .AsNoTracking() + .ProjectToAdminResponse() .FirstOrDefaultAsync(x => x.Id == id); if (star == null) - throw new HttpApiException("No star with that id found", 404); + return Problem("No star with that id found", statusCode: 404); - return StarMapper.ToAdminResponse(star); + return star; } [HttpPost] [Authorize(Policy = "permissions:admin.servers.stars.create")] - public async Task Create([FromBody] CreateStarRequest request) + public async Task> Create([FromBody] CreateStarRequest request) { var star = StarMapper.ToStar(request); @@ -95,7 +93,7 @@ public class StarsController : Controller [HttpPatch("{id:int}")] [Authorize(Policy = "permissions:admin.servers.stars.update")] - public async Task Update( + public async Task> Update( [FromRoute] int id, [FromBody] UpdateStarRequest request ) @@ -105,7 +103,7 @@ public class StarsController : Controller .FirstOrDefaultAsync(x => x.Id == id); if (star == null) - throw new HttpApiException("No star with that id found", 404); + return Problem("No star with that id found", statusCode: 404); StarMapper.Merge(request, star); await StarRepository.Update(star); @@ -115,15 +113,16 @@ public class StarsController : Controller [HttpDelete("{id:int}")] [Authorize(Policy = "permissions:admin.servers.stars.delete")] - public async Task Delete([FromRoute] int id) + public async Task Delete([FromRoute] int id) { var star = await StarRepository .Get() .FirstOrDefaultAsync(x => x.Id == id); if (star == null) - throw new HttpApiException("No star with that id found", 404); + return Problem("No star with that id found", statusCode: 404); await StarRepository.Remove(star); + return NoContent(); } } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/FilesController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/FilesController.cs index f099de0..6132295 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/FilesController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/FilesController.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; -using Moonlight.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Services; using MoonlightServers.DaemonShared.Enums; @@ -38,11 +37,14 @@ public class FilesController : Controller } [HttpGet("list")] - public async Task List([FromRoute] int serverId, [FromQuery] string path) + public async Task> List([FromRoute] int serverId, [FromQuery] string path) { var server = await GetServerById(serverId, ServerPermissionLevel.Read); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); - var entries = await ServerFileSystemService.List(server, path); + var entries = await ServerFileSystemService.List(server.Value, path); return entries.Select(x => new ServerFilesEntryResponse() { @@ -55,41 +57,62 @@ public class FilesController : Controller } [HttpPost("move")] - public async Task Move([FromRoute] int serverId, [FromQuery] string oldPath, [FromQuery] string newPath) + public async Task Move([FromRoute] int serverId, [FromQuery] string oldPath, [FromQuery] string newPath) { var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); - await ServerFileSystemService.Move(server, oldPath, newPath); + await ServerFileSystemService.Move(server.Value, oldPath, newPath); + return NoContent(); } [HttpDelete("delete")] - public async Task Delete([FromRoute] int serverId, [FromQuery] string path) + public async Task Delete([FromRoute] int serverId, [FromQuery] string path) { var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); - await ServerFileSystemService.Delete(server, path); + await ServerFileSystemService.Delete(server.Value, path); + return NoContent(); } [HttpPost("mkdir")] - public async Task Mkdir([FromRoute] int serverId, [FromQuery] string path) + public async Task Mkdir([FromRoute] int serverId, [FromQuery] string path) { var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); - await ServerFileSystemService.Mkdir(server, path); + await ServerFileSystemService.Mkdir(server.Value, path); + return NoContent(); } [HttpPost("touch")] - public async Task Touch([FromRoute] int serverId, [FromQuery] string path) + public async Task Touch([FromRoute] int serverId, [FromQuery] string path) { var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); - await ServerFileSystemService.Mkdir(server, path); + await ServerFileSystemService.Mkdir(server.Value, path); + return NoContent(); } [HttpGet("upload")] - public async Task Upload([FromRoute] int serverId) + public async Task> Upload([FromRoute] int serverId) { - var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite); + var serverResult = await GetServerById(serverId, ServerPermissionLevel.ReadWrite); + + if (serverResult.Value == null) + return serverResult.Result ?? Problem("Unable to retrieve server"); + + var server = serverResult.Value; var accessToken = NodeService.CreateAccessToken( server.Node, @@ -114,9 +137,14 @@ public class FilesController : Controller } [HttpGet("download")] - public async Task Download([FromRoute] int serverId, [FromQuery] string path) + public async Task> Download([FromRoute] int serverId, [FromQuery] string path) { - var server = await GetServerById(serverId, ServerPermissionLevel.Read); + var serverResult = await GetServerById(serverId, ServerPermissionLevel.Read); + + if (serverResult.Value == null) + return serverResult.Result ?? Problem("Unable to retrieve server"); + + var server = serverResult.Value; var accessToken = NodeService.CreateAccessToken( server.Node, @@ -142,28 +170,36 @@ public class FilesController : Controller } [HttpPost("compress")] - public async Task Compress([FromRoute] int serverId, [FromBody] ServerFilesCompressRequest request) + public async Task Compress([FromRoute] int serverId, [FromBody] ServerFilesCompressRequest request) { var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); if (!Enum.TryParse(request.Type, true, out CompressType type)) - throw new HttpApiException("Invalid compress type provided", 400); + return Problem("Invalid compress type provided", statusCode: 400); - await ServerFileSystemService.Compress(server, type, request.Items, request.Destination); + await ServerFileSystemService.Compress(server.Value, type, request.Items, request.Destination); + return Ok(); } [HttpPost("decompress")] - public async Task Decompress([FromRoute] int serverId, [FromBody] ServerFilesDecompressRequest request) + public async Task Decompress([FromRoute] int serverId, [FromBody] ServerFilesDecompressRequest request) { var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); if (!Enum.TryParse(request.Type, true, out CompressType type)) - throw new HttpApiException("Invalid compress type provided", 400); + return Problem("Invalid decompress type provided", statusCode: 400); - await ServerFileSystemService.Decompress(server, type, request.Path, request.Destination); + await ServerFileSystemService.Decompress(server.Value, type, request.Path, request.Destination); + return NoContent(); } - private async Task GetServerById(int serverId, ServerPermissionLevel level) + private async Task> GetServerById(int serverId, ServerPermissionLevel level) { var server = await ServerRepository .Get() @@ -171,7 +207,7 @@ public class FilesController : Controller .FirstOrDefaultAsync(x => x.Id == serverId); if (server == null) - throw new HttpApiException("No server with this id found", 404); + return Problem("No server with this id found", statusCode: 404); var authorizeResult = await AuthorizeService.Authorize( User, server, @@ -181,9 +217,9 @@ public class FilesController : Controller if (!authorizeResult.Succeeded) { - throw new HttpApiException( + return Problem( authorizeResult.Message ?? "No permission for the requested resource", - 403 + statusCode: 403 ); } diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/PowerController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/PowerController.cs index d5392ac..11a15f6 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/PowerController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/PowerController.cs @@ -14,7 +14,7 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Client; [ApiController] [Authorize] -[Route("api/client/servers")] +[Route("api/client/servers/{serverId:int}")] public class PowerController : Controller { private readonly DatabaseRepository ServerRepository; @@ -32,31 +32,46 @@ public class PowerController : Controller AuthorizeService = authorizeService; } - [HttpPost("{serverId:int}/start")] + [HttpPost("start")] [Authorize] - public async Task Start([FromRoute] int serverId) + public async Task Start([FromRoute] int serverId) { var server = await GetServerById(serverId); - await ServerService.Start(server); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); + + await ServerService.Start(server.Value); + return NoContent(); } - [HttpPost("{serverId:int}/stop")] + [HttpPost("stop")] [Authorize] - public async Task Stop([FromRoute] int serverId) + public async Task Stop([FromRoute] int serverId) { var server = await GetServerById(serverId); - await ServerService.Stop(server); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); + + await ServerService.Stop(server.Value); + return NoContent(); } - [HttpPost("{serverId:int}/kill")] + [HttpPost("kill")] [Authorize] - public async Task Kill([FromRoute] int serverId) + public async Task Kill([FromRoute] int serverId) { var server = await GetServerById(serverId); - await ServerService.Kill(server); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); + + await ServerService.Kill(server.Value); + return NoContent(); } - private async Task GetServerById(int serverId) + private async Task> GetServerById(int serverId) { var server = await ServerRepository .Get() @@ -64,7 +79,7 @@ public class PowerController : Controller .FirstOrDefaultAsync(x => x.Id == serverId); if (server == null) - throw new HttpApiException("No server with this id found", 404); + return Problem("No server with this id found", statusCode: 404); var authorizeResult = await AuthorizeService.Authorize( User, server, @@ -74,9 +89,9 @@ public class PowerController : Controller if (!authorizeResult.Succeeded) { - throw new HttpApiException( + return Problem( authorizeResult.Message ?? "No permission for the requested resource", - 403 + statusCode: 403 ); } diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs index b7450be..b4b9ded 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/ServersController.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; +using MoonCore.Extended.Models; using MoonCore.Models; using Moonlight.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Database.Entities; @@ -51,15 +52,12 @@ public class ServersController : Controller } [HttpGet] - public async Task> GetAll( - [FromQuery] [Range(0, int.MaxValue)] int page, - [FromQuery] [Range(0, 100)] int pageSize - ) + public async Task>> GetAll([FromQuery] PagedOptions options) { - var userIdClaim = User.FindFirstValue("userId"); + var userIdClaim = User.FindFirstValue("UserId"); if (string.IsNullOrEmpty(userIdClaim)) - throw new HttpApiException("Only users are able to use this endpoint", 400); + return Problem("Only users are able to use this endpoint", statusCode: 400); var userId = int.Parse(userIdClaim); @@ -71,11 +69,12 @@ public class ServersController : Controller .Where(x => x.OwnerId == userId); var count = await query.CountAsync(); - + var items = await query .OrderBy(x => x.Id) - .Skip(page * pageSize) - .Take(pageSize) + .Skip(options.Page * options.PageSize) + .Take(options.PageSize) + .AsNoTracking() .ToArrayAsync(); var mappedItems = items.Select(x => new ServerDetailResponse() @@ -98,23 +97,20 @@ public class ServersController : Controller return new PagedData() { Items = mappedItems, - CurrentPage = page, - PageSize = pageSize, + CurrentPage = options.Page, TotalItems = count, - TotalPages = count == 0 ? 0 : count / pageSize + PageSize = options.PageSize, + TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize) }; } [HttpGet("shared")] - public async Task> GetAllShared( - [FromQuery] [Range(0, int.MaxValue)] int page, - [FromQuery] [Range(0, 100)] int pageSize - ) + public async Task>> GetAllShared([FromQuery] PagedOptions options) { - var userIdClaim = User.FindFirstValue("userId"); + var userIdClaim = User.FindFirstValue("UserId"); if (string.IsNullOrEmpty(userIdClaim)) - throw new HttpApiException("Only users are able to use this endpoint", 400); + return Problem("Only users are able to use this endpoint", statusCode: 400); var userId = int.Parse(userIdClaim); @@ -129,11 +125,11 @@ public class ServersController : Controller .Where(x => x.UserId == userId); var count = await query.CountAsync(); - + var items = await query .OrderBy(x => x.Id) - .Skip(page * pageSize) - .Take(pageSize) + .Skip(options.Page * options.PageSize) + .Take(options.PageSize) .ToArrayAsync(); var ownerIds = items @@ -171,15 +167,15 @@ public class ServersController : Controller return new PagedData() { Items = mappedItems, - CurrentPage = page, - PageSize = pageSize, + CurrentPage = options.Page, TotalItems = count, - TotalPages = count == 0 ? 0 : count / pageSize + PageSize = options.PageSize, + TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize) }; } [HttpGet("{serverId:int}")] - public async Task Get([FromRoute] int serverId) + public async Task> Get([FromRoute] int serverId) { var server = await ServerRepository .Get() @@ -189,7 +185,7 @@ public class ServersController : Controller .FirstOrDefaultAsync(x => x.Id == serverId); if (server == null) - throw new HttpApiException("No server with this id found", 404); + return Problem("No server with this id found", statusCode: 404); var authorizationResult = await AuthorizeService.Authorize( User, @@ -200,9 +196,9 @@ public class ServersController : Controller if (!authorizationResult.Succeeded) { - throw new HttpApiException( + return Problem( authorizationResult.Message ?? "No server with this id found", - 404 + statusCode: 404 ); } @@ -242,15 +238,18 @@ public class ServersController : Controller } [HttpGet("{serverId:int}/status")] - public async Task GetStatus([FromRoute] int serverId) + public async Task> GetStatus([FromRoute] int serverId) { var server = await GetServerById( serverId, ServerPermissionConstants.Console, ServerPermissionLevel.None ); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); - var status = await ServerService.GetStatus(server); + var status = await ServerService.GetStatus(server.Value); return new ServerStatusResponse() { @@ -259,13 +258,18 @@ public class ServersController : Controller } [HttpGet("{serverId:int}/ws")] - public async Task GetWebSocket([FromRoute] int serverId) + public async Task> GetWebSocket([FromRoute] int serverId) { - var server = await GetServerById( + var serverResult = await GetServerById( serverId, ServerPermissionConstants.Console, ServerPermissionLevel.Read ); + + if (serverResult.Value == null) + return serverResult.Result ?? Problem("Unable to retrieve server"); + + var server = serverResult.Value; // TODO: Handle transparent node proxy @@ -288,15 +292,18 @@ public class ServersController : Controller } [HttpGet("{serverId:int}/logs")] - public async Task GetLogs([FromRoute] int serverId) + public async Task> GetLogs([FromRoute] int serverId) { var server = await GetServerById( serverId, ServerPermissionConstants.Console, ServerPermissionLevel.Read ); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); - var logs = await ServerService.GetLogs(server); + var logs = await ServerService.GetLogs(server.Value); return new ServerLogsResponse() { @@ -305,15 +312,18 @@ public class ServersController : Controller } [HttpGet("{serverId:int}/stats")] - public async Task GetStats([FromRoute] int serverId) + public async Task> GetStats([FromRoute] int serverId) { var server = await GetServerById( serverId, ServerPermissionConstants.Console, ServerPermissionLevel.Read ); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); - var stats = await ServerService.GetStats(server); + var stats = await ServerService.GetStats(server.Value); return new ServerStatsResponse() { @@ -327,7 +337,7 @@ public class ServersController : Controller } [HttpPost("{serverId:int}/command")] - public async Task Command([FromRoute] int serverId, [FromBody] ServerCommandRequest request) + public async Task Command([FromRoute] int serverId, [FromBody] ServerCommandRequest request) { var server = await GetServerById( serverId, @@ -335,10 +345,15 @@ public class ServersController : Controller ServerPermissionLevel.ReadWrite ); - await ServerService.RunCommand(server, request.Command); + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); + + await ServerService.RunCommand(server.Value, request.Command); + + return NoContent(); } - private async Task GetServerById(int serverId, string permissionId, ServerPermissionLevel level) + private async Task> GetServerById(int serverId, string permissionId, ServerPermissionLevel level) { var server = await ServerRepository .Get() @@ -346,15 +361,15 @@ public class ServersController : Controller .FirstOrDefaultAsync(x => x.Id == serverId); if (server == null) - throw new HttpApiException("No server with this id found", 404); + return Problem("No server with this id found", statusCode: 404); var authorizeResult = await AuthorizeService.Authorize(User, server, permissionId, level); if (!authorizeResult.Succeeded) { - throw new HttpApiException( + return Problem( authorizeResult.Message ?? "No permission for the requested resource", - 403 + statusCode: 403 ); } diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/SettingsController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/SettingsController.cs index e359030..62f9b95 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/SettingsController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/SettingsController.cs @@ -32,13 +32,18 @@ public class SettingsController : Controller [HttpPost("{serverId:int}/install")] [Authorize] - public async Task Install([FromRoute] int serverId) + public async Task Install([FromRoute] int serverId) { var server = await GetServerById(serverId); - await ServerService.Install(server); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); + + await ServerService.Install(server.Value); + return NoContent(); } - private async Task GetServerById(int serverId) + private async Task> GetServerById(int serverId) { var server = await ServerRepository .Get() @@ -46,7 +51,7 @@ public class SettingsController : Controller .FirstOrDefaultAsync(x => x.Id == serverId); if (server == null) - throw new HttpApiException("No server with this id found", 404); + return Problem("No server with this id found", statusCode: 404); var authorizeResult = await AuthorizeService.Authorize( User, server, @@ -56,9 +61,9 @@ public class SettingsController : Controller if (!authorizeResult.Succeeded) { - throw new HttpApiException( + return Problem( authorizeResult.Message ?? "No permission for the requested resource", - 403 + statusCode: 403 ); } diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/SharesController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/SharesController.cs index ebf2f1e..4d2fcfa 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/SharesController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/SharesController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; +using MoonCore.Extended.Models; using MoonCore.Models; using Moonlight.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Database.Entities; @@ -41,24 +42,26 @@ public class SharesController : Controller } [HttpGet] - public async Task> GetAll( + public async Task>> GetAll( [FromRoute] int serverId, - [FromQuery] [Range(0, int.MaxValue)] int page, - [FromQuery] [Range(1, 100)] int pageSize + [FromQuery] PagedOptions options ) { var server = await GetServerById(serverId); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); var query = ShareRepository .Get() - .Where(x => x.Server.Id == server.Id); + .Where(x => x.Server.Id == server.Value.Id); var count = await query.CountAsync(); var items = await query .OrderBy(x => x.Id) - .Skip(page * pageSize) - .Take(pageSize) + .Skip(options.Page * options.PageSize) + .Take(options.PageSize) .ToArrayAsync(); var userIds = items @@ -81,27 +84,30 @@ public class SharesController : Controller return new PagedData() { Items = mappedItems, - CurrentPage = page, - PageSize = pageSize, + CurrentPage = options.Page, + PageSize = options.PageSize, TotalItems = count, - TotalPages = count == 0 ? 0 : count / pageSize + TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize) }; } [HttpGet("{id:int}")] - public async Task Get( + public async Task> Get( [FromRoute] int serverId, [FromRoute] int id ) { var server = await GetServerById(serverId); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); var share = await ShareRepository .Get() - .FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.Id == id); + .FirstOrDefaultAsync(x => x.Server.Id == server.Value.Id && x.Id == id); if (share == null) - throw new HttpApiException("A share with that id cannot be found", 404); + return Problem("A share with that id cannot be found", statusCode: 404); var user = await UserRepository .Get() @@ -118,23 +124,26 @@ public class SharesController : Controller } [HttpPost] - public async Task Create( + public async Task> Create( [FromRoute] int serverId, [FromBody] CreateShareRequest request ) { var server = await GetServerById(serverId); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); var user = await UserRepository .Get() .FirstOrDefaultAsync(x => x.Username == request.Username); if (user == null) - throw new HttpApiException("A user with that username could not be found", 400); + return Problem("A user with that username could not be found", statusCode: 400); var share = new ServerShare() { - Server = server, + Server = server.Value, Content = ShareMapper.MapToServerShareContent(request.Permissions), CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, @@ -154,20 +163,23 @@ public class SharesController : Controller } [HttpPatch("{id:int}")] - public async Task Update( + public async Task> Update( [FromRoute] int serverId, [FromRoute] int id, [FromBody] UpdateShareRequest request ) { var server = await GetServerById(serverId); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); var share = await ShareRepository .Get() - .FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.Id == id); + .FirstOrDefaultAsync(x => x.Server.Id == server.Value.Id && x.Id == id); if (share == null) - throw new HttpApiException("A share with that id cannot be found", 404); + return Problem("A share with that id cannot be found", statusCode: 404); share.Content = ShareMapper.MapToServerShareContent(request.Permissions); @@ -180,7 +192,7 @@ public class SharesController : Controller .FirstOrDefaultAsync(x => x.Id == share.UserId); if (user == null) - throw new HttpApiException("A user with that id could not be found", 400); + return Problem("A user with that id could not be found", statusCode: 400); var mappedItem = new ServerShareResponse() { @@ -193,31 +205,35 @@ public class SharesController : Controller } [HttpDelete("{id:int}")] - public async Task Delete( + public async Task Delete( [FromRoute] int serverId, [FromRoute] int id ) { var server = await GetServerById(serverId); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); var share = await ShareRepository .Get() - .FirstOrDefaultAsync(x => x.Server.Id == server.Id && x.Id == id); + .FirstOrDefaultAsync(x => x.Server.Id == server.Value.Id && x.Id == id); if (share == null) - throw new HttpApiException("A share with that id cannot be found", 404); + return Problem("A share with that id cannot be found", statusCode: 404); await ShareRepository.Remove(share); + return NoContent(); } - private async Task GetServerById(int serverId) + private async Task> GetServerById(int serverId) { var server = await ServerRepository .Get() .FirstOrDefaultAsync(x => x.Id == serverId); if (server == null) - throw new HttpApiException("No server with this id found", 404); + return Problem("No server with this id found", statusCode: 404); var authorizeResult = await AuthorizeService.Authorize( User, server, @@ -227,9 +243,9 @@ public class SharesController : Controller if (!authorizeResult.Succeeded) { - throw new HttpApiException( + return Problem( authorizeResult.Message ?? "No permission for the requested resource", - 403 + statusCode: 403 ); } diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/VariablesController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/VariablesController.cs index fc0fa0d..df5ff0c 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Client/VariablesController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/VariablesController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; +using MoonCore.Extended.Models; using MoonCore.Models; using Moonlight.ApiServer.Database.Entities; using MoonlightServers.ApiServer.Database.Entities; @@ -40,24 +41,26 @@ public class VariablesController : Controller } [HttpGet] - public async Task> Get( + public async Task>> Get( [FromRoute] int serverId, - [FromQuery] [Range(0, int.MaxValue)] int page, - [FromQuery] [Range(1, 100)] int pageSize + [FromQuery] PagedOptions options ) { var server = await GetServerById(serverId, ServerPermissionLevel.Read); + + if (server.Value == null) + return server.Result ?? Problem("Unable to retrieve server"); var query = StarVariableRepository .Get() - .Where(x => x.Star.Id == server.Star.Id); + .Where(x => x.Star.Id == server.Value.Star.Id); var count = await query.CountAsync(); var starVariables = await query .OrderBy(x => x.Id) - .Skip(page * pageSize) - .Take(pageSize) + .Skip(options.Page * options.PageSize) + .Take(options.PageSize) .ToArrayAsync(); var starVariableKeys = starVariables @@ -66,7 +69,7 @@ public class VariablesController : Controller var serverVariables = await ServerVariableRepository .Get() - .Where(x => x.Server.Id == server.Id && starVariableKeys.Contains(x.Key)) + .Where(x => x.Server.Id == server.Value.Id && starVariableKeys.Contains(x.Key)) .ToArrayAsync(); var responses = starVariables.Select(starVariable => @@ -87,22 +90,27 @@ public class VariablesController : Controller return new PagedData() { Items = responses, - CurrentPage = page, - PageSize = pageSize, + CurrentPage = options.Page, + PageSize = options.PageSize, TotalItems = count, - TotalPages = count == 0 ? 0 : count / pageSize + TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize) }; } [HttpPut] - public async Task UpdateSingle( + public async Task> UpdateSingle( [FromRoute] int serverId, [FromBody] UpdateServerVariableRequest request ) { // TODO: Handle filter - var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite); + var serverResult = await GetServerById(serverId, ServerPermissionLevel.ReadWrite); + + if (serverResult.Value == null) + return serverResult.Result ?? Problem("Unable to retrieve server"); + + var server = serverResult.Value; var serverVariable = server.Variables.FirstOrDefault(x => x.Key == request.Key); var starVariable = server.Star.Variables.FirstOrDefault(x => x.Key == request.Key); @@ -125,12 +133,17 @@ public class VariablesController : Controller } [HttpPatch] - public async Task Update( + public async Task> Update( [FromRoute] int serverId, [FromBody] UpdateServerVariableRangeRequest request ) { - var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite); + var serverResult = await GetServerById(serverId, ServerPermissionLevel.ReadWrite); + + if (serverResult.Value == null) + return serverResult.Result ?? Problem("Unable to retrieve server"); + + var server = serverResult.Value; foreach (var variable in request.Variables) { @@ -164,15 +177,17 @@ public class VariablesController : Controller }).ToArray(); } - private async Task GetServerById(int serverId, ServerPermissionLevel level) + private async Task> GetServerById(int serverId, ServerPermissionLevel level) { var server = await ServerRepository .Get() .Include(x => x.Star) + .ThenInclude(x => x.Variables) + .Include(x => x.Variables) .FirstOrDefaultAsync(x => x.Id == serverId); if (server == null) - throw new HttpApiException("No server with this id found", 404); + return Problem("No server with this id found", statusCode: 404); var authorizeResult = await AuthorizeService.Authorize( User, server, @@ -182,9 +197,9 @@ public class VariablesController : Controller if (!authorizeResult.Succeeded) { - throw new HttpApiException( + return Problem( authorizeResult.Message ?? "No permission for the requested resource", - 403 + statusCode: 403 ); } diff --git a/MoonlightServers.ApiServer/Implementations/ServerAuthFilters/OwnerAuthFilter.cs b/MoonlightServers.ApiServer/Implementations/ServerAuthFilters/OwnerAuthFilter.cs index b4532f6..a2b88a3 100644 --- a/MoonlightServers.ApiServer/Implementations/ServerAuthFilters/OwnerAuthFilter.cs +++ b/MoonlightServers.ApiServer/Implementations/ServerAuthFilters/OwnerAuthFilter.cs @@ -19,7 +19,7 @@ public class OwnerAuthFilter : IServerAuthorizationFilter ServerPermissionLevel requiredLevel ) { - var userIdValue = user.FindFirstValue("userId"); + var userIdValue = user.FindFirstValue("UserId"); if (string.IsNullOrEmpty(userIdValue)) // This is the case for api keys return Task.FromResult(null); diff --git a/MoonlightServers.ApiServer/Mappers/AllocationMapper.cs b/MoonlightServers.ApiServer/Mappers/AllocationMapper.cs index a751110..f4a3790 100644 --- a/MoonlightServers.ApiServer/Mappers/AllocationMapper.cs +++ b/MoonlightServers.ApiServer/Mappers/AllocationMapper.cs @@ -11,4 +11,8 @@ public static partial class AllocationMapper public static partial NodeAllocationResponse ToNodeAllocation(Allocation allocation); public static partial Allocation ToAllocation(CreateNodeAllocationRequest request); public static partial void Merge(UpdateNodeAllocationRequest request, Allocation allocation); + + // EF Projections + + public static partial IQueryable ProjectToAdminResponse(this IQueryable allocations); } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Mappers/DockerImageMapper.cs b/MoonlightServers.ApiServer/Mappers/DockerImageMapper.cs index 0af7027..1e5b806 100644 --- a/MoonlightServers.ApiServer/Mappers/DockerImageMapper.cs +++ b/MoonlightServers.ApiServer/Mappers/DockerImageMapper.cs @@ -8,7 +8,11 @@ namespace MoonlightServers.ApiServer.Mappers; [Mapper(AllowNullPropertyAssignment = false)] public static partial class DockerImageMapper { - public static partial StarDockerImageDetailResponse ToAdminResponse(StarDockerImage dockerImage); + public static partial StarDockerImageResponse ToAdminResponse(StarDockerImage dockerImage); public static partial StarDockerImage ToDockerImage(CreateStarDockerImageRequest request); public static partial void Merge(UpdateStarDockerImageRequest request, StarDockerImage variable); + + // EF Migrations + + public static partial IQueryable ProjectToAdminResponse(this IQueryable dockerImages); } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Mappers/NodeMapper.cs b/MoonlightServers.ApiServer/Mappers/NodeMapper.cs index bd0a5d4..f7a7e76 100644 --- a/MoonlightServers.ApiServer/Mappers/NodeMapper.cs +++ b/MoonlightServers.ApiServer/Mappers/NodeMapper.cs @@ -11,4 +11,8 @@ public static partial class NodeMapper public static partial NodeResponse ToAdminNodeResponse(Node node); public static partial Node ToNode(CreateNodeRequest request); public static partial void Merge(UpdateNodeRequest request, Node node); + + // EF Projections + + public static partial IQueryable ProjectToAdminResponse(this IQueryable nodes); } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Mappers/ServerMapper.cs b/MoonlightServers.ApiServer/Mappers/ServerMapper.cs index bc4241e..2242acd 100644 --- a/MoonlightServers.ApiServer/Mappers/ServerMapper.cs +++ b/MoonlightServers.ApiServer/Mappers/ServerMapper.cs @@ -1,6 +1,7 @@ using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.Shared.Http.Requests.Admin.Servers; using MoonlightServers.Shared.Http.Responses.Admin.Servers; +using MoonlightServers.Shared.Http.Responses.Client.Servers; using Riok.Mapperly.Abstractions; namespace MoonlightServers.ApiServer.Mappers; @@ -20,6 +21,11 @@ public static partial class ServerMapper private static partial ServerResponse ToAdminServerResponse_Internal(Server server); + [MapperIgnoreSource(nameof(CreateServerRequest.Variables))] public static partial Server ToServer(CreateServerRequest request); public static partial void Merge(UpdateServerRequest request, Server server); + + // EF Projections + + public static partial IQueryable ProjectToAdminResponse(this IQueryable servers); } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Mappers/ServerVariableMapper.cs b/MoonlightServers.ApiServer/Mappers/ServerVariableMapper.cs index 2c8fd3a..4f2fa64 100644 --- a/MoonlightServers.ApiServer/Mappers/ServerVariableMapper.cs +++ b/MoonlightServers.ApiServer/Mappers/ServerVariableMapper.cs @@ -8,4 +8,8 @@ namespace MoonlightServers.ApiServer.Mappers; public static partial class ServerVariableMapper { public static partial ServerVariableResponse ToAdminResponse(ServerVariable serverVariable); + + // EF Projections + + public static partial IQueryable ProjectToAdminResponse(this IQueryable variables); } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Mappers/StarMapper.cs b/MoonlightServers.ApiServer/Mappers/StarMapper.cs index 9a61485..f1f7f34 100644 --- a/MoonlightServers.ApiServer/Mappers/StarMapper.cs +++ b/MoonlightServers.ApiServer/Mappers/StarMapper.cs @@ -9,7 +9,11 @@ namespace MoonlightServers.ApiServer.Mappers; [Mapper(AllowNullPropertyAssignment = false)] public static partial class StarMapper { - public static partial StarDetailResponse ToAdminResponse(Star star); + public static partial StarResponse ToAdminResponse(Star star); public static partial Star ToStar(CreateStarRequest request); public static partial void Merge(UpdateStarRequest request, Star star); + + // EF Projections + + public static partial IQueryable ProjectToAdminResponse(this IQueryable stars); } \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Mappers/StarVariableMapper.cs b/MoonlightServers.ApiServer/Mappers/StarVariableMapper.cs index 6b16817..3a5fbca 100644 --- a/MoonlightServers.ApiServer/Mappers/StarVariableMapper.cs +++ b/MoonlightServers.ApiServer/Mappers/StarVariableMapper.cs @@ -8,7 +8,11 @@ namespace MoonlightServers.ApiServer.Mappers; [Mapper(AllowNullPropertyAssignment = false)] public static partial class StarVariableMapper { - public static partial StarVariableDetailResponse ToAdminResponse(StarVariable variable); + public static partial StarVariableResponse ToAdminResponse(StarVariable variable); public static partial StarVariable ToStarVariable(CreateStarVariableRequest request); public static partial void Merge(UpdateStarVariableRequest request, StarVariable variable); + + // EF Projections + + public static partial IQueryable ProjectToAdminResponse(this IQueryable variables); } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/DownloadController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/DownloadController.cs deleted file mode 100644 index 6e5a51d..0000000 --- a/MoonlightServers.Daemon/Http/Controllers/Servers/DownloadController.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using MoonCore.Exceptions; -using MoonlightServers.Daemon.ServerSystem.SubSystems; -using MoonlightServers.Daemon.Services; - -namespace MoonlightServers.Daemon.Http.Controllers.Servers; - -[ApiController] -[Route("api/servers/download")] -[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverDownload")] -public class DownloadController : Controller -{ - private readonly ServerService ServerService; - - public DownloadController(ServerService serverService) - { - ServerService = serverService; - } - - [HttpGet] - public async Task Download() - { - var serverId = int.Parse(User.Claims.First(x => x.Type == "serverId").Value); - var path = User.Claims.First(x => x.Type == "path").Value; - - var server = ServerService.Find(serverId); - - if (server == null) - throw new HttpApiException("No server with this id found", 404); - - var storageSubSystem = server.GetRequiredSubSystem(); - - var fileSystem = await storageSubSystem.GetFileSystem(); - - await fileSystem.Read( - path, - async dataStream => - { - await Results.File(dataStream).ExecuteAsync(HttpContext); - } - ); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/ServerFileSystemController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/ServerFileSystemController.cs deleted file mode 100644 index 8c7d5e2..0000000 --- a/MoonlightServers.Daemon/Http/Controllers/Servers/ServerFileSystemController.cs +++ /dev/null @@ -1,99 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using MoonCore.Exceptions; -using MoonlightServers.Daemon.Helpers; -using MoonlightServers.Daemon.ServerSystem.SubSystems; -using MoonlightServers.Daemon.Services; -using MoonlightServers.DaemonShared.DaemonSide.Http.Requests; -using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers; - -namespace MoonlightServers.Daemon.Http.Controllers.Servers; - -[Authorize] -[ApiController] -[Route("api/servers/{id:int}/files")] -public class ServerFileSystemController : Controller -{ - private readonly ServerService ServerService; - - public ServerFileSystemController(ServerService serverService) - { - ServerService = serverService; - } - - [HttpGet("list")] - public async Task List([FromRoute] int id, [FromQuery] string path = "") - { - var fileSystem = await GetFileSystemById(id); - - return await fileSystem.List(path); - } - - [HttpPost("move")] - public async Task Move([FromRoute] int id, [FromQuery] string oldPath, [FromQuery] string newPath) - { - var fileSystem = await GetFileSystemById(id); - - await fileSystem.Move(oldPath, newPath); - } - - [HttpDelete("delete")] - public async Task Delete([FromRoute] int id, [FromQuery] string path) - { - var fileSystem = await GetFileSystemById(id); - - await fileSystem.Delete(path); - } - - [HttpPost("mkdir")] - public async Task Mkdir([FromRoute] int id, [FromQuery] string path) - { - var fileSystem = await GetFileSystemById(id); - - await fileSystem.Mkdir(path); - } - - [HttpPost("touch")] - public async Task Touch([FromRoute] int id, [FromQuery] string path) - { - var fileSystem = await GetFileSystemById(id); - - await fileSystem.Touch(path); - } - - [HttpPost("compress")] - public async Task Compress([FromRoute] int id, [FromBody] ServerFilesCompressRequest request) - { - var fileSystem = await GetFileSystemById(id); - - await fileSystem.Compress( - request.Items, - request.Destination, - request.Type - ); - } - - [HttpPost("decompress")] - public async Task Decompress([FromRoute] int id, [FromBody] ServerFilesDecompressRequest request) - { - var fileSystem = await GetFileSystemById(id); - - await fileSystem.Decompress( - request.Path, - request.Destination, - request.Type - ); - } - - private async Task GetFileSystemById(int serverId) - { - var server = ServerService.Find(serverId); - - if (server == null) - throw new HttpApiException("No server with this id found", 404); - - var storageSubSystem = server.GetRequiredSubSystem(); - - return await storageSubSystem.GetFileSystem(); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs deleted file mode 100644 index c04232f..0000000 --- a/MoonlightServers.Daemon/Http/Controllers/Servers/ServerPowerController.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using MoonCore.Exceptions; -using MoonlightServers.Daemon.Enums; -using MoonlightServers.Daemon.Services; -using ServerTrigger = MoonlightServers.Daemon.ServerSystem.ServerTrigger; - -namespace MoonlightServers.Daemon.Http.Controllers.Servers; - -[Authorize] -[ApiController] -[Route("api/servers")] -public class ServerPowerController : Controller -{ - private readonly NewServerService ServerService; - - public ServerPowerController(NewServerService serverService) - { - ServerService = serverService; - } - - [HttpPost("{serverId:int}/start")] - public async Task Start(int serverId) - { - var server = ServerService.Find(serverId); - - if (server == null) - throw new HttpApiException("No server with this id found", 404); - - await server.StateMachine.FireAsync(ServerTrigger.Start); - } - - [HttpPost("{serverId:int}/stop")] - public async Task Stop(int serverId) - { - var server = ServerService.Find(serverId); - - if (server == null) - throw new HttpApiException("No server with this id found", 404); - - await server.StateMachine.FireAsync(ServerTrigger.Stop); - } - - [HttpPost("{serverId:int}/install")] - public async Task Install(int serverId) - { - var server = ServerService.Find(serverId); - - if (server == null) - throw new HttpApiException("No server with this id found", 404); - - await server.StateMachine.FireAsync(ServerTrigger.Install); - } - - [HttpPost("{serverId:int}/kill")] - public async Task Kill(int serverId) - { - var server = ServerService.Find(serverId); - - if (server == null) - throw new HttpApiException("No server with this id found", 404); - - await server.StateMachine.FireAsync(ServerTrigger.Kill); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs deleted file mode 100644 index 519c793..0000000 --- a/MoonlightServers.Daemon/Http/Controllers/Servers/ServersController.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using MoonCore.Exceptions; -using MoonlightServers.Daemon.ServerSystem.SubSystems; -using MoonlightServers.Daemon.Services; -using MoonlightServers.DaemonShared.DaemonSide.Http.Requests; -using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers; -using MoonlightServers.DaemonShared.Enums; - -namespace MoonlightServers.Daemon.Http.Controllers.Servers; - -[Authorize] -[ApiController] -[Route("api/servers/{serverId:int}")] -public class ServersController : Controller -{ - private readonly NewServerService ServerService; - - public ServersController(NewServerService serverService) - { - ServerService = serverService; - } - - [HttpPost("sync")] - public async Task Sync([FromRoute] int serverId) - { - await ServerService.Sync(serverId); - } - - [HttpDelete] - public async Task Delete([FromRoute] int serverId) - { - await ServerService.Delete(serverId); - } - - [HttpGet("status")] - public Task GetStatus([FromRoute] int serverId) - { - var server = ServerService.Find(serverId); - - if (server == null) - throw new HttpApiException("No server with this id found", 404); - - var result = new ServerStatusResponse() - { - State = (ServerState)server.StateMachine.State - }; - - return Task.FromResult(result); - } - - [HttpGet("logs")] - public async Task GetLogs([FromRoute] int serverId) - { - var server = ServerService.Find(serverId); - - if (server == null) - throw new HttpApiException("No server with this id found", 404); - - var messages = server.Console.GetOutput(); - - return new ServerLogsResponse() - { - Messages = messages - }; - } - - [HttpGet("stats")] - public Task GetStats([FromRoute] int serverId) - { - var server = ServerService.Find(serverId); - - if (server == null) - throw new HttpApiException("No server with this id found", 404); -/* - var statsSubSystem = server.GetRequiredSubSystem(); - - return Task.FromResult(new() - { - CpuUsage = statsSubSystem.CurrentStats.CpuUsage, - MemoryUsage = statsSubSystem.CurrentStats.MemoryUsage, - NetworkRead = statsSubSystem.CurrentStats.NetworkRead, - NetworkWrite = statsSubSystem.CurrentStats.NetworkWrite, - IoRead = statsSubSystem.CurrentStats.IoRead, - IoWrite = statsSubSystem.CurrentStats.IoWrite - });*/ - - return Task.FromResult(new() - { - CpuUsage = 0, - MemoryUsage = 0, - NetworkRead = 0, - NetworkWrite = 0, - IoRead = 0, - IoWrite = 0 - }); - } - - [HttpPost("command")] - public async Task Command([FromRoute] int serverId, [FromBody] ServerCommandRequest request) - { - var server = ServerService.Find(serverId); - - if (server == null) - throw new HttpApiException("No server with this id found", 404); - - await server.Console.WriteToInput(request.Command); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/UploadController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/UploadController.cs deleted file mode 100644 index fc62007..0000000 --- a/MoonlightServers.Daemon/Http/Controllers/Servers/UploadController.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using MoonCore.Exceptions; -using MoonCore.Helpers; -using MoonlightServers.Daemon.Configuration; -using MoonlightServers.Daemon.ServerSystem.SubSystems; -using MoonlightServers.Daemon.Services; - -namespace MoonlightServers.Daemon.Http.Controllers.Servers; - -[ApiController] -[Route("api/servers/upload")] -[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverUpload")] -public class UploadController : Controller -{ - private readonly AppConfiguration Configuration; - private readonly ServerService ServerService; - - public UploadController( - ServerService serverService, - AppConfiguration configuration - ) - { - ServerService = serverService; - Configuration = configuration; - } - - [HttpPost] - public async Task Upload( - [FromQuery] long totalSize, - [FromQuery] int chunkId, - [FromQuery] string path - ) - { - var chunkSize = ByteConverter.FromMegaBytes(Configuration.Files.UploadChunkSize).Bytes; - var uploadLimit = ByteConverter.FromMegaBytes(Configuration.Files.UploadSizeLimit).Bytes; - - #region File validation - - if (Request.Form.Files.Count != 1) - throw new HttpApiException("You need to provide exactly one file", 400); - - var file = Request.Form.Files[0]; - - if (file.Length > chunkSize) - throw new HttpApiException("The provided data exceeds the chunk size limit", 400); - - #endregion - - var serverId = int.Parse(User.Claims.First(x => x.Type == "serverId").Value); - - #region Chunk calculation and validation - - if(totalSize > uploadLimit) - throw new HttpApiException("Invalid upload request: Exceeding upload limit", 400); - - var chunks = totalSize / chunkSize; - chunks += totalSize % chunkSize > 0 ? 1 : 0; - - if (chunkId > chunks) - throw new HttpApiException("Invalid chunk id: Out of bounds", 400); - - var positionToSkipTo = chunkSize * chunkId; - - #endregion - - var server = ServerService.Find(serverId); - - if (server == null) - throw new HttpApiException("No server with this id found", 404); - - var storageSubSystem = server.GetRequiredSubSystem(); - - var fileSystem = await storageSubSystem.GetFileSystem(); - - var dataStream = file.OpenReadStream(); - - await fileSystem.CreateChunk( - path, - totalSize, - positionToSkipTo, - dataStream - ); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj index 089b3d8..42dc3d1 100644 --- a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj +++ b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj @@ -19,7 +19,9 @@ + + diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IConsole.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IConsole.cs deleted file mode 100644 index 986bae5..0000000 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/IConsole.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace MoonlightServers.Daemon.ServerSys.Abstractions; - -public interface IConsole : IServerComponent -{ - public IAsyncObservable OnOutput { get; } - public IAsyncObservable OnInput { get; } - - public Task AttachToRuntime(); - public Task AttachToInstallation(); - - /// - /// Detaches any attached consoles. Usually either runtime or install is attached - /// - /// - public Task Detach(); - - public Task CollectFromRuntime(); - public Task CollectFromInstallation(); - - public Task WriteToOutput(string content); - public Task WriteToInput(string content); - public Task WriteToMoonlight(string content); - - public Task ClearOutput(); - public string[] GetOutput(); -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IFileSystem.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IFileSystem.cs deleted file mode 100644 index 419ec53..0000000 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/IFileSystem.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MoonlightServers.Daemon.ServerSys.Abstractions; - -public interface IFileSystem : IServerComponent -{ - public bool IsMounted { get; } - public bool Exists { get; } - - public Task Create(); - public Task Mount(); - public Task Unmount(); - public Task Delete(); - - public string GetExternalPath(); -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IInstaller.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IInstaller.cs deleted file mode 100644 index db3bdf1..0000000 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/IInstaller.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MoonlightServers.Daemon.ServerSys.Abstractions; - -public interface IInstaller : IServerComponent -{ - public IAsyncObservable OnExited { get; } - public bool IsRunning { get; } - - public Task Setup(); - public Task Start(); - public Task Abort(); - public Task Cleanup(); - - public Task SearchForCrash(); -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IOnlineDetection.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IOnlineDetection.cs deleted file mode 100644 index b0a6905..0000000 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/IOnlineDetection.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MoonlightServers.Daemon.ServerSys.Abstractions; - -public interface IOnlineDetection : IServerComponent -{ - -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IProvisioner.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IProvisioner.cs deleted file mode 100644 index 5a74394..0000000 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/IProvisioner.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MoonlightServers.Daemon.ServerSys.Abstractions; - -public interface IProvisioner : IServerComponent -{ - public IAsyncObservable OnExited { get; } - public bool IsProvisioned { get; } - - public Task Provision(); - public Task Start(); - public Task Stop(); - public Task Kill(); - public Task Deprovision(); - - public Task SearchForCrash(); -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IRestorer.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IRestorer.cs deleted file mode 100644 index 3b2eb65..0000000 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/IRestorer.cs +++ /dev/null @@ -1,8 +0,0 @@ -using MoonlightServers.Daemon.ServerSystem; - -namespace MoonlightServers.Daemon.ServerSys.Abstractions; - -public interface IRestorer : IServerComponent -{ - public Task Restore(); -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IServerComponent.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IServerComponent.cs deleted file mode 100644 index e2ad11e..0000000 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/IServerComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MoonlightServers.Daemon.ServerSys.Abstractions; - -public interface IServerComponent : IAsyncDisposable -{ - public Task Initialize(); - public Task Sync(); -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/IStatistics.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/IStatistics.cs deleted file mode 100644 index d6411fd..0000000 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/IStatistics.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MoonlightServers.Daemon.ServerSys.Abstractions; - -public interface IStatistics : IServerComponent -{ - public IAsyncObservable OnStats { get; } - - public Task SubscribeToRuntime(); - public Task SubscribeToInstallation(); - - public ServerStats[] GetStats(int count); -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/Server.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/Server.cs deleted file mode 100644 index 67c3ca4..0000000 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/Server.cs +++ /dev/null @@ -1,348 +0,0 @@ -using Microsoft.AspNetCore.SignalR; -using MoonCore.Observability; -using MoonlightServers.Daemon.Extensions; -using MoonlightServers.Daemon.Http.Hubs; -using MoonlightServers.Daemon.Mappers; -using MoonlightServers.Daemon.ServerSystem; -using MoonlightServers.Daemon.Services; -using Stateless; - -namespace MoonlightServers.Daemon.ServerSys.Abstractions; - -public class Server : IAsyncDisposable -{ - public IConsole Console { get; } - public IFileSystem FileSystem { get; } - public IInstaller Installer { get; } - public IProvisioner Provisioner { get; } - public IRestorer Restorer { get; } - public IStatistics Statistics { get; } - public IOnlineDetection OnlineDetection { get; } - public StateMachine StateMachine { get; private set; } - public ServerContext Context { get; } - public IAsyncObservable OnState => OnStateSubject; - - private readonly EventSubject OnStateSubject = new(); - private readonly ILogger Logger; - private readonly RemoteService RemoteService; - private readonly ServerConfigurationMapper Mapper; - private readonly IHubContext HubContext; - - private IAsyncDisposable? ProvisionExitSubscription; - private IAsyncDisposable? InstallerExitSubscription; - private IAsyncDisposable? ConsoleSubscription; - - public Server( - ILoggerFactory loggerFactory, - IConsole console, - IFileSystem fileSystem, - IInstaller installer, - IProvisioner provisioner, - IRestorer restorer, - IStatistics statistics, - IOnlineDetection onlineDetection, - ServerContext context, - RemoteService remoteService, - ServerConfigurationMapper mapper, - IHubContext hubContext) - { - Logger = loggerFactory.CreateLogger($"Servers.Instance.{context.Configuration.Id}.{nameof(Server)}"); - Console = console; - FileSystem = fileSystem; - Installer = installer; - Provisioner = provisioner; - Restorer = restorer; - Statistics = statistics; - Context = context; - RemoteService = remoteService; - Mapper = mapper; - HubContext = hubContext; - OnlineDetection = onlineDetection; - } - - public async Task Initialize() - { - Logger.LogDebug("Initializing server components"); - - IServerComponent[] components = [Console, Restorer, FileSystem, Installer, Provisioner, Statistics, OnlineDetection]; - - foreach (var serverComponent in components) - { - try - { - await serverComponent.Initialize(); - } - catch (Exception e) - { - Logger.LogError( - e, - "Error initializing server component: {type}", - serverComponent.GetType().Name.GetType().FullName - ); - - throw; - } - } - - Logger.LogDebug("Restoring server"); - var restoredState = await Restorer.Restore(); - - if (restoredState == ServerState.Offline) - Logger.LogDebug("Restorer didnt find anything to restore. State is offline"); - else - Logger.LogDebug("Restored server to state: {state}", restoredState); - - CreateStateMachine(restoredState); - - await SetupHubEvents(); - - // Setup event handling - ProvisionExitSubscription = await Provisioner.OnExited.SubscribeEventAsync(async _ => - await StateMachine.FireAsync(ServerTrigger.Exited) - ); - - InstallerExitSubscription = await Installer.OnExited.SubscribeEventAsync(async _ => - await StateMachine.FireAsync(ServerTrigger.Exited) - ); - } - - public async Task Sync() - { - IServerComponent[] components = [Console, Restorer, FileSystem, Installer, Provisioner, Statistics, OnlineDetection]; - - foreach (var component in components) - await component.Sync(); - } - - private void CreateStateMachine(ServerState initialState) - { - StateMachine = new StateMachine(initialState, FiringMode.Queued); - - StateMachine.OnTransitionedAsync(async transition - => await OnStateSubject.OnNextAsync(transition.Destination) - ); - - // Configure basic state machine flow - - StateMachine.Configure(ServerState.Offline) - .Permit(ServerTrigger.Start, ServerState.Starting) - .Permit(ServerTrigger.Install, ServerState.Installing) - .PermitReentry(ServerTrigger.FailSafe); - - StateMachine.Configure(ServerState.Starting) - .Permit(ServerTrigger.OnlineDetected, ServerState.Online) - .Permit(ServerTrigger.FailSafe, ServerState.Offline) - .Permit(ServerTrigger.Exited, ServerState.Offline) - .Permit(ServerTrigger.Stop, ServerState.Stopping) - .Permit(ServerTrigger.Kill, ServerState.Stopping); - - StateMachine.Configure(ServerState.Online) - .Permit(ServerTrigger.Stop, ServerState.Stopping) - .Permit(ServerTrigger.Kill, ServerState.Stopping) - .Permit(ServerTrigger.Exited, ServerState.Offline); - - StateMachine.Configure(ServerState.Stopping) - .PermitReentry(ServerTrigger.FailSafe) - .PermitReentry(ServerTrigger.Kill) - .Permit(ServerTrigger.Exited, ServerState.Offline); - - StateMachine.Configure(ServerState.Installing) - .Permit(ServerTrigger.FailSafe, ServerState.Offline) // TODO: Add kill - .Permit(ServerTrigger.Exited, ServerState.Offline); - - // Handle transitions - - StateMachine.Configure(ServerState.Starting) - .OnEntryAsync(HandleStart) - .OnExitFromAsync(ServerTrigger.Exited, HandleRuntimeExit); - - StateMachine.Configure(ServerState.Installing) - .OnEntryAsync(HandleInstall) - .OnExitFromAsync(ServerTrigger.Exited, HandleInstallExit); - - StateMachine.Configure(ServerState.Online) - .OnExitFromAsync(ServerTrigger.Exited, HandleRuntimeExit); - - StateMachine.Configure(ServerState.Stopping) - .OnEntryFromAsync(ServerTrigger.Stop, HandleStop) - .OnEntryFromAsync(ServerTrigger.Kill, HandleKill) - .OnExitFromAsync(ServerTrigger.Exited, HandleRuntimeExit); - } - - private async Task SetupHubEvents() - { - var groupName = Context.Configuration.Id.ToString(); - - ConsoleSubscription = await Console.OnOutput.SubscribeAsync(async line => - { - await HubContext.Clients.Group(groupName).SendAsync( - "ConsoleOutput", - line - ); - }); - - StateMachine.OnTransitionedAsync(async transition => - { - await HubContext.Clients.Group(groupName).SendAsync( - "StateChanged", - transition.Destination.ToString() - ); - }); - } - - public async Task Delete() - { - if (Installer.IsRunning) - { - Logger.LogDebug("Installer still running. Aborting and cleaning up"); - - await Installer.Abort(); - await Installer.Cleanup(); - } - - if (Provisioner.IsProvisioned) - await Provisioner.Deprovision(); - - if (FileSystem.IsMounted) - await FileSystem.Unmount(); - - await FileSystem.Delete(); - } - - #region State machine handlers - - private async Task HandleStart() - { - try - { - // Plan for starting the server: - // 1. Fetch latest configuration from panel (maybe: and perform sync) - // 2. Ensure that the file system exists - // 3. Mount the file system - // 4. Provision the container - // 5. Attach console to container - // 6. Start the container - - // 1. Fetch latest configuration from panel - Logger.LogDebug("Fetching latest server configuration"); - await Console.WriteToMoonlight("Fetching latest server configuration"); - - var serverDataResponse = await RemoteService.GetServer(Context.Configuration.Id); - Context.Configuration = Mapper.FromServerDataResponse(serverDataResponse); - - // 2. Ensure that the file system exists - if (!FileSystem.Exists) - { - await Console.WriteToMoonlight("Creating storage"); - await FileSystem.Create(); - } - - // 3. Mount the file system - if (!FileSystem.IsMounted) - { - await Console.WriteToMoonlight("Mounting storage"); - await FileSystem.Mount(); - } - - // 4. Provision the container - await Console.WriteToMoonlight("Provisioning runtime"); - await Provisioner.Provision(); - - // 5. Attach console to container - await Console.AttachToRuntime(); - - // 6. Start the container - await Provisioner.Start(); - } - catch (Exception e) - { - Logger.LogError(e, "An error occured while starting the server"); - } - } - - private async Task HandleStop() - { - await Provisioner.Stop(); - } - - private async Task HandleKill() - { - await Provisioner.Kill(); - } - - private async Task HandleRuntimeExit() - { - Logger.LogDebug("Detected runtime exit"); - - Logger.LogDebug("Detaching from console"); - await Console.Detach(); - - Logger.LogDebug("Deprovisioning"); - await Console.WriteToMoonlight("Deprovisioning"); - await Provisioner.Deprovision(); - } - - private async Task HandleInstall() - { - // Plan: - // 1. Fetch the latest installation data - // 2. Setup installation environment - // 3. Attach console to installation - // 4. Start the installation - - Logger.LogDebug("Installing"); - - Logger.LogDebug("Setting up"); - await Console.WriteToMoonlight("Setting up installation"); - - // 1. Fetch the latest installation data - Logger.LogDebug("Fetching installation data"); - await Console.WriteToMoonlight("Fetching installation data"); - - Context.InstallConfiguration = await RemoteService.GetServerInstallation(Context.Configuration.Id); - - // 2. Setup installation environment - await Installer.Setup(); - - // 3. Attach console to installation - await Console.AttachToInstallation(); - - // 4. Start the installation - await Installer.Start(); - } - - private async Task HandleInstallExit() - { - Logger.LogDebug("Detected install exit"); - - Logger.LogDebug("Detaching from console"); - await Console.Detach(); - - Logger.LogDebug("Cleaning up"); - await Console.WriteToMoonlight("Cleaning up"); - await Installer.Cleanup(); - - await Console.WriteToMoonlight("Installation completed"); - } - - #endregion - - public async ValueTask DisposeAsync() - { - if (ProvisionExitSubscription != null) - await ProvisionExitSubscription.DisposeAsync(); - - if (InstallerExitSubscription != null) - await InstallerExitSubscription.DisposeAsync(); - - if (ConsoleSubscription != null) - await ConsoleSubscription.DisposeAsync(); - - await Console.DisposeAsync(); - await FileSystem.DisposeAsync(); - await Installer.DisposeAsync(); - await Provisioner.DisposeAsync(); - await Restorer.DisposeAsync(); - await Statistics.DisposeAsync(); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/ServerContext.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/ServerContext.cs deleted file mode 100644 index 93899d4..0000000 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/ServerContext.cs +++ /dev/null @@ -1,12 +0,0 @@ -using MoonlightServers.Daemon.Models.Cache; -using MoonlightServers.DaemonShared.PanelSide.Http.Responses; - -namespace MoonlightServers.Daemon.ServerSys.Abstractions; - -public record ServerContext -{ - public ServerConfiguration Configuration { get; set; } - public AsyncServiceScope ServiceScope { get; set; } - public ServerInstallDataResponse InstallConfiguration { get; set; } - public Server Self { get; set; } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/ServerCrash.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/ServerCrash.cs deleted file mode 100644 index 1e696a7..0000000 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/ServerCrash.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace MoonlightServers.Daemon.ServerSys.Abstractions; - -public record ServerCrash(); \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Abstractions/ServerStats.cs b/MoonlightServers.Daemon/ServerSys/Abstractions/ServerStats.cs deleted file mode 100644 index a6c1605..0000000 --- a/MoonlightServers.Daemon/ServerSys/Abstractions/ServerStats.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace MoonlightServers.Daemon.ServerSys.Abstractions; - -public record ServerStats(); \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Implementations/DefaultRestorer.cs b/MoonlightServers.Daemon/ServerSys/Implementations/DefaultRestorer.cs deleted file mode 100644 index d007bea..0000000 --- a/MoonlightServers.Daemon/ServerSys/Implementations/DefaultRestorer.cs +++ /dev/null @@ -1,68 +0,0 @@ -using MoonlightServers.Daemon.ServerSys.Abstractions; -using MoonlightServers.Daemon.ServerSystem; - -namespace MoonlightServers.Daemon.ServerSys.Implementations; - -public class DefaultRestorer : IRestorer -{ - private readonly ILogger Logger; - private readonly IConsole Console; - private readonly IProvisioner Provisioner; - private readonly IInstaller Installer; - private readonly IStatistics Statistics; - - public DefaultRestorer( - ILoggerFactory loggerFactory, - ServerContext context, - IConsole console, - IProvisioner provisioner, - IStatistics statistics, - IInstaller installer - ) - { - Logger = loggerFactory.CreateLogger($"Servers.Instance.{context.Configuration.Id}.{nameof(DefaultRestorer)}"); - Console = console; - Provisioner = provisioner; - Statistics = statistics; - Installer = installer; - } - - public Task Initialize() - => Task.CompletedTask; - - public Task Sync() - => Task.CompletedTask; - - public async Task Restore() - { - Logger.LogDebug("Restoring server state"); - - if (Provisioner.IsProvisioned) - { - Logger.LogDebug("Detected runtime to restore"); - - await Console.CollectFromRuntime(); - await Console.AttachToRuntime(); - await Statistics.SubscribeToRuntime(); - - return ServerState.Online; - } - - if (Installer.IsRunning) - { - Logger.LogDebug("Detected installation to restore"); - - await Console.CollectFromInstallation(); - await Console.AttachToInstallation(); - await Statistics.SubscribeToInstallation(); - - return ServerState.Installing; - } - - Logger.LogDebug("Nothing found to restore"); - return ServerState.Offline; - } - - public ValueTask DisposeAsync() - => ValueTask.CompletedTask; -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Implementations/DockerConsole.cs b/MoonlightServers.Daemon/ServerSys/Implementations/DockerConsole.cs deleted file mode 100644 index 650b93a..0000000 --- a/MoonlightServers.Daemon/ServerSys/Implementations/DockerConsole.cs +++ /dev/null @@ -1,230 +0,0 @@ -using System.Text; -using Docker.DotNet; -using Docker.DotNet.Models; -using MoonCore.Helpers; -using MoonCore.Observability; -using MoonlightServers.Daemon.ServerSys.Abstractions; - -namespace MoonlightServers.Daemon.ServerSys.Implementations; - -public class DockerConsole : IConsole -{ - public IAsyncObservable OnOutput => OnOutputSubject; - public IAsyncObservable OnInput => OnInputSubject; - - private readonly EventSubject OnOutputSubject = new(); - private readonly EventSubject OnInputSubject = new(); - - private readonly ConcurrentList OutputCache = new(); - private readonly DockerClient DockerClient; - private readonly ILogger Logger; - private readonly ServerContext Context; - - private MultiplexedStream? CurrentStream; - private CancellationTokenSource Cts = new(); - - private const string MlPrefix = - "\x1b[1;38;2;200;90;200mM\x1b[1;38;2;204;110;230mo\x1b[1;38;2;170;130;245mo\x1b[1;38;2;140;150;255mn\x1b[1;38;2;110;180;255ml\x1b[1;38;2;100;200;255mi\x1b[1;38;2;100;220;255mg\x1b[1;38;2;120;235;255mh\x1b[1;38;2;140;250;255mt\x1b[0m \x1b[3;38;2;200;200;200m{0}\x1b[0m\n\r"; - - public DockerConsole( - ServerContext context, - DockerClient dockerClient, - ILoggerFactory loggerFactory - ) - { - Context = context; - DockerClient = dockerClient; - Logger = loggerFactory.CreateLogger($"Servers.Instance.{context.Configuration.Id}.{nameof(DockerConsole)}"); - } - - public Task Initialize() - => Task.CompletedTask; - - public Task Sync() - => Task.CompletedTask; - - public async Task AttachToRuntime() - { - var containerName = $"moonlight-runtime-{Context.Configuration.Id}"; - await AttachStream(containerName); - } - - public async Task AttachToInstallation() - { - var containerName = $"moonlight-install-{Context.Configuration.Id}"; - await AttachStream(containerName); - } - - public async Task Detach() - { - Logger.LogDebug("Detaching stream"); - - if (!Cts.IsCancellationRequested) - await Cts.CancelAsync(); - } - - public async Task CollectFromRuntime() - => await CollectFromContainer($"moonlight-runtime-{Context.Configuration.Id}"); - - public async Task CollectFromInstallation() - => await CollectFromContainer($"moonlight-install-{Context.Configuration.Id}"); - - private async Task CollectFromContainer(string containerName) - { - var logStream = await DockerClient.Containers.GetContainerLogsAsync(containerName, true, new() - { - Follow = false, - ShowStderr = true, - ShowStdout = true - }); - - var combinedOutput = await logStream.ReadOutputToEndAsync(CancellationToken.None); - var contentToAdd = combinedOutput.stdout + combinedOutput.stderr; - - await WriteToOutput(contentToAdd); - } - - private async Task AttachStream(string containerName) - { - // This stops any previously existing stream reading if - // any is currently running - if (!Cts.IsCancellationRequested) - await Cts.CancelAsync(); - - // Reset - Cts = new(); - - Task.Run(async () => - { - // This loop is here to reconnect to the container if for some reason the container - // attach stream fails before the server tasks have been canceled i.e. the before the server - // goes offline - - while (!Cts.Token.IsCancellationRequested) - { - try - { - CurrentStream = await DockerClient.Containers.AttachContainerAsync( - containerName, - true, - new ContainerAttachParameters() - { - Stderr = true, - Stdin = true, - Stdout = true, - Stream = true - }, - Cts.Token - ); - - var buffer = new byte[1024]; - - try - { - // Read while server tasks are not canceled - while (!Cts.Token.IsCancellationRequested) - { - var readResult = await CurrentStream.ReadOutputAsync( - buffer, - 0, - buffer.Length, - Cts.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 WriteToOutput(decodedText); - } - } - catch (TaskCanceledException) - { - // Ignored - } - catch (OperationCanceledException) - { - // Ignored - } - catch (Exception e) - { - Logger.LogWarning(e, "An unhandled error occured while reading from container stream"); - } - finally - { - CurrentStream.Dispose(); - } - } - catch (TaskCanceledException) - { - // ignored - } - catch (Exception e) - { - Logger.LogError(e, "An error occured while attaching to container"); - } - } - - - // Reset stream so no further inputs will be piped to it - CurrentStream = null; - - Logger.LogDebug("Disconnected from container stream"); - }, Cts.Token); - } - - public async Task WriteToOutput(string content) - { - OutputCache.Add(content); - - if (OutputCache.Count > 250) // TODO: Config - OutputCache.RemoveRange(0, 100); - - await OnOutputSubject.OnNextAsync(content); - } - - public async Task WriteToInput(string content) - { - if (CurrentStream == null) - return; - - var contentBuffer = Encoding.UTF8.GetBytes(content); - - await CurrentStream.WriteAsync( - contentBuffer, - 0, - contentBuffer.Length, - Cts.Token - ); - - await OnInputSubject.OnNextAsync(content); - } - - public async Task WriteToMoonlight(string content) - => await WriteToOutput(string.Format(MlPrefix, content)); - - public Task ClearOutput() - { - OutputCache.Clear(); - return Task.CompletedTask; - } - - public string[] GetOutput() - => OutputCache.ToArray(); - - public async ValueTask DisposeAsync() - { - if (!Cts.IsCancellationRequested) - { - await Cts.CancelAsync(); - Cts.Dispose(); - } - - if (CurrentStream != null) - CurrentStream.Dispose(); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Implementations/DockerInstaller.cs b/MoonlightServers.Daemon/ServerSys/Implementations/DockerInstaller.cs deleted file mode 100644 index eb35bb6..0000000 --- a/MoonlightServers.Daemon/ServerSys/Implementations/DockerInstaller.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Docker.DotNet; -using Docker.DotNet.Models; -using MoonCore.Observability; -using MoonlightServers.Daemon.Configuration; -using MoonlightServers.Daemon.Extensions; -using MoonlightServers.Daemon.Helpers; -using MoonlightServers.Daemon.Mappers; -using MoonlightServers.Daemon.ServerSys.Abstractions; -using MoonlightServers.Daemon.Services; - -namespace MoonlightServers.Daemon.ServerSys.Implementations; - -public class DockerInstaller : IInstaller -{ - public IAsyncObservable OnExited => OnExitedSubject; - public bool IsRunning { get; private set; } = false; - - private readonly EventSubject OnExitedSubject = new(); - - private readonly ILogger Logger; - private readonly DockerEventService EventService; - private readonly IConsole Console; - private readonly DockerClient DockerClient; - private readonly ServerContext Context; - private readonly DockerImageService ImageService; - private readonly IFileSystem FileSystem; - private readonly AppConfiguration Configuration; - private readonly ServerConfigurationMapper Mapper; - - private string? ContainerId; - private string ContainerName; - - private string InstallHostPath; - - private IAsyncDisposable? ContainerEventSubscription; - - public DockerInstaller( - ILoggerFactory loggerFactory, - DockerEventService eventService, - IConsole console, - DockerClient dockerClient, - ServerContext context, - DockerImageService imageService, - IFileSystem fileSystem, - AppConfiguration configuration, - ServerConfigurationMapper mapper - ) - { - Logger = loggerFactory.CreateLogger($"Servers.Instance.{context.Configuration.Id}.{nameof(DockerInstaller)}"); - EventService = eventService; - Console = console; - DockerClient = dockerClient; - Context = context; - ImageService = imageService; - FileSystem = fileSystem; - Configuration = configuration; - Mapper = mapper; - } - - public async Task Initialize() - { - ContainerName = $"moonlight-install-{Context.Configuration.Id}"; - InstallHostPath = - Path.GetFullPath(Path.Combine(Configuration.Storage.Install, Context.Configuration.Id.ToString())); - - ContainerEventSubscription = await EventService - .OnContainerEvent - .SubscribeEventAsync(HandleContainerEvent); - - // Check for any already existing runtime container to reclaim - Logger.LogDebug("Searching for orphan container to reclaim"); - - try - { - var container = await DockerClient.Containers.InspectContainerAsync(ContainerName); - - ContainerId = container.ID; - IsRunning = container.State.Running; - } - catch (DockerContainerNotFoundException) - { - // Ignored - } - } - - private async ValueTask HandleContainerEvent(Message message) - { - // Only handle events for our own container - if (message.ID != ContainerId) - return; - - // Only handle die events - if (message.Action != "die") - return; - - await OnExitedSubject.OnNextAsync(message); - } - - public Task Sync() - => Task.CompletedTask; - - public async Task Setup() - { - // Plan of action: - // 1. Ensure no other container with that name exist - // 2. Ensure the docker image has been downloaded - // 3. Create the installation volume and place script in there - // 4. Create the container from the configuration in the meta - - // 1. Ensure no other container with that name exist - try - { - Logger.LogDebug("Searching for orphan container"); - - var possibleContainer = await DockerClient.Containers.InspectContainerAsync(ContainerName); - - Logger.LogDebug("Orphan container found. Removing it"); - await Console.WriteToMoonlight("Found orphan container. Removing it"); - - await EnsureContainerOffline(possibleContainer); - - Logger.LogInformation("Removing orphan container"); - await DockerClient.Containers.RemoveContainerAsync(ContainerName, new()); - } - catch (DockerContainerNotFoundException) - { - // Ignored - } - - // 2. Ensure the docker image has been downloaded - await Console.WriteToMoonlight("Downloading docker image"); - - await ImageService.Download(Context.Configuration.DockerImage, async message => - { - try - { - await Console.WriteToMoonlight(message); - } - catch (Exception) - { - // Ignored. Not handling it here could cause an application wide crash afaik - } - }); - - // 3. Create the installation volume and place script in there - - await Console.WriteToMoonlight("Creating storage"); - - if(Directory.Exists(InstallHostPath)) - Directory.Delete(InstallHostPath, true); - - Directory.CreateDirectory(InstallHostPath); - - await File.WriteAllTextAsync(Path.Combine(InstallHostPath, "install.sh"), Context.InstallConfiguration.Script); - - // 4. Create the container from the configuration in the meta - var runtimeFsPath = FileSystem.GetExternalPath(); - - var parameters = Mapper.ToInstallParameters( - Context.Configuration, - Context.InstallConfiguration, - runtimeFsPath, - InstallHostPath, - ContainerName - ); - - var createdContainer = await DockerClient.Containers.CreateContainerAsync(parameters); - - ContainerId = createdContainer.ID; - - Logger.LogDebug("Created container"); - await Console.WriteToMoonlight("Created container"); - } - - public async Task Start() - { - Logger.LogDebug("Starting container"); - await Console.WriteToMoonlight("Starting container"); - await DockerClient.Containers.StartContainerAsync(ContainerId, new()); - } - - public async Task Abort() - { - await EnsureContainerOffline(); - } - - public async Task Cleanup() - { - // Plan of action: - // 1. Search for the container by id or name - // 2. Ensure container is offline - // 3. Remove the container - // 4. Delete installation volume if it exists - - // 1. Search for the container by id or name - ContainerInspectResponse? container = null; - - try - { - if (string.IsNullOrEmpty(ContainerId)) - container = await DockerClient.Containers.InspectContainerAsync(ContainerName); - else - container = await DockerClient.Containers.InspectContainerAsync(ContainerId); - } - catch (DockerContainerNotFoundException) - { - // Ignored - - Logger.LogDebug("Runtime container could not be found. Reporting deprovision success"); - } - - // No container found? We are done here then - if (container == null) - return; - - // 2. Ensure container is offline - await EnsureContainerOffline(container); - - // 3. Remove the container - Logger.LogInformation("Removing container"); - await Console.WriteToMoonlight("Removing container"); - - await DockerClient.Containers.RemoveContainerAsync(container.ID, new()); - - // 4. Delete installation volume if it exists - - if (Directory.Exists(InstallHostPath)) - { - Logger.LogInformation("Removing storage"); - await Console.WriteToMoonlight("Removing storage"); - - Directory.Delete(InstallHostPath, true); - } - } - - public async Task SearchForCrash() - { - return null; - } - - private async Task EnsureContainerOffline(ContainerInspectResponse? container = null) - { - try - { - if (string.IsNullOrEmpty(ContainerId)) - container = await DockerClient.Containers.InspectContainerAsync(ContainerName); - else - container = await DockerClient.Containers.InspectContainerAsync(ContainerId); - } - catch (DockerContainerNotFoundException) - { - Logger.LogDebug("No container found to ensure its offline"); - - // Ignored - } - - // No container found? We are done here then - if (container == null) - return; - - // Check if container is running - if (!container.State.Running) - return; - - await Console.WriteToMoonlight("Killing container"); - await DockerClient.Containers.KillContainerAsync(ContainerId, new()); - } - - public async ValueTask DisposeAsync() - { - OnExitedSubject.Dispose(); - - if (ContainerEventSubscription != null) - await ContainerEventSubscription.DisposeAsync(); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Implementations/DockerProvisioner.cs b/MoonlightServers.Daemon/ServerSys/Implementations/DockerProvisioner.cs deleted file mode 100644 index e088ccc..0000000 --- a/MoonlightServers.Daemon/ServerSys/Implementations/DockerProvisioner.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System.Reactive.Concurrency; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Docker.DotNet; -using Docker.DotNet.Models; -using MoonCore.Observability; -using MoonlightServers.Daemon.Extensions; -using MoonlightServers.Daemon.Helpers; -using MoonlightServers.Daemon.Mappers; -using MoonlightServers.Daemon.ServerSys.Abstractions; -using MoonlightServers.Daemon.Services; - -namespace MoonlightServers.Daemon.ServerSys.Implementations; - -public class DockerProvisioner : IProvisioner -{ - public IAsyncObservable OnExited => OnExitedSubject; - public bool IsProvisioned { get; private set; } - - private readonly DockerClient DockerClient; - private readonly ILogger Logger; - private readonly DockerEventService EventService; - private readonly ServerContext Context; - private readonly IConsole Console; - private readonly DockerImageService ImageService; - private readonly ServerConfigurationMapper Mapper; - private readonly IFileSystem FileSystem; - - private EventSubject OnExitedSubject = new(); - - private string? ContainerId; - private string ContainerName; - private IAsyncDisposable? ContainerEventSubscription; - - public DockerProvisioner( - DockerClient dockerClient, - ILoggerFactory loggerFactory, - DockerEventService eventService, - ServerContext context, - IConsole console, - DockerImageService imageService, - ServerConfigurationMapper mapper, - IFileSystem fileSystem - ) - { - DockerClient = dockerClient; - Logger = loggerFactory.CreateLogger($"Servers.Instance.{context.Configuration.Id}.{nameof(DockerProvisioner)}"); - EventService = eventService; - Context = context; - Console = console; - ImageService = imageService; - Mapper = mapper; - FileSystem = fileSystem; - } - - public async Task Initialize() - { - ContainerName = $"moonlight-runtime-{Context.Configuration.Id}"; - - ContainerEventSubscription = await EventService - .OnContainerEvent - .SubscribeEventAsync(HandleContainerEvent); - - // Check for any already existing runtime container to reclaim - Logger.LogDebug("Searching for orphan container to reclaim"); - - try - { - var container = await DockerClient.Containers.InspectContainerAsync(ContainerName); - - ContainerId = container.ID; - IsProvisioned = container.State.Running; - } - catch (DockerContainerNotFoundException) - { - // Ignored - } - } - - private async ValueTask HandleContainerEvent(Message message) - { - // Only handle events for our own container - if (message.ID != ContainerId) - return; - - // Only handle die events - if (message.Action != "die") - return; - - await OnExitedSubject.OnNextAsync(message); - } - - public Task Sync() - { - return Task.CompletedTask; // TODO: Implement - } - - public async Task Provision() - { - // Plan of action: - // 1. Ensure no other container with that name exist - // 2. Ensure the docker image has been downloaded - // 3. Create the container from the configuration in the meta - - // 1. Ensure no other container with that name exist - - try - { - Logger.LogDebug("Searching for orphan container"); - - var possibleContainer = await DockerClient.Containers.InspectContainerAsync(ContainerName); - - Logger.LogDebug("Orphan container found. Removing it"); - await Console.WriteToMoonlight("Found orphan container. Removing it"); - - await EnsureContainerOffline(possibleContainer); - - Logger.LogDebug("Removing orphan container"); - await DockerClient.Containers.RemoveContainerAsync(ContainerName, new()); - } - catch (DockerContainerNotFoundException) - { - // Ignored - } - - // 2. Ensure the docker image has been downloaded - await Console.WriteToMoonlight("Downloading docker image"); - - await ImageService.Download(Context.Configuration.DockerImage, async message => - { - try - { - await Console.WriteToMoonlight(message); - } - catch (Exception) - { - // Ignored. Not handling it here could cause an application wide crash afaik - } - }); - - // 3. Create the container from the configuration in the meta - var hostFsPath = FileSystem.GetExternalPath(); - - var parameters = Mapper.ToRuntimeParameters( - Context.Configuration, - hostFsPath, - ContainerName - ); - - var createdContainer = await DockerClient.Containers.CreateContainerAsync(parameters); - - ContainerId = createdContainer.ID; - - Logger.LogDebug("Created container"); - await Console.WriteToMoonlight("Created container"); - } - - public async Task Start() - { - if(string.IsNullOrEmpty(ContainerId)) - throw new ArgumentNullException(nameof(ContainerId), "Container id of runtime is unknown"); - - await Console.WriteToMoonlight("Starting container"); - await DockerClient.Containers.StartContainerAsync(ContainerId, new()); - } - - public async Task Stop() - { - if (Context.Configuration.StopCommand.StartsWith('^')) - { - await DockerClient.Containers.KillContainerAsync(ContainerId, new() - { - Signal = Context.Configuration.StopCommand.Substring(1) - }); - } - else - await Console.WriteToInput(Context.Configuration.StopCommand + "\n\r"); - } - - public async Task Kill() - { - await EnsureContainerOffline(); - } - - public async Task Deprovision() - { - // Plan of action: - // 1. Search for the container by id or name - // 2. Ensure container is offline - // 3. Remove the container - - // 1. Search for the container by id or name - ContainerInspectResponse? container = null; - - try - { - if (string.IsNullOrEmpty(ContainerId)) - container = await DockerClient.Containers.InspectContainerAsync(ContainerName); - else - container = await DockerClient.Containers.InspectContainerAsync(ContainerId); - } - catch (DockerContainerNotFoundException) - { - // Ignored - - Logger.LogDebug("Runtime container could not be found. Reporting deprovision success"); - } - - // No container found? We are done here then - if (container == null) - return; - - // 2. Ensure container is offline - await EnsureContainerOffline(container); - - // 3. Remove the container - Logger.LogDebug("Removing container"); - await Console.WriteToMoonlight("Removing container"); - - await DockerClient.Containers.RemoveContainerAsync(container.ID, new()); - } - - private async Task EnsureContainerOffline(ContainerInspectResponse? container = null) - { - try - { - if (string.IsNullOrEmpty(ContainerId)) - container = await DockerClient.Containers.InspectContainerAsync(ContainerName); - else - container = await DockerClient.Containers.InspectContainerAsync(ContainerId); - } - catch (DockerContainerNotFoundException) - { - // Ignored - } - - // No container found? We are done here then - if (container == null) - return; - - // Check if container is running - if (!container.State.Running) - return; - - await Console.WriteToMoonlight("Killing container"); - await DockerClient.Containers.KillContainerAsync(ContainerId, new()); - } - - public Task SearchForCrash() - { - throw new NotImplementedException(); - } - - public async ValueTask DisposeAsync() - { - OnExitedSubject.Dispose(); - - if (ContainerEventSubscription != null) - await ContainerEventSubscription.DisposeAsync(); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Implementations/DockerStatistics.cs b/MoonlightServers.Daemon/ServerSys/Implementations/DockerStatistics.cs deleted file mode 100644 index 23a9617..0000000 --- a/MoonlightServers.Daemon/ServerSys/Implementations/DockerStatistics.cs +++ /dev/null @@ -1,34 +0,0 @@ -using MoonCore.Observability; -using MoonlightServers.Daemon.Helpers; -using MoonlightServers.Daemon.ServerSys.Abstractions; - -namespace MoonlightServers.Daemon.ServerSys.Implementations; - -public class DockerStatistics : IStatistics -{ - public IAsyncObservable OnStats => OnStatsSubject; - - private readonly EventSubject OnStatsSubject = new(); - - public Task Initialize() - => Task.CompletedTask; - - public Task Sync() - => Task.CompletedTask; - - public Task SubscribeToRuntime() - => Task.CompletedTask; - - public Task SubscribeToInstallation() - => Task.CompletedTask; - - public ServerStats[] GetStats(int count) - { - return []; - } - - public async ValueTask DisposeAsync() - { - OnStatsSubject.Dispose(); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Implementations/RawFileSystem.cs b/MoonlightServers.Daemon/ServerSys/Implementations/RawFileSystem.cs deleted file mode 100644 index 9db665f..0000000 --- a/MoonlightServers.Daemon/ServerSys/Implementations/RawFileSystem.cs +++ /dev/null @@ -1,60 +0,0 @@ -using MoonlightServers.Daemon.Configuration; -using MoonlightServers.Daemon.ServerSys.Abstractions; - -namespace MoonlightServers.Daemon.ServerSys.Implementations; - -public class RawFileSystem : IFileSystem -{ - public bool IsMounted { get; private set; } - public bool Exists { get; private set; } - - private readonly ServerContext Context; - private readonly AppConfiguration Configuration; - private string HostPath; - - public RawFileSystem(ServerContext context, AppConfiguration configuration) - { - Context = context; - Configuration = configuration; - } - - public Task Initialize() - { - HostPath = Path.Combine(Directory.GetCurrentDirectory(), Configuration.Storage.Volumes, Context.Configuration.Id.ToString()); - - return Task.CompletedTask; - } - - public Task Sync() - => Task.CompletedTask; - - public Task Create() - { - Directory.CreateDirectory(HostPath); - return Task.CompletedTask; - } - - public Task Mount() - { - IsMounted = true; - return Task.CompletedTask; - } - - public Task Unmount() - { - IsMounted = false; - return Task.CompletedTask; - } - - public Task Delete() - { - Directory.Delete(HostPath, true); - return Task.CompletedTask; - } - - public string GetExternalPath() - => HostPath; - - public ValueTask DisposeAsync() - => ValueTask.CompletedTask; -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/Implementations/RegexOnlineDetection.cs b/MoonlightServers.Daemon/ServerSys/Implementations/RegexOnlineDetection.cs deleted file mode 100644 index 7fc841a..0000000 --- a/MoonlightServers.Daemon/ServerSys/Implementations/RegexOnlineDetection.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Text.RegularExpressions; -using MoonCore.Observability; -using MoonlightServers.Daemon.ServerSys.Abstractions; -using MoonlightServers.Daemon.ServerSystem; - -namespace MoonlightServers.Daemon.ServerSys.Implementations; - -public class RegexOnlineDetection : IOnlineDetection -{ - private readonly ServerContext Context; - private readonly IConsole Console; - private readonly ILogger Logger; - - private Regex? Regex; - private IAsyncDisposable? ConsoleSubscription; - private IAsyncDisposable? StateSubscription; - - public RegexOnlineDetection( - ServerContext context, - IConsole console, - ILoggerFactory loggerFactory) - { - Context = context; - Console = console; - Logger = loggerFactory.CreateLogger($"Servers.Instance.{context.Configuration.Id}.{nameof(RegexOnlineDetection)}"); - } - - public async Task Initialize() - { - Logger.LogDebug("Subscribing to state changes"); - - StateSubscription = await Context.Self.OnState.SubscribeAsync(async state => - { - if (state == ServerState.Starting) // Subscribe to console when starting - { - Logger.LogDebug("Detected state change to online. Subscribing to console in order to check for the regex matches"); - - if(ConsoleSubscription != null) - await ConsoleSubscription.DisposeAsync(); - - try - { - Regex = new(Context.Configuration.OnlineDetection, RegexOptions.Compiled); - } - catch (Exception e) - { - Logger.LogError(e, "An error occured while building regex expression. Please make sure the regex is valid"); - } - - ConsoleSubscription = await Console.OnOutput.SubscribeEventAsync(HandleOutput); - } - else if (ConsoleSubscription != null) // Unsubscribe from console when any other state and not already unsubscribed - { - Logger.LogDebug("Detected state change to {state}. Unsubscribing from console", state); - - await ConsoleSubscription.DisposeAsync(); - ConsoleSubscription = null; - } - }); - } - - private async ValueTask HandleOutput(string line) - { - // Handle here just to make sure. Shouldn't be required as we - // unsubscribe from the console, as soon as we go online (or any other state). - // The regex should also not be null as we initialize it in the handler above but whatevers - if(Context.Self.StateMachine.State != ServerState.Starting || Regex == null) - return; - - if(Regex.Matches(line).Count == 0) - return; - - await Context.Self.StateMachine.FireAsync(ServerTrigger.OnlineDetected); - } - - public Task Sync() - { - Regex = new(Context.Configuration.OnlineDetection, RegexOptions.Compiled); - return Task.CompletedTask; - } - - public async ValueTask DisposeAsync() - { - if(ConsoleSubscription != null) - await ConsoleSubscription.DisposeAsync(); - - if(StateSubscription != null) - await StateSubscription.DisposeAsync(); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSys/ServerFactory.cs b/MoonlightServers.Daemon/ServerSys/ServerFactory.cs deleted file mode 100644 index f9beb7d..0000000 --- a/MoonlightServers.Daemon/ServerSys/ServerFactory.cs +++ /dev/null @@ -1,30 +0,0 @@ -using MoonlightServers.Daemon.Models.Cache; -using MoonlightServers.Daemon.ServerSys.Abstractions; - -namespace MoonlightServers.Daemon.ServerSys; - -public class ServerFactory -{ - private readonly IServiceProvider ServiceProvider; - - public ServerFactory(IServiceProvider serviceProvider) - { - ServiceProvider = serviceProvider; - } - - public Server CreateServer(ServerConfiguration configuration) - { - var scope = ServiceProvider.CreateAsyncScope(); - - var context = scope.ServiceProvider.GetRequiredService(); - - context.Configuration = configuration; - context.ServiceScope = scope; - - var server = scope.ServiceProvider.GetRequiredService(); - - context.Self = server; - - return server; - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/ServerState.cs b/MoonlightServers.Daemon/ServerSystem/Enums/ServerState.cs similarity index 52% rename from MoonlightServers.Daemon/ServerSystem/ServerState.cs rename to MoonlightServers.Daemon/ServerSystem/Enums/ServerState.cs index 693f495..39603aa 100644 --- a/MoonlightServers.Daemon/ServerSystem/ServerState.cs +++ b/MoonlightServers.Daemon/ServerSystem/Enums/ServerState.cs @@ -1,4 +1,4 @@ -namespace MoonlightServers.Daemon.ServerSystem; +namespace MoonlightServers.Daemon.ServerSystem.Enums; public enum ServerState { @@ -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 diff --git a/MoonlightServers.Daemon/ServerSystem/Enums/ServerTrigger.cs b/MoonlightServers.Daemon/ServerSystem/Enums/ServerTrigger.cs new file mode 100644 index 0000000..1338e47 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Enums/ServerTrigger.cs @@ -0,0 +1,12 @@ +namespace MoonlightServers.Daemon.ServerSystem.Enums; + +public enum ServerTrigger +{ + Start = 0, + Stop = 1, + Kill = 2, + DetectOnline = 3, + Install = 4, + Fail = 5, + Exited = 6 +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Handlers/ShutdownHandler.cs b/MoonlightServers.Daemon/ServerSystem/Handlers/ShutdownHandler.cs new file mode 100644 index 0000000..76ec2dc --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Handlers/ShutdownHandler.cs @@ -0,0 +1,42 @@ +using MoonlightServers.Daemon.ServerSystem.Enums; +using MoonlightServers.Daemon.ServerSystem.Interfaces; +using MoonlightServers.Daemon.ServerSystem.Models; +using Stateless; + +namespace MoonlightServers.Daemon.ServerSystem.Handlers; + +public class ShutdownHandler : IServerStateHandler +{ + private readonly ServerContext ServerContext; + + public ShutdownHandler(ServerContext serverContext) + { + ServerContext = serverContext; + } + + public async Task ExecuteAsync(StateMachine.Transition transition) + { + // Filter (we only want to handle exists from the runtime, so we filter out the installing state) + if (transition is + { + Destination: ServerState.Offline, + Source: not ServerState.Installing, + Trigger: ServerTrigger.Exited + }) + return; + + // Plan: + // 1. Handle possible crash + // 2. Remove runtime + + // 1. Handle possible crash + // TODO: Handle crash here + + // 2. Remove runtime + + await ServerContext.Server.Runtime.DestroyAsync(); + } + + public ValueTask DisposeAsync() + => ValueTask.CompletedTask; +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Handlers/StartupHandler.cs b/MoonlightServers.Daemon/ServerSystem/Handlers/StartupHandler.cs new file mode 100644 index 0000000..3fbfe62 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Handlers/StartupHandler.cs @@ -0,0 +1,91 @@ +using MoonlightServers.Daemon.ServerSystem.Enums; +using MoonlightServers.Daemon.ServerSystem.Interfaces; +using MoonlightServers.Daemon.ServerSystem.Models; +using Stateless; + +namespace MoonlightServers.Daemon.ServerSystem.Handlers; + +public class StartupHandler : IServerStateHandler +{ + private IAsyncDisposable? ExitSubscription; + + private readonly ServerContext Context; + private Server Server => Context.Server; + + public StartupHandler(ServerContext context) + { + Context = context; + } + + public async Task ExecuteAsync(StateMachine.Transition transition) + { + // Filter + if (transition is not {Source: ServerState.Offline, Destination: ServerState.Starting, Trigger: ServerTrigger.Start}) + return; + + // Plan: + // 1. Fetch latest configuration + // 2. Check if file system exists + // 3. Check if file system is mounted + // 4. Run file system checks + // 5. Create runtime + // 6. Attach console + // 7. Attach statistics collector + // 8. Create online detector + // 9. Start runtime + + // 1. Fetch latest configuration + // TODO + + // 2. Check if file system exists + if (!await Server.RuntimeFileSystem.CheckExistsAsync()) + await Server.RuntimeFileSystem.CreateAsync(); + + // 3. Check if file system is mounted + if (!await Server.RuntimeFileSystem.CheckMountedAsync()) + await Server.RuntimeFileSystem.CheckMountedAsync(); + + // 4. Run file system checks + await Server.RuntimeFileSystem.PerformChecksAsync(); + + // 5. Create runtime + var hostPath = await Server.RuntimeFileSystem.GetPathAsync(); + + await Server.Runtime.CreateAsync(hostPath); + + if (ExitSubscription == null) + { + ExitSubscription = await Server.Runtime.SubscribeExited(OnRuntimeExited); + } + + // 6. Attach console + + await Server.Console.AttachRuntimeAsync(); + + // 7. Attach statistics collector + + await Server.Statistics.AttachRuntimeAsync(); + + // 8. Create online detector + + await Server.OnlineDetector.CreateAsync(); + await Server.OnlineDetector.DestroyAsync(); + + // 9. Start runtime + + await Server.Runtime.StartAsync(); + } + + private async Task OnRuntimeExited(int exitCode) + { + // TODO: Notify the crash handler component of the exit code + + await Server.StateMachine.FireAsync(ServerTrigger.Exited); + } + + public async ValueTask DisposeAsync() + { + if (ExitSubscription != null) + await ExitSubscription.DisposeAsync(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IConsole.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IConsole.cs new file mode 100644 index 0000000..fe891e3 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IConsole.cs @@ -0,0 +1,72 @@ +namespace MoonlightServers.Daemon.ServerSystem.Interfaces; + +public interface IConsole : IServerComponent +{ + /// + /// Writes to the standard input of the console. If attached to the runtime when using docker for example this + /// would write into the containers standard input. + /// This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required + /// + /// The content to write + /// + public Task WriteStdInAsync(string content); + /// + /// Writes to the standard output of the console. If attached to the runtime when using docker for example this + /// would write into the containers standard output. + /// This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required + /// + /// The content to write + /// + public Task WriteStdOutAsync(string content); + + /// + /// Writes a system message to the standard output with the moonlight console prefix + /// This method *does* add the newline separator at the end + /// + /// The content to write into the standard output + /// + public Task WriteMoonlightAsync(string content); + + /// + /// Attaches the console to the runtime environment + /// + /// + public Task AttachRuntimeAsync(); + + /// + /// Attaches the console to the installation environment + /// + /// + public Task AttachInstallationAsync(); + + /// + /// Fetches all output from the runtime environment and write them into the cache without triggering any events + /// + /// + public Task FetchRuntimeAsync(); + + /// + /// Fetches all output from the installation environment and write them into the cache without triggering any events + /// + /// + public Task FetchInstallationAsync(); + + /// + /// Clears the cache of the standard output received by the environments + /// + /// + public Task ClearCacheAsync(); + + /// + /// Gets the content from the standard output cache + /// + /// The content from the cache + public Task> GetCacheAsync(); + + /// + /// Subscribes to standard output receive events + /// + /// Callback which will be invoked whenever a new line is received + /// Subscription disposable to unsubscribe from the event + public Task SubscribeStdOutAsync(Func callback); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IFileSystem.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IFileSystem.cs new file mode 100644 index 0000000..1510057 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IFileSystem.cs @@ -0,0 +1,54 @@ +namespace MoonlightServers.Daemon.ServerSystem.Interfaces; + +public interface IFileSystem : IServerComponent +{ + /// + /// Gets the path of the file system on the host operating system to be reused by other components + /// + /// Path to the file systems storage location + public Task GetPathAsync(); + + /// + /// Checks if the file system exists + /// + /// True if it does exist. False if it doesn't exist + public Task CheckExistsAsync(); + + /// + /// Checks if the file system is mounted + /// + /// True if its mounted, False if it is not mounted + public Task CheckMountedAsync(); + + /// + /// Creates the file system. E.g. Creating a virtual disk, formatting it + /// + /// + public Task CreateAsync(); + + /// + /// Performs checks and optimisations on the file system. + /// E.g. checking for corrupted files, resizing a virtual disk or adjusting file permissions + /// Requires to be called before or the file system to be in a mounted state + /// + /// + public Task PerformChecksAsync(); + + /// + /// Mounts the file system + /// + /// + public Task MountAsync(); + + /// + /// Unmounts the file system + /// + /// + public Task UnmountAsync(); + + /// + /// Destroys the file system and its contents + /// + /// + public Task DestroyAsync(); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IInstallation.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IInstallation.cs new file mode 100644 index 0000000..1ac540d --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IInstallation.cs @@ -0,0 +1,50 @@ +namespace MoonlightServers.Daemon.ServerSystem.Interfaces; + +public interface IInstallation : IServerComponent +{ + /// + /// Checks if the installation environment exists. It doesn't matter if it is currently running or not + /// + /// True if it exists, False if it doesn't + public Task CheckExistsAsync(); + + /// + /// Creates the installation environment + /// + /// The host path of the runtime storage location + /// The host path of the installation file system + /// + public Task CreateAsync(string runtimePath, string hostPath); + + /// + /// Starts the installation + /// + /// + public Task StartAsync(); + + /// + /// Kills the current installation immediately + /// + /// + public Task KillAsync(); + + /// + /// Removes the installation. E.g. removes the docker container + /// + /// + public Task DestroyAsync(); + + /// + /// Subscribes to the event when the installation exists + /// + /// The callback to invoke whenever the installation exists + /// Subscription disposable to unsubscribe from the event + public Task SubscribeExited(Func callback); + + /// + /// Connects an existing installation to this abstraction in order to restore it. + /// E.g. fetching the container id and using it for exit events + /// + /// + public Task RestoreAsync(); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IOnlineDetector.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IOnlineDetector.cs new file mode 100644 index 0000000..21cf894 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IOnlineDetector.cs @@ -0,0 +1,23 @@ +namespace MoonlightServers.Daemon.ServerSystem.Interfaces; + +public interface IOnlineDetector : IServerComponent +{ + /// + /// Creates the detection engine for the online state + /// + /// + public Task CreateAsync(); + + /// + /// Handles the detection of the online state based on the received output + /// + /// The excerpt of the output + /// True if the detection showed that the server is online. False if the detection didnt find anything + public Task HandleOutputAsync(string line); + + /// + /// Destroys the detection engine for the online state + /// + /// + public Task DestroyAsync(); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IReporter.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IReporter.cs new file mode 100644 index 0000000..4ae4552 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IReporter.cs @@ -0,0 +1,18 @@ +namespace MoonlightServers.Daemon.ServerSystem.Interfaces; + +public interface IReporter : IServerComponent +{ + /// + /// Writes both in the server logs as well in the server console the provided message as a status update + /// + /// The message to write + /// + public Task StatusAsync(string message); + + /// + /// Writes both in the server logs as well in the server console the provided message as an error + /// + /// The message to write + /// + public Task ErrorAsync(string message); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IRestorer.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IRestorer.cs new file mode 100644 index 0000000..e14ffca --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IRestorer.cs @@ -0,0 +1,16 @@ +namespace MoonlightServers.Daemon.ServerSystem.Interfaces; + +public interface IRestorer : IServerComponent +{ + /// + /// Checks for any running runtime environment from which the state can be restored from + /// + /// + public Task HandleRuntimeAsync(); + + /// + /// Checks for any running installation environment from which the state can be restored from + /// + /// + public Task HandleInstallationAsync(); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IRuntime.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IRuntime.cs new file mode 100644 index 0000000..6490d9f --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IRuntime.cs @@ -0,0 +1,55 @@ +namespace MoonlightServers.Daemon.ServerSystem.Interfaces; + +public interface IRuntime : IServerComponent +{ + /// + /// Checks if the runtime does exist. This includes already running instances + /// + /// True if it exists, False if it doesn't + public Task CheckExistsAsync(); + + /// + /// Creates the runtime with the specified path as the storage path where the server files should be stored in + /// + /// + /// + public Task CreateAsync(string path); + + /// + /// Starts the runtime. This requires to be called before this function + /// + /// + public Task StartAsync(); + + /// + /// Performs a live update on the runtime. When this method is called the current server configuration has already been updated + /// + /// + public Task UpdateAsync(); + + /// + /// Kills the current runtime immediately + /// + /// + public Task KillAsync(); + + /// + /// Destroys the runtime. When implemented using docker this would remove the container used for hosting the runtime + /// + /// + public Task DestroyAsync(); + + /// + /// This subscribes to the exited event of the runtime + /// + /// The callback gets invoked whenever the runtime exites + /// Subscription disposable to unsubscribe from the event + public Task SubscribeExited(Func callback); + + /// + /// Connects an existing runtime to this abstraction in order to restore it. + /// E.g. fetching the container id and using it for exit events + /// + /// + public Task RestoreAsync(); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IServerComponent.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IServerComponent.cs new file mode 100644 index 0000000..0414d2e --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IServerComponent.cs @@ -0,0 +1,10 @@ +namespace MoonlightServers.Daemon.ServerSystem.Interfaces; + +public interface IServerComponent : IAsyncDisposable +{ + /// + /// Initializes the server component + /// + /// + public Task InitializeAsync(); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IServerStateHandler.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IServerStateHandler.cs new file mode 100644 index 0000000..6a0c775 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IServerStateHandler.cs @@ -0,0 +1,9 @@ +using MoonlightServers.Daemon.ServerSystem.Enums; +using Stateless; + +namespace MoonlightServers.Daemon.ServerSystem.Interfaces; + +public interface IServerStateHandler : IAsyncDisposable +{ + public Task ExecuteAsync(StateMachine.Transition transition); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Interfaces/IStatistics.cs b/MoonlightServers.Daemon/ServerSystem/Interfaces/IStatistics.cs new file mode 100644 index 0000000..1de7e18 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Interfaces/IStatistics.cs @@ -0,0 +1,30 @@ +using MoonlightServers.Daemon.ServerSystem.Models; + +namespace MoonlightServers.Daemon.ServerSystem.Interfaces; + +public interface IStatistics : IServerComponent +{ + /// + /// Attaches the statistics collector to the currently running runtime + /// + /// + public Task AttachRuntimeAsync(); + + /// + /// Attaches the statistics collector to the currently running installation + /// + /// + public Task AttachInstallationAsync(); + + /// + /// Clears the statistics cache + /// + /// + public Task ClearCacheAsync(); + + /// + /// Gets the statistics data from the cache + /// + /// All data from the cache + public Task> GetCacheAsync(); +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Models/ServerContext.cs b/MoonlightServers.Daemon/ServerSystem/Models/ServerContext.cs new file mode 100644 index 0000000..e62597a --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Models/ServerContext.cs @@ -0,0 +1,11 @@ +using MoonlightServers.Daemon.Models.Cache; + +namespace MoonlightServers.Daemon.ServerSystem.Models; + +public class ServerContext +{ + public ServerConfiguration Configuration { get; set; } + public int Identifier { get; set; } + public AsyncServiceScope ServiceScope { get; set; } + public Server Server { get; set; } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Models/StatisticsData.cs b/MoonlightServers.Daemon/ServerSystem/Models/StatisticsData.cs new file mode 100644 index 0000000..210d121 --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/Models/StatisticsData.cs @@ -0,0 +1,6 @@ +namespace MoonlightServers.Daemon.ServerSystem.Models; + +public class StatisticsData +{ + +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/Server.cs b/MoonlightServers.Daemon/ServerSystem/Server.cs index db7f439..92b39d3 100644 --- a/MoonlightServers.Daemon/ServerSystem/Server.cs +++ b/MoonlightServers.Daemon/ServerSystem/Server.cs @@ -1,57 +1,82 @@ -using Microsoft.AspNetCore.SignalR; -using MoonCore.Exceptions; -using MoonlightServers.Daemon.Http.Hubs; -using MoonlightServers.Daemon.Models.Cache; +using MoonlightServers.Daemon.ServerSystem.Enums; +using MoonlightServers.Daemon.ServerSystem.Interfaces; +using MoonlightServers.Daemon.ServerSystem.Models; using Stateless; namespace MoonlightServers.Daemon.ServerSystem; -public class Server : IAsyncDisposable +public partial class Server : IAsyncDisposable { - public ServerConfiguration Configuration { get; set; } - public CancellationToken TaskCancellation => TaskCancellationSource.Token; - internal StateMachine StateMachine { get; private set; } - private CancellationTokenSource TaskCancellationSource; + public int Identifier => InnerContext.Identifier; + public ServerContext Context => InnerContext; - private Dictionary SubSystems = new(); - private ServerState InternalState = ServerState.Offline; + public IConsole Console { get; } + public IFileSystem RuntimeFileSystem { get; } + public IFileSystem InstallationFileSystem { get; } + public IInstallation Installation { get; } + public IOnlineDetector OnlineDetector { get; } + public IReporter Reporter { get; } + public IRestorer Restorer { get; } + public IRuntime Runtime { get; } + public IStatistics Statistics { get; } + public StateMachine StateMachine { get; private set; } - private readonly IHubContext HubContext; - private readonly IServiceScope ServiceScope; - private readonly ILoggerFactory LoggerFactory; + private readonly IServerStateHandler[] Handlers; + + private readonly IServerComponent[] AllComponents; + private readonly ServerContext InnerContext; private readonly ILogger Logger; - + public Server( - ServerConfiguration configuration, - IServiceScope serviceScope, - IHubContext hubContext + ILogger logger, + ServerContext context, + IConsole console, + IFileSystem runtimeFileSystem, + IFileSystem installationFileSystem, + IInstallation installation, + IOnlineDetector onlineDetector, + IReporter reporter, + IRestorer restorer, + IRuntime runtime, + IStatistics statistics, + IServerStateHandler[] handlers ) { - Configuration = configuration; - ServiceScope = serviceScope; - HubContext = hubContext; + Logger = logger; + InnerContext = context; + Console = console; + RuntimeFileSystem = runtimeFileSystem; + InstallationFileSystem = installationFileSystem; + Installation = installation; + OnlineDetector = onlineDetector; + Reporter = reporter; + Restorer = restorer; + Runtime = runtime; + Statistics = statistics; - TaskCancellationSource = new CancellationTokenSource(); + AllComponents = + [ + Console, RuntimeFileSystem, InstallationFileSystem, Installation, OnlineDetector, Reporter, Restorer, + Runtime, Statistics + ]; - LoggerFactory = serviceScope.ServiceProvider.GetRequiredService(); - Logger = LoggerFactory.CreateLogger($"Server {Configuration.Id}"); + Handlers = handlers; + } + private void ConfigureStateMachine(ServerState initialState) + { StateMachine = new StateMachine( - () => InternalState, - state => InternalState = state, - FiringMode.Queued + initialState, FiringMode.Queued ); - // Configure basic state machine flow - StateMachine.Configure(ServerState.Offline) .Permit(ServerTrigger.Start, ServerState.Starting) .Permit(ServerTrigger.Install, ServerState.Installing) - .PermitReentry(ServerTrigger.FailSafe); + .PermitReentry(ServerTrigger.Fail); StateMachine.Configure(ServerState.Starting) - .Permit(ServerTrigger.OnlineDetected, ServerState.Online) - .Permit(ServerTrigger.FailSafe, ServerState.Offline) + .Permit(ServerTrigger.DetectOnline, ServerState.Online) + .Permit(ServerTrigger.Fail, ServerState.Offline) .Permit(ServerTrigger.Exited, ServerState.Offline) .Permit(ServerTrigger.Stop, ServerState.Stopping) .Permit(ServerTrigger.Kill, ServerState.Stopping); @@ -62,128 +87,98 @@ public class Server : IAsyncDisposable .Permit(ServerTrigger.Exited, ServerState.Offline); StateMachine.Configure(ServerState.Stopping) - .PermitReentry(ServerTrigger.FailSafe) + .PermitReentry(ServerTrigger.Fail) .PermitReentry(ServerTrigger.Kill) .Permit(ServerTrigger.Exited, ServerState.Offline); StateMachine.Configure(ServerState.Installing) - .Permit(ServerTrigger.FailSafe, ServerState.Offline) + .Permit(ServerTrigger.Fail, ServerState.Offline) // TODO: Add kill .Permit(ServerTrigger.Exited, ServerState.Offline); - - StateMachine.Configure(ServerState.Offline) - .OnEntryAsync(async () => - { - // Configure task reset when server goes offline - - if (!TaskCancellationSource.IsCancellationRequested) - await TaskCancellationSource.CancelAsync(); - }) - .OnExit(() => - { - // Activate tasks when the server goes online - // If we don't separate the disabling and enabling - // of the tasks and would do both it in just the offline handler - // we would have edge cases where reconnect loops would already have the new task activated - // while they are supposed to shut down. I tested the handling of the state machine, - // and it executes on exit before the other listeners from the other sub systems - TaskCancellationSource = new(); - }); + } - // Setup websocket notify for state changes + private void ConfigureStateMachineEvents() + { + // Configure the calling of the handlers StateMachine.OnTransitionedAsync(async transition => { - await HubContext.Clients - .Group(Configuration.Id.ToString()) - .SendAsync("StateChanged", transition.Destination.ToString()); + var hasFailed = false; + + foreach (var handler in Handlers) + { + try + { + await handler.ExecuteAsync(transition); + } + catch (Exception e) + { + Logger.LogError( + e, + "Handler {name} has thrown an unexpected exception", + handler.GetType().FullName + ); + + hasFailed = true; + break; + } + } + + if(!hasFailed) + return; // Everything went fine, we can exit now + + // Something has failed, lets check if we can handle the error + // via a fail trigger + + if(!StateMachine.CanFire(ServerTrigger.Fail)) + return; + + // Trigger the fail so the server gets a chance to handle the error softly + await StateMachine.FireAsync(ServerTrigger.Fail); }); } - public async Task Initialize(Type[] subSystemTypes) + private async Task HandleSaveAsync(Func callback) { - foreach (var type in subSystemTypes) + try { - var logger = LoggerFactory.CreateLogger($"Server {Configuration.Id} - {type.Name}"); - - var subSystem = ActivatorUtilities.CreateInstance( - ServiceScope.ServiceProvider, - type, - this, - logger - ) as ServerSubSystem; - - if (subSystem == null) - { - Logger.LogError("Unable to construct server sub system: {name}", type.Name); - continue; - } - - SubSystems.Add(type, subSystem); + await callback.Invoke(); } + catch (Exception e) + { + Logger.LogError(e, "An error occured while handling"); - foreach (var type in SubSystems.Keys) - { - try - { - await SubSystems[type].Initialize(); - } - catch (Exception e) - { - Logger.LogError("An unhandled error occured while initializing sub system {name}: {e}", type.Name, e); - } + await StateMachine.FireAsync(ServerTrigger.Fail); } } - public async Task Trigger(ServerTrigger trigger) + private async Task HandleIgnoredAsync(Func callback) { - if (!StateMachine.CanFire(trigger)) - throw new HttpApiException($"The trigger {trigger} is not supported during the state {StateMachine.State}", 400); - - await StateMachine.FireAsync(trigger); + try + { + await callback.Invoke(); + } + catch (Exception e) + { + Logger.LogError(e, "An error occured while handling"); + } } - public async Task Delete() + public async Task InitializeAsync() { - foreach (var subSystem in SubSystems.Values) - await subSystem.Delete(); - } + foreach (var component in AllComponents) + await component.InitializeAsync(); - // This method completely bypasses the state machine. - // Using this method without any checks will lead to - // broken server states. Use with caution - public void OverrideState(ServerState state) - { - InternalState = state; - } + var restoredState = ServerState.Offline; - public T? GetSubSystem() where T : ServerSubSystem - { - var type = typeof(T); - var subSystem = SubSystems.GetValueOrDefault(type); - - if (subSystem == null) - return null; - - return subSystem as T; - } - - public T GetRequiredSubSystem() where T : ServerSubSystem - { - var subSystem = GetSubSystem(); - - if (subSystem == null) - throw new AggregateException("Unable to resolve requested sub system"); - - return subSystem; + ConfigureStateMachine(restoredState); + ConfigureStateMachineEvents(); } public async ValueTask DisposeAsync() { - if (!TaskCancellationSource.IsCancellationRequested) - await TaskCancellationSource.CancelAsync(); - - foreach (var subSystem in SubSystems.Values) - await subSystem.DisposeAsync(); + foreach (var handler in Handlers) + await handler.DisposeAsync(); - ServiceScope.Dispose(); + foreach (var component in AllComponents) + await component.DisposeAsync(); } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs b/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs new file mode 100644 index 0000000..78bac7f --- /dev/null +++ b/MoonlightServers.Daemon/ServerSystem/ServerFactory.cs @@ -0,0 +1,66 @@ +using MoonlightServers.Daemon.Models.Cache; +using MoonlightServers.Daemon.ServerSystem.Interfaces; +using MoonlightServers.Daemon.ServerSystem.Models; + +namespace MoonlightServers.Daemon.ServerSystem; + +public class ServerFactory +{ + private readonly IServiceProvider ServiceProvider; + + public ServerFactory(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public async Task Create(ServerConfiguration configuration) + { + var scope = ServiceProvider.CreateAsyncScope(); + + var loggerFactory = scope.ServiceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger($"Servers.Instance.{configuration.Id}.{nameof(Server)}"); + + var context = scope.ServiceProvider.GetRequiredService(); + + context.Identifier = configuration.Id; + context.Configuration = configuration; + context.ServiceScope = scope; + + // Define all required components + + IConsole console; + IFileSystem runtimeFs; + IFileSystem installFs; + IInstallation installation; + IOnlineDetector onlineDetector; + IReporter reporter; + IRestorer restorer; + IRuntime runtime; + IStatistics statistics; + + // Resolve the components + // TODO: Add a plugin hook for dynamically resolving components and checking if any is unset + + // Resolve server from di + var server = new Server( + logger, + context, + // Now all components + console, + runtimeFs, + installFs, + installation, + onlineDetector, + reporter, + restorer, + runtime, + statistics, + // And now all the handlers + [] + ); + + context.Server = server; + + return server; + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/ServerSubSystem.cs b/MoonlightServers.Daemon/ServerSystem/ServerSubSystem.cs deleted file mode 100644 index e977b32..0000000 --- a/MoonlightServers.Daemon/ServerSystem/ServerSubSystem.cs +++ /dev/null @@ -1,27 +0,0 @@ -using MoonlightServers.Daemon.Models.Cache; -using Stateless; - -namespace MoonlightServers.Daemon.ServerSystem; - -public abstract class ServerSubSystem : IAsyncDisposable -{ - protected Server Server { get; private set; } - protected ServerConfiguration Configuration => Server.Configuration; - protected ILogger Logger { get; private set; } - protected StateMachine StateMachine => Server.StateMachine; - - protected ServerSubSystem(Server server, ILogger logger) - { - Server = server; - Logger = logger; - } - - public virtual Task Initialize() - => Task.CompletedTask; - - public virtual Task Delete() - => Task.CompletedTask; - - public virtual ValueTask DisposeAsync() - => ValueTask.CompletedTask; -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/ServerTrigger.cs b/MoonlightServers.Daemon/ServerSystem/ServerTrigger.cs deleted file mode 100644 index 48a3256..0000000 --- a/MoonlightServers.Daemon/ServerSystem/ServerTrigger.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MoonlightServers.Daemon.ServerSystem; - -public enum ServerTrigger -{ - Start = 0, - Stop = 1, - Kill = 2, - Install = 3, - Exited = 4, - OnlineDetected = 5, - FailSafe = 6 -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/SubSystems/ConsoleSubSystem.cs b/MoonlightServers.Daemon/ServerSystem/SubSystems/ConsoleSubSystem.cs deleted file mode 100644 index 58efb67..0000000 --- a/MoonlightServers.Daemon/ServerSystem/SubSystems/ConsoleSubSystem.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System.Text; -using Docker.DotNet; -using Docker.DotNet.Models; -using Microsoft.AspNetCore.SignalR; -using MoonlightServers.Daemon.Http.Hubs; - -namespace MoonlightServers.Daemon.ServerSystem.SubSystems; - -public class ConsoleSubSystem : ServerSubSystem -{ - public event Func? OnOutput; - public event Func? OnInput; - - private MultiplexedStream? Stream; - private readonly List OutputCache = new(); - - private readonly IHubContext HubContext; - private readonly DockerClient DockerClient; - - public ConsoleSubSystem( - Server server, - ILogger logger, - IHubContext hubContext, - DockerClient dockerClient - ) : base(server, logger) - { - HubContext = hubContext; - DockerClient = dockerClient; - } - - public override Task Initialize() - { - OnInput += async content => - { - if (Stream == null) - return; - - var contentBuffer = Encoding.UTF8.GetBytes(content); - - await Stream.WriteAsync( - contentBuffer, - 0, - contentBuffer.Length, - Server.TaskCancellation - ); - }; - - return Task.CompletedTask; - } - - public Task Attach(string containerId) - { - // Reading - Task.Run(async () => - { - // This loop is here to reconnect to the container if for some reason the container - // attach stream fails before the server tasks have been canceled i.e. the before the server - // goes offline - - while (!Server.TaskCancellation.IsCancellationRequested) - { - try - { - Stream = await DockerClient.Containers.AttachContainerAsync(containerId, - true, - new ContainerAttachParameters() - { - Stderr = true, - Stdin = true, - Stdout = true, - Stream = true - }, - Server.TaskCancellation - ); - - var buffer = new byte[1024]; - - try - { - // Read while server tasks are not canceled - while (!Server.TaskCancellation.IsCancellationRequested) - { - var readResult = await Stream.ReadOutputAsync( - buffer, - 0, - buffer.Length, - Server.TaskCancellation - ); - - 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 WriteOutput(decodedText); - } - } - catch (TaskCanceledException) - { - // Ignored - } - catch (OperationCanceledException) - { - // Ignored - } - catch (Exception e) - { - Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", e); - } - finally - { - Stream.Dispose(); - } - } - catch (TaskCanceledException) - { - // ignored - } - catch (Exception e) - { - Logger.LogError("An error occured while attaching to container: {e}", e); - } - } - - - // Reset stream so no further inputs will be piped to it - Stream = null; - - Logger.LogDebug("Disconnected from container stream"); - }); - - return Task.CompletedTask; - } - - public async Task WriteOutput(string output) - { - lock (OutputCache) - { - // Shrink cache if it exceeds the maximum - if (OutputCache.Count > 400) - OutputCache.RemoveRange(0, 100); - - OutputCache.Add(output); - } - - if (OnOutput != null) - await OnOutput.Invoke(output); - - await HubContext.Clients - .Group(Configuration.Id.ToString()) - .SendAsync("ConsoleOutput", output); - } - - public async Task WriteMoonlight(string output) - { - await WriteOutput( - $"\x1b[0;38;2;255;255;255;48;2;124;28;230m Moonlight \x1b[0m\x1b[38;5;250m\x1b[3m {output}\x1b[0m\n\r"); - } - - public async Task WriteInput(string input) - { - if (OnInput != null) - await OnInput.Invoke(input); - } - - public Task RetrieveCache() - { - string[] result; - - lock (OutputCache) - result = OutputCache.ToArray(); - - return Task.FromResult(result); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/SubSystems/DebugSubSystem.cs b/MoonlightServers.Daemon/ServerSystem/SubSystems/DebugSubSystem.cs deleted file mode 100644 index bb22184..0000000 --- a/MoonlightServers.Daemon/ServerSystem/SubSystems/DebugSubSystem.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace MoonlightServers.Daemon.ServerSystem.SubSystems; - -public class DebugSubSystem : ServerSubSystem -{ - public DebugSubSystem(Server server, ILogger logger) : base(server, logger) - { - - } - - public override Task Initialize() - { - StateMachine.OnTransitioned(transition => - { - Logger.LogTrace("State: {state} via {trigger}", transition.Destination, transition.Trigger); - }); - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/SubSystems/InstallationSubSystem.cs b/MoonlightServers.Daemon/ServerSystem/SubSystems/InstallationSubSystem.cs deleted file mode 100644 index 2f522ed..0000000 --- a/MoonlightServers.Daemon/ServerSystem/SubSystems/InstallationSubSystem.cs +++ /dev/null @@ -1,239 +0,0 @@ -using Docker.DotNet; -using MoonlightServers.Daemon.Configuration; -using MoonlightServers.Daemon.Extensions; -using MoonlightServers.Daemon.Services; - -namespace MoonlightServers.Daemon.ServerSystem.SubSystems; - -public class InstallationSubSystem : ServerSubSystem -{ - public string? CurrentContainerId { get; set; } - - private readonly DockerClient DockerClient; - private readonly RemoteService RemoteService; - private readonly DockerImageService DockerImageService; - private readonly AppConfiguration AppConfiguration; - - public InstallationSubSystem( - Server server, - ILogger logger, - DockerClient dockerClient, - RemoteService remoteService, - DockerImageService dockerImageService, - AppConfiguration appConfiguration - ) : base(server, logger) - { - DockerClient = dockerClient; - RemoteService = remoteService; - DockerImageService = dockerImageService; - AppConfiguration = appConfiguration; - } - - public override Task Initialize() - { - StateMachine.Configure(ServerState.Installing) - .OnEntryAsync(HandleProvision); - - StateMachine.Configure(ServerState.Installing) - .OnExitAsync(HandleDeprovision); - - return Task.CompletedTask; - } - - #region Provision - - private async Task HandleProvision() - { - try - { - await Provision(); - } - catch (Exception e) - { - Logger.LogError("An error occured while provisioning installation: {e}", e); - - await StateMachine.FireAsync(ServerTrigger.FailSafe); - } - } - - private async Task Provision() - { - // What will happen here: - // 1. Remove possible existing container - // 2. Fetch latest configuration & install configuration - // 3. Ensure the storage location exists - // 4. Copy script to set location - // 5. Ensure the docker image has been downloaded - // 6. Create the docker container - // 7. Attach the console - // 8. Start the container - - // Define some shared variables: - var containerName = $"moonlight-install-{Configuration.Id}"; - - var consoleSubSystem = Server.GetRequiredSubSystem(); - - // Reset container tracking id, so if we kill an old container it won't - // trigger an Exited event :> - CurrentContainerId = null; - - // 1. Remove possible existing container - - try - { - var existingContainer = await DockerClient.Containers - .InspectContainerAsync(containerName); - - if (existingContainer.State.Running) - { - Logger.LogDebug("Killing old docker container"); - await consoleSubSystem.WriteMoonlight("Killing old container"); - - await DockerClient.Containers.KillContainerAsync(existingContainer.ID, new()); - } - - Logger.LogDebug("Removing old docker container"); - await consoleSubSystem.WriteMoonlight("Removing old container"); - - await DockerClient.Containers.RemoveContainerAsync(existingContainer.ID, new()); - } - catch (DockerContainerNotFoundException) - { - // Ignored - } - - // 2. Fetch latest configuration - - Logger.LogDebug("Fetching latest configuration from panel"); - await consoleSubSystem.WriteMoonlight("Updating configuration"); - - var serverData = await RemoteService.GetServer(Configuration.Id); - var latestConfiguration = serverData.ToServerConfiguration(); - - Server.Configuration = latestConfiguration; - - var installData = await RemoteService.GetServerInstallation(Configuration.Id); - - // 3. Ensure the storage locations exists - - Logger.LogDebug("Ensuring storage"); - - var storageSubSystem = Server.GetRequiredSubSystem(); - - if (!await storageSubSystem.RequestRuntimeVolume()) - { - Logger.LogDebug("Unable to continue provision because the server file system isn't ready"); - await consoleSubSystem.WriteMoonlight("Server file system is not ready yet. Try again later"); - - await StateMachine.FireAsync(ServerTrigger.FailSafe); - return; - } - - var runtimePath = storageSubSystem.RuntimeVolumePath; - - await storageSubSystem.EnsureInstallVolume(); - var installPath = storageSubSystem.InstallVolumePath; - - // 4. Copy script to location - - var content = installData.Script.Replace("\r\n", "\n"); - await File.WriteAllTextAsync(Path.Combine(installPath, "install.sh"), content); - - // 5. Ensure the docker image is downloaded - - Logger.LogDebug("Downloading docker image"); - await consoleSubSystem.WriteMoonlight("Downloading docker image"); - - await DockerImageService.Download(installData.DockerImage, - async updateMessage => { await consoleSubSystem.WriteMoonlight(updateMessage); }); - - Logger.LogDebug("Docker image downloaded"); - await consoleSubSystem.WriteMoonlight("Downloaded docker image"); - - // 6. Create the docker container - - Logger.LogDebug("Creating docker container"); - await consoleSubSystem.WriteMoonlight("Creating container"); - - var containerParams = Configuration.ToInstallationCreateParameters( - AppConfiguration, - runtimePath, - installPath, - containerName, - installData.DockerImage, - installData.Shell - ); - - var creationResult = await DockerClient.Containers.CreateContainerAsync(containerParams); - CurrentContainerId = creationResult.ID; - - // 7. Attach the console - - Logger.LogDebug("Attaching console"); - await consoleSubSystem.Attach(CurrentContainerId); - - // 8. Start the docker container - - Logger.LogDebug("Starting docker container"); - await consoleSubSystem.WriteMoonlight("Starting container"); - - await DockerClient.Containers.StartContainerAsync(containerName, new()); - } - - #endregion - - #region Deprovision - - private async Task HandleDeprovision() - { - try - { - await Deprovision(); - } - catch (Exception e) - { - Logger.LogError("An error occured while deprovisioning installation: {e}", e); - - await StateMachine.FireAsync(ServerTrigger.FailSafe); - } - } - - private async Task Deprovision() - { - // Handle possible unknown container id calls - if (string.IsNullOrEmpty(CurrentContainerId)) - { - Logger.LogDebug("Skipping deprovisioning as the current container id is not set"); - return; - } - - var consoleSubSystem = Server.GetRequiredSubSystem(); - - // Destroy container - - try - { - Logger.LogDebug("Removing docker container"); - await consoleSubSystem.WriteMoonlight("Removing container"); - - await DockerClient.Containers.RemoveContainerAsync(CurrentContainerId, new()); - } - catch (DockerContainerNotFoundException) - { - // Ignored - } - - CurrentContainerId = null; - - // Remove install volume - - var storageSubSystem = Server.GetRequiredSubSystem(); - - Logger.LogDebug("Removing installation data"); - await consoleSubSystem.WriteMoonlight("Removing installation data"); - - await storageSubSystem.DeleteInstallVolume(); - } - - #endregion -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/SubSystems/OnlineDetectionService.cs b/MoonlightServers.Daemon/ServerSystem/SubSystems/OnlineDetectionService.cs deleted file mode 100644 index ffadb13..0000000 --- a/MoonlightServers.Daemon/ServerSystem/SubSystems/OnlineDetectionService.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Text.RegularExpressions; - -namespace MoonlightServers.Daemon.ServerSystem.SubSystems; - -public class OnlineDetectionService : ServerSubSystem -{ - // We are compiling the regex when the first output has been received - // and resetting it after the server has stopped to maximize the performance - // but allowing the startup detection string to change :> - - private Regex? CompiledRegex = null; - - public OnlineDetectionService(Server server, ILogger logger) : base(server, logger) - { - - } - - public override Task Initialize() - { - var consoleSubSystem = Server.GetRequiredSubSystem(); - - consoleSubSystem.OnOutput += async line => - { - if(StateMachine.State != ServerState.Starting) - return; - - if (CompiledRegex == null) - CompiledRegex = new Regex(Configuration.OnlineDetection, RegexOptions.Compiled); - - if (Regex.Matches(line, Configuration.OnlineDetection).Count == 0) - return; - - await StateMachine.FireAsync(ServerTrigger.OnlineDetected); - }; - - StateMachine.Configure(ServerState.Offline) - .OnEntryAsync(_ => - { - CompiledRegex = null; - return Task.CompletedTask; - }); - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/SubSystems/ProvisionSubSystem.cs b/MoonlightServers.Daemon/ServerSystem/SubSystems/ProvisionSubSystem.cs deleted file mode 100644 index 42a7927..0000000 --- a/MoonlightServers.Daemon/ServerSystem/SubSystems/ProvisionSubSystem.cs +++ /dev/null @@ -1,226 +0,0 @@ -using Docker.DotNet; -using MoonlightServers.Daemon.Configuration; -using MoonlightServers.Daemon.Extensions; -using MoonlightServers.Daemon.Services; -using Stateless; - -namespace MoonlightServers.Daemon.ServerSystem.SubSystems; - -public class ProvisionSubSystem : ServerSubSystem -{ - public string? CurrentContainerId { get; set; } - - private readonly DockerClient DockerClient; - private readonly AppConfiguration AppConfiguration; - private readonly RemoteService RemoteService; - private readonly DockerImageService DockerImageService; - - public ProvisionSubSystem( - Server server, - ILogger logger, - DockerClient dockerClient, - AppConfiguration appConfiguration, - RemoteService remoteService, - DockerImageService dockerImageService - ) : base(server, logger) - { - DockerClient = dockerClient; - AppConfiguration = appConfiguration; - RemoteService = remoteService; - DockerImageService = dockerImageService; - } - - public override Task Initialize() - { - StateMachine.Configure(ServerState.Starting) - .OnEntryFromAsync(ServerTrigger.Start, HandleProvision); - - StateMachine.Configure(ServerState.Offline) - .OnEntryAsync(HandleDeprovision); - - return Task.CompletedTask; - } - - #region Provisioning - - private async Task HandleProvision() - { - try - { - await Provision(); - } - catch (Exception e) - { - Logger.LogError("An error occured while provisioning server: {e}", e); - - await StateMachine.FireAsync(ServerTrigger.FailSafe); - } - } - - private async Task Provision() - { - // What will happen here: - // 1. Remove possible existing container - // 2. Fetch latest configuration - // 3. Ensure the storage location exists - // 4. Ensure the docker image has been downloaded - // 5. Create the docker container - // 6. Attach the console - // 7. Attach to stats - // 8. Start the container - - // Define some shared variables: - var containerName = $"moonlight-runtime-{Configuration.Id}"; - - var consoleSubSystem = Server.GetRequiredSubSystem(); - - // Reset container tracking id, so if we kill an old container it won't - // trigger an Exited event :> - CurrentContainerId = null; - - // 1. Remove possible existing container - - try - { - var existingContainer = await DockerClient.Containers - .InspectContainerAsync(containerName); - - if (existingContainer.State.Running) - { - Logger.LogDebug("Killing old docker container"); - await consoleSubSystem.WriteMoonlight("Killing old container"); - - await DockerClient.Containers.KillContainerAsync(existingContainer.ID, new()); - } - - Logger.LogDebug("Removing old docker container"); - await consoleSubSystem.WriteMoonlight("Removing old container"); - - await DockerClient.Containers.RemoveContainerAsync(existingContainer.ID, new()); - } - catch (DockerContainerNotFoundException) - { - // Ignored - } - - // 2. Fetch latest configuration - - Logger.LogDebug("Fetching latest configuration from panel"); - await consoleSubSystem.WriteMoonlight("Updating configuration"); - - var serverData = await RemoteService.GetServer(Configuration.Id); - var latestConfiguration = serverData.ToServerConfiguration(); - - Server.Configuration = latestConfiguration; - - // 3. Ensure the storage location exists - - Logger.LogDebug("Ensuring storage"); - - var storageSubSystem = Server.GetRequiredSubSystem(); - - if (!await storageSubSystem.RequestRuntimeVolume()) - { - Logger.LogDebug("Unable to continue provision because the server file system isn't ready"); - await consoleSubSystem.WriteMoonlight("Server file system is not ready yet. Try again later"); - - await StateMachine.FireAsync(ServerTrigger.FailSafe); - return; - } - - var volumePath = storageSubSystem.RuntimeVolumePath; - - // 4. Ensure the docker image is downloaded - - Logger.LogDebug("Downloading docker image"); - await consoleSubSystem.WriteMoonlight("Downloading docker image"); - - await DockerImageService.Download(Configuration.DockerImage, async updateMessage => - { - await consoleSubSystem.WriteMoonlight(updateMessage); - }); - - Logger.LogDebug("Docker image downloaded"); - await consoleSubSystem.WriteMoonlight("Downloaded docker image"); - - // 5. Create the docker container - - Logger.LogDebug("Creating docker container"); - await consoleSubSystem.WriteMoonlight("Creating container"); - - var containerParams = Configuration.ToRuntimeCreateParameters( - AppConfiguration, - volumePath, - containerName - ); - - var creationResult = await DockerClient.Containers.CreateContainerAsync(containerParams); - CurrentContainerId = creationResult.ID; - - // 6. Attach the console - - Logger.LogDebug("Attaching console"); - await consoleSubSystem.Attach(CurrentContainerId); - - // 7. Attach stats stream - - var statsSubSystem = Server.GetRequiredSubSystem(); - - await statsSubSystem.Attach(CurrentContainerId); - - // 8. Start the docker container - - Logger.LogDebug("Starting docker container"); - await consoleSubSystem.WriteMoonlight("Starting container"); - - await DockerClient.Containers.StartContainerAsync(containerName, new()); - } - - #endregion - - #region Deprovision - - private async Task HandleDeprovision(StateMachine.Transition transition) - { - try - { - await Deprovision(); - } - catch (Exception e) - { - Logger.LogError("An error occured while provisioning server: {e}", e); - - await StateMachine.FireAsync(ServerTrigger.FailSafe); - } - } - - private async Task Deprovision() - { - // Handle possible unknown container id calls - if (string.IsNullOrEmpty(CurrentContainerId)) - { - Logger.LogDebug("Skipping deprovisioning as the current container id is not set"); - return; - } - - var consoleSubSystem = Server.GetRequiredSubSystem(); - - // Destroy container - - try - { - Logger.LogDebug("Removing docker container"); - await consoleSubSystem.WriteMoonlight("Removing container"); - - await DockerClient.Containers.RemoveContainerAsync(CurrentContainerId, new()); - } - catch (DockerContainerNotFoundException) - { - // Ignored - } - - CurrentContainerId = null; - } - - #endregion -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/SubSystems/RestoreSubSystem.cs b/MoonlightServers.Daemon/ServerSystem/SubSystems/RestoreSubSystem.cs deleted file mode 100644 index 0eb72b8..0000000 --- a/MoonlightServers.Daemon/ServerSystem/SubSystems/RestoreSubSystem.cs +++ /dev/null @@ -1,117 +0,0 @@ -using Docker.DotNet; - -namespace MoonlightServers.Daemon.ServerSystem.SubSystems; - -public class RestoreSubSystem : ServerSubSystem -{ - private readonly DockerClient DockerClient; - - public RestoreSubSystem(Server server, ILogger logger, DockerClient dockerClient) : base(server, logger) - { - DockerClient = dockerClient; - } - - public override async Task Initialize() - { - Logger.LogDebug("Searching for restorable container"); - - // Handle possible runtime container - - var runtimeContainerName = $"moonlight-runtime-{Configuration.Id}"; - - try - { - var runtimeContainer = await DockerClient.Containers.InspectContainerAsync(runtimeContainerName); - - if (runtimeContainer.State.Running) - { - var provisionSubSystem = Server.GetRequiredSubSystem(); - - // Override values - provisionSubSystem.CurrentContainerId = runtimeContainer.ID; - Server.OverrideState(ServerState.Online); - - // Update and attach console - - var consoleSubSystem = Server.GetRequiredSubSystem(); - - var logStream = await DockerClient.Containers.GetContainerLogsAsync(runtimeContainerName, true, new () - { - Follow = false, - ShowStderr = true, - ShowStdout = true - }); - - var (standardOutput, standardError) = await logStream.ReadOutputToEndAsync(CancellationToken.None); - - // We split up the read output data into their lines to prevent overloading - // the console by one large string - - foreach (var line in standardOutput.Split("\n")) - await consoleSubSystem.WriteOutput(line + "\n"); - - foreach (var line in standardError.Split("\n")) - await consoleSubSystem.WriteOutput(line + "\n"); - - await consoleSubSystem.Attach(provisionSubSystem.CurrentContainerId); - - // Attach stats - var statsSubSystem = Server.GetRequiredSubSystem(); - await statsSubSystem.Attach(provisionSubSystem.CurrentContainerId); - - // Done :> - Logger.LogInformation("Restored runtime container successfully"); - return; - } - } - catch (DockerContainerNotFoundException) - { - // Ignored - } - - // Handle possible installation container - - var installContainerName = $"moonlight-install-{Configuration.Id}"; - - try - { - var installContainer = await DockerClient.Containers.InspectContainerAsync(installContainerName); - - if (installContainer.State.Running) - { - var installationSubSystem = Server.GetRequiredSubSystem(); - - // Override values - installationSubSystem.CurrentContainerId = installContainer.ID; - Server.OverrideState(ServerState.Installing); - - var consoleSubSystem = Server.GetRequiredSubSystem(); - - var logStream = await DockerClient.Containers.GetContainerLogsAsync(installContainerName, true, new () - { - Follow = false, - ShowStderr = true, - ShowStdout = true - }); - - var (standardOutput, standardError) = await logStream.ReadOutputToEndAsync(CancellationToken.None); - - // We split up the read output data into their lines to prevent overloading - // the console by one large string - - foreach (var line in standardOutput.Split("\n")) - await consoleSubSystem.WriteOutput(line + "\n"); - - foreach (var line in standardError.Split("\n")) - await consoleSubSystem.WriteOutput(line + "\n"); - - await consoleSubSystem.Attach(installationSubSystem.CurrentContainerId); - return; - } - } - catch (DockerContainerNotFoundException) - { - // Ignored - } - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/SubSystems/ShutdownSubSystem.cs b/MoonlightServers.Daemon/ServerSystem/SubSystems/ShutdownSubSystem.cs deleted file mode 100644 index 8865e59..0000000 --- a/MoonlightServers.Daemon/ServerSystem/SubSystems/ShutdownSubSystem.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Docker.DotNet; - -namespace MoonlightServers.Daemon.ServerSystem.SubSystems; - -public class ShutdownSubSystem : ServerSubSystem -{ - private readonly DockerClient DockerClient; - - public ShutdownSubSystem( - Server server, - ILogger logger, - DockerClient dockerClient - ) : base(server, logger) - { - DockerClient = dockerClient; - } - - public override Task Initialize() - { - StateMachine.Configure(ServerState.Stopping) - .OnEntryFromAsync(ServerTrigger.Stop, HandleStop) - .OnEntryFromAsync(ServerTrigger.Kill, HandleKill); - - return Task.CompletedTask; - } - - #region Stopping - - private async Task HandleStop() - { - try - { - await Stop(); - } - catch (Exception e) - { - Logger.LogError("An error occured while stopping container: {e}", e); - await StateMachine.FireAsync(ServerTrigger.FailSafe); - } - } - - private async Task Stop() - { - var provisionSubSystem = Server.GetRequiredSubSystem(); - - // Handle signal stopping - if (Configuration.StopCommand.StartsWith('^')) - { - await DockerClient.Containers.KillContainerAsync(provisionSubSystem.CurrentContainerId, new() - { - Signal = Configuration.StopCommand.Replace("^", "") - }); - } - else // Handle input stopping - { - var consoleSubSystem = Server.GetRequiredSubSystem(); - await consoleSubSystem.WriteInput($"{Configuration.StopCommand}\n\r"); - } - } - - #endregion - - private async Task HandleKill() - { - try - { - await Kill(); - } - catch (Exception e) - { - Logger.LogError("An error occured while killing container: {e}", e); - await StateMachine.FireAsync(ServerTrigger.FailSafe); - } - } - - private async Task Kill() - { - var provisionSubSystem = Server.GetRequiredSubSystem(); - - await DockerClient.Containers.KillContainerAsync( - provisionSubSystem.CurrentContainerId, - new() - ); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/SubSystems/StatsSubSystem.cs b/MoonlightServers.Daemon/ServerSystem/SubSystems/StatsSubSystem.cs deleted file mode 100644 index 8f47454..0000000 --- a/MoonlightServers.Daemon/ServerSystem/SubSystems/StatsSubSystem.cs +++ /dev/null @@ -1,150 +0,0 @@ -using Docker.DotNet; -using Docker.DotNet.Models; -using Microsoft.AspNetCore.SignalR; -using MoonlightServers.Daemon.Http.Hubs; -using MoonlightServers.DaemonShared.DaemonSide.Models; - -namespace MoonlightServers.Daemon.ServerSystem.SubSystems; - -public class StatsSubSystem : ServerSubSystem -{ - public ServerStats CurrentStats { get; private set; } - - private readonly DockerClient DockerClient; - private readonly IHubContext HubContext; - - public StatsSubSystem( - Server server, - ILogger logger, - DockerClient dockerClient, - IHubContext hubContext - ) : base(server, logger) - { - DockerClient = dockerClient; - HubContext = hubContext; - - CurrentStats = new(); - } - - public Task Attach(string containerId) - { - Logger.LogDebug("Attaching to stats stream"); - - Task.Run(async () => - { - while (!Server.TaskCancellation.IsCancellationRequested) - { - try - { - await DockerClient.Containers.GetContainerStatsAsync( - containerId, - new() - { - Stream = true - }, - new Progress(async response => - { - try - { - var stats = ConvertToStats(response); - - // Update current stats for usage of other components - CurrentStats = stats; - - await HubContext.Clients - .Group(Configuration.Id.ToString()) - .SendAsync("StatsUpdated", stats); - } - catch (Exception e) - { - Logger.LogError("An error occured handling stats update: {e}", e); - } - }), - Server.TaskCancellation - ); - } - catch (TaskCanceledException) - { - // Ignored - } - catch (Exception e) - { - Logger.LogError("An error occured while loading container stats: {e}", e); - } - } - - // Reset current stats - CurrentStats = new(); - - Logger.LogDebug("Stopped fetching container stats"); - }); - - return Task.CompletedTask; - } - - private ServerStats ConvertToStats(ContainerStatsResponse response) - { - var result = new ServerStats(); - - #region CPU - - if(response.CPUStats != null && response.PreCPUStats.CPUUsage != null) // Sometimes some values are just null >:/ - { - var cpuDelta = (float)response.CPUStats.CPUUsage.TotalUsage - response.PreCPUStats.CPUUsage.TotalUsage; - var cpuSystemDelta = (float)response.CPUStats.SystemUsage - response.PreCPUStats.SystemUsage; - - var cpuCoreCount = (int)response.CPUStats.OnlineCPUs; - - if (cpuCoreCount == 0 && response.CPUStats.CPUUsage.PercpuUsage != null) - cpuCoreCount = response.CPUStats.CPUUsage.PercpuUsage.Count; - - var cpuPercent = 0f; - - if (cpuSystemDelta > 0.0f && cpuDelta > 0.0f) - { - cpuPercent = (cpuDelta / cpuSystemDelta) * 100; - - if (cpuCoreCount > 0) - cpuPercent *= cpuCoreCount; - } - - result.CpuUsage = Math.Round(cpuPercent * 1000) / 1000; - } - - #endregion - - #region Memory - - result.MemoryUsage = response.MemoryStats.Usage; - - #endregion - - #region Network - - if (response.Networks != null) - { - foreach (var network in response.Networks) - { - result.NetworkRead += network.Value.RxBytes; - result.NetworkWrite += network.Value.TxBytes; - } - } - - #endregion - - #region IO - - if (response.BlkioStats.IoServiceBytesRecursive != null) - { - result.IoRead = response.BlkioStats.IoServiceBytesRecursive - .FirstOrDefault(x => x.Op == "read")?.Value ?? 0; - - result.IoWrite = response.BlkioStats.IoServiceBytesRecursive - .FirstOrDefault(x => x.Op == "write")?.Value ?? 0; - } - - #endregion - - return result; - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/ServerSystem/SubSystems/StorageSubSystem.cs b/MoonlightServers.Daemon/ServerSystem/SubSystems/StorageSubSystem.cs deleted file mode 100644 index 34cabce..0000000 --- a/MoonlightServers.Daemon/ServerSystem/SubSystems/StorageSubSystem.cs +++ /dev/null @@ -1,464 +0,0 @@ -using System.Diagnostics; -using Mono.Unix.Native; -using MoonCore.Exceptions; -using MoonCore.Helpers; -using MoonCore.Unix.Exceptions; -using MoonCore.Unix.SecureFs; -using MoonlightServers.Daemon.Configuration; -using MoonlightServers.Daemon.Helpers; - -namespace MoonlightServers.Daemon.ServerSystem.SubSystems; - -public class StorageSubSystem : ServerSubSystem -{ - private readonly AppConfiguration AppConfiguration; - - private SecureFileSystem SecureFileSystem; - private ServerFileSystem ServerFileSystem; - private ConsoleSubSystem ConsoleSubSystem; - - public string RuntimeVolumePath { get; private set; } - public string InstallVolumePath { get; private set; } - public string VirtualDiskPath { get; private set; } - - public bool IsVirtualDiskMounted { get; private set; } - public bool IsInitialized { get; private set; } = false; - public bool IsFileSystemAccessorCreated { get; private set; } = false; - - public StorageSubSystem( - Server server, - ILogger logger, - AppConfiguration appConfiguration - ) : base(server, logger) - { - AppConfiguration = appConfiguration; - - // Runtime Volume - var runtimePath = Path.Combine(AppConfiguration.Storage.Volumes, Configuration.Id.ToString()); - - if (!runtimePath.StartsWith('/')) - runtimePath = Path.Combine(Directory.GetCurrentDirectory(), runtimePath); - - RuntimeVolumePath = runtimePath; - - // Install Volume - var installPath = Path.Combine(AppConfiguration.Storage.Install, Configuration.Id.ToString()); - - if (!installPath.StartsWith('/')) - installPath = Path.Combine(Directory.GetCurrentDirectory(), installPath); - - InstallVolumePath = installPath; - - // Virtual Disk - if (!Configuration.UseVirtualDisk) - return; - - var virtualDiskPath = Path.Combine(AppConfiguration.Storage.VirtualDisks, $"{Configuration.Id}.img"); - - if (!virtualDiskPath.StartsWith('/')) - virtualDiskPath = Path.Combine(Directory.GetCurrentDirectory(), virtualDiskPath); - - VirtualDiskPath = virtualDiskPath; - } - - public override async Task Initialize() - { - ConsoleSubSystem = Server.GetRequiredSubSystem(); - - try - { - await Reinitialize(); - } - catch (Exception e) - { - await ConsoleSubSystem.WriteMoonlight( - "Unable to initialize server file system. Please contact the administrator" - ); - - Logger.LogError("An unhandled error occured while lazy initializing server file system: {e}", e); - - throw; - } - } - - public override async Task Delete() - { - if (Configuration.UseVirtualDisk) - await DeleteVirtualDisk(); - - await DeleteRuntimeVolume(); - await DeleteInstallVolume(); - } - - public async Task Reinitialize() - { - if (IsInitialized && StateMachine.State != ServerState.Offline) - { - throw new HttpApiException( - "Unable to reinitialize storage sub system while the server is not offline", - 400 - ); - } - - IsInitialized = false; - - await EnsureRuntimeVolumeCreated(); - - if (Configuration.UseVirtualDisk) - { - // Load the state of a possible mount already existing. - // This ensures we are aware of the mount state after a restart. - // Without that we would get errors when mounting - IsVirtualDiskMounted = await CheckVirtualDiskMounted(); - - // Ensure we have the virtual disk created and in the correct size - await EnsureVirtualDisk(); - } - - IsInitialized = true; - } - - #region Runtime - - public async Task GetFileSystem() - { - if (!await RequestRuntimeVolume(skipPermissions: true)) - throw new HttpApiException("The file system is still initializing. Please try again later", 503); - - return ServerFileSystem; - } - - // This method allows other sub systems to request access to the runtime volume. - // The return value specifies if the request to the runtime volume is possible or not - public async Task RequestRuntimeVolume(bool skipPermissions = false) - { - // If the initialization is still running we don't want to allow access to the runtime volume at all - if (!IsInitialized) - return false; - - // If we use virtual disks and the disk isn't already mounted, we need to mount it now - if (Configuration.UseVirtualDisk && !IsVirtualDiskMounted) - await MountVirtualDisk(); - - if (!IsFileSystemAccessorCreated) - await CreateFileSystemAccessor(); - - if (!skipPermissions) - await EnsureRuntimePermissions(); - - return true; - } - - private Task EnsureRuntimeVolumeCreated() - { - // Create the volume directory if required - if (!Directory.Exists(RuntimeVolumePath)) - Directory.CreateDirectory(RuntimeVolumePath); - - return Task.CompletedTask; - } - - private async Task DeleteRuntimeVolume() - { - // Already deleted? Then we don't want to care about anything at all - if (!Directory.Exists(RuntimeVolumePath)) - return; - - // If we use a virtual disk there are no files to delete via the - // secure file system as the virtual disk is already gone by now - if (Configuration.UseVirtualDisk) - { - Directory.Delete(RuntimeVolumePath, true); - return; - } - - // If we still habe a file system accessor, we reuse it :) - if (IsFileSystemAccessorCreated) - { - foreach (var entry in SecureFileSystem.ReadDir("/")) - { - if (entry.IsFile) - SecureFileSystem.Remove(entry.Name); - else - SecureFileSystem.RemoveAll(entry.Name); - } - - await DestroyFileSystemAccessor(); - } - else - { - // If the file system accessor has already been removed we create a temporary one. - // This handles the case when a server was never accessed and as such there is no accessor created yet - - var sfs = new SecureFileSystem(RuntimeVolumePath); - - foreach (var entry in sfs.ReadDir("/")) - { - if (entry.IsFile) - sfs.Remove(entry.Name); - else - sfs.RemoveAll(entry.Name); - } - - sfs.Dispose(); - } - - Directory.Delete(RuntimeVolumePath, true); - } - - private Task EnsureRuntimePermissions() - { - ArgumentNullException.ThrowIfNull(SecureFileSystem); - - //TODO: Config - var uid = (int)Syscall.getuid(); - var gid = (int)Syscall.getgid(); - - if (uid == 0) - { - uid = 998; - gid = 998; - } - - // Chown all content of the runtime volume - foreach (var entry in SecureFileSystem.ReadDir("/")) - { - if (entry.IsFile) - SecureFileSystem.Chown(entry.Name, uid, gid); - else - SecureFileSystem.ChownAll(entry.Name, uid, gid); - } - - // Chown also the main path of the volume - if (Syscall.chown(RuntimeVolumePath, uid, gid) != 0) - { - var error = Stdlib.GetLastError(); - throw new SyscallException(error, "An error occured while chowning runtime volume"); - } - - return Task.CompletedTask; - } - - private Task CreateFileSystemAccessor() - { - SecureFileSystem = new(RuntimeVolumePath); - ServerFileSystem = new(SecureFileSystem); - - IsFileSystemAccessorCreated = true; - - return Task.CompletedTask; - } - - private Task DestroyFileSystemAccessor() - { - if (!SecureFileSystem.IsDisposed) - SecureFileSystem.Dispose(); - - IsFileSystemAccessorCreated = false; - - return Task.CompletedTask; - } - - #endregion - - #region Installation - - public Task EnsureInstallVolume() - { - if (!Directory.Exists(InstallVolumePath)) - Directory.CreateDirectory(InstallVolumePath); - - return Task.CompletedTask; - } - - public Task DeleteInstallVolume() - { - if (!Directory.Exists(InstallVolumePath)) - return Task.CompletedTask; - - Directory.Delete(InstallVolumePath, true); - return Task.CompletedTask; - } - - #endregion - - #region Virtual disks - - private async Task MountVirtualDisk() - { - await ConsoleSubSystem.WriteMoonlight("Mounting virtual disk"); - await ExecuteCommand("mount", $"-t auto -o loop {VirtualDiskPath} {RuntimeVolumePath}", true); - - IsVirtualDiskMounted = true; - } - - private async Task UnmountVirtualDisk() - { - await ConsoleSubSystem.WriteMoonlight("Unmounting virtual disk"); - await ExecuteCommand("umount", RuntimeVolumePath, handleExitCode: true); - - IsVirtualDiskMounted = false; - } - - private async Task CheckVirtualDiskMounted() - => await ExecuteCommand("findmnt", RuntimeVolumePath) == 0; - - private async Task EnsureVirtualDisk() - { - var existingDiskInfo = new FileInfo(VirtualDiskPath); - - // Check if we need to create the disk or just check for the size - if (existingDiskInfo.Exists) - { - var expectedSize = ByteConverter.FromMegaBytes(Configuration.Disk).Bytes; - - // If the disk size matches, we are done here - if (expectedSize == existingDiskInfo.Length) - { - Logger.LogDebug("Virtual disk size matches expected size"); - return; - } - - // We cant resize while the server is running as this would lead to possible file corruptions - // and crashes of the software the server is running - if (StateMachine.State != ServerState.Offline) - { - Logger.LogDebug("Skipping disk resizing while server is not offline"); - await ConsoleSubSystem.WriteMoonlight("Skipping disk resizing as the server is not offline"); - return; - } - - if (expectedSize > existingDiskInfo.Length) - { - Logger.LogDebug("Detected smaller disk size as expected. Resizing now"); - await ConsoleSubSystem.WriteMoonlight("Preparing to resize virtual disk"); - - // If the file system accessor is still open we need to destroy it in order to to resize - if (IsFileSystemAccessorCreated) - await DestroyFileSystemAccessor(); - - // If the disk is still mounted we need to unmount it in order to resize - if (IsVirtualDiskMounted) - await UnmountVirtualDisk(); - - // Resize the disk image file - Logger.LogDebug("Resizing virtual disk file"); - - var fileStream = File.Open(VirtualDiskPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None); - - fileStream.SetLength(expectedSize); - - await fileStream.FlushAsync(); - fileStream.Close(); - await fileStream.DisposeAsync(); - - // Now we need to run the file system check on the disk - Logger.LogDebug("Checking virtual disk for corruptions using e2fsck"); - await ConsoleSubSystem.WriteMoonlight("Checking virtual disk for any corruptions"); - - await ExecuteCommand( - "e2fsck", - $"{AppConfiguration.Storage.VirtualDiskOptions.E2FsckParameters} {VirtualDiskPath}", - handleExitCode: true - ); - - // Resize the file system - Logger.LogDebug("Resizing filesystem of virtual disk using resize2fs"); - await ConsoleSubSystem.WriteMoonlight("Resizing virtual disk"); - - await ExecuteCommand("resize2fs", VirtualDiskPath, handleExitCode: true); - - // Done :> - Logger.LogDebug("Successfully resized virtual disk"); - await ConsoleSubSystem.WriteMoonlight("Resize of virtual disk completed"); - } - else if (existingDiskInfo.Length > expectedSize) - { - Logger.LogDebug("Shrink from {expected} to {existing} detected", expectedSize, existingDiskInfo.Length); - - await ConsoleSubSystem.WriteMoonlight( - "Unable to shrink virtual disk. Virtual disk will stay unmodified" - ); - - Logger.LogWarning( - "Server disk limit was lower then the size of the virtual disk. Virtual disk wont be resized to prevent loss of files"); - } - } - else - { - // Create the image file and adjust the size - Logger.LogDebug("Creating virtual disk"); - await ConsoleSubSystem.WriteMoonlight("Creating virtual disk file. Please be patient"); - - var fileStream = File.Open(VirtualDiskPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); - - fileStream.SetLength( - ByteConverter.FromMegaBytes(Configuration.Disk).Bytes - ); - - await fileStream.FlushAsync(); - fileStream.Close(); - await fileStream.DisposeAsync(); - - // Now we want to format it - Logger.LogDebug("Formatting virtual disk"); - await ConsoleSubSystem.WriteMoonlight("Formatting virtual disk. This can take a bit"); - - await ExecuteCommand( - "mkfs", - $"-t {AppConfiguration.Storage.VirtualDiskOptions.FileSystemType} {VirtualDiskPath}", - handleExitCode: true - ); - - // Done :) - Logger.LogDebug("Successfully created virtual disk"); - await ConsoleSubSystem.WriteMoonlight("Virtual disk created"); - } - } - - private async Task DeleteVirtualDisk() - { - if (IsFileSystemAccessorCreated) - await DestroyFileSystemAccessor(); - - if (IsVirtualDiskMounted) - await UnmountVirtualDisk(); - - File.Delete(VirtualDiskPath); - } - - private async Task ExecuteCommand(string command, string arguments, bool handleExitCode = false) - { - var psi = new ProcessStartInfo() - { - FileName = command, - Arguments = arguments, - RedirectStandardError = true, - RedirectStandardOutput = true - }; - - var process = Process.Start(psi); - - if (process == null) - throw new AggregateException("The spawned process reference is null"); - - await process.WaitForExitAsync(); - - if (process.ExitCode == 0 || !handleExitCode) - return process.ExitCode; - - var output = await process.StandardOutput.ReadToEndAsync(); - output += await process.StandardError.ReadToEndAsync(); - - throw new Exception($"The command {command} failed: {output}"); - } - - #endregion - - public override async ValueTask DisposeAsync() - { - // We check for that just to ensure that we no longer access the file system - if (IsFileSystemAccessorCreated) - await DestroyFileSystemAccessor(); - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Services/NewServerService.cs b/MoonlightServers.Daemon/Services/NewServerService.cs deleted file mode 100644 index dbf3c19..0000000 --- a/MoonlightServers.Daemon/Services/NewServerService.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System.Collections.Concurrent; -using MoonCore.Exceptions; -using MoonCore.Helpers; -using MoonCore.Models; -using MoonlightServers.Daemon.Mappers; -using MoonlightServers.Daemon.Models.Cache; -using MoonlightServers.Daemon.ServerSys; -using MoonlightServers.Daemon.ServerSystem; -using MoonlightServers.DaemonShared.PanelSide.Http.Responses; -using Server = MoonlightServers.Daemon.ServerSys.Abstractions.Server; - -namespace MoonlightServers.Daemon.Services; - -public class NewServerService : IHostedLifecycleService -{ - private readonly ILogger Logger; - private readonly ServerFactory ServerFactory; - private readonly RemoteService RemoteService; - private readonly ServerConfigurationMapper Mapper; - - private readonly ConcurrentDictionary Servers = new(); - - public NewServerService( - ILogger logger, - ServerFactory serverFactory, - RemoteService remoteService, - ServerConfigurationMapper mapper - ) - { - Logger = logger; - ServerFactory = serverFactory; - RemoteService = remoteService; - Mapper = mapper; - } - - public async Task InitializeAllFromPanel() - { - var servers = await PagedData.All(async (page, pageSize) => - await RemoteService.GetServers(page, pageSize) - ); - - foreach (var serverDataResponse in servers) - { - var configuration = Mapper.FromServerDataResponse(serverDataResponse); - - try - { - await Initialize(configuration); - } - catch (Exception e) - { - Logger.LogError(e, "An error occured while initializing server: {id}", serverDataResponse.Id); - } - } - } - - public async Task Initialize(ServerConfiguration serverConfiguration) - { - var server = ServerFactory.CreateServer(serverConfiguration); - - Servers[serverConfiguration.Id] = server; - - await server.Initialize(); - - return server; - } - - public Server? Find(int serverId) - => Servers.GetValueOrDefault(serverId); - - public async Task Sync(int serverId) - { - var server = Find(serverId); - - if (server == null) - throw new ArgumentException("No server with this id found", nameof(serverId)); - - var serverData = await RemoteService.GetServer(serverId); - var config = Mapper.FromServerDataResponse(serverData); - - server.Context.Configuration = config; - - await server.Sync(); - } - - public async Task Delete(int serverId) - { - var server = Find(serverId); - - if (server == null) - throw new ArgumentException("No server with this id found", nameof(serverId)); - - if (server.StateMachine.State == ServerState.Installing) - throw new HttpApiException("Unable to delete a server while it is installing", 400); - - if (server.StateMachine.State != ServerState.Offline) - { - // If the server is not offline we need to wait until it goes offline, we - // do that by creating the serverOfflineWaiter task completion source which will get triggered - // when the event handler for state changes gets informed that the server state is now offline - - var serverOfflineWaiter = new TaskCompletionSource(); - var timeoutCancellation = new CancellationTokenSource(); - - // Set timeout to 10 seconds, this gives the server 10 seconds to go offline, before the request fails - timeoutCancellation.CancelAfter(TimeSpan.FromSeconds(10)); - - // Subscribe to state updates in order to get notified when the server is offline - server.StateMachine.OnTransitioned(transition => - { - // Only listen for changes to offline - if (transition.Destination != ServerState.Offline) - return; - - // If the timeout has already been reached, ignore all changes - if (timeoutCancellation.IsCancellationRequested) - return; - - // Server is finally offline, notify the request that we now can delete the server - serverOfflineWaiter.SetResult(); - }); - - // Now we trigger the kill and waiting for the server to be deleted - await server.StateMachine.FireAsync(ServerTrigger.Kill); - - try - { - await serverOfflineWaiter.Task.WaitAsync(timeoutCancellation.Token); - - await DeleteServer_Unhandled(server); - } - catch (TaskCanceledException) - { - Logger.LogWarning( - "Deletion of server {id} failed because it didnt stop in time despite being killed", - server.Context.Configuration.Id - ); - - throw new HttpApiException( - "Could not kill the server in time for the deletion. Please try again later", - 500 - ); - } - } - else - await DeleteServer_Unhandled(server); - } - - private async Task DeleteServer_Unhandled(Server server) - { - await server.Delete(); - await server.DisposeAsync(); - - Servers.Remove(server.Context.Configuration.Id, out _); - } - - #region Lifetime - - public Task StartAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - public Task StopAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - public async Task StartedAsync(CancellationToken cancellationToken) - { - await InitializeAllFromPanel(); - } - - public Task StartingAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - public Task StoppedAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - public async Task StoppingAsync(CancellationToken cancellationToken) - { - foreach (var server in Servers.Values) - await server.DisposeAsync(); - } - - #endregion -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Services/ServerService.cs b/MoonlightServers.Daemon/Services/ServerService.cs deleted file mode 100644 index bfdbfee..0000000 --- a/MoonlightServers.Daemon/Services/ServerService.cs +++ /dev/null @@ -1,364 +0,0 @@ -using System.Collections.Concurrent; -using Docker.DotNet; -using Docker.DotNet.Models; -using Microsoft.AspNetCore.SignalR; -using MoonCore.Attributes; -using MoonCore.Exceptions; -using MoonCore.Models; -using MoonlightServers.Daemon.Extensions; -using MoonlightServers.Daemon.Http.Hubs; -using MoonlightServers.Daemon.Models.Cache; -using MoonlightServers.Daemon.ServerSystem; -using MoonlightServers.Daemon.ServerSystem.SubSystems; -using MoonlightServers.DaemonShared.PanelSide.Http.Responses; - -namespace MoonlightServers.Daemon.Services; - -[Singleton] -public class ServerService : IHostedLifecycleService -{ - private readonly ConcurrentDictionary Servers = new(); - - private readonly RemoteService RemoteService; - private readonly DockerClient DockerClient; - private readonly IServiceProvider ServiceProvider; - private readonly CancellationTokenSource TaskCancellation; - private readonly ILogger Logger; - private readonly IHubContext HubContext; - - public ServerService( - RemoteService remoteService, - IServiceProvider serviceProvider, - DockerClient dockerClient, - ILogger logger, - IHubContext hubContext - ) - { - RemoteService = remoteService; - ServiceProvider = serviceProvider; - DockerClient = dockerClient; - Logger = logger; - HubContext = hubContext; - - TaskCancellation = new CancellationTokenSource(); - } - - public async Task Sync(int serverId) - { - var serverData = await RemoteService.GetServer(serverId); - var configuration = serverData.ToServerConfiguration(); - - await Sync(serverId, configuration); - } - - public async Task Sync(int serverId, ServerConfiguration configuration) - { - if (Servers.TryGetValue(serverId, out var server)) - server.Configuration = configuration; - else - await Initialize(serverId); - } - - public Server? Find(int serverId) - => Servers.GetValueOrDefault(serverId); - - public async Task Initialize(int serverId) - { - var serverData = await RemoteService.GetServer(serverId); - var configuration = serverData.ToServerConfiguration(); - - await Initialize(configuration); - } - - public async Task Initialize(ServerConfiguration configuration) - { - var serverScope = ServiceProvider.CreateScope(); - - var server = new Server(configuration, serverScope, HubContext); - - Type[] subSystems = - [ - // The restore sub system needs to be on top in order for the state machine having the - // correct state when all other sub systems initialize - typeof(RestoreSubSystem), - typeof(ProvisionSubSystem), - typeof(StorageSubSystem), - typeof(DebugSubSystem), - typeof(ShutdownSubSystem), - typeof(ConsoleSubSystem), - typeof(OnlineDetectionService), - typeof(InstallationSubSystem), - typeof(StatsSubSystem) - ]; - - await server.Initialize(subSystems); - - Servers[configuration.Id] = server; - } - - public async Task Delete(int serverId) - { - var server = Find(serverId); - - // If a server with this id doesn't exist we can just exit - if (server == null) - return; - - if (server.StateMachine.State == ServerState.Installing) - throw new HttpApiException("Unable to delete a server while it is installing", 400); - - if (server.StateMachine.State != ServerState.Offline) - { - // If the server is not offline we need to wait until it goes offline, we - // do that by creating the serverOfflineWaiter task completion source which will get triggered - // when the event handler for state changes gets informed that the server state is now offline - - var serverOfflineWaiter = new TaskCompletionSource(); - var timeoutCancellation = new CancellationTokenSource(); - - // Set timeout to 10 seconds, this gives the server 10 seconds to go offline, before the request fails - timeoutCancellation.CancelAfter(TimeSpan.FromSeconds(10)); - - // Subscribe to state updates in order to get notified when the server is offline - server.StateMachine.OnTransitioned(transition => - { - // Only listen for changes to offline - if (transition.Destination != ServerState.Offline) - return; - - // If the timeout has already been reached, ignore all changes - if (timeoutCancellation.IsCancellationRequested) - return; - - // Server is finally offline, notify the request that we now can delete the server - serverOfflineWaiter.SetResult(); - }); - - // Now we trigger the kill and waiting for the server to be deleted - await server.StateMachine.FireAsync(ServerTrigger.Kill); - - try - { - await serverOfflineWaiter.Task.WaitAsync(timeoutCancellation.Token); - - await DeleteServer_Unhandled(server); - } - catch (TaskCanceledException) - { - Logger.LogWarning( - "Deletion of server {id} failed because it didnt stop in time despite being killed", - server.Configuration.Id - ); - - throw new HttpApiException( - "Could not kill the server in time for the deletion. Please try again later", - 500 - ); - } - } - else - await DeleteServer_Unhandled(server); - } - - private async Task DeleteServer_Unhandled(Server server) - { - await server.Delete(); - await server.DisposeAsync(); - - Servers.Remove(server.Configuration.Id, out _); - } - - #region Batch Initialization - - public async Task InitializeAll() - { - var initialPage = await RemoteService.GetServers(0, 1); - - const int pageSize = 25; - var pages = (initialPage.TotalItems == 0 ? 0 : (initialPage.TotalItems - 1) / pageSize) + - 1; // The +1 is to handle the pages starting at 0 - - // Create and fill a queue with pages to initialize - var batchesLeft = new ConcurrentQueue(); - - for (var i = 0; i < pages; i++) - batchesLeft.Enqueue(i); - - var tasksCount = pages > 5 ? 5 : pages; - var tasks = new List(); - - Logger.LogInformation( - "Starting initialization for {count} server(s) with {tasksCount} worker(s)", - initialPage.TotalItems, - tasksCount - ); - - for (var i = 0; i < tasksCount; i++) - { - var id = i + 0; - var task = Task.Run(() => BatchRunner(batchesLeft, id)); - - tasks.Add(task); - } - - await Task.WhenAll(tasks); - - Logger.LogInformation("Initialization completed"); - } - - private async Task BatchRunner(ConcurrentQueue queue, int id) - { - while (!queue.IsEmpty) - { - if (!queue.TryDequeue(out var page)) - continue; - - await InitializeBatch(page, 25); - - Logger.LogDebug("Worker {id}: Finished initialization of page {page}", id, page); - } - - Logger.LogDebug("Worker {id}: Finished", id); - } - - private async Task InitializeBatch(int page, int pageSize) - { - var servers = await RemoteService.GetServers(page, pageSize); - - var configurations = servers.Items - .Select(x => x.ToServerConfiguration()) - .ToArray(); - - foreach (var configuration in configurations) - { - try - { - await Sync(configuration.Id, configuration); - } - catch (Exception e) - { - Logger.LogError( - "An unhandled error occured while initializing server {id}: {e}", - configuration.Id, - e - ); - } - } - } - - #endregion - - #region Docker Monitoring - - private Task StartContainerMonitoring() - { - Task.Run(async () => - { - // Restart unless shutdown is requested - while (!TaskCancellation.Token.IsCancellationRequested) - { - try - { - Logger.LogTrace("Starting to monitor events"); - - await DockerClient.System.MonitorEventsAsync(new(), - new Progress(async message => - { - // Filter out unwanted events - if (message.Action != "die") - return; - - // TODO: Implement a cached lookup using a shared dictionary by the sub system - - var server = Servers.Values.FirstOrDefault(serverToCheck => - { - var provisionSubSystem = serverToCheck.GetRequiredSubSystem(); - - if (provisionSubSystem.CurrentContainerId == message.ID) - return true; - - var installationSubSystem = serverToCheck.GetRequiredSubSystem(); - - if (installationSubSystem.CurrentContainerId == message.ID) - return true; - - return false; - }); - - // If the container does not match any server we can ignore it - if (server == null) - return; - - await server.StateMachine.FireAsync(ServerTrigger.Exited); - }), TaskCancellation.Token); - } - catch (TaskCanceledException) - { - // Can be ignored - } - catch (Exception e) - { - Logger.LogError("An unhandled error occured while monitoring events: {e}", e); - } - } - }); - - return Task.CompletedTask; - } - - #endregion - - #region Lifetime - - public Task StartAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - public Task StopAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - public async Task StartedAsync(CancellationToken cancellationToken) - { - await StartContainerMonitoring(); - - await InitializeAll(); - } - - public Task StartingAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - public async Task StoppedAsync(CancellationToken cancellationToken) - { - foreach (var server in Servers.Values) - await server.DisposeAsync(); - - await TaskCancellation.CancelAsync(); - } - - public Task StoppingAsync(CancellationToken cancellationToken) - => Task.CompletedTask; - - #endregion - - /* - *var existingContainers = await dockerClient.Containers.ListContainersAsync(new() - { - All = true, - Limit = null, - Filters = new Dictionary>() - { - { - "label", - new Dictionary() - { - { - "Software=Moonlight-Panel", - true - } - } - } - } - }); - * - * - */ -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Startup.cs b/MoonlightServers.Daemon/Startup.cs index 9fa8f5d..7cfeedd 100644 --- a/MoonlightServers.Daemon/Startup.cs +++ b/MoonlightServers.Daemon/Startup.cs @@ -1,29 +1,16 @@ -using System.Reactive.Concurrency; -using System.Reactive.Linq; using System.Text; using System.Text.Json; using Docker.DotNet; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Http.Connections; using Microsoft.IdentityModel.Tokens; using MoonCore.EnvConfiguration; using MoonCore.Extended.Extensions; using MoonCore.Extensions; using MoonCore.Helpers; using MoonCore.Logging; -using MoonCore.Observability; using MoonlightServers.Daemon.Configuration; -using MoonlightServers.Daemon.Extensions; using MoonlightServers.Daemon.Helpers; using MoonlightServers.Daemon.Http.Hubs; -using MoonlightServers.Daemon.Mappers; -using MoonlightServers.Daemon.Models.Cache; -using MoonlightServers.Daemon.ServerSys; -using MoonlightServers.Daemon.ServerSys.Abstractions; -using MoonlightServers.Daemon.ServerSys.Implementations; -using MoonlightServers.Daemon.ServerSystem; -using MoonlightServers.Daemon.Services; -using Server = MoonlightServers.Daemon.ServerSystem.Server; namespace MoonlightServers.Daemon; @@ -73,79 +60,6 @@ public class Startup await MapBase(); await MapHubs(); - Task.Run(async () => - { - try - { - Console.WriteLine("Press enter to create server instance"); - Console.ReadLine(); - - var config = new ServerConfiguration() - { - Allocations = [ - new ServerConfiguration.AllocationConfiguration() - { - IpAddress = "0.0.0.0", - Port = 25565 - } - ], - Cpu = 400, - Disk = 10240, - DockerImage = "ghcr.io/parkervcp/yolks:java_21", - Id = 69, - Memory = 4096, - OnlineDetection = "\\)! For help, type ", - StartupCommand = "java -Xms128M -Xmx{{SERVER_MEMORY}}M -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}", - StopCommand = "stop", - Variables = new() - { - { - "SERVER_JARFILE", - "server.jar" - } - } - }; - - var factory = WebApplication.Services.GetRequiredService(); - var server = factory.CreateServer(config); - - await using var consoleSub = await server.Console.OnOutput - .SubscribeEventAsync(line => - { - Console.Write(line); - return ValueTask.CompletedTask; - }); - - await using var stateSub = await server.OnState.SubscribeEventAsync(state => - { - Console.WriteLine($"State: {state}"); - return ValueTask.CompletedTask; - }); - - await server.Initialize(); - - Console.ReadLine(); - - if(server.StateMachine.State == ServerState.Offline) - await server.StateMachine.FireAsync(ServerTrigger.Start); - else - await server.StateMachine.FireAsync(ServerTrigger.Stop); - - Console.ReadLine(); - - await server.StateMachine.FireAsync(ServerTrigger.Install); - - Console.ReadLine(); - - await server.Context.ServiceScope.DisposeAsync(); - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - }); - await WebApplication.RunAsync(); } @@ -331,28 +245,6 @@ public class Startup private Task RegisterServers() { - WebApplicationBuilder.Services.AddSingleton(); - WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService()); - - WebApplicationBuilder.Services.AddSingleton(); - WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService()); - - WebApplicationBuilder.Services.AddSingleton(); - - WebApplicationBuilder.Services.AddSingleton(); - - // Server scoped stuff - WebApplicationBuilder.Services.AddScoped(); - WebApplicationBuilder.Services.AddScoped(); - WebApplicationBuilder.Services.AddScoped(); - WebApplicationBuilder.Services.AddScoped(); - WebApplicationBuilder.Services.AddScoped(); - WebApplicationBuilder.Services.AddScoped(); - WebApplicationBuilder.Services.AddScoped(); - WebApplicationBuilder.Services.AddScoped(); - - WebApplicationBuilder.Services.AddScoped(); - return Task.CompletedTask; } diff --git a/MoonlightServers.Frontend/UI/Components/Servers/CreatePartials/General.razor b/MoonlightServers.Frontend/UI/Components/Servers/CreatePartials/General.razor index 384dd5d..5329c3c 100644 --- a/MoonlightServers.Frontend/UI/Components/Servers/CreatePartials/General.razor +++ b/MoonlightServers.Frontend/UI/Components/Servers/CreatePartials/General.razor @@ -32,7 +32,7 @@
- LoadStars() + private async Task LoadStars() { - return await PagedData.All(async (page, pageSize) => - await ApiClient.GetJson>( + return await PagedData.All(async (page, pageSize) => + await ApiClient.GetJson>( $"api/admin/servers/stars?page={page}&pageSize={pageSize}" ) ); diff --git a/MoonlightServers.Frontend/UI/Components/Servers/CreatePartials/Variables.razor b/MoonlightServers.Frontend/UI/Components/Servers/CreatePartials/Variables.razor index cdc5dd4..0d88743 100644 --- a/MoonlightServers.Frontend/UI/Components/Servers/CreatePartials/Variables.razor +++ b/MoonlightServers.Frontend/UI/Components/Servers/CreatePartials/Variables.razor @@ -43,7 +43,7 @@ [Parameter] public CreateServerRequest Request { get; set; } [Parameter] public Create Parent { get; set; } - private StarVariableDetailResponse[] StarVariables; + private StarVariableResponse[] StarVariables; private async Task Load(LazyLoader _) { @@ -53,14 +53,14 @@ return; } - StarVariables = await PagedData.All(async (page, pageSize) => - await ApiClient.GetJson>( + StarVariables = await PagedData.All(async (page, pageSize) => + await ApiClient.GetJson>( $"api/admin/servers/stars/{Parent.Star.Id}/variables?page={page}&pageSize={pageSize}" ) ); } - private async Task UpdateValue(StarVariableDetailResponse starVariable, ChangeEventArgs args) + private async Task UpdateValue(StarVariableResponse starVariable, ChangeEventArgs args) { var value = args.Value?.ToString() ?? ""; diff --git a/MoonlightServers.Frontend/UI/Components/Servers/UpdatePartials/Variables.razor b/MoonlightServers.Frontend/UI/Components/Servers/UpdatePartials/Variables.razor index c50e20c..4b18578 100644 --- a/MoonlightServers.Frontend/UI/Components/Servers/UpdatePartials/Variables.razor +++ b/MoonlightServers.Frontend/UI/Components/Servers/UpdatePartials/Variables.razor @@ -45,13 +45,13 @@ [Parameter] public UpdateServerRequest Request { get; set; } [Parameter] public ServerResponse Server { get; set; } - private StarVariableDetailResponse[] StarVariables; + private StarVariableResponse[] StarVariables; private ServerVariableResponse[] ServerVariables; private async Task Load(LazyLoader _) { - StarVariables = await PagedData.All(async (page, pageSize) => - await ApiClient.GetJson>( + StarVariables = await PagedData.All(async (page, pageSize) => + await ApiClient.GetJson>( $"api/admin/servers/stars/{Server.StarId}/variables?page={page}&pageSize={pageSize}" ) ); diff --git a/MoonlightServers.Frontend/UI/Components/Stars/Modals/UpdateDockerImageModal.razor b/MoonlightServers.Frontend/UI/Components/Stars/Modals/UpdateDockerImageModal.razor index 7eda8d2..3b91fb4 100644 --- a/MoonlightServers.Frontend/UI/Components/Stars/Modals/UpdateDockerImageModal.razor +++ b/MoonlightServers.Frontend/UI/Components/Stars/Modals/UpdateDockerImageModal.razor @@ -34,7 +34,7 @@ @code { [Parameter] public Func OnSubmit { get; set; } - [Parameter] public StarDockerImageDetailResponse DockerImage { get; set; } + [Parameter] public StarDockerImageResponse DockerImage { get; set; } private UpdateStarDockerImageRequest Form; private HandleForm HandleForm; diff --git a/MoonlightServers.Frontend/UI/Components/Stars/Modals/UpdateVariableModal.razor b/MoonlightServers.Frontend/UI/Components/Stars/Modals/UpdateVariableModal.razor index 1f9a6bf..045ef98 100644 --- a/MoonlightServers.Frontend/UI/Components/Stars/Modals/UpdateVariableModal.razor +++ b/MoonlightServers.Frontend/UI/Components/Stars/Modals/UpdateVariableModal.razor @@ -65,7 +65,7 @@ @code { [Parameter] public Func OnSubmit { get; set; } - [Parameter] public StarVariableDetailResponse Variable { get; set; } + [Parameter] public StarVariableResponse Variable { get; set; } private UpdateStarVariableRequest Form; private HandleForm HandleForm; diff --git a/MoonlightServers.Frontend/UI/Components/Stars/UpdatePartials/DockerImage.razor b/MoonlightServers.Frontend/UI/Components/Stars/UpdatePartials/DockerImage.razor index dd2e5ee..5581225 100644 --- a/MoonlightServers.Frontend/UI/Components/Stars/UpdatePartials/DockerImage.razor +++ b/MoonlightServers.Frontend/UI/Components/Stars/UpdatePartials/DockerImage.razor @@ -46,14 +46,14 @@ @code { - [Parameter] public StarDetailResponse Star { get; set; } + [Parameter] public StarResponse Star { get; set; } - private StarDockerImageDetailResponse[] DockerImages; + private StarDockerImageResponse[] DockerImages; private LazyLoader LazyLoader; private async Task Load(LazyLoader _) { - var pagedVariables = await ApiClient.GetJson>( + var pagedVariables = await ApiClient.GetJson>( $"api/admin/servers/stars/{Star.Id}/dockerImages?page=0&pageSize=50" ); @@ -76,7 +76,7 @@ }); } - private async Task UpdateDockerImage(StarDockerImageDetailResponse dockerImage) + private async Task UpdateDockerImage(StarDockerImageResponse dockerImage) { Func onSubmit = async request => { @@ -93,7 +93,7 @@ }); } - private async Task DeleteDockerImage(StarDockerImageDetailResponse dockerImage) + private async Task DeleteDockerImage(StarDockerImageResponse dockerImage) { await AlertService.ConfirmDanger( "Delete docker image", diff --git a/MoonlightServers.Frontend/UI/Components/Stars/UpdatePartials/Misc.razor b/MoonlightServers.Frontend/UI/Components/Stars/UpdatePartials/Misc.razor index b640ac9..924650b 100644 --- a/MoonlightServers.Frontend/UI/Components/Stars/UpdatePartials/Misc.razor +++ b/MoonlightServers.Frontend/UI/Components/Stars/UpdatePartials/Misc.razor @@ -49,13 +49,13 @@ @code { [Parameter] public UpdateStarRequest Request { get; set; } - [Parameter] public StarDetailResponse Star { get; set; } + [Parameter] public StarResponse Star { get; set; } - private List DockerImages; + private List DockerImages; private async Task Load(LazyLoader _) { - var pagedVariables = await ApiClient.GetJson>( + var pagedVariables = await ApiClient.GetJson>( $"api/admin/servers/stars/{Star.Id}/dockerImages?page=0&pageSize=50" ); diff --git a/MoonlightServers.Frontend/UI/Components/Stars/UpdatePartials/Variables.razor b/MoonlightServers.Frontend/UI/Components/Stars/UpdatePartials/Variables.razor index 2e73e97..41619a0 100644 --- a/MoonlightServers.Frontend/UI/Components/Stars/UpdatePartials/Variables.razor +++ b/MoonlightServers.Frontend/UI/Components/Stars/UpdatePartials/Variables.razor @@ -46,14 +46,14 @@ @code { - [Parameter] public StarDetailResponse Star { get; set; } + [Parameter] public StarResponse Star { get; set; } - private StarVariableDetailResponse[] CurrentVariables; + private StarVariableResponse[] CurrentVariables; private LazyLoader LazyLoader; private async Task Load(LazyLoader arg) { - var pagedVariables = await ApiClient.GetJson>( + var pagedVariables = await ApiClient.GetJson>( $"api/admin/servers/stars/{Star.Id}/variables?page=0&pageSize=50" ); @@ -76,7 +76,7 @@ }, "max-w-xl"); } - private async Task UpdateVariable(StarVariableDetailResponse variable) + private async Task UpdateVariable(StarVariableResponse variable) { Func onSubmit = async request => { @@ -93,7 +93,7 @@ }, "max-w-xl"); } - private async Task DeleteVariable(StarVariableDetailResponse variable) + private async Task DeleteVariable(StarVariableResponse variable) { await AlertService.ConfirmDanger( "Delete variable", diff --git a/MoonlightServers.Frontend/UI/Views/Admin/All/Create.razor b/MoonlightServers.Frontend/UI/Views/Admin/All/Create.razor index 8c47f84..224419a 100644 --- a/MoonlightServers.Frontend/UI/Views/Admin/All/Create.razor +++ b/MoonlightServers.Frontend/UI/Views/Admin/All/Create.razor @@ -53,7 +53,7 @@ public List Allocations = new(); public UserResponse? Owner; - public StarDetailResponse? Star; + public StarResponse? Star; public NodeResponse? Node; protected override void OnInitialized() diff --git a/MoonlightServers.Frontend/UI/Views/Admin/All/Index.razor b/MoonlightServers.Frontend/UI/Views/Admin/All/Index.razor index 3866882..1bc51d2 100644 --- a/MoonlightServers.Frontend/UI/Views/Admin/All/Index.razor +++ b/MoonlightServers.Frontend/UI/Views/Admin/All/Index.razor @@ -83,7 +83,7 @@ { private DataTable Table; - private List Stars = new(); + private List Stars = new(); private List Nodes = new(); private async Task> LoadData(PaginationOptions options) @@ -101,7 +101,7 @@ if (Stars.All(x => x.Id != item.StarId)) { - var star = await ApiClient.GetJson($"api/admin/servers/stars/{item.StarId}"); + var star = await ApiClient.GetJson($"api/admin/servers/stars/{item.StarId}"); Stars.Add(star); } } diff --git a/MoonlightServers.Frontend/UI/Views/Admin/Stars/Index.razor b/MoonlightServers.Frontend/UI/Views/Admin/Stars/Index.razor index ab90c1a..005a7e1 100644 --- a/MoonlightServers.Frontend/UI/Views/Admin/Stars/Index.razor +++ b/MoonlightServers.Frontend/UI/Views/Admin/Stars/Index.razor @@ -35,21 +35,21 @@
- + - + - - + + @context.Name - - - + + +
@if (!string.IsNullOrEmpty(context.DonateUrl)) @@ -89,19 +89,19 @@ @code { - private DataTable Table; + private DataTable Table; - private async Task> LoadData(PaginationOptions options) - => await ApiClient.GetJson>($"api/admin/servers/stars?page={options.Page}&pageSize={options.PerPage}"); + private async Task> LoadData(PaginationOptions options) + => await ApiClient.GetJson>($"api/admin/servers/stars?page={options.Page}&pageSize={options.PerPage}"); - private async Task Delete(StarDetailResponse detailResponse) + private async Task Delete(StarResponse response) { await AlertService.ConfirmDanger( "Star deletion", - $"Do you really want to delete the star '{detailResponse.Name}'", + $"Do you really want to delete the star '{response.Name}'", async () => { - await ApiClient.Delete($"api/admin/servers/stars/{detailResponse.Id}"); + await ApiClient.Delete($"api/admin/servers/stars/{response.Id}"); await ToastService.Success("Successfully deleted star"); await Table.Refresh(); @@ -109,7 +109,7 @@ ); } - private async Task Export(StarDetailResponse star) + private async Task Export(StarResponse star) { var json = await ApiClient.GetString($"api/admin/servers/stars/{star.Id}/export"); @@ -145,7 +145,7 @@ var content = new MultipartFormDataContent(); content.Add(new StreamContent(stream), "file", file.Name); - var star = await ApiClient.PostJson("api/admin/servers/stars/import", content); + var star = await ApiClient.PostJson("api/admin/servers/stars/import", content); await ToastService.Success($"Successfully imported '{star.Name}'"); } diff --git a/MoonlightServers.Frontend/UI/Views/Admin/Stars/Update.razor b/MoonlightServers.Frontend/UI/Views/Admin/Stars/Update.razor index 1a8438f..6dba3db 100644 --- a/MoonlightServers.Frontend/UI/Views/Admin/Stars/Update.razor +++ b/MoonlightServers.Frontend/UI/Views/Admin/Stars/Update.razor @@ -68,11 +68,11 @@ private HandleForm Form; private UpdateStarRequest Request; - private StarDetailResponse Detail; + private StarResponse Detail; private async Task Load(LazyLoader _) { - Detail = await ApiClient.GetJson($"api/admin/servers/stars/{Id}"); + Detail = await ApiClient.GetJson($"api/admin/servers/stars/{Id}"); Request = new() { Name = Detail.Name, diff --git a/MoonlightServers.Shared/Http/Responses/Admin/StarDockerImages/StarDockerImageDetailResponse.cs b/MoonlightServers.Shared/Http/Responses/Admin/StarDockerImages/StarDockerImageResponse.cs similarity index 84% rename from MoonlightServers.Shared/Http/Responses/Admin/StarDockerImages/StarDockerImageDetailResponse.cs rename to MoonlightServers.Shared/Http/Responses/Admin/StarDockerImages/StarDockerImageResponse.cs index 4a786fb..90ac049 100644 --- a/MoonlightServers.Shared/Http/Responses/Admin/StarDockerImages/StarDockerImageDetailResponse.cs +++ b/MoonlightServers.Shared/Http/Responses/Admin/StarDockerImages/StarDockerImageResponse.cs @@ -1,6 +1,6 @@ namespace MoonlightServers.Shared.Http.Responses.Admin.StarDockerImages; -public class StarDockerImageDetailResponse +public class StarDockerImageResponse { public int Id { get; set; } diff --git a/MoonlightServers.Shared/Http/Responses/Admin/StarVariables/StarVariableDetailResponse.cs b/MoonlightServers.Shared/Http/Responses/Admin/StarVariables/StarVariableResponse.cs similarity index 92% rename from MoonlightServers.Shared/Http/Responses/Admin/StarVariables/StarVariableDetailResponse.cs rename to MoonlightServers.Shared/Http/Responses/Admin/StarVariables/StarVariableResponse.cs index 86e2252..64375a1 100644 --- a/MoonlightServers.Shared/Http/Responses/Admin/StarVariables/StarVariableDetailResponse.cs +++ b/MoonlightServers.Shared/Http/Responses/Admin/StarVariables/StarVariableResponse.cs @@ -2,7 +2,7 @@ using MoonlightServers.Shared.Enums; namespace MoonlightServers.Shared.Http.Responses.Admin.StarVariables; -public class StarVariableDetailResponse +public class StarVariableResponse { public int Id { get; set; } diff --git a/MoonlightServers.Shared/Http/Responses/Admin/Stars/StarDetailResponse.cs b/MoonlightServers.Shared/Http/Responses/Admin/Stars/StarResponse.cs similarity index 96% rename from MoonlightServers.Shared/Http/Responses/Admin/Stars/StarDetailResponse.cs rename to MoonlightServers.Shared/Http/Responses/Admin/Stars/StarResponse.cs index 2958a9c..7d4fa27 100644 --- a/MoonlightServers.Shared/Http/Responses/Admin/Stars/StarDetailResponse.cs +++ b/MoonlightServers.Shared/Http/Responses/Admin/Stars/StarResponse.cs @@ -3,7 +3,7 @@ using MoonlightServers.Shared.Http.Responses.Admin.StarVariables; namespace MoonlightServers.Shared.Http.Responses.Admin.Stars; -public class StarDetailResponse +public class StarResponse { public int Id { get; set; }