Cleaned up interfaces. Extracted server state machine trigger handler to seperated classes. Removed legacy code
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
|
using MoonCore.Extended.Models;
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
using MoonlightServers.ApiServer.Mappers;
|
using MoonlightServers.ApiServer.Mappers;
|
||||||
@@ -28,56 +28,57 @@ public class NodeAllocationsController : Controller
|
|||||||
AllocationRepository = allocationRepository;
|
AllocationRepository = allocationRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet]
|
||||||
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
|
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
|
||||||
public async Task<IPagedData<NodeAllocationResponse>> Get(
|
public async Task<ActionResult<IPagedData<NodeAllocationResponse>>> Get(
|
||||||
[FromRoute] int nodeId,
|
[FromRoute] int nodeId,
|
||||||
[FromQuery] [Range(0, int.MaxValue)] int page,
|
[FromQuery] PagedOptions options
|
||||||
[FromQuery] [Range(1, 100)] int pageSize
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
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
|
var allocations = await AllocationRepository
|
||||||
.Get()
|
.Get()
|
||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.Skip(page * pageSize)
|
.Skip(options.Page * options.PageSize)
|
||||||
.Take(pageSize)
|
.Take(options.PageSize)
|
||||||
.Where(x => x.Node.Id == nodeId)
|
.Where(x => x.Node.Id == nodeId)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToAdminResponse()
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var mappedAllocations = allocations
|
|
||||||
.Select(AllocationMapper.ToNodeAllocation)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new PagedData<NodeAllocationResponse>()
|
return new PagedData<NodeAllocationResponse>()
|
||||||
{
|
{
|
||||||
Items = mappedAllocations,
|
Items = allocations,
|
||||||
CurrentPage = page,
|
CurrentPage = options.Page,
|
||||||
PageSize = pageSize,
|
PageSize = options.PageSize,
|
||||||
TotalItems = count,
|
TotalItems = count,
|
||||||
TotalPages = count == 0 ? 0 : (count - 1) / pageSize
|
TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
|
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
|
||||||
public async Task<NodeAllocationResponse> GetSingle([FromRoute] int nodeId, [FromRoute] int id)
|
public async Task<ActionResult<NodeAllocationResponse>> GetSingle([FromRoute] int nodeId, [FromRoute] int id)
|
||||||
{
|
{
|
||||||
var allocation = await AllocationRepository
|
var allocation = await AllocationRepository
|
||||||
.Get()
|
.Get()
|
||||||
.Where(x => x.Node.Id == nodeId)
|
.Where(x => x.Node.Id == nodeId)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToAdminResponse()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (allocation == null)
|
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")]
|
[Authorize(Policy = "permissions:admin.servers.nodes.create")]
|
||||||
public async Task<NodeAllocationResponse> Create(
|
public async Task<ActionResult<NodeAllocationResponse>> Create(
|
||||||
[FromRoute] int nodeId,
|
[FromRoute] int nodeId,
|
||||||
[FromBody] CreateNodeAllocationRequest request
|
[FromBody] CreateNodeAllocationRequest request
|
||||||
)
|
)
|
||||||
@@ -87,7 +88,7 @@ public class NodeAllocationsController : Controller
|
|||||||
.FirstOrDefaultAsync(x => x.Id == nodeId);
|
.FirstOrDefaultAsync(x => x.Id == nodeId);
|
||||||
|
|
||||||
if (node == null)
|
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);
|
var allocation = AllocationMapper.ToAllocation(request);
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ public class NodeAllocationsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("{id:int}")]
|
[HttpPatch("{id:int}")]
|
||||||
public async Task<NodeAllocationResponse> Update(
|
public async Task<ActionResult<NodeAllocationResponse>> Update(
|
||||||
[FromRoute] int nodeId,
|
[FromRoute] int nodeId,
|
||||||
[FromRoute] int id,
|
[FromRoute] int id,
|
||||||
[FromBody] UpdateNodeAllocationRequest request
|
[FromBody] UpdateNodeAllocationRequest request
|
||||||
@@ -109,7 +110,7 @@ public class NodeAllocationsController : Controller
|
|||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (allocation == null)
|
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);
|
AllocationMapper.Merge(request, allocation);
|
||||||
await AllocationRepository.Update(allocation);
|
await AllocationRepository.Update(allocation);
|
||||||
@@ -118,7 +119,7 @@ public class NodeAllocationsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:int}")]
|
[HttpDelete("{id:int}")]
|
||||||
public async Task Delete([FromRoute] int nodeId, [FromRoute] int id)
|
public async Task<ActionResult> Delete([FromRoute] int nodeId, [FromRoute] int id)
|
||||||
{
|
{
|
||||||
var allocation = await AllocationRepository
|
var allocation = await AllocationRepository
|
||||||
.Get()
|
.Get()
|
||||||
@@ -126,32 +127,44 @@ public class NodeAllocationsController : Controller
|
|||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (allocation == null)
|
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);
|
await AllocationRepository.Remove(allocation);
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("range")]
|
[HttpPost("range")]
|
||||||
public async Task CreateRange([FromRoute] int nodeId, [FromBody] CreateNodeAllocationRangeRequest rangeRequest)
|
public async Task<ActionResult> 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
|
var node = await NodeRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == nodeId);
|
.FirstOrDefaultAsync(x => x.Id == nodeId);
|
||||||
|
|
||||||
if (node == null)
|
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()
|
.Get()
|
||||||
.Where(x => x.Node.Id == nodeId)
|
.Where(x => x.Port >= request.Start && x.Port <= request.End &&
|
||||||
.ToArray();
|
x.IpAddress == request.IpAddress)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToArrayAsync();
|
||||||
|
|
||||||
var ports = new List<int>();
|
var ports = new List<int>();
|
||||||
|
|
||||||
for (var i = rangeRequest.Start; i < rangeRequest.End; i++)
|
for (var i = request.Start; i < request.End; i++)
|
||||||
{
|
{
|
||||||
// Skip existing allocations
|
// Skip existing allocations
|
||||||
if (existingAllocations.Any(x => x.Port == i && x.IpAddress == rangeRequest.IpAddress))
|
if (existingAllocations.Any(x => x.Port == i))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
ports.Add(i);
|
ports.Add(i);
|
||||||
@@ -160,17 +173,18 @@ public class NodeAllocationsController : Controller
|
|||||||
var allocations = ports
|
var allocations = ports
|
||||||
.Select(port => new Allocation()
|
.Select(port => new Allocation()
|
||||||
{
|
{
|
||||||
IpAddress = rangeRequest.IpAddress,
|
IpAddress = request.IpAddress,
|
||||||
Port = port,
|
Port = port,
|
||||||
Node = node
|
Node = node
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
await AllocationRepository.RunTransaction(async set => { await set.AddRangeAsync(allocations); });
|
await AllocationRepository.RunTransaction(async set => { await set.AddRangeAsync(allocations); });
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("all")]
|
[HttpDelete("all")]
|
||||||
public async Task DeleteAll([FromRoute] int nodeId)
|
public async Task<ActionResult> DeleteAll([FromRoute] int nodeId)
|
||||||
{
|
{
|
||||||
var allocations = AllocationRepository
|
var allocations = AllocationRepository
|
||||||
.Get()
|
.Get()
|
||||||
@@ -178,14 +192,14 @@ public class NodeAllocationsController : Controller
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
await AllocationRepository.RunTransaction(set => { set.RemoveRange(allocations); });
|
await AllocationRepository.RunTransaction(set => { set.RemoveRange(allocations); });
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("free")]
|
[HttpGet("free")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
|
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
|
||||||
public async Task<IPagedData<NodeAllocationResponse>> GetFree(
|
public async Task<IPagedData<NodeAllocationResponse>> GetFree(
|
||||||
[FromRoute] int nodeId,
|
[FromRoute] int nodeId,
|
||||||
[FromQuery] int page,
|
[FromQuery] PagedOptions options,
|
||||||
[FromQuery] [Range(1, 100)] int pageSize,
|
|
||||||
[FromQuery] int serverId = -1
|
[FromQuery] int serverId = -1
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -202,19 +216,21 @@ public class NodeAllocationsController : Controller
|
|||||||
.Where(x => x.Server == null || x.Server.Id == serverId);
|
.Where(x => x.Server == null || x.Server.Id == serverId);
|
||||||
|
|
||||||
var count = await freeAllocationsQuery.CountAsync();
|
var count = await freeAllocationsQuery.CountAsync();
|
||||||
var allocations = await freeAllocationsQuery.ToArrayAsync();
|
|
||||||
|
|
||||||
var mappedAllocations = allocations
|
var allocations = await freeAllocationsQuery
|
||||||
.Select(AllocationMapper.ToNodeAllocation)
|
.Skip(options.Page * options.PageSize)
|
||||||
.ToArray();
|
.Take(options.PageSize)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToAdminResponse()
|
||||||
|
.ToArrayAsync();
|
||||||
|
|
||||||
return new PagedData<NodeAllocationResponse>()
|
return new PagedData<NodeAllocationResponse>()
|
||||||
{
|
{
|
||||||
Items = mappedAllocations,
|
Items = allocations,
|
||||||
CurrentPage = page,
|
CurrentPage = options.Page,
|
||||||
PageSize = pageSize,
|
PageSize = options.PageSize,
|
||||||
TotalItems = count,
|
TotalItems = count,
|
||||||
TotalPages = count == 0 ? 0 : (count - 1) / pageSize
|
TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
using MoonlightServers.ApiServer.Services;
|
using MoonlightServers.ApiServer.Services;
|
||||||
using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys;
|
using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys;
|
||||||
@@ -24,9 +25,12 @@ public class NodeStatusController : Controller
|
|||||||
|
|
||||||
[HttpGet("{nodeId:int}/system/status")]
|
[HttpGet("{nodeId:int}/system/status")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.nodes.status")]
|
[Authorize(Policy = "permissions:admin.servers.nodes.status")]
|
||||||
public async Task<NodeSystemStatusResponse> GetStatus([FromRoute] int nodeId)
|
public async Task<ActionResult<NodeSystemStatusResponse>> 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;
|
NodeSystemStatusResponse response;
|
||||||
|
|
||||||
@@ -35,7 +39,7 @@ public class NodeStatusController : Controller
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var statusResponse = await NodeService.GetSystemStatus(node);
|
var statusResponse = await NodeService.GetSystemStatus(node.Value);
|
||||||
|
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
|
|
||||||
@@ -65,14 +69,14 @@ public class NodeStatusController : Controller
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Node GetNode(int nodeId)
|
private async Task<ActionResult<Node>> GetNode(int nodeId)
|
||||||
{
|
{
|
||||||
var result = NodeRepository
|
var result = await NodeRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefault(x => x.Id == nodeId);
|
.FirstOrDefaultAsync(x => x.Id == nodeId);
|
||||||
|
|
||||||
if (result == null)
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
|
using MoonCore.Extended.Models;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
@@ -19,62 +20,55 @@ public class NodesController : Controller
|
|||||||
{
|
{
|
||||||
private readonly DatabaseRepository<Node> NodeRepository;
|
private readonly DatabaseRepository<Node> NodeRepository;
|
||||||
|
|
||||||
public NodesController(
|
public NodesController(DatabaseRepository<Node> nodeRepository)
|
||||||
DatabaseRepository<Node> nodeRepository
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
NodeRepository = nodeRepository;
|
NodeRepository = nodeRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
|
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
|
||||||
public async Task<IPagedData<NodeResponse>> Get(
|
public async Task<IPagedData<NodeResponse>> Get([FromQuery] PagedOptions options)
|
||||||
[FromQuery] [Range(0, int.MaxValue)] int page,
|
|
||||||
[FromQuery] [Range(1, 100)] int pageSize
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
var query = NodeRepository
|
var count = await NodeRepository.Get().CountAsync();
|
||||||
.Get();
|
|
||||||
|
|
||||||
var count = await query.CountAsync();
|
var items = await NodeRepository
|
||||||
|
.Get()
|
||||||
var items = await query
|
|
||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.Skip(page * pageSize)
|
.Skip(options.Page * options.PageSize)
|
||||||
.Take(pageSize)
|
.Take(options.PageSize)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToAdminResponse()
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var mappedItems = items
|
|
||||||
.Select(NodeMapper.ToAdminNodeResponse)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new PagedData<NodeResponse>()
|
return new PagedData<NodeResponse>()
|
||||||
{
|
{
|
||||||
Items = mappedItems,
|
Items = items,
|
||||||
CurrentPage = page,
|
CurrentPage = options.Page,
|
||||||
PageSize = pageSize,
|
PageSize = options.PageSize,
|
||||||
TotalItems = count,
|
TotalItems = count,
|
||||||
TotalPages = count == 0 ? 0 : count / pageSize
|
TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
|
[Authorize(Policy = "permissions:admin.servers.nodes.get")]
|
||||||
public async Task<NodeResponse> GetSingle([FromRoute] int id)
|
public async Task<ActionResult<NodeResponse>> GetSingle([FromRoute] int id)
|
||||||
{
|
{
|
||||||
var node = await NodeRepository
|
var node = await NodeRepository
|
||||||
.Get()
|
.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToAdminResponse()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (node == null)
|
if (node == null)
|
||||||
throw new HttpApiException("No node with this id found", 404);
|
return Problem("No node with this id found", statusCode: 404);
|
||||||
|
|
||||||
return NodeMapper.ToAdminNodeResponse(node);
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Policy = "permissions:admin.servers.nodes.create")]
|
[Authorize(Policy = "permissions:admin.servers.nodes.create")]
|
||||||
public async Task<NodeResponse> Create([FromBody] CreateNodeRequest request)
|
public async Task<ActionResult<NodeResponse>> Create([FromBody] CreateNodeRequest request)
|
||||||
{
|
{
|
||||||
var node = NodeMapper.ToNode(request);
|
var node = NodeMapper.ToNode(request);
|
||||||
|
|
||||||
@@ -88,14 +82,14 @@ public class NodesController : Controller
|
|||||||
|
|
||||||
[HttpPatch("{id:int}")]
|
[HttpPatch("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.nodes.update")]
|
[Authorize(Policy = "permissions:admin.servers.nodes.update")]
|
||||||
public async Task<NodeResponse> Update([FromRoute] int id, [FromBody] UpdateNodeRequest request)
|
public async Task<ActionResult<NodeResponse>> Update([FromRoute] int id, [FromBody] UpdateNodeRequest request)
|
||||||
{
|
{
|
||||||
var node = await NodeRepository
|
var node = await NodeRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (node == null)
|
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);
|
NodeMapper.Merge(request, node);
|
||||||
await NodeRepository.Update(node);
|
await NodeRepository.Update(node);
|
||||||
@@ -105,15 +99,16 @@ public class NodesController : Controller
|
|||||||
|
|
||||||
[HttpDelete("{id:int}")]
|
[HttpDelete("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.nodes.delete")]
|
[Authorize(Policy = "permissions:admin.servers.nodes.delete")]
|
||||||
public async Task Delete([FromRoute] int id)
|
public async Task<ActionResult> Delete([FromRoute] int id)
|
||||||
{
|
{
|
||||||
var node = await NodeRepository
|
var node = await NodeRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (node == null)
|
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);
|
await NodeRepository.Remove(node);
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,12 +27,16 @@ public class StatisticsController : Controller
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
[SuppressMessage("ReSharper.DPA", "DPA0011: High execution time of MVC action", MessageId = "time: 1142ms",
|
[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")]
|
Justification = "The daemon has an artificial delay of one second to calculate accurate cpu usage values")]
|
||||||
public async Task<StatisticsResponse> Get([FromRoute] int nodeId)
|
public async Task<ActionResult<StatisticsResponse>> Get([FromRoute] int nodeId)
|
||||||
{
|
{
|
||||||
var node = await GetNode(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()
|
Cpu = new()
|
||||||
{
|
{
|
||||||
@@ -62,12 +66,16 @@ public class StatisticsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("docker")]
|
[HttpGet("docker")]
|
||||||
public async Task<DockerStatisticsResponse> GetDocker([FromRoute] int nodeId)
|
public async Task<ActionResult<DockerStatisticsResponse>> GetDocker([FromRoute] int nodeId)
|
||||||
{
|
{
|
||||||
var node = await GetNode(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,
|
BuildCacheReclaimable = statistics.BuildCacheReclaimable,
|
||||||
BuildCacheUsed = statistics.BuildCacheUsed,
|
BuildCacheUsed = statistics.BuildCacheUsed,
|
||||||
@@ -79,14 +87,14 @@ public class StatisticsController : Controller
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Node> GetNode(int nodeId)
|
private async Task<ActionResult<Node>> GetNode(int nodeId)
|
||||||
{
|
{
|
||||||
var result = await NodeRepository
|
var result = await NodeRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == nodeId);
|
.FirstOrDefaultAsync(x => x.Id == nodeId);
|
||||||
|
|
||||||
if (result == null)
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
|
using MoonCore.Extended.Models;
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
using MoonlightServers.ApiServer.Mappers;
|
using MoonlightServers.ApiServer.Mappers;
|
||||||
@@ -18,8 +19,10 @@ public class ServerVariablesController : Controller
|
|||||||
private readonly DatabaseRepository<ServerVariable> VariableRepository;
|
private readonly DatabaseRepository<ServerVariable> VariableRepository;
|
||||||
private readonly DatabaseRepository<Server> ServerRepository;
|
private readonly DatabaseRepository<Server> ServerRepository;
|
||||||
|
|
||||||
public ServerVariablesController(DatabaseRepository<ServerVariable> variableRepository,
|
public ServerVariablesController(
|
||||||
DatabaseRepository<Server> serverRepository)
|
DatabaseRepository<ServerVariable> variableRepository,
|
||||||
|
DatabaseRepository<Server> serverRepository
|
||||||
|
)
|
||||||
{
|
{
|
||||||
VariableRepository = variableRepository;
|
VariableRepository = variableRepository;
|
||||||
ServerRepository = serverRepository;
|
ServerRepository = serverRepository;
|
||||||
@@ -27,10 +30,9 @@ public class ServerVariablesController : Controller
|
|||||||
|
|
||||||
[HttpGet("{serverId:int}/variables")]
|
[HttpGet("{serverId:int}/variables")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.read")]
|
[Authorize(Policy = "permissions:admin.servers.read")]
|
||||||
public async Task<PagedData<ServerVariableResponse>> Get(
|
public async Task<ActionResult<PagedData<ServerVariableResponse>>> Get(
|
||||||
[FromRoute] int serverId,
|
[FromRoute] int serverId,
|
||||||
[FromQuery] [Range(0, int.MaxValue)] int page,
|
[FromQuery] PagedOptions options
|
||||||
[FromQuery] [Range(1, 100)] int pageSize
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var serverExists = await ServerRepository
|
var serverExists = await ServerRepository
|
||||||
@@ -38,20 +40,29 @@ public class ServerVariablesController : Controller
|
|||||||
.AnyAsync(x => x.Id == serverId);
|
.AnyAsync(x => x.Id == serverId);
|
||||||
|
|
||||||
if (!serverExists)
|
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()
|
.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)
|
.OrderBy(x => x.Id)
|
||||||
.Skip(page * pageSize)
|
.Skip(options.Page * options.PageSize)
|
||||||
.Take(pageSize)
|
.Take(options.PageSize)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToAdminResponse()
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var castedVariables = variables
|
return new PagedData<ServerVariableResponse>()
|
||||||
.Select(ServerVariableMapper.ToAdminResponse)
|
{
|
||||||
.ToArray();
|
Items = variables,
|
||||||
|
CurrentPage = options.Page,
|
||||||
return PagedData<ServerVariableResponse>.Create(castedVariables, page, pageSize);
|
PageSize = options.PageSize,
|
||||||
|
TotalItems = count,
|
||||||
|
TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using MoonCore.Extended.Helpers;
|
using MoonCore.Extended.Helpers;
|
||||||
|
using MoonCore.Extended.Models;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
@@ -53,41 +54,36 @@ public class ServersController : Controller
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize(Policy = "permissions:admin.servers.read")]
|
[Authorize(Policy = "permissions:admin.servers.read")]
|
||||||
public async Task<IPagedData<ServerResponse>> Get(
|
public async Task<ActionResult<IPagedData<ServerResponse>>> Get([FromQuery] PagedOptions options)
|
||||||
[FromQuery] [Range(0, int.MaxValue)] int page,
|
|
||||||
[FromQuery] [Range(1, 100)] int pageSize
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
var count = await ServerRepository.Get().CountAsync();
|
var count = await ServerRepository.Get().CountAsync();
|
||||||
|
|
||||||
var items = await ServerRepository
|
var servers = await ServerRepository
|
||||||
.Get()
|
.Get()
|
||||||
.Include(x => x.Node)
|
.Include(x => x.Node)
|
||||||
.Include(x => x.Allocations)
|
.Include(x => x.Allocations)
|
||||||
.Include(x => x.Variables)
|
.Include(x => x.Variables)
|
||||||
.Include(x => x.Star)
|
.Include(x => x.Star)
|
||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.Skip(page * pageSize)
|
.Skip(options.Page * options.PageSize)
|
||||||
.Take(pageSize)
|
.Take(options.PageSize)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToAdminResponse()
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var mappedItems = items
|
|
||||||
.Select(ServerMapper.ToAdminServerResponse)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new PagedData<ServerResponse>()
|
return new PagedData<ServerResponse>()
|
||||||
{
|
{
|
||||||
Items = mappedItems,
|
Items = servers,
|
||||||
CurrentPage = page,
|
CurrentPage = options.Page,
|
||||||
PageSize = pageSize,
|
PageSize = options.PageSize,
|
||||||
TotalItems = count,
|
TotalItems = count,
|
||||||
TotalPages = count == 0 ? 0 : count / pageSize
|
TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.read")]
|
[Authorize(Policy = "permissions:admin.servers.read")]
|
||||||
public async Task<ServerResponse> GetSingle([FromRoute] int id)
|
public async Task<ActionResult<ServerResponse>> GetSingle([FromRoute] int id)
|
||||||
{
|
{
|
||||||
var server = await ServerRepository
|
var server = await ServerRepository
|
||||||
.Get()
|
.Get()
|
||||||
@@ -95,21 +91,23 @@ public class ServersController : Controller
|
|||||||
.Include(x => x.Allocations)
|
.Include(x => x.Allocations)
|
||||||
.Include(x => x.Variables)
|
.Include(x => x.Variables)
|
||||||
.Include(x => x.Star)
|
.Include(x => x.Star)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToAdminResponse()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with that id found", 404);
|
return Problem("No server with that id found", statusCode: 404);
|
||||||
|
|
||||||
return ServerMapper.ToAdminServerResponse(server);
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Policy = "permissions:admin.servers.write")]
|
[Authorize(Policy = "permissions:admin.servers.write")]
|
||||||
public async Task<ServerResponse> Create([FromBody] CreateServerRequest request)
|
public async Task<ActionResult<ServerResponse>> Create([FromBody] CreateServerRequest request)
|
||||||
{
|
{
|
||||||
// Check if owner user exist
|
// Check if owner user exist
|
||||||
if (UserRepository.Get().All(x => x.Id != request.OwnerId))
|
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
|
// Check if the star exists
|
||||||
var star = await StarRepository
|
var star = await StarRepository
|
||||||
@@ -119,14 +117,14 @@ public class ServersController : Controller
|
|||||||
.FirstOrDefaultAsync(x => x.Id == request.StarId);
|
.FirstOrDefaultAsync(x => x.Id == request.StarId);
|
||||||
|
|
||||||
if (star == null)
|
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
|
var node = await NodeRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == request.NodeId);
|
.FirstOrDefaultAsync(x => x.Id == request.NodeId);
|
||||||
|
|
||||||
if (node == null)
|
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<Allocation>();
|
var allocations = new List<Allocation>();
|
||||||
|
|
||||||
@@ -161,9 +159,9 @@ public class ServersController : Controller
|
|||||||
|
|
||||||
if (allocations.Count < star.RequiredAllocations)
|
if (allocations.Count < star.RequiredAllocations)
|
||||||
{
|
{
|
||||||
throw new HttpApiException(
|
return Problem(
|
||||||
$"Unable to find enough free allocations. Found: {allocations.Count}, Required: {star.RequiredAllocations}",
|
$"Unable to find enough free allocations. Found: {allocations.Count}, Required: {star.RequiredAllocations}",
|
||||||
400
|
statusCode: 400
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,7 +202,7 @@ public class ServersController : Controller
|
|||||||
Logger.LogError("Unable to sync server to node the server is assigned to: {e}", e);
|
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
|
// 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);
|
await ServerRepository.Remove(finalServer);
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
@@ -215,7 +213,7 @@ public class ServersController : Controller
|
|||||||
|
|
||||||
[HttpPatch("{id:int}")]
|
[HttpPatch("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.write")]
|
[Authorize(Policy = "permissions:admin.servers.write")]
|
||||||
public async Task<ServerResponse> Update([FromRoute] int id, [FromBody] UpdateServerRequest request)
|
public async Task<ActionResult<ServerResponse>> Update([FromRoute] int id, [FromBody] UpdateServerRequest request)
|
||||||
{
|
{
|
||||||
//TODO: Handle shrinking virtual disk
|
//TODO: Handle shrinking virtual disk
|
||||||
|
|
||||||
@@ -228,7 +226,7 @@ public class ServersController : Controller
|
|||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (server == null)
|
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);
|
ServerMapper.Merge(request, server);
|
||||||
|
|
||||||
@@ -255,9 +253,9 @@ public class ServersController : Controller
|
|||||||
// Check if the specified allocations are enough for the star
|
// Check if the specified allocations are enough for the star
|
||||||
if (allocations.Count < server.Star.RequiredAllocations)
|
if (allocations.Count < server.Star.RequiredAllocations)
|
||||||
{
|
{
|
||||||
throw new HttpApiException(
|
return Problem(
|
||||||
$"You need to specify at least {server.Star.RequiredAllocations} allocation(s)",
|
$"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}")]
|
[HttpDelete("{id:int}")]
|
||||||
public async Task Delete([FromRoute] int id, [FromQuery] bool force = false)
|
public async Task<ActionResult> Delete([FromRoute] int id, [FromQuery] bool force = false)
|
||||||
{
|
{
|
||||||
var server = await ServerRepository
|
var server = await ServerRepository
|
||||||
.Get()
|
.Get()
|
||||||
@@ -299,7 +297,7 @@ public class ServersController : Controller
|
|||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (server == null)
|
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.Variables.Clear();
|
||||||
server.Backups.Clear();
|
server.Backups.Clear();
|
||||||
@@ -325,5 +323,6 @@ public class ServersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
await ServerRepository.Remove(server);
|
await ServerRepository.Remove(server);
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ using MoonCore.Exceptions;
|
|||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using MoonCore.Extended.Helpers;
|
using MoonCore.Extended.Helpers;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using MoonCore.Extended.Models;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
@@ -32,10 +33,9 @@ public class StarDockerImagesController : Controller
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.get")]
|
[Authorize(Policy = "permissions:admin.servers.stars.get")]
|
||||||
public async Task<IPagedData<StarDockerImageDetailResponse>> Get(
|
public async Task<ActionResult<IPagedData<StarDockerImageResponse>>> Get(
|
||||||
[FromRoute] int starId,
|
[FromRoute] int starId,
|
||||||
[FromQuery] [Range(0, int.MaxValue)] int page,
|
[FromQuery] PagedOptions options
|
||||||
[FromQuery] [Range(1, 100)] int pageSize
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var starExists = StarRepository
|
var starExists = StarRepository
|
||||||
@@ -43,7 +43,7 @@ public class StarDockerImagesController : Controller
|
|||||||
.Any(x => x.Id == starId);
|
.Any(x => x.Id == starId);
|
||||||
|
|
||||||
if (!starExists)
|
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
|
var query = DockerImageRepository
|
||||||
.Get()
|
.Get()
|
||||||
@@ -51,50 +51,50 @@ public class StarDockerImagesController : Controller
|
|||||||
|
|
||||||
var count = await query.CountAsync();
|
var count = await query.CountAsync();
|
||||||
|
|
||||||
var items = await query
|
var dockerImages = await query
|
||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.Skip(page * pageSize)
|
.Skip(options.Page * options.PageSize)
|
||||||
.Take(pageSize)
|
.Take(options.PageSize)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToAdminResponse()
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var mappedItems = items
|
return new PagedData<StarDockerImageResponse>()
|
||||||
.Select(DockerImageMapper.ToAdminResponse)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new PagedData<StarDockerImageDetailResponse>()
|
|
||||||
{
|
{
|
||||||
Items = mappedItems,
|
Items = dockerImages,
|
||||||
CurrentPage = page,
|
CurrentPage = options.Page,
|
||||||
PageSize = pageSize,
|
PageSize = options.PageSize,
|
||||||
TotalItems = count,
|
TotalItems = count,
|
||||||
TotalPages = count == 0 ? 0 : count / pageSize
|
TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.read")]
|
[Authorize(Policy = "permissions:admin.servers.stars.read")]
|
||||||
public async Task<StarDockerImageDetailResponse> GetSingle([FromRoute] int starId, [FromRoute] int id)
|
public async Task<ActionResult<StarDockerImageResponse>> GetSingle([FromRoute] int starId, [FromRoute] int id)
|
||||||
{
|
{
|
||||||
var starExists = StarRepository
|
var starExists = StarRepository
|
||||||
.Get()
|
.Get()
|
||||||
.Any(x => x.Id == starId);
|
.Any(x => x.Id == starId);
|
||||||
|
|
||||||
if (!starExists)
|
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
|
var dockerImage = await DockerImageRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
|
.Where(x => x.Id == id && x.Star.Id == starId)
|
||||||
|
.ProjectToAdminResponse()
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (dockerImage == null)
|
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")]
|
[Authorize(Policy = "permissions:admin.servers.stars.write")]
|
||||||
public async Task<StarDockerImageDetailResponse> Create(
|
public async Task<ActionResult<StarDockerImageResponse>> Create(
|
||||||
[FromRoute] int starId,
|
[FromRoute] int starId,
|
||||||
[FromBody] CreateStarDockerImageRequest request
|
[FromBody] CreateStarDockerImageRequest request
|
||||||
)
|
)
|
||||||
@@ -104,7 +104,7 @@ public class StarDockerImagesController : Controller
|
|||||||
.FirstOrDefaultAsync(x => x.Id == starId);
|
.FirstOrDefaultAsync(x => x.Id == starId);
|
||||||
|
|
||||||
if (star == null)
|
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);
|
var dockerImage = DockerImageMapper.ToDockerImage(request);
|
||||||
dockerImage.Star = star;
|
dockerImage.Star = star;
|
||||||
@@ -116,7 +116,7 @@ public class StarDockerImagesController : Controller
|
|||||||
|
|
||||||
[HttpPatch("{id:int}")]
|
[HttpPatch("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.write")]
|
[Authorize(Policy = "permissions:admin.servers.stars.write")]
|
||||||
public async Task<StarDockerImageDetailResponse> Update(
|
public async Task<ActionResult<StarDockerImageResponse>> Update(
|
||||||
[FromRoute] int starId,
|
[FromRoute] int starId,
|
||||||
[FromRoute] int id,
|
[FromRoute] int id,
|
||||||
[FromBody] UpdateStarDockerImageRequest request
|
[FromBody] UpdateStarDockerImageRequest request
|
||||||
@@ -127,14 +127,14 @@ public class StarDockerImagesController : Controller
|
|||||||
.Any(x => x.Id == starId);
|
.Any(x => x.Id == starId);
|
||||||
|
|
||||||
if (!starExists)
|
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
|
var dockerImage = await DockerImageRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
|
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
|
||||||
|
|
||||||
if (dockerImage == null)
|
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);
|
DockerImageMapper.Merge(request, dockerImage);
|
||||||
await DockerImageRepository.Update(dockerImage);
|
await DockerImageRepository.Update(dockerImage);
|
||||||
@@ -144,22 +144,23 @@ public class StarDockerImagesController : Controller
|
|||||||
|
|
||||||
[HttpDelete("{id:int}")]
|
[HttpDelete("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.write")]
|
[Authorize(Policy = "permissions:admin.servers.stars.write")]
|
||||||
public async Task Delete([FromRoute] int starId, [FromRoute] int id)
|
public async Task<ActionResult> Delete([FromRoute] int starId, [FromRoute] int id)
|
||||||
{
|
{
|
||||||
var starExists = StarRepository
|
var starExists = StarRepository
|
||||||
.Get()
|
.Get()
|
||||||
.Any(x => x.Id == starId);
|
.Any(x => x.Id == starId);
|
||||||
|
|
||||||
if (!starExists)
|
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
|
var dockerImage = await DockerImageRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
|
.FirstOrDefaultAsync(x => x.Id == id && x.Star.Id == starId);
|
||||||
|
|
||||||
if (dockerImage == null)
|
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);
|
await DockerImageRepository.Remove(dockerImage);
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,18 +23,15 @@ public class StarImportExportController : Controller
|
|||||||
|
|
||||||
[HttpGet("{starId:int}/export")]
|
[HttpGet("{starId:int}/export")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.get")]
|
[Authorize(Policy = "permissions:admin.servers.stars.get")]
|
||||||
public async Task Export([FromRoute] int starId)
|
public async Task<ActionResult> Export([FromRoute] int starId)
|
||||||
{
|
{
|
||||||
var exportedStar = await ImportExportService.Export(starId);
|
var exportedStar = await ImportExportService.Export(starId);
|
||||||
|
return Content(exportedStar, "application/json");
|
||||||
Response.StatusCode = 200;
|
|
||||||
Response.ContentType = "application/json";
|
|
||||||
await Response.WriteAsync(exportedStar);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("import")]
|
[HttpPost("import")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.create")]
|
[Authorize(Policy = "permissions:admin.servers.stars.create")]
|
||||||
public async Task<StarDetailResponse> Import()
|
public async Task<StarResponse> Import()
|
||||||
{
|
{
|
||||||
if (Request.Form.Files.Count == 0)
|
if (Request.Form.Files.Count == 0)
|
||||||
throw new HttpApiException("No file to import provided", 400);
|
throw new HttpApiException("No file to import provided", 400);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
|
using MoonCore.Extended.Models;
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
using MoonlightServers.ApiServer.Mappers;
|
using MoonlightServers.ApiServer.Mappers;
|
||||||
@@ -29,10 +30,9 @@ public class StarVariablesController : Controller
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.get")]
|
[Authorize(Policy = "permissions:admin.servers.stars.get")]
|
||||||
public async Task<IPagedData<StarVariableDetailResponse>> Get(
|
public async Task<ActionResult<IPagedData<StarVariableResponse>>> Get(
|
||||||
[FromRoute] int starId,
|
[FromRoute] int starId,
|
||||||
[FromQuery] [Range(0, int.MaxValue)] int page,
|
[FromQuery] PagedOptions options
|
||||||
[FromQuery] [Range(1, 100)] int pageSize
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var starExists = StarRepository
|
var starExists = StarRepository
|
||||||
@@ -40,7 +40,7 @@ public class StarVariablesController : Controller
|
|||||||
.Any(x => x.Id == starId);
|
.Any(x => x.Id == starId);
|
||||||
|
|
||||||
if (!starExists)
|
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
|
var query = VariableRepository
|
||||||
.Get()
|
.Get()
|
||||||
@@ -48,29 +48,27 @@ public class StarVariablesController : Controller
|
|||||||
|
|
||||||
var count = await query.CountAsync();
|
var count = await query.CountAsync();
|
||||||
|
|
||||||
var items = await query
|
var variables = await query
|
||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.Skip(page * pageSize)
|
.Skip(options.Page * options.PageSize)
|
||||||
.Take(pageSize)
|
.Take(options.PageSize)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToAdminResponse()
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var mappedItems = items
|
return new PagedData<StarVariableResponse>()
|
||||||
.Select(StarVariableMapper.ToAdminResponse)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new PagedData<StarVariableDetailResponse>()
|
|
||||||
{
|
{
|
||||||
Items = mappedItems,
|
Items = variables,
|
||||||
CurrentPage = page,
|
CurrentPage = options.Page,
|
||||||
PageSize = pageSize,
|
PageSize = options.PageSize,
|
||||||
TotalItems = count,
|
TotalItems = count,
|
||||||
TotalPages = count == 0 ? 0 : count / pageSize
|
TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.get")]
|
[Authorize(Policy = "permissions:admin.servers.stars.get")]
|
||||||
public async Task<StarVariableDetailResponse> GetSingle(
|
public async Task<StarVariableResponse> GetSingle(
|
||||||
[FromRoute] int starId,
|
[FromRoute] int starId,
|
||||||
[FromRoute] int id
|
[FromRoute] int id
|
||||||
)
|
)
|
||||||
@@ -94,7 +92,7 @@ public class StarVariablesController : Controller
|
|||||||
|
|
||||||
[HttpPost("")]
|
[HttpPost("")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.create")]
|
[Authorize(Policy = "permissions:admin.servers.stars.create")]
|
||||||
public async Task<StarVariableDetailResponse> Create([FromRoute] int starId,
|
public async Task<StarVariableResponse> Create([FromRoute] int starId,
|
||||||
[FromBody] CreateStarVariableRequest request)
|
[FromBody] CreateStarVariableRequest request)
|
||||||
{
|
{
|
||||||
var star = StarRepository
|
var star = StarRepository
|
||||||
@@ -114,7 +112,7 @@ public class StarVariablesController : Controller
|
|||||||
|
|
||||||
[HttpPatch("{id:int}")]
|
[HttpPatch("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.update")]
|
[Authorize(Policy = "permissions:admin.servers.stars.update")]
|
||||||
public async Task<StarVariableDetailResponse> Update(
|
public async Task<StarVariableResponse> Update(
|
||||||
[FromRoute] int starId,
|
[FromRoute] int starId,
|
||||||
[FromRoute] int id,
|
[FromRoute] int id,
|
||||||
[FromBody] UpdateStarVariableRequest request
|
[FromBody] UpdateStarVariableRequest request
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
|
using MoonCore.Extended.Models;
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
using MoonlightServers.ApiServer.Mappers;
|
using MoonlightServers.ApiServer.Mappers;
|
||||||
@@ -25,51 +26,48 @@ public class StarsController : Controller
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.read")]
|
[Authorize(Policy = "permissions:admin.servers.stars.read")]
|
||||||
public async Task<IPagedData<StarDetailResponse>> Get(
|
public async Task<ActionResult<IPagedData<StarResponse>>> Get([FromQuery] PagedOptions options)
|
||||||
[FromQuery] [Range(0, int.MaxValue)] int page,
|
|
||||||
[FromQuery] [Range(1, 100)] int pageSize
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
var count = await StarRepository.Get().CountAsync();
|
var count = await StarRepository.Get().CountAsync();
|
||||||
|
|
||||||
var items = await StarRepository
|
var stars = await StarRepository
|
||||||
.Get()
|
.Get()
|
||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.Skip(page * pageSize)
|
.Skip(options.Page * options.PageSize)
|
||||||
.Take(pageSize)
|
.Take(options.PageSize)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToAdminResponse()
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var mappedItems = items
|
return new PagedData<StarResponse>()
|
||||||
.Select(StarMapper.ToAdminResponse)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new PagedData<StarDetailResponse>()
|
|
||||||
{
|
{
|
||||||
CurrentPage = page,
|
CurrentPage = options.Page,
|
||||||
Items = mappedItems,
|
Items = stars,
|
||||||
PageSize = pageSize,
|
PageSize = options.PageSize,
|
||||||
TotalItems = count,
|
TotalItems = count,
|
||||||
TotalPages = count == 0 ? 0 : count / pageSize
|
TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.read")]
|
[Authorize(Policy = "permissions:admin.servers.stars.read")]
|
||||||
public async Task<StarDetailResponse> GetSingle([FromRoute] int id)
|
public async Task<ActionResult<StarResponse>> GetSingle([FromRoute] int id)
|
||||||
{
|
{
|
||||||
var star = await StarRepository
|
var star = await StarRepository
|
||||||
.Get()
|
.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectToAdminResponse()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (star == null)
|
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]
|
[HttpPost]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.create")]
|
[Authorize(Policy = "permissions:admin.servers.stars.create")]
|
||||||
public async Task<StarDetailResponse> Create([FromBody] CreateStarRequest request)
|
public async Task<ActionResult<StarResponse>> Create([FromBody] CreateStarRequest request)
|
||||||
{
|
{
|
||||||
var star = StarMapper.ToStar(request);
|
var star = StarMapper.ToStar(request);
|
||||||
|
|
||||||
@@ -95,7 +93,7 @@ public class StarsController : Controller
|
|||||||
|
|
||||||
[HttpPatch("{id:int}")]
|
[HttpPatch("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.update")]
|
[Authorize(Policy = "permissions:admin.servers.stars.update")]
|
||||||
public async Task<StarDetailResponse> Update(
|
public async Task<ActionResult<StarResponse>> Update(
|
||||||
[FromRoute] int id,
|
[FromRoute] int id,
|
||||||
[FromBody] UpdateStarRequest request
|
[FromBody] UpdateStarRequest request
|
||||||
)
|
)
|
||||||
@@ -105,7 +103,7 @@ public class StarsController : Controller
|
|||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (star == null)
|
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);
|
StarMapper.Merge(request, star);
|
||||||
await StarRepository.Update(star);
|
await StarRepository.Update(star);
|
||||||
@@ -115,15 +113,16 @@ public class StarsController : Controller
|
|||||||
|
|
||||||
[HttpDelete("{id:int}")]
|
[HttpDelete("{id:int}")]
|
||||||
[Authorize(Policy = "permissions:admin.servers.stars.delete")]
|
[Authorize(Policy = "permissions:admin.servers.stars.delete")]
|
||||||
public async Task Delete([FromRoute] int id)
|
public async Task<ActionResult> Delete([FromRoute] int id)
|
||||||
{
|
{
|
||||||
var star = await StarRepository
|
var star = await StarRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
if (star == null)
|
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);
|
await StarRepository.Remove(star);
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
using MoonlightServers.ApiServer.Services;
|
using MoonlightServers.ApiServer.Services;
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
using MoonlightServers.DaemonShared.Enums;
|
||||||
@@ -38,11 +37,14 @@ public class FilesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("list")]
|
[HttpGet("list")]
|
||||||
public async Task<ServerFilesEntryResponse[]> List([FromRoute] int serverId, [FromQuery] string path)
|
public async Task<ActionResult<ServerFilesEntryResponse[]>> List([FromRoute] int serverId, [FromQuery] string path)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(serverId, ServerPermissionLevel.Read);
|
var server = await GetServerById(serverId, ServerPermissionLevel.Read);
|
||||||
|
|
||||||
var entries = await ServerFileSystemService.List(server, path);
|
if (server.Value == null)
|
||||||
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
|
var entries = await ServerFileSystemService.List(server.Value, path);
|
||||||
|
|
||||||
return entries.Select(x => new ServerFilesEntryResponse()
|
return entries.Select(x => new ServerFilesEntryResponse()
|
||||||
{
|
{
|
||||||
@@ -55,41 +57,62 @@ public class FilesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("move")]
|
[HttpPost("move")]
|
||||||
public async Task Move([FromRoute] int serverId, [FromQuery] string oldPath, [FromQuery] string newPath)
|
public async Task<ActionResult> Move([FromRoute] int serverId, [FromQuery] string oldPath, [FromQuery] string newPath)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite);
|
var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite);
|
||||||
|
|
||||||
await ServerFileSystemService.Move(server, oldPath, newPath);
|
if (server.Value == null)
|
||||||
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
|
await ServerFileSystemService.Move(server.Value, oldPath, newPath);
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("delete")]
|
[HttpDelete("delete")]
|
||||||
public async Task Delete([FromRoute] int serverId, [FromQuery] string path)
|
public async Task<ActionResult> Delete([FromRoute] int serverId, [FromQuery] string path)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite);
|
var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite);
|
||||||
|
|
||||||
await ServerFileSystemService.Delete(server, path);
|
if (server.Value == null)
|
||||||
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
|
await ServerFileSystemService.Delete(server.Value, path);
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("mkdir")]
|
[HttpPost("mkdir")]
|
||||||
public async Task Mkdir([FromRoute] int serverId, [FromQuery] string path)
|
public async Task<ActionResult> Mkdir([FromRoute] int serverId, [FromQuery] string path)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite);
|
var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite);
|
||||||
|
|
||||||
await ServerFileSystemService.Mkdir(server, path);
|
if (server.Value == null)
|
||||||
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
|
await ServerFileSystemService.Mkdir(server.Value, path);
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("touch")]
|
[HttpPost("touch")]
|
||||||
public async Task Touch([FromRoute] int serverId, [FromQuery] string path)
|
public async Task<ActionResult> Touch([FromRoute] int serverId, [FromQuery] string path)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite);
|
var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite);
|
||||||
|
|
||||||
await ServerFileSystemService.Mkdir(server, path);
|
if (server.Value == null)
|
||||||
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
|
await ServerFileSystemService.Mkdir(server.Value, path);
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("upload")]
|
[HttpGet("upload")]
|
||||||
public async Task<ServerFilesUploadResponse> Upload([FromRoute] int serverId)
|
public async Task<ActionResult<ServerFilesUploadResponse>> 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(
|
var accessToken = NodeService.CreateAccessToken(
|
||||||
server.Node,
|
server.Node,
|
||||||
@@ -114,9 +137,14 @@ public class FilesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("download")]
|
[HttpGet("download")]
|
||||||
public async Task<ServerFilesDownloadResponse> Download([FromRoute] int serverId, [FromQuery] string path)
|
public async Task<ActionResult<ServerFilesDownloadResponse>> 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(
|
var accessToken = NodeService.CreateAccessToken(
|
||||||
server.Node,
|
server.Node,
|
||||||
@@ -142,28 +170,36 @@ public class FilesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("compress")]
|
[HttpPost("compress")]
|
||||||
public async Task Compress([FromRoute] int serverId, [FromBody] ServerFilesCompressRequest request)
|
public async Task<ActionResult> Compress([FromRoute] int serverId, [FromBody] ServerFilesCompressRequest request)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite);
|
var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite);
|
||||||
|
|
||||||
if (!Enum.TryParse(request.Type, true, out CompressType type))
|
if (server.Value == null)
|
||||||
throw new HttpApiException("Invalid compress type provided", 400);
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
await ServerFileSystemService.Compress(server, type, request.Items, request.Destination);
|
if (!Enum.TryParse(request.Type, true, out CompressType type))
|
||||||
|
return Problem("Invalid compress type provided", statusCode: 400);
|
||||||
|
|
||||||
|
await ServerFileSystemService.Compress(server.Value, type, request.Items, request.Destination);
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("decompress")]
|
[HttpPost("decompress")]
|
||||||
public async Task Decompress([FromRoute] int serverId, [FromBody] ServerFilesDecompressRequest request)
|
public async Task<ActionResult> Decompress([FromRoute] int serverId, [FromBody] ServerFilesDecompressRequest request)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite);
|
var server = await GetServerById(serverId, ServerPermissionLevel.ReadWrite);
|
||||||
|
|
||||||
if (!Enum.TryParse(request.Type, true, out CompressType type))
|
if (server.Value == null)
|
||||||
throw new HttpApiException("Invalid compress type provided", 400);
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
await ServerFileSystemService.Decompress(server, type, request.Path, request.Destination);
|
if (!Enum.TryParse(request.Type, true, out CompressType type))
|
||||||
|
return Problem("Invalid decompress type provided", statusCode: 400);
|
||||||
|
|
||||||
|
await ServerFileSystemService.Decompress(server.Value, type, request.Path, request.Destination);
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Server> GetServerById(int serverId, ServerPermissionLevel level)
|
private async Task<ActionResult<Server>> GetServerById(int serverId, ServerPermissionLevel level)
|
||||||
{
|
{
|
||||||
var server = await ServerRepository
|
var server = await ServerRepository
|
||||||
.Get()
|
.Get()
|
||||||
@@ -171,7 +207,7 @@ public class FilesController : Controller
|
|||||||
.FirstOrDefaultAsync(x => x.Id == serverId);
|
.FirstOrDefaultAsync(x => x.Id == serverId);
|
||||||
|
|
||||||
if (server == null)
|
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(
|
var authorizeResult = await AuthorizeService.Authorize(
|
||||||
User, server,
|
User, server,
|
||||||
@@ -181,9 +217,9 @@ public class FilesController : Controller
|
|||||||
|
|
||||||
if (!authorizeResult.Succeeded)
|
if (!authorizeResult.Succeeded)
|
||||||
{
|
{
|
||||||
throw new HttpApiException(
|
return Problem(
|
||||||
authorizeResult.Message ?? "No permission for the requested resource",
|
authorizeResult.Message ?? "No permission for the requested resource",
|
||||||
403
|
statusCode: 403
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Client;
|
|||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[Route("api/client/servers")]
|
[Route("api/client/servers/{serverId:int}")]
|
||||||
public class PowerController : Controller
|
public class PowerController : Controller
|
||||||
{
|
{
|
||||||
private readonly DatabaseRepository<Server> ServerRepository;
|
private readonly DatabaseRepository<Server> ServerRepository;
|
||||||
@@ -32,31 +32,46 @@ public class PowerController : Controller
|
|||||||
AuthorizeService = authorizeService;
|
AuthorizeService = authorizeService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{serverId:int}/start")]
|
[HttpPost("start")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task Start([FromRoute] int serverId)
|
public async Task<ActionResult> Start([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(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]
|
[Authorize]
|
||||||
public async Task Stop([FromRoute] int serverId)
|
public async Task<ActionResult> Stop([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(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]
|
[Authorize]
|
||||||
public async Task Kill([FromRoute] int serverId)
|
public async Task<ActionResult> Kill([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(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<Server> GetServerById(int serverId)
|
private async Task<ActionResult<Server>> GetServerById(int serverId)
|
||||||
{
|
{
|
||||||
var server = await ServerRepository
|
var server = await ServerRepository
|
||||||
.Get()
|
.Get()
|
||||||
@@ -64,7 +79,7 @@ public class PowerController : Controller
|
|||||||
.FirstOrDefaultAsync(x => x.Id == serverId);
|
.FirstOrDefaultAsync(x => x.Id == serverId);
|
||||||
|
|
||||||
if (server == null)
|
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(
|
var authorizeResult = await AuthorizeService.Authorize(
|
||||||
User, server,
|
User, server,
|
||||||
@@ -74,9 +89,9 @@ public class PowerController : Controller
|
|||||||
|
|
||||||
if (!authorizeResult.Succeeded)
|
if (!authorizeResult.Succeeded)
|
||||||
{
|
{
|
||||||
throw new HttpApiException(
|
return Problem(
|
||||||
authorizeResult.Message ?? "No permission for the requested resource",
|
authorizeResult.Message ?? "No permission for the requested resource",
|
||||||
403
|
statusCode: 403
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
|
using MoonCore.Extended.Models;
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
@@ -51,15 +52,12 @@ public class ServersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<PagedData<ServerDetailResponse>> GetAll(
|
public async Task<ActionResult<PagedData<ServerDetailResponse>>> GetAll([FromQuery] PagedOptions options)
|
||||||
[FromQuery] [Range(0, int.MaxValue)] int page,
|
|
||||||
[FromQuery] [Range(0, 100)] int pageSize
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
var userIdClaim = User.FindFirstValue("userId");
|
var userIdClaim = User.FindFirstValue("UserId");
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(userIdClaim))
|
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);
|
var userId = int.Parse(userIdClaim);
|
||||||
|
|
||||||
@@ -74,8 +72,9 @@ public class ServersController : Controller
|
|||||||
|
|
||||||
var items = await query
|
var items = await query
|
||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.Skip(page * pageSize)
|
.Skip(options.Page * options.PageSize)
|
||||||
.Take(pageSize)
|
.Take(options.PageSize)
|
||||||
|
.AsNoTracking()
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var mappedItems = items.Select(x => new ServerDetailResponse()
|
var mappedItems = items.Select(x => new ServerDetailResponse()
|
||||||
@@ -98,23 +97,20 @@ public class ServersController : Controller
|
|||||||
return new PagedData<ServerDetailResponse>()
|
return new PagedData<ServerDetailResponse>()
|
||||||
{
|
{
|
||||||
Items = mappedItems,
|
Items = mappedItems,
|
||||||
CurrentPage = page,
|
CurrentPage = options.Page,
|
||||||
PageSize = pageSize,
|
|
||||||
TotalItems = count,
|
TotalItems = count,
|
||||||
TotalPages = count == 0 ? 0 : count / pageSize
|
PageSize = options.PageSize,
|
||||||
|
TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("shared")]
|
[HttpGet("shared")]
|
||||||
public async Task<PagedData<ServerDetailResponse>> GetAllShared(
|
public async Task<ActionResult<PagedData<ServerDetailResponse>>> GetAllShared([FromQuery] PagedOptions options)
|
||||||
[FromQuery] [Range(0, int.MaxValue)] int page,
|
|
||||||
[FromQuery] [Range(0, 100)] int pageSize
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
var userIdClaim = User.FindFirstValue("userId");
|
var userIdClaim = User.FindFirstValue("UserId");
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(userIdClaim))
|
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);
|
var userId = int.Parse(userIdClaim);
|
||||||
|
|
||||||
@@ -132,8 +128,8 @@ public class ServersController : Controller
|
|||||||
|
|
||||||
var items = await query
|
var items = await query
|
||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.Skip(page * pageSize)
|
.Skip(options.Page * options.PageSize)
|
||||||
.Take(pageSize)
|
.Take(options.PageSize)
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var ownerIds = items
|
var ownerIds = items
|
||||||
@@ -171,15 +167,15 @@ public class ServersController : Controller
|
|||||||
return new PagedData<ServerDetailResponse>()
|
return new PagedData<ServerDetailResponse>()
|
||||||
{
|
{
|
||||||
Items = mappedItems,
|
Items = mappedItems,
|
||||||
CurrentPage = page,
|
CurrentPage = options.Page,
|
||||||
PageSize = pageSize,
|
|
||||||
TotalItems = count,
|
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}")]
|
[HttpGet("{serverId:int}")]
|
||||||
public async Task<ServerDetailResponse> Get([FromRoute] int serverId)
|
public async Task<ActionResult<ServerDetailResponse>> Get([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = await ServerRepository
|
var server = await ServerRepository
|
||||||
.Get()
|
.Get()
|
||||||
@@ -189,7 +185,7 @@ public class ServersController : Controller
|
|||||||
.FirstOrDefaultAsync(x => x.Id == serverId);
|
.FirstOrDefaultAsync(x => x.Id == serverId);
|
||||||
|
|
||||||
if (server == null)
|
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(
|
var authorizationResult = await AuthorizeService.Authorize(
|
||||||
User,
|
User,
|
||||||
@@ -200,9 +196,9 @@ public class ServersController : Controller
|
|||||||
|
|
||||||
if (!authorizationResult.Succeeded)
|
if (!authorizationResult.Succeeded)
|
||||||
{
|
{
|
||||||
throw new HttpApiException(
|
return Problem(
|
||||||
authorizationResult.Message ?? "No server with this id found",
|
authorizationResult.Message ?? "No server with this id found",
|
||||||
404
|
statusCode: 404
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +238,7 @@ public class ServersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{serverId:int}/status")]
|
[HttpGet("{serverId:int}/status")]
|
||||||
public async Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
|
public async Task<ActionResult<ServerStatusResponse>> GetStatus([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(
|
var server = await GetServerById(
|
||||||
serverId,
|
serverId,
|
||||||
@@ -250,7 +246,10 @@ public class ServersController : Controller
|
|||||||
ServerPermissionLevel.None
|
ServerPermissionLevel.None
|
||||||
);
|
);
|
||||||
|
|
||||||
var status = await ServerService.GetStatus(server);
|
if (server.Value == null)
|
||||||
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
|
var status = await ServerService.GetStatus(server.Value);
|
||||||
|
|
||||||
return new ServerStatusResponse()
|
return new ServerStatusResponse()
|
||||||
{
|
{
|
||||||
@@ -259,14 +258,19 @@ public class ServersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{serverId:int}/ws")]
|
[HttpGet("{serverId:int}/ws")]
|
||||||
public async Task<ServerWebSocketResponse> GetWebSocket([FromRoute] int serverId)
|
public async Task<ActionResult<ServerWebSocketResponse>> GetWebSocket([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(
|
var serverResult = await GetServerById(
|
||||||
serverId,
|
serverId,
|
||||||
ServerPermissionConstants.Console,
|
ServerPermissionConstants.Console,
|
||||||
ServerPermissionLevel.Read
|
ServerPermissionLevel.Read
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (serverResult.Value == null)
|
||||||
|
return serverResult.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
|
var server = serverResult.Value;
|
||||||
|
|
||||||
// TODO: Handle transparent node proxy
|
// TODO: Handle transparent node proxy
|
||||||
|
|
||||||
var accessToken = NodeService.CreateAccessToken(server.Node, parameters =>
|
var accessToken = NodeService.CreateAccessToken(server.Node, parameters =>
|
||||||
@@ -288,7 +292,7 @@ public class ServersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{serverId:int}/logs")]
|
[HttpGet("{serverId:int}/logs")]
|
||||||
public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId)
|
public async Task<ActionResult<ServerLogsResponse>> GetLogs([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(
|
var server = await GetServerById(
|
||||||
serverId,
|
serverId,
|
||||||
@@ -296,7 +300,10 @@ public class ServersController : Controller
|
|||||||
ServerPermissionLevel.Read
|
ServerPermissionLevel.Read
|
||||||
);
|
);
|
||||||
|
|
||||||
var logs = await ServerService.GetLogs(server);
|
if (server.Value == null)
|
||||||
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
|
var logs = await ServerService.GetLogs(server.Value);
|
||||||
|
|
||||||
return new ServerLogsResponse()
|
return new ServerLogsResponse()
|
||||||
{
|
{
|
||||||
@@ -305,7 +312,7 @@ public class ServersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{serverId:int}/stats")]
|
[HttpGet("{serverId:int}/stats")]
|
||||||
public async Task<ServerStatsResponse> GetStats([FromRoute] int serverId)
|
public async Task<ActionResult<ServerStatsResponse>> GetStats([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(
|
var server = await GetServerById(
|
||||||
serverId,
|
serverId,
|
||||||
@@ -313,7 +320,10 @@ public class ServersController : Controller
|
|||||||
ServerPermissionLevel.Read
|
ServerPermissionLevel.Read
|
||||||
);
|
);
|
||||||
|
|
||||||
var stats = await ServerService.GetStats(server);
|
if (server.Value == null)
|
||||||
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
|
var stats = await ServerService.GetStats(server.Value);
|
||||||
|
|
||||||
return new ServerStatsResponse()
|
return new ServerStatsResponse()
|
||||||
{
|
{
|
||||||
@@ -327,7 +337,7 @@ public class ServersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{serverId:int}/command")]
|
[HttpPost("{serverId:int}/command")]
|
||||||
public async Task Command([FromRoute] int serverId, [FromBody] ServerCommandRequest request)
|
public async Task<ActionResult> Command([FromRoute] int serverId, [FromBody] ServerCommandRequest request)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(
|
var server = await GetServerById(
|
||||||
serverId,
|
serverId,
|
||||||
@@ -335,10 +345,15 @@ public class ServersController : Controller
|
|||||||
ServerPermissionLevel.ReadWrite
|
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<Server> GetServerById(int serverId, string permissionId, ServerPermissionLevel level)
|
private async Task<ActionResult<Server>> GetServerById(int serverId, string permissionId, ServerPermissionLevel level)
|
||||||
{
|
{
|
||||||
var server = await ServerRepository
|
var server = await ServerRepository
|
||||||
.Get()
|
.Get()
|
||||||
@@ -346,15 +361,15 @@ public class ServersController : Controller
|
|||||||
.FirstOrDefaultAsync(x => x.Id == serverId);
|
.FirstOrDefaultAsync(x => x.Id == serverId);
|
||||||
|
|
||||||
if (server == null)
|
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);
|
var authorizeResult = await AuthorizeService.Authorize(User, server, permissionId, level);
|
||||||
|
|
||||||
if (!authorizeResult.Succeeded)
|
if (!authorizeResult.Succeeded)
|
||||||
{
|
{
|
||||||
throw new HttpApiException(
|
return Problem(
|
||||||
authorizeResult.Message ?? "No permission for the requested resource",
|
authorizeResult.Message ?? "No permission for the requested resource",
|
||||||
403
|
statusCode: 403
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,13 +32,18 @@ public class SettingsController : Controller
|
|||||||
|
|
||||||
[HttpPost("{serverId:int}/install")]
|
[HttpPost("{serverId:int}/install")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task Install([FromRoute] int serverId)
|
public async Task<ActionResult> Install([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(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<Server> GetServerById(int serverId)
|
private async Task<ActionResult<Server>> GetServerById(int serverId)
|
||||||
{
|
{
|
||||||
var server = await ServerRepository
|
var server = await ServerRepository
|
||||||
.Get()
|
.Get()
|
||||||
@@ -46,7 +51,7 @@ public class SettingsController : Controller
|
|||||||
.FirstOrDefaultAsync(x => x.Id == serverId);
|
.FirstOrDefaultAsync(x => x.Id == serverId);
|
||||||
|
|
||||||
if (server == null)
|
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(
|
var authorizeResult = await AuthorizeService.Authorize(
|
||||||
User, server,
|
User, server,
|
||||||
@@ -56,9 +61,9 @@ public class SettingsController : Controller
|
|||||||
|
|
||||||
if (!authorizeResult.Succeeded)
|
if (!authorizeResult.Succeeded)
|
||||||
{
|
{
|
||||||
throw new HttpApiException(
|
return Problem(
|
||||||
authorizeResult.Message ?? "No permission for the requested resource",
|
authorizeResult.Message ?? "No permission for the requested resource",
|
||||||
403
|
statusCode: 403
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
|
using MoonCore.Extended.Models;
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
@@ -41,24 +42,26 @@ public class SharesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<PagedData<ServerShareResponse>> GetAll(
|
public async Task<ActionResult<IPagedData<ServerShareResponse>>> GetAll(
|
||||||
[FromRoute] int serverId,
|
[FromRoute] int serverId,
|
||||||
[FromQuery] [Range(0, int.MaxValue)] int page,
|
[FromQuery] PagedOptions options
|
||||||
[FromQuery] [Range(1, 100)] int pageSize
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(serverId);
|
var server = await GetServerById(serverId);
|
||||||
|
|
||||||
|
if (server.Value == null)
|
||||||
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
var query = ShareRepository
|
var query = ShareRepository
|
||||||
.Get()
|
.Get()
|
||||||
.Where(x => x.Server.Id == server.Id);
|
.Where(x => x.Server.Id == server.Value.Id);
|
||||||
|
|
||||||
var count = await query.CountAsync();
|
var count = await query.CountAsync();
|
||||||
|
|
||||||
var items = await query
|
var items = await query
|
||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.Skip(page * pageSize)
|
.Skip(options.Page * options.PageSize)
|
||||||
.Take(pageSize)
|
.Take(options.PageSize)
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var userIds = items
|
var userIds = items
|
||||||
@@ -81,27 +84,30 @@ public class SharesController : Controller
|
|||||||
return new PagedData<ServerShareResponse>()
|
return new PagedData<ServerShareResponse>()
|
||||||
{
|
{
|
||||||
Items = mappedItems,
|
Items = mappedItems,
|
||||||
CurrentPage = page,
|
CurrentPage = options.Page,
|
||||||
PageSize = pageSize,
|
PageSize = options.PageSize,
|
||||||
TotalItems = count,
|
TotalItems = count,
|
||||||
TotalPages = count == 0 ? 0 : count / pageSize
|
TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("{id:int}")]
|
||||||
public async Task<ServerShareResponse> Get(
|
public async Task<ActionResult<ServerShareResponse>> Get(
|
||||||
[FromRoute] int serverId,
|
[FromRoute] int serverId,
|
||||||
[FromRoute] int id
|
[FromRoute] int id
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(serverId);
|
var server = await GetServerById(serverId);
|
||||||
|
|
||||||
|
if (server.Value == null)
|
||||||
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
var share = await ShareRepository
|
var share = await ShareRepository
|
||||||
.Get()
|
.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)
|
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
|
var user = await UserRepository
|
||||||
.Get()
|
.Get()
|
||||||
@@ -118,23 +124,26 @@ public class SharesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<ServerShareResponse> Create(
|
public async Task<ActionResult<ServerShareResponse>> Create(
|
||||||
[FromRoute] int serverId,
|
[FromRoute] int serverId,
|
||||||
[FromBody] CreateShareRequest request
|
[FromBody] CreateShareRequest request
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(serverId);
|
var server = await GetServerById(serverId);
|
||||||
|
|
||||||
|
if (server.Value == null)
|
||||||
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
var user = await UserRepository
|
var user = await UserRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Username == request.Username);
|
.FirstOrDefaultAsync(x => x.Username == request.Username);
|
||||||
|
|
||||||
if (user == null)
|
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()
|
var share = new ServerShare()
|
||||||
{
|
{
|
||||||
Server = server,
|
Server = server.Value,
|
||||||
Content = ShareMapper.MapToServerShareContent(request.Permissions),
|
Content = ShareMapper.MapToServerShareContent(request.Permissions),
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
UpdatedAt = DateTime.UtcNow,
|
UpdatedAt = DateTime.UtcNow,
|
||||||
@@ -154,7 +163,7 @@ public class SharesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("{id:int}")]
|
[HttpPatch("{id:int}")]
|
||||||
public async Task<ServerShareResponse> Update(
|
public async Task<ActionResult<ServerShareResponse>> Update(
|
||||||
[FromRoute] int serverId,
|
[FromRoute] int serverId,
|
||||||
[FromRoute] int id,
|
[FromRoute] int id,
|
||||||
[FromBody] UpdateShareRequest request
|
[FromBody] UpdateShareRequest request
|
||||||
@@ -162,12 +171,15 @@ public class SharesController : Controller
|
|||||||
{
|
{
|
||||||
var server = await GetServerById(serverId);
|
var server = await GetServerById(serverId);
|
||||||
|
|
||||||
|
if (server.Value == null)
|
||||||
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
var share = await ShareRepository
|
var share = await ShareRepository
|
||||||
.Get()
|
.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)
|
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);
|
share.Content = ShareMapper.MapToServerShareContent(request.Permissions);
|
||||||
|
|
||||||
@@ -180,7 +192,7 @@ public class SharesController : Controller
|
|||||||
.FirstOrDefaultAsync(x => x.Id == share.UserId);
|
.FirstOrDefaultAsync(x => x.Id == share.UserId);
|
||||||
|
|
||||||
if (user == null)
|
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()
|
var mappedItem = new ServerShareResponse()
|
||||||
{
|
{
|
||||||
@@ -193,31 +205,35 @@ public class SharesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:int}")]
|
[HttpDelete("{id:int}")]
|
||||||
public async Task Delete(
|
public async Task<ActionResult> Delete(
|
||||||
[FromRoute] int serverId,
|
[FromRoute] int serverId,
|
||||||
[FromRoute] int id
|
[FromRoute] int id
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(serverId);
|
var server = await GetServerById(serverId);
|
||||||
|
|
||||||
|
if (server.Value == null)
|
||||||
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
var share = await ShareRepository
|
var share = await ShareRepository
|
||||||
.Get()
|
.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)
|
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);
|
await ShareRepository.Remove(share);
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Server> GetServerById(int serverId)
|
private async Task<ActionResult<Server>> GetServerById(int serverId)
|
||||||
{
|
{
|
||||||
var server = await ServerRepository
|
var server = await ServerRepository
|
||||||
.Get()
|
.Get()
|
||||||
.FirstOrDefaultAsync(x => x.Id == serverId);
|
.FirstOrDefaultAsync(x => x.Id == serverId);
|
||||||
|
|
||||||
if (server == null)
|
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(
|
var authorizeResult = await AuthorizeService.Authorize(
|
||||||
User, server,
|
User, server,
|
||||||
@@ -227,9 +243,9 @@ public class SharesController : Controller
|
|||||||
|
|
||||||
if (!authorizeResult.Succeeded)
|
if (!authorizeResult.Succeeded)
|
||||||
{
|
{
|
||||||
throw new HttpApiException(
|
return Problem(
|
||||||
authorizeResult.Message ?? "No permission for the requested resource",
|
authorizeResult.Message ?? "No permission for the requested resource",
|
||||||
403
|
statusCode: 403
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
|
using MoonCore.Extended.Models;
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
@@ -40,24 +41,26 @@ public class VariablesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<PagedData<ServerVariableDetailResponse>> Get(
|
public async Task<ActionResult<PagedData<ServerVariableDetailResponse>>> Get(
|
||||||
[FromRoute] int serverId,
|
[FromRoute] int serverId,
|
||||||
[FromQuery] [Range(0, int.MaxValue)] int page,
|
[FromQuery] PagedOptions options
|
||||||
[FromQuery] [Range(1, 100)] int pageSize
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var server = await GetServerById(serverId, ServerPermissionLevel.Read);
|
var server = await GetServerById(serverId, ServerPermissionLevel.Read);
|
||||||
|
|
||||||
|
if (server.Value == null)
|
||||||
|
return server.Result ?? Problem("Unable to retrieve server");
|
||||||
|
|
||||||
var query = StarVariableRepository
|
var query = StarVariableRepository
|
||||||
.Get()
|
.Get()
|
||||||
.Where(x => x.Star.Id == server.Star.Id);
|
.Where(x => x.Star.Id == server.Value.Star.Id);
|
||||||
|
|
||||||
var count = await query.CountAsync();
|
var count = await query.CountAsync();
|
||||||
|
|
||||||
var starVariables = await query
|
var starVariables = await query
|
||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.Skip(page * pageSize)
|
.Skip(options.Page * options.PageSize)
|
||||||
.Take(pageSize)
|
.Take(options.PageSize)
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var starVariableKeys = starVariables
|
var starVariableKeys = starVariables
|
||||||
@@ -66,7 +69,7 @@ public class VariablesController : Controller
|
|||||||
|
|
||||||
var serverVariables = await ServerVariableRepository
|
var serverVariables = await ServerVariableRepository
|
||||||
.Get()
|
.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();
|
.ToArrayAsync();
|
||||||
|
|
||||||
var responses = starVariables.Select(starVariable =>
|
var responses = starVariables.Select(starVariable =>
|
||||||
@@ -87,22 +90,27 @@ public class VariablesController : Controller
|
|||||||
return new PagedData<ServerVariableDetailResponse>()
|
return new PagedData<ServerVariableDetailResponse>()
|
||||||
{
|
{
|
||||||
Items = responses,
|
Items = responses,
|
||||||
CurrentPage = page,
|
CurrentPage = options.Page,
|
||||||
PageSize = pageSize,
|
PageSize = options.PageSize,
|
||||||
TotalItems = count,
|
TotalItems = count,
|
||||||
TotalPages = count == 0 ? 0 : count / pageSize
|
TotalPages = (int)Math.Ceiling(Math.Max(0, count) / (double)options.PageSize)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
public async Task<ServerVariableDetailResponse> UpdateSingle(
|
public async Task<ActionResult<ServerVariableDetailResponse>> UpdateSingle(
|
||||||
[FromRoute] int serverId,
|
[FromRoute] int serverId,
|
||||||
[FromBody] UpdateServerVariableRequest request
|
[FromBody] UpdateServerVariableRequest request
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// TODO: Handle filter
|
// 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 serverVariable = server.Variables.FirstOrDefault(x => x.Key == request.Key);
|
||||||
var starVariable = server.Star.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]
|
[HttpPatch]
|
||||||
public async Task<ServerVariableDetailResponse[]> Update(
|
public async Task<ActionResult<ServerVariableDetailResponse[]>> Update(
|
||||||
[FromRoute] int serverId,
|
[FromRoute] int serverId,
|
||||||
[FromBody] UpdateServerVariableRangeRequest request
|
[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)
|
foreach (var variable in request.Variables)
|
||||||
{
|
{
|
||||||
@@ -164,15 +177,17 @@ public class VariablesController : Controller
|
|||||||
}).ToArray();
|
}).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Server> GetServerById(int serverId, ServerPermissionLevel level)
|
private async Task<ActionResult<Server>> GetServerById(int serverId, ServerPermissionLevel level)
|
||||||
{
|
{
|
||||||
var server = await ServerRepository
|
var server = await ServerRepository
|
||||||
.Get()
|
.Get()
|
||||||
.Include(x => x.Star)
|
.Include(x => x.Star)
|
||||||
|
.ThenInclude(x => x.Variables)
|
||||||
|
.Include(x => x.Variables)
|
||||||
.FirstOrDefaultAsync(x => x.Id == serverId);
|
.FirstOrDefaultAsync(x => x.Id == serverId);
|
||||||
|
|
||||||
if (server == null)
|
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(
|
var authorizeResult = await AuthorizeService.Authorize(
|
||||||
User, server,
|
User, server,
|
||||||
@@ -182,9 +197,9 @@ public class VariablesController : Controller
|
|||||||
|
|
||||||
if (!authorizeResult.Succeeded)
|
if (!authorizeResult.Succeeded)
|
||||||
{
|
{
|
||||||
throw new HttpApiException(
|
return Problem(
|
||||||
authorizeResult.Message ?? "No permission for the requested resource",
|
authorizeResult.Message ?? "No permission for the requested resource",
|
||||||
403
|
statusCode: 403
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public class OwnerAuthFilter : IServerAuthorizationFilter
|
|||||||
ServerPermissionLevel requiredLevel
|
ServerPermissionLevel requiredLevel
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var userIdValue = user.FindFirstValue("userId");
|
var userIdValue = user.FindFirstValue("UserId");
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(userIdValue)) // This is the case for api keys
|
if (string.IsNullOrEmpty(userIdValue)) // This is the case for api keys
|
||||||
return Task.FromResult<ServerAuthorizationResult?>(null);
|
return Task.FromResult<ServerAuthorizationResult?>(null);
|
||||||
|
|||||||
@@ -11,4 +11,8 @@ public static partial class AllocationMapper
|
|||||||
public static partial NodeAllocationResponse ToNodeAllocation(Allocation allocation);
|
public static partial NodeAllocationResponse ToNodeAllocation(Allocation allocation);
|
||||||
public static partial Allocation ToAllocation(CreateNodeAllocationRequest request);
|
public static partial Allocation ToAllocation(CreateNodeAllocationRequest request);
|
||||||
public static partial void Merge(UpdateNodeAllocationRequest request, Allocation allocation);
|
public static partial void Merge(UpdateNodeAllocationRequest request, Allocation allocation);
|
||||||
|
|
||||||
|
// EF Projections
|
||||||
|
|
||||||
|
public static partial IQueryable<NodeAllocationResponse> ProjectToAdminResponse(this IQueryable<Allocation> allocations);
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,11 @@ namespace MoonlightServers.ApiServer.Mappers;
|
|||||||
[Mapper(AllowNullPropertyAssignment = false)]
|
[Mapper(AllowNullPropertyAssignment = false)]
|
||||||
public static partial class DockerImageMapper
|
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 StarDockerImage ToDockerImage(CreateStarDockerImageRequest request);
|
||||||
public static partial void Merge(UpdateStarDockerImageRequest request, StarDockerImage variable);
|
public static partial void Merge(UpdateStarDockerImageRequest request, StarDockerImage variable);
|
||||||
|
|
||||||
|
// EF Migrations
|
||||||
|
|
||||||
|
public static partial IQueryable<StarDockerImageResponse> ProjectToAdminResponse(this IQueryable<StarDockerImage> dockerImages);
|
||||||
}
|
}
|
||||||
@@ -11,4 +11,8 @@ public static partial class NodeMapper
|
|||||||
public static partial NodeResponse ToAdminNodeResponse(Node node);
|
public static partial NodeResponse ToAdminNodeResponse(Node node);
|
||||||
public static partial Node ToNode(CreateNodeRequest request);
|
public static partial Node ToNode(CreateNodeRequest request);
|
||||||
public static partial void Merge(UpdateNodeRequest request, Node node);
|
public static partial void Merge(UpdateNodeRequest request, Node node);
|
||||||
|
|
||||||
|
// EF Projections
|
||||||
|
|
||||||
|
public static partial IQueryable<NodeResponse> ProjectToAdminResponse(this IQueryable<Node> nodes);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
using MoonlightServers.Shared.Http.Requests.Admin.Servers;
|
using MoonlightServers.Shared.Http.Requests.Admin.Servers;
|
||||||
using MoonlightServers.Shared.Http.Responses.Admin.Servers;
|
using MoonlightServers.Shared.Http.Responses.Admin.Servers;
|
||||||
|
using MoonlightServers.Shared.Http.Responses.Client.Servers;
|
||||||
using Riok.Mapperly.Abstractions;
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
namespace MoonlightServers.ApiServer.Mappers;
|
namespace MoonlightServers.ApiServer.Mappers;
|
||||||
@@ -20,6 +21,11 @@ public static partial class ServerMapper
|
|||||||
|
|
||||||
private static partial ServerResponse ToAdminServerResponse_Internal(Server server);
|
private static partial ServerResponse ToAdminServerResponse_Internal(Server server);
|
||||||
|
|
||||||
|
[MapperIgnoreSource(nameof(CreateServerRequest.Variables))]
|
||||||
public static partial Server ToServer(CreateServerRequest request);
|
public static partial Server ToServer(CreateServerRequest request);
|
||||||
public static partial void Merge(UpdateServerRequest request, Server server);
|
public static partial void Merge(UpdateServerRequest request, Server server);
|
||||||
|
|
||||||
|
// EF Projections
|
||||||
|
|
||||||
|
public static partial IQueryable<ServerResponse> ProjectToAdminResponse(this IQueryable<Server> servers);
|
||||||
}
|
}
|
||||||
@@ -8,4 +8,8 @@ namespace MoonlightServers.ApiServer.Mappers;
|
|||||||
public static partial class ServerVariableMapper
|
public static partial class ServerVariableMapper
|
||||||
{
|
{
|
||||||
public static partial ServerVariableResponse ToAdminResponse(ServerVariable serverVariable);
|
public static partial ServerVariableResponse ToAdminResponse(ServerVariable serverVariable);
|
||||||
|
|
||||||
|
// EF Projections
|
||||||
|
|
||||||
|
public static partial IQueryable<ServerVariableResponse> ProjectToAdminResponse(this IQueryable<ServerVariable> variables);
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,11 @@ namespace MoonlightServers.ApiServer.Mappers;
|
|||||||
[Mapper(AllowNullPropertyAssignment = false)]
|
[Mapper(AllowNullPropertyAssignment = false)]
|
||||||
public static partial class StarMapper
|
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 Star ToStar(CreateStarRequest request);
|
||||||
public static partial void Merge(UpdateStarRequest request, Star star);
|
public static partial void Merge(UpdateStarRequest request, Star star);
|
||||||
|
|
||||||
|
// EF Projections
|
||||||
|
|
||||||
|
public static partial IQueryable<StarResponse> ProjectToAdminResponse(this IQueryable<Star> stars);
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,11 @@ namespace MoonlightServers.ApiServer.Mappers;
|
|||||||
[Mapper(AllowNullPropertyAssignment = false)]
|
[Mapper(AllowNullPropertyAssignment = false)]
|
||||||
public static partial class StarVariableMapper
|
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 StarVariable ToStarVariable(CreateStarVariableRequest request);
|
||||||
public static partial void Merge(UpdateStarVariableRequest request, StarVariable variable);
|
public static partial void Merge(UpdateStarVariableRequest request, StarVariable variable);
|
||||||
|
|
||||||
|
// EF Projections
|
||||||
|
|
||||||
|
public static partial IQueryable<StarVariableResponse> ProjectToAdminResponse(this IQueryable<StarVariable> variables);
|
||||||
}
|
}
|
||||||
@@ -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<StorageSubSystem>();
|
|
||||||
|
|
||||||
var fileSystem = await storageSubSystem.GetFileSystem();
|
|
||||||
|
|
||||||
await fileSystem.Read(
|
|
||||||
path,
|
|
||||||
async dataStream =>
|
|
||||||
{
|
|
||||||
await Results.File(dataStream).ExecuteAsync(HttpContext);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ServerFileSystemResponse[]> 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<ServerFileSystem> 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<StorageSubSystem>();
|
|
||||||
|
|
||||||
return await storageSubSystem.GetFileSystem();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ServerStatusResponse> 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<ServerLogsResponse> 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<ServerStatsResponse> 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<StatsSubSystem>();
|
|
||||||
|
|
||||||
return Task.FromResult<ServerStatsResponse>(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<ServerStatsResponse>(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<StorageSubSystem>();
|
|
||||||
|
|
||||||
var fileSystem = await storageSubSystem.GetFileSystem();
|
|
||||||
|
|
||||||
var dataStream = file.OpenReadStream();
|
|
||||||
|
|
||||||
await fileSystem.CreateChunk(
|
|
||||||
path,
|
|
||||||
totalSize,
|
|
||||||
positionToSkipTo,
|
|
||||||
dataStream
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="Http\Controllers\Servers\" />
|
||||||
<Folder Include="Http\Middleware\" />
|
<Folder Include="Http\Middleware\" />
|
||||||
|
<Folder Include="ServerSystem\Docker\Components\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
|
|
||||||
|
|
||||||
public interface IConsole : IServerComponent
|
|
||||||
{
|
|
||||||
public IAsyncObservable<string> OnOutput { get; }
|
|
||||||
public IAsyncObservable<string> OnInput { get; }
|
|
||||||
|
|
||||||
public Task AttachToRuntime();
|
|
||||||
public Task AttachToInstallation();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Detaches any attached consoles. Usually either runtime or install is attached
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
|
|
||||||
|
|
||||||
public interface IInstaller : IServerComponent
|
|
||||||
{
|
|
||||||
public IAsyncObservable<object> OnExited { get; }
|
|
||||||
public bool IsRunning { get; }
|
|
||||||
|
|
||||||
public Task Setup();
|
|
||||||
public Task Start();
|
|
||||||
public Task Abort();
|
|
||||||
public Task Cleanup();
|
|
||||||
|
|
||||||
public Task<ServerCrash?> SearchForCrash();
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
|
|
||||||
|
|
||||||
public interface IOnlineDetection : IServerComponent
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
|
|
||||||
|
|
||||||
public interface IProvisioner : IServerComponent
|
|
||||||
{
|
|
||||||
public IAsyncObservable<object> OnExited { get; }
|
|
||||||
public bool IsProvisioned { get; }
|
|
||||||
|
|
||||||
public Task Provision();
|
|
||||||
public Task Start();
|
|
||||||
public Task Stop();
|
|
||||||
public Task Kill();
|
|
||||||
public Task Deprovision();
|
|
||||||
|
|
||||||
public Task<ServerCrash?> SearchForCrash();
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using MoonlightServers.Daemon.ServerSystem;
|
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
|
|
||||||
|
|
||||||
public interface IRestorer : IServerComponent
|
|
||||||
{
|
|
||||||
public Task<ServerState> Restore();
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
|
|
||||||
|
|
||||||
public interface IServerComponent : IAsyncDisposable
|
|
||||||
{
|
|
||||||
public Task Initialize();
|
|
||||||
public Task Sync();
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
|
|
||||||
|
|
||||||
public interface IStatistics : IServerComponent
|
|
||||||
{
|
|
||||||
public IAsyncObservable<ServerStats> OnStats { get; }
|
|
||||||
|
|
||||||
public Task SubscribeToRuntime();
|
|
||||||
public Task SubscribeToInstallation();
|
|
||||||
|
|
||||||
public ServerStats[] GetStats(int count);
|
|
||||||
}
|
|
||||||
@@ -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<ServerState, ServerTrigger> StateMachine { get; private set; }
|
|
||||||
public ServerContext Context { get; }
|
|
||||||
public IAsyncObservable<ServerState> OnState => OnStateSubject;
|
|
||||||
|
|
||||||
private readonly EventSubject<ServerState> OnStateSubject = new();
|
|
||||||
private readonly ILogger Logger;
|
|
||||||
private readonly RemoteService RemoteService;
|
|
||||||
private readonly ServerConfigurationMapper Mapper;
|
|
||||||
private readonly IHubContext<ServerWebSocketHub> 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<ServerWebSocketHub> 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<ServerState, ServerTrigger>(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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
|
|
||||||
|
|
||||||
public record ServerCrash();
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
|
|
||||||
|
|
||||||
public record ServerStats();
|
|
||||||
@@ -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<ServerState> 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;
|
|
||||||
}
|
|
||||||
@@ -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<string> OnOutput => OnOutputSubject;
|
|
||||||
public IAsyncObservable<string> OnInput => OnInputSubject;
|
|
||||||
|
|
||||||
private readonly EventSubject<string> OnOutputSubject = new();
|
|
||||||
private readonly EventSubject<string> OnInputSubject = new();
|
|
||||||
|
|
||||||
private readonly ConcurrentList<string> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<object> OnExited => OnExitedSubject;
|
|
||||||
public bool IsRunning { get; private set; } = false;
|
|
||||||
|
|
||||||
private readonly EventSubject<Message> 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<ServerCrash?> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<object> 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<object> 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<ServerCrash?> SearchForCrash()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
OnExitedSubject.Dispose();
|
|
||||||
|
|
||||||
if (ContainerEventSubscription != null)
|
|
||||||
await ContainerEventSubscription.DisposeAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ServerStats> OnStats => OnStatsSubject;
|
|
||||||
|
|
||||||
private readonly EventSubject<ServerStats> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ServerContext>();
|
|
||||||
|
|
||||||
context.Configuration = configuration;
|
|
||||||
context.ServiceScope = scope;
|
|
||||||
|
|
||||||
var server = scope.ServiceProvider.GetRequiredService<Server>();
|
|
||||||
|
|
||||||
context.Self = server;
|
|
||||||
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace MoonlightServers.Daemon.ServerSystem;
|
namespace MoonlightServers.Daemon.ServerSystem.Enums;
|
||||||
|
|
||||||
public enum ServerState
|
public enum ServerState
|
||||||
{
|
{
|
||||||
@@ -6,5 +6,6 @@ public enum ServerState
|
|||||||
Starting = 1,
|
Starting = 1,
|
||||||
Online = 2,
|
Online = 2,
|
||||||
Stopping = 3,
|
Stopping = 3,
|
||||||
Installing = 4
|
Installing = 4,
|
||||||
|
Locked = 5
|
||||||
}
|
}
|
||||||
12
MoonlightServers.Daemon/ServerSystem/Enums/ServerTrigger.cs
Normal file
12
MoonlightServers.Daemon/ServerSystem/Enums/ServerTrigger.cs
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -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<ServerState, ServerTrigger>.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;
|
||||||
|
}
|
||||||
@@ -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<ServerState, ServerTrigger>.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
72
MoonlightServers.Daemon/ServerSystem/Interfaces/IConsole.cs
Normal file
72
MoonlightServers.Daemon/ServerSystem/Interfaces/IConsole.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||||
|
|
||||||
|
public interface IConsole : IServerComponent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// <remarks>This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required</remarks>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The content to write</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task WriteStdInAsync(string content);
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// <remarks>This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required</remarks>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The content to write</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task WriteStdOutAsync(string content);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a system message to the standard output with the moonlight console prefix
|
||||||
|
/// <remarks>This method *does* add the newline separator at the end</remarks>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The content to write into the standard output</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task WriteMoonlightAsync(string content);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attaches the console to the runtime environment
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task AttachRuntimeAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attaches the console to the installation environment
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task AttachInstallationAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches all output from the runtime environment and write them into the cache without triggering any events
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task FetchRuntimeAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches all output from the installation environment and write them into the cache without triggering any events
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task FetchInstallationAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the cache of the standard output received by the environments
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task ClearCacheAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the content from the standard output cache
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The content from the cache</returns>
|
||||||
|
public Task<IEnumerable<string>> GetCacheAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribes to standard output receive events
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="callback">Callback which will be invoked whenever a new line is received</param>
|
||||||
|
/// <returns>Subscription disposable to unsubscribe from the event</returns>
|
||||||
|
public Task<IAsyncDisposable> SubscribeStdOutAsync(Func<string, Task> callback);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||||
|
|
||||||
|
public interface IFileSystem : IServerComponent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the path of the file system on the host operating system to be reused by other components
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Path to the file systems storage location</returns>
|
||||||
|
public Task<string> GetPathAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the file system exists
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if it does exist. False if it doesn't exist</returns>
|
||||||
|
public Task<bool> CheckExistsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the file system is mounted
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if its mounted, False if it is not mounted</returns>
|
||||||
|
public Task<bool> CheckMountedAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the file system. E.g. Creating a virtual disk, formatting it
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task CreateAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs checks and optimisations on the file system.
|
||||||
|
/// E.g. checking for corrupted files, resizing a virtual disk or adjusting file permissions
|
||||||
|
/// <remarks>Requires <see cref="MountAsync"/> to be called before or the file system to be in a mounted state</remarks>
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task PerformChecksAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mounts the file system
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task MountAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unmounts the file system
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task UnmountAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Destroys the file system and its contents
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task DestroyAsync();
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||||
|
|
||||||
|
public interface IInstallation : IServerComponent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the installation environment exists. It doesn't matter if it is currently running or not
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if it exists, False if it doesn't</returns>
|
||||||
|
public Task<bool> CheckExistsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the installation environment
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="runtimePath">The host path of the runtime storage location</param>
|
||||||
|
/// <param name="hostPath">The host path of the installation file system</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task CreateAsync(string runtimePath, string hostPath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the installation
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task StartAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kills the current installation immediately
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task KillAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the installation. E.g. removes the docker container
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task DestroyAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribes to the event when the installation exists
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="callback">The callback to invoke whenever the installation exists</param>
|
||||||
|
/// <returns>Subscription disposable to unsubscribe from the event</returns>
|
||||||
|
public Task<IAsyncDisposable> SubscribeExited(Func<int, Task> callback);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connects an existing installation to this abstraction in order to restore it.
|
||||||
|
/// E.g. fetching the container id and using it for exit events
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task RestoreAsync();
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||||
|
|
||||||
|
public interface IOnlineDetector : IServerComponent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the detection engine for the online state
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task CreateAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the detection of the online state based on the received output
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="line">The excerpt of the output</param>
|
||||||
|
/// <returns>True if the detection showed that the server is online. False if the detection didnt find anything</returns>
|
||||||
|
public Task<bool> HandleOutputAsync(string line);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Destroys the detection engine for the online state
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task DestroyAsync();
|
||||||
|
}
|
||||||
18
MoonlightServers.Daemon/ServerSystem/Interfaces/IReporter.cs
Normal file
18
MoonlightServers.Daemon/ServerSystem/Interfaces/IReporter.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||||
|
|
||||||
|
public interface IReporter : IServerComponent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Writes both in the server logs as well in the server console the provided message as a status update
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to write</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task StatusAsync(string message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes both in the server logs as well in the server console the provided message as an error
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to write</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task ErrorAsync(string message);
|
||||||
|
}
|
||||||
16
MoonlightServers.Daemon/ServerSystem/Interfaces/IRestorer.cs
Normal file
16
MoonlightServers.Daemon/ServerSystem/Interfaces/IRestorer.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||||
|
|
||||||
|
public interface IRestorer : IServerComponent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks for any running runtime environment from which the state can be restored from
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task<bool> HandleRuntimeAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks for any running installation environment from which the state can be restored from
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task<bool> HandleInstallationAsync();
|
||||||
|
}
|
||||||
55
MoonlightServers.Daemon/ServerSystem/Interfaces/IRuntime.cs
Normal file
55
MoonlightServers.Daemon/ServerSystem/Interfaces/IRuntime.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||||
|
|
||||||
|
public interface IRuntime : IServerComponent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the runtime does exist. This includes already running instances
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if it exists, False if it doesn't</returns>
|
||||||
|
public Task<bool> CheckExistsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the runtime with the specified path as the storage path where the server files should be stored in
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task CreateAsync(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the runtime. This requires <see cref="CreateAsync"/> to be called before this function
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task StartAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs a live update on the runtime. When this method is called the current server configuration has already been updated
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task UpdateAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kills the current runtime immediately
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task KillAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Destroys the runtime. When implemented using docker this would remove the container used for hosting the runtime
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task DestroyAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This subscribes to the exited event of the runtime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="callback">The callback gets invoked whenever the runtime exites</param>
|
||||||
|
/// <returns>Subscription disposable to unsubscribe from the event</returns>
|
||||||
|
public Task<IAsyncDisposable> SubscribeExited(Func<int, Task> callback);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connects an existing runtime to this abstraction in order to restore it.
|
||||||
|
/// E.g. fetching the container id and using it for exit events
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task RestoreAsync();
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||||
|
|
||||||
|
public interface IServerComponent : IAsyncDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the server component
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task InitializeAsync();
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using MoonlightServers.Daemon.ServerSystem.Enums;
|
||||||
|
using Stateless;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||||
|
|
||||||
|
public interface IServerStateHandler : IAsyncDisposable
|
||||||
|
{
|
||||||
|
public Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||||
|
|
||||||
|
public interface IStatistics : IServerComponent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attaches the statistics collector to the currently running runtime
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task AttachRuntimeAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attaches the statistics collector to the currently running installation
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task AttachInstallationAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the statistics cache
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task ClearCacheAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the statistics data from the cache
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>All data from the cache</returns>
|
||||||
|
public Task<IEnumerable<StatisticsData>> GetCacheAsync();
|
||||||
|
}
|
||||||
11
MoonlightServers.Daemon/ServerSystem/Models/ServerContext.cs
Normal file
11
MoonlightServers.Daemon/ServerSystem/Models/ServerContext.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace MoonlightServers.Daemon.ServerSystem.Models;
|
||||||
|
|
||||||
|
public class StatisticsData
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,57 +1,82 @@
|
|||||||
using Microsoft.AspNetCore.SignalR;
|
using MoonlightServers.Daemon.ServerSystem.Enums;
|
||||||
using MoonCore.Exceptions;
|
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||||
using MoonlightServers.Daemon.Http.Hubs;
|
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||||
using MoonlightServers.Daemon.Models.Cache;
|
|
||||||
using Stateless;
|
using Stateless;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.ServerSystem;
|
namespace MoonlightServers.Daemon.ServerSystem;
|
||||||
|
|
||||||
public class Server : IAsyncDisposable
|
public partial class Server : IAsyncDisposable
|
||||||
{
|
{
|
||||||
public ServerConfiguration Configuration { get; set; }
|
public int Identifier => InnerContext.Identifier;
|
||||||
public CancellationToken TaskCancellation => TaskCancellationSource.Token;
|
public ServerContext Context => InnerContext;
|
||||||
internal StateMachine<ServerState, ServerTrigger> StateMachine { get; private set; }
|
|
||||||
private CancellationTokenSource TaskCancellationSource;
|
|
||||||
|
|
||||||
private Dictionary<Type, ServerSubSystem> SubSystems = new();
|
public IConsole Console { get; }
|
||||||
private ServerState InternalState = ServerState.Offline;
|
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<ServerState, ServerTrigger> StateMachine { get; private set; }
|
||||||
|
|
||||||
private readonly IHubContext<ServerWebSocketHub> HubContext;
|
private readonly IServerStateHandler[] Handlers;
|
||||||
private readonly IServiceScope ServiceScope;
|
|
||||||
private readonly ILoggerFactory LoggerFactory;
|
private readonly IServerComponent[] AllComponents;
|
||||||
|
private readonly ServerContext InnerContext;
|
||||||
private readonly ILogger Logger;
|
private readonly ILogger Logger;
|
||||||
|
|
||||||
public Server(
|
public Server(
|
||||||
ServerConfiguration configuration,
|
ILogger logger,
|
||||||
IServiceScope serviceScope,
|
ServerContext context,
|
||||||
IHubContext<ServerWebSocketHub> hubContext
|
IConsole console,
|
||||||
|
IFileSystem runtimeFileSystem,
|
||||||
|
IFileSystem installationFileSystem,
|
||||||
|
IInstallation installation,
|
||||||
|
IOnlineDetector onlineDetector,
|
||||||
|
IReporter reporter,
|
||||||
|
IRestorer restorer,
|
||||||
|
IRuntime runtime,
|
||||||
|
IStatistics statistics,
|
||||||
|
IServerStateHandler[] handlers
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Configuration = configuration;
|
Logger = logger;
|
||||||
ServiceScope = serviceScope;
|
InnerContext = context;
|
||||||
HubContext = hubContext;
|
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<ILoggerFactory>();
|
Handlers = handlers;
|
||||||
Logger = LoggerFactory.CreateLogger($"Server {Configuration.Id}");
|
}
|
||||||
|
|
||||||
|
private void ConfigureStateMachine(ServerState initialState)
|
||||||
|
{
|
||||||
StateMachine = new StateMachine<ServerState, ServerTrigger>(
|
StateMachine = new StateMachine<ServerState, ServerTrigger>(
|
||||||
() => InternalState,
|
initialState, FiringMode.Queued
|
||||||
state => InternalState = state,
|
|
||||||
FiringMode.Queued
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Configure basic state machine flow
|
|
||||||
|
|
||||||
StateMachine.Configure(ServerState.Offline)
|
StateMachine.Configure(ServerState.Offline)
|
||||||
.Permit(ServerTrigger.Start, ServerState.Starting)
|
.Permit(ServerTrigger.Start, ServerState.Starting)
|
||||||
.Permit(ServerTrigger.Install, ServerState.Installing)
|
.Permit(ServerTrigger.Install, ServerState.Installing)
|
||||||
.PermitReentry(ServerTrigger.FailSafe);
|
.PermitReentry(ServerTrigger.Fail);
|
||||||
|
|
||||||
StateMachine.Configure(ServerState.Starting)
|
StateMachine.Configure(ServerState.Starting)
|
||||||
.Permit(ServerTrigger.OnlineDetected, ServerState.Online)
|
.Permit(ServerTrigger.DetectOnline, ServerState.Online)
|
||||||
.Permit(ServerTrigger.FailSafe, ServerState.Offline)
|
.Permit(ServerTrigger.Fail, ServerState.Offline)
|
||||||
.Permit(ServerTrigger.Exited, ServerState.Offline)
|
.Permit(ServerTrigger.Exited, ServerState.Offline)
|
||||||
.Permit(ServerTrigger.Stop, ServerState.Stopping)
|
.Permit(ServerTrigger.Stop, ServerState.Stopping)
|
||||||
.Permit(ServerTrigger.Kill, ServerState.Stopping);
|
.Permit(ServerTrigger.Kill, ServerState.Stopping);
|
||||||
@@ -62,128 +87,98 @@ public class Server : IAsyncDisposable
|
|||||||
.Permit(ServerTrigger.Exited, ServerState.Offline);
|
.Permit(ServerTrigger.Exited, ServerState.Offline);
|
||||||
|
|
||||||
StateMachine.Configure(ServerState.Stopping)
|
StateMachine.Configure(ServerState.Stopping)
|
||||||
.PermitReentry(ServerTrigger.FailSafe)
|
.PermitReentry(ServerTrigger.Fail)
|
||||||
.PermitReentry(ServerTrigger.Kill)
|
.PermitReentry(ServerTrigger.Kill)
|
||||||
.Permit(ServerTrigger.Exited, ServerState.Offline);
|
.Permit(ServerTrigger.Exited, ServerState.Offline);
|
||||||
|
|
||||||
StateMachine.Configure(ServerState.Installing)
|
StateMachine.Configure(ServerState.Installing)
|
||||||
.Permit(ServerTrigger.FailSafe, ServerState.Offline)
|
.Permit(ServerTrigger.Fail, ServerState.Offline) // TODO: Add kill
|
||||||
.Permit(ServerTrigger.Exited, ServerState.Offline);
|
.Permit(ServerTrigger.Exited, ServerState.Offline);
|
||||||
|
}
|
||||||
|
|
||||||
StateMachine.Configure(ServerState.Offline)
|
private void ConfigureStateMachineEvents()
|
||||||
.OnEntryAsync(async () =>
|
|
||||||
{
|
{
|
||||||
// Configure task reset when server goes offline
|
// Configure the calling of the handlers
|
||||||
|
|
||||||
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
|
|
||||||
StateMachine.OnTransitionedAsync(async transition =>
|
StateMachine.OnTransitionedAsync(async transition =>
|
||||||
{
|
{
|
||||||
await HubContext.Clients
|
var hasFailed = false;
|
||||||
.Group(Configuration.Id.ToString())
|
|
||||||
.SendAsync("StateChanged", transition.Destination.ToString());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Initialize(Type[] subSystemTypes)
|
foreach (var handler in Handlers)
|
||||||
{
|
|
||||||
foreach (var type in subSystemTypes)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var type in SubSystems.Keys)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await SubSystems[type].Initialize();
|
await handler.ExecuteAsync(transition);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.LogError("An unhandled error occured while initializing sub system {name}: {e}", type.Name, e);
|
Logger.LogError(
|
||||||
}
|
e,
|
||||||
|
"Handler {name} has thrown an unexpected exception",
|
||||||
|
handler.GetType().FullName
|
||||||
|
);
|
||||||
|
|
||||||
|
hasFailed = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Trigger(ServerTrigger trigger)
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleSaveAsync(Func<Task> callback)
|
||||||
{
|
{
|
||||||
if (!StateMachine.CanFire(trigger))
|
try
|
||||||
throw new HttpApiException($"The trigger {trigger} is not supported during the state {StateMachine.State}", 400);
|
{
|
||||||
|
await callback.Invoke();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "An error occured while handling");
|
||||||
|
|
||||||
await StateMachine.FireAsync(trigger);
|
await StateMachine.FireAsync(ServerTrigger.Fail);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Delete()
|
private async Task HandleIgnoredAsync(Func<Task> callback)
|
||||||
{
|
{
|
||||||
foreach (var subSystem in SubSystems.Values)
|
try
|
||||||
await subSystem.Delete();
|
{
|
||||||
|
await callback.Invoke();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e, "An error occured while handling");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method completely bypasses the state machine.
|
public async Task InitializeAsync()
|
||||||
// Using this method without any checks will lead to
|
|
||||||
// broken server states. Use with caution
|
|
||||||
public void OverrideState(ServerState state)
|
|
||||||
{
|
{
|
||||||
InternalState = state;
|
foreach (var component in AllComponents)
|
||||||
}
|
await component.InitializeAsync();
|
||||||
|
|
||||||
public T? GetSubSystem<T>() where T : ServerSubSystem
|
var restoredState = ServerState.Offline;
|
||||||
{
|
|
||||||
var type = typeof(T);
|
|
||||||
var subSystem = SubSystems.GetValueOrDefault(type);
|
|
||||||
|
|
||||||
if (subSystem == null)
|
ConfigureStateMachine(restoredState);
|
||||||
return null;
|
ConfigureStateMachineEvents();
|
||||||
|
|
||||||
return subSystem as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
public T GetRequiredSubSystem<T>() where T : ServerSubSystem
|
|
||||||
{
|
|
||||||
var subSystem = GetSubSystem<T>();
|
|
||||||
|
|
||||||
if (subSystem == null)
|
|
||||||
throw new AggregateException("Unable to resolve requested sub system");
|
|
||||||
|
|
||||||
return subSystem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (!TaskCancellationSource.IsCancellationRequested)
|
foreach (var handler in Handlers)
|
||||||
await TaskCancellationSource.CancelAsync();
|
await handler.DisposeAsync();
|
||||||
|
|
||||||
foreach (var subSystem in SubSystems.Values)
|
foreach (var component in AllComponents)
|
||||||
await subSystem.DisposeAsync();
|
await component.DisposeAsync();
|
||||||
|
|
||||||
ServiceScope.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
66
MoonlightServers.Daemon/ServerSystem/ServerFactory.cs
Normal file
66
MoonlightServers.Daemon/ServerSystem/ServerFactory.cs
Normal file
@@ -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<Server> Create(ServerConfiguration configuration)
|
||||||
|
{
|
||||||
|
var scope = ServiceProvider.CreateAsyncScope();
|
||||||
|
|
||||||
|
var loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||||
|
var logger = loggerFactory.CreateLogger($"Servers.Instance.{configuration.Id}.{nameof(Server)}");
|
||||||
|
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<ServerContext>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ServerState, ServerTrigger> 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;
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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<string, Task>? OnOutput;
|
|
||||||
public event Func<string, Task>? OnInput;
|
|
||||||
|
|
||||||
private MultiplexedStream? Stream;
|
|
||||||
private readonly List<string> OutputCache = new();
|
|
||||||
|
|
||||||
private readonly IHubContext<ServerWebSocketHub> HubContext;
|
|
||||||
private readonly DockerClient DockerClient;
|
|
||||||
|
|
||||||
public ConsoleSubSystem(
|
|
||||||
Server server,
|
|
||||||
ILogger logger,
|
|
||||||
IHubContext<ServerWebSocketHub> 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<string[]> RetrieveCache()
|
|
||||||
{
|
|
||||||
string[] result;
|
|
||||||
|
|
||||||
lock (OutputCache)
|
|
||||||
result = OutputCache.ToArray();
|
|
||||||
|
|
||||||
return Task.FromResult(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ConsoleSubSystem>();
|
|
||||||
|
|
||||||
// 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<StorageSubSystem>();
|
|
||||||
|
|
||||||
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<ConsoleSubSystem>();
|
|
||||||
|
|
||||||
// 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<StorageSubSystem>();
|
|
||||||
|
|
||||||
Logger.LogDebug("Removing installation data");
|
|
||||||
await consoleSubSystem.WriteMoonlight("Removing installation data");
|
|
||||||
|
|
||||||
await storageSubSystem.DeleteInstallVolume();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -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>();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ConsoleSubSystem>();
|
|
||||||
|
|
||||||
// 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<StorageSubSystem>();
|
|
||||||
|
|
||||||
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<StatsSubSystem>();
|
|
||||||
|
|
||||||
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<ServerState, ServerTrigger>.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<ConsoleSubSystem>();
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -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<ProvisionSubSystem>();
|
|
||||||
|
|
||||||
// Override values
|
|
||||||
provisionSubSystem.CurrentContainerId = runtimeContainer.ID;
|
|
||||||
Server.OverrideState(ServerState.Online);
|
|
||||||
|
|
||||||
// Update and attach console
|
|
||||||
|
|
||||||
var consoleSubSystem = Server.GetRequiredSubSystem<ConsoleSubSystem>();
|
|
||||||
|
|
||||||
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<StatsSubSystem>();
|
|
||||||
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<InstallationSubSystem>();
|
|
||||||
|
|
||||||
// Override values
|
|
||||||
installationSubSystem.CurrentContainerId = installContainer.ID;
|
|
||||||
Server.OverrideState(ServerState.Installing);
|
|
||||||
|
|
||||||
var consoleSubSystem = Server.GetRequiredSubSystem<ConsoleSubSystem>();
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ProvisionSubSystem>();
|
|
||||||
|
|
||||||
// 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<ConsoleSubSystem>();
|
|
||||||
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<ProvisionSubSystem>();
|
|
||||||
|
|
||||||
await DockerClient.Containers.KillContainerAsync(
|
|
||||||
provisionSubSystem.CurrentContainerId,
|
|
||||||
new()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ServerWebSocketHub> HubContext;
|
|
||||||
|
|
||||||
public StatsSubSystem(
|
|
||||||
Server server,
|
|
||||||
ILogger logger,
|
|
||||||
DockerClient dockerClient,
|
|
||||||
IHubContext<ServerWebSocketHub> 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<ContainerStatsResponse>(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ConsoleSubSystem>();
|
|
||||||
|
|
||||||
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<ServerFileSystem> 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<bool> 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<bool> 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<int> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ServerService> Logger;
|
|
||||||
private readonly ServerFactory ServerFactory;
|
|
||||||
private readonly RemoteService RemoteService;
|
|
||||||
private readonly ServerConfigurationMapper Mapper;
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<int, Server> Servers = new();
|
|
||||||
|
|
||||||
public NewServerService(
|
|
||||||
ILogger<ServerService> logger,
|
|
||||||
ServerFactory serverFactory,
|
|
||||||
RemoteService remoteService,
|
|
||||||
ServerConfigurationMapper mapper
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Logger = logger;
|
|
||||||
ServerFactory = serverFactory;
|
|
||||||
RemoteService = remoteService;
|
|
||||||
Mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InitializeAllFromPanel()
|
|
||||||
{
|
|
||||||
var servers = await PagedData<ServerDataResponse>.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<Server> 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
|
|
||||||
}
|
|
||||||
@@ -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<int, Server> Servers = new();
|
|
||||||
|
|
||||||
private readonly RemoteService RemoteService;
|
|
||||||
private readonly DockerClient DockerClient;
|
|
||||||
private readonly IServiceProvider ServiceProvider;
|
|
||||||
private readonly CancellationTokenSource TaskCancellation;
|
|
||||||
private readonly ILogger<ServerService> Logger;
|
|
||||||
private readonly IHubContext<ServerWebSocketHub> HubContext;
|
|
||||||
|
|
||||||
public ServerService(
|
|
||||||
RemoteService remoteService,
|
|
||||||
IServiceProvider serviceProvider,
|
|
||||||
DockerClient dockerClient,
|
|
||||||
ILogger<ServerService> logger,
|
|
||||||
IHubContext<ServerWebSocketHub> 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<int>();
|
|
||||||
|
|
||||||
for (var i = 0; i < pages; i++)
|
|
||||||
batchesLeft.Enqueue(i);
|
|
||||||
|
|
||||||
var tasksCount = pages > 5 ? 5 : pages;
|
|
||||||
var tasks = new List<Task>();
|
|
||||||
|
|
||||||
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<int> 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<Message>(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<ProvisionSubSystem>();
|
|
||||||
|
|
||||||
if (provisionSubSystem.CurrentContainerId == message.ID)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
var installationSubSystem = serverToCheck.GetRequiredSubSystem<InstallationSubSystem>();
|
|
||||||
|
|
||||||
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<string, IDictionary<string, bool>>()
|
|
||||||
{
|
|
||||||
{
|
|
||||||
"label",
|
|
||||||
new Dictionary<string, bool>()
|
|
||||||
{
|
|
||||||
{
|
|
||||||
"Software=Moonlight-Panel",
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,16 @@
|
|||||||
using System.Reactive.Concurrency;
|
|
||||||
using System.Reactive.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Docker.DotNet;
|
using Docker.DotNet;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Http.Connections;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using MoonCore.EnvConfiguration;
|
using MoonCore.EnvConfiguration;
|
||||||
using MoonCore.Extended.Extensions;
|
using MoonCore.Extended.Extensions;
|
||||||
using MoonCore.Extensions;
|
using MoonCore.Extensions;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonCore.Logging;
|
using MoonCore.Logging;
|
||||||
using MoonCore.Observability;
|
|
||||||
using MoonlightServers.Daemon.Configuration;
|
using MoonlightServers.Daemon.Configuration;
|
||||||
using MoonlightServers.Daemon.Extensions;
|
|
||||||
using MoonlightServers.Daemon.Helpers;
|
using MoonlightServers.Daemon.Helpers;
|
||||||
using MoonlightServers.Daemon.Http.Hubs;
|
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;
|
namespace MoonlightServers.Daemon;
|
||||||
|
|
||||||
@@ -73,79 +60,6 @@ public class Startup
|
|||||||
await MapBase();
|
await MapBase();
|
||||||
await MapHubs();
|
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<ServerFactory>();
|
|
||||||
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();
|
await WebApplication.RunAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,28 +245,6 @@ public class Startup
|
|||||||
|
|
||||||
private Task RegisterServers()
|
private Task RegisterServers()
|
||||||
{
|
{
|
||||||
WebApplicationBuilder.Services.AddSingleton<NewServerService>();
|
|
||||||
WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<NewServerService>());
|
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddSingleton<DockerEventService>();
|
|
||||||
WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<DockerEventService>());
|
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddSingleton<ServerConfigurationMapper>();
|
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddSingleton<ServerFactory>();
|
|
||||||
|
|
||||||
// Server scoped stuff
|
|
||||||
WebApplicationBuilder.Services.AddScoped<IConsole, DockerConsole>();
|
|
||||||
WebApplicationBuilder.Services.AddScoped<IFileSystem, RawFileSystem>();
|
|
||||||
WebApplicationBuilder.Services.AddScoped<IRestorer, DefaultRestorer>();
|
|
||||||
WebApplicationBuilder.Services.AddScoped<IInstaller, DockerInstaller>();
|
|
||||||
WebApplicationBuilder.Services.AddScoped<IProvisioner, DockerProvisioner>();
|
|
||||||
WebApplicationBuilder.Services.AddScoped<IStatistics, DockerStatistics>();
|
|
||||||
WebApplicationBuilder.Services.AddScoped<IOnlineDetection, RegexOnlineDetection>();
|
|
||||||
WebApplicationBuilder.Services.AddScoped<ServerContext>();
|
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddScoped<ServerSys.Abstractions.Server>();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<div class="sm:col-span-2">
|
<div class="sm:col-span-2">
|
||||||
<label class="block text-sm font-medium leading-6 text-base-content">Star</label>
|
<label class="block text-sm font-medium leading-6 text-base-content">Star</label>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<InputItem TItem="StarDetailResponse"
|
<InputItem TItem="StarResponse"
|
||||||
DisplayField="@(x => x.Name)"
|
DisplayField="@(x => x.Name)"
|
||||||
SearchField="@(x => x.Name)"
|
SearchField="@(x => x.Name)"
|
||||||
ItemSource="LoadStars"
|
ItemSource="LoadStars"
|
||||||
@@ -91,10 +91,10 @@
|
|||||||
[Parameter] public CreateServerRequest Request { get; set; }
|
[Parameter] public CreateServerRequest Request { get; set; }
|
||||||
[Parameter] public Create Parent { get; set; }
|
[Parameter] public Create Parent { get; set; }
|
||||||
|
|
||||||
private async Task<StarDetailResponse[]> LoadStars()
|
private async Task<StarResponse[]> LoadStars()
|
||||||
{
|
{
|
||||||
return await PagedData<StarDetailResponse>.All(async (page, pageSize) =>
|
return await PagedData<StarResponse>.All(async (page, pageSize) =>
|
||||||
await ApiClient.GetJson<PagedData<StarDetailResponse>>(
|
await ApiClient.GetJson<PagedData<StarResponse>>(
|
||||||
$"api/admin/servers/stars?page={page}&pageSize={pageSize}"
|
$"api/admin/servers/stars?page={page}&pageSize={pageSize}"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
[Parameter] public CreateServerRequest Request { get; set; }
|
[Parameter] public CreateServerRequest Request { get; set; }
|
||||||
[Parameter] public Create Parent { get; set; }
|
[Parameter] public Create Parent { get; set; }
|
||||||
|
|
||||||
private StarVariableDetailResponse[] StarVariables;
|
private StarVariableResponse[] StarVariables;
|
||||||
|
|
||||||
private async Task Load(LazyLoader _)
|
private async Task Load(LazyLoader _)
|
||||||
{
|
{
|
||||||
@@ -53,14 +53,14 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StarVariables = await PagedData<StarVariableDetailResponse>.All(async (page, pageSize) =>
|
StarVariables = await PagedData<StarVariableResponse>.All(async (page, pageSize) =>
|
||||||
await ApiClient.GetJson<PagedData<StarVariableDetailResponse>>(
|
await ApiClient.GetJson<PagedData<StarVariableResponse>>(
|
||||||
$"api/admin/servers/stars/{Parent.Star.Id}/variables?page={page}&pageSize={pageSize}"
|
$"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() ?? "";
|
var value = args.Value?.ToString() ?? "";
|
||||||
|
|
||||||
|
|||||||
@@ -45,13 +45,13 @@
|
|||||||
[Parameter] public UpdateServerRequest Request { get; set; }
|
[Parameter] public UpdateServerRequest Request { get; set; }
|
||||||
[Parameter] public ServerResponse Server { get; set; }
|
[Parameter] public ServerResponse Server { get; set; }
|
||||||
|
|
||||||
private StarVariableDetailResponse[] StarVariables;
|
private StarVariableResponse[] StarVariables;
|
||||||
private ServerVariableResponse[] ServerVariables;
|
private ServerVariableResponse[] ServerVariables;
|
||||||
|
|
||||||
private async Task Load(LazyLoader _)
|
private async Task Load(LazyLoader _)
|
||||||
{
|
{
|
||||||
StarVariables = await PagedData<StarVariableDetailResponse>.All(async (page, pageSize) =>
|
StarVariables = await PagedData<StarVariableResponse>.All(async (page, pageSize) =>
|
||||||
await ApiClient.GetJson<PagedData<StarVariableDetailResponse>>(
|
await ApiClient.GetJson<PagedData<StarVariableResponse>>(
|
||||||
$"api/admin/servers/stars/{Server.StarId}/variables?page={page}&pageSize={pageSize}"
|
$"api/admin/servers/stars/{Server.StarId}/variables?page={page}&pageSize={pageSize}"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[Parameter] public Func<UpdateStarDockerImageRequest, Task> OnSubmit { get; set; }
|
[Parameter] public Func<UpdateStarDockerImageRequest, Task> OnSubmit { get; set; }
|
||||||
[Parameter] public StarDockerImageDetailResponse DockerImage { get; set; }
|
[Parameter] public StarDockerImageResponse DockerImage { get; set; }
|
||||||
|
|
||||||
private UpdateStarDockerImageRequest Form;
|
private UpdateStarDockerImageRequest Form;
|
||||||
private HandleForm HandleForm;
|
private HandleForm HandleForm;
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[Parameter] public Func<UpdateStarVariableRequest, Task> OnSubmit { get; set; }
|
[Parameter] public Func<UpdateStarVariableRequest, Task> OnSubmit { get; set; }
|
||||||
[Parameter] public StarVariableDetailResponse Variable { get; set; }
|
[Parameter] public StarVariableResponse Variable { get; set; }
|
||||||
|
|
||||||
private UpdateStarVariableRequest Form;
|
private UpdateStarVariableRequest Form;
|
||||||
private HandleForm HandleForm;
|
private HandleForm HandleForm;
|
||||||
|
|||||||
@@ -46,14 +46,14 @@
|
|||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[Parameter] public StarDetailResponse Star { get; set; }
|
[Parameter] public StarResponse Star { get; set; }
|
||||||
|
|
||||||
private StarDockerImageDetailResponse[] DockerImages;
|
private StarDockerImageResponse[] DockerImages;
|
||||||
private LazyLoader LazyLoader;
|
private LazyLoader LazyLoader;
|
||||||
|
|
||||||
private async Task Load(LazyLoader _)
|
private async Task Load(LazyLoader _)
|
||||||
{
|
{
|
||||||
var pagedVariables = await ApiClient.GetJson<PagedData<StarDockerImageDetailResponse>>(
|
var pagedVariables = await ApiClient.GetJson<PagedData<StarDockerImageResponse>>(
|
||||||
$"api/admin/servers/stars/{Star.Id}/dockerImages?page=0&pageSize=50"
|
$"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<UpdateStarDockerImageRequest, Task> onSubmit = async request =>
|
Func<UpdateStarDockerImageRequest, Task> onSubmit = async request =>
|
||||||
{
|
{
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeleteDockerImage(StarDockerImageDetailResponse dockerImage)
|
private async Task DeleteDockerImage(StarDockerImageResponse dockerImage)
|
||||||
{
|
{
|
||||||
await AlertService.ConfirmDanger(
|
await AlertService.ConfirmDanger(
|
||||||
"Delete docker image",
|
"Delete docker image",
|
||||||
|
|||||||
@@ -49,13 +49,13 @@
|
|||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[Parameter] public UpdateStarRequest Request { get; set; }
|
[Parameter] public UpdateStarRequest Request { get; set; }
|
||||||
[Parameter] public StarDetailResponse Star { get; set; }
|
[Parameter] public StarResponse Star { get; set; }
|
||||||
|
|
||||||
private List<StarDockerImageDetailResponse> DockerImages;
|
private List<StarDockerImageResponse> DockerImages;
|
||||||
|
|
||||||
private async Task Load(LazyLoader _)
|
private async Task Load(LazyLoader _)
|
||||||
{
|
{
|
||||||
var pagedVariables = await ApiClient.GetJson<PagedData<StarDockerImageDetailResponse>>(
|
var pagedVariables = await ApiClient.GetJson<PagedData<StarDockerImageResponse>>(
|
||||||
$"api/admin/servers/stars/{Star.Id}/dockerImages?page=0&pageSize=50"
|
$"api/admin/servers/stars/{Star.Id}/dockerImages?page=0&pageSize=50"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -46,14 +46,14 @@
|
|||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[Parameter] public StarDetailResponse Star { get; set; }
|
[Parameter] public StarResponse Star { get; set; }
|
||||||
|
|
||||||
private StarVariableDetailResponse[] CurrentVariables;
|
private StarVariableResponse[] CurrentVariables;
|
||||||
private LazyLoader LazyLoader;
|
private LazyLoader LazyLoader;
|
||||||
|
|
||||||
private async Task Load(LazyLoader arg)
|
private async Task Load(LazyLoader arg)
|
||||||
{
|
{
|
||||||
var pagedVariables = await ApiClient.GetJson<PagedData<StarVariableDetailResponse>>(
|
var pagedVariables = await ApiClient.GetJson<PagedData<StarVariableResponse>>(
|
||||||
$"api/admin/servers/stars/{Star.Id}/variables?page=0&pageSize=50"
|
$"api/admin/servers/stars/{Star.Id}/variables?page=0&pageSize=50"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
}, "max-w-xl");
|
}, "max-w-xl");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateVariable(StarVariableDetailResponse variable)
|
private async Task UpdateVariable(StarVariableResponse variable)
|
||||||
{
|
{
|
||||||
Func<UpdateStarVariableRequest, Task> onSubmit = async request =>
|
Func<UpdateStarVariableRequest, Task> onSubmit = async request =>
|
||||||
{
|
{
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
}, "max-w-xl");
|
}, "max-w-xl");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeleteVariable(StarVariableDetailResponse variable)
|
private async Task DeleteVariable(StarVariableResponse variable)
|
||||||
{
|
{
|
||||||
await AlertService.ConfirmDanger(
|
await AlertService.ConfirmDanger(
|
||||||
"Delete variable",
|
"Delete variable",
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
public List<NodeAllocationResponse> Allocations = new();
|
public List<NodeAllocationResponse> Allocations = new();
|
||||||
public UserResponse? Owner;
|
public UserResponse? Owner;
|
||||||
public StarDetailResponse? Star;
|
public StarResponse? Star;
|
||||||
public NodeResponse? Node;
|
public NodeResponse? Node;
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
{
|
{
|
||||||
private DataTable<ServerResponse> Table;
|
private DataTable<ServerResponse> Table;
|
||||||
|
|
||||||
private List<StarDetailResponse> Stars = new();
|
private List<StarResponse> Stars = new();
|
||||||
private List<NodeResponse> Nodes = new();
|
private List<NodeResponse> Nodes = new();
|
||||||
|
|
||||||
private async Task<IPagedData<ServerResponse>> LoadData(PaginationOptions options)
|
private async Task<IPagedData<ServerResponse>> LoadData(PaginationOptions options)
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
|
|
||||||
if (Stars.All(x => x.Id != item.StarId))
|
if (Stars.All(x => x.Id != item.StarId))
|
||||||
{
|
{
|
||||||
var star = await ApiClient.GetJson<StarDetailResponse>($"api/admin/servers/stars/{item.StarId}");
|
var star = await ApiClient.GetJson<StarResponse>($"api/admin/servers/stars/{item.StarId}");
|
||||||
Stars.Add(star);
|
Stars.Add(star);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,21 +35,21 @@
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable @ref="Table" TItem="StarDetailResponse">
|
<DataTable @ref="Table" TItem="StarResponse">
|
||||||
<Configuration>
|
<Configuration>
|
||||||
<Pagination TItem="StarDetailResponse" ItemSource="LoadData" />
|
<Pagination TItem="StarResponse" ItemSource="LoadData" />
|
||||||
|
|
||||||
<DataTableColumn TItem="StarDetailResponse" Field="@(x => x.Id)" Name="Id"/>
|
<DataTableColumn TItem="StarResponse" Field="@(x => x.Id)" Name="Id"/>
|
||||||
<DataTableColumn TItem="StarDetailResponse" Field="@(x => x.Name)" Name="Name">
|
<DataTableColumn TItem="StarResponse" Field="@(x => x.Name)" Name="Name">
|
||||||
<ColumnTemplate>
|
<ColumnTemplate>
|
||||||
<a class="text-primary" href="/admin/servers/stars/update/@(context.Id)">
|
<a class="text-primary" href="/admin/servers/stars/update/@(context.Id)">
|
||||||
@context.Name
|
@context.Name
|
||||||
</a>
|
</a>
|
||||||
</ColumnTemplate>
|
</ColumnTemplate>
|
||||||
</DataTableColumn>
|
</DataTableColumn>
|
||||||
<DataTableColumn TItem="StarDetailResponse" Field="@(x => x.Version)" Name="Version"/>
|
<DataTableColumn TItem="StarResponse" Field="@(x => x.Version)" Name="Version"/>
|
||||||
<DataTableColumn TItem="StarDetailResponse" Field="@(x => x.Author)" Name="Author"/>
|
<DataTableColumn TItem="StarResponse" Field="@(x => x.Author)" Name="Author"/>
|
||||||
<DataTableColumn TItem="StarDetailResponse">
|
<DataTableColumn TItem="StarResponse">
|
||||||
<ColumnTemplate>
|
<ColumnTemplate>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
@if (!string.IsNullOrEmpty(context.DonateUrl))
|
@if (!string.IsNullOrEmpty(context.DonateUrl))
|
||||||
@@ -89,19 +89,19 @@
|
|||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
private DataTable<StarDetailResponse> Table;
|
private DataTable<StarResponse> Table;
|
||||||
|
|
||||||
private async Task<IPagedData<StarDetailResponse>> LoadData(PaginationOptions options)
|
private async Task<IPagedData<StarResponse>> LoadData(PaginationOptions options)
|
||||||
=> await ApiClient.GetJson<PagedData<StarDetailResponse>>($"api/admin/servers/stars?page={options.Page}&pageSize={options.PerPage}");
|
=> await ApiClient.GetJson<PagedData<StarResponse>>($"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(
|
await AlertService.ConfirmDanger(
|
||||||
"Star deletion",
|
"Star deletion",
|
||||||
$"Do you really want to delete the star '{detailResponse.Name}'",
|
$"Do you really want to delete the star '{response.Name}'",
|
||||||
async () =>
|
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 ToastService.Success("Successfully deleted star");
|
||||||
|
|
||||||
await Table.Refresh();
|
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");
|
var json = await ApiClient.GetString($"api/admin/servers/stars/{star.Id}/export");
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
var content = new MultipartFormDataContent();
|
var content = new MultipartFormDataContent();
|
||||||
content.Add(new StreamContent(stream), "file", file.Name);
|
content.Add(new StreamContent(stream), "file", file.Name);
|
||||||
|
|
||||||
var star = await ApiClient.PostJson<StarDetailResponse>("api/admin/servers/stars/import", content);
|
var star = await ApiClient.PostJson<StarResponse>("api/admin/servers/stars/import", content);
|
||||||
|
|
||||||
await ToastService.Success($"Successfully imported '{star.Name}'");
|
await ToastService.Success($"Successfully imported '{star.Name}'");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,11 +68,11 @@
|
|||||||
|
|
||||||
private HandleForm Form;
|
private HandleForm Form;
|
||||||
private UpdateStarRequest Request;
|
private UpdateStarRequest Request;
|
||||||
private StarDetailResponse Detail;
|
private StarResponse Detail;
|
||||||
|
|
||||||
private async Task Load(LazyLoader _)
|
private async Task Load(LazyLoader _)
|
||||||
{
|
{
|
||||||
Detail = await ApiClient.GetJson<StarDetailResponse>($"api/admin/servers/stars/{Id}");
|
Detail = await ApiClient.GetJson<StarResponse>($"api/admin/servers/stars/{Id}");
|
||||||
Request = new()
|
Request = new()
|
||||||
{
|
{
|
||||||
Name = Detail.Name,
|
Name = Detail.Name,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace MoonlightServers.Shared.Http.Responses.Admin.StarDockerImages;
|
namespace MoonlightServers.Shared.Http.Responses.Admin.StarDockerImages;
|
||||||
|
|
||||||
public class StarDockerImageDetailResponse
|
public class StarDockerImageResponse
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@ using MoonlightServers.Shared.Enums;
|
|||||||
|
|
||||||
namespace MoonlightServers.Shared.Http.Responses.Admin.StarVariables;
|
namespace MoonlightServers.Shared.Http.Responses.Admin.StarVariables;
|
||||||
|
|
||||||
public class StarVariableDetailResponse
|
public class StarVariableResponse
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ using MoonlightServers.Shared.Http.Responses.Admin.StarVariables;
|
|||||||
|
|
||||||
namespace MoonlightServers.Shared.Http.Responses.Admin.Stars;
|
namespace MoonlightServers.Shared.Http.Responses.Admin.Stars;
|
||||||
|
|
||||||
public class StarDetailResponse
|
public class StarResponse
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
Reference in New Issue
Block a user