Started implementing server service and daemon controllers

This commit is contained in:
2025-09-15 21:47:07 +02:00
parent 32f447d268
commit 91fb15a03e
11 changed files with 318 additions and 8 deletions

View File

@@ -92,8 +92,9 @@ public class ServersController : Controller
.Include(x => x.Variables)
.Include(x => x.Star)
.AsNoTracking()
.Where(x => x.Id == id)
.ProjectToAdminResponse()
.FirstOrDefaultAsync(x => x.Id == id);
.FirstOrDefaultAsync();
if (server == null)
return Problem("No server with that id found", statusCode: 404);

View File

@@ -0,0 +1,77 @@
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[ApiController]
[Route("api/servers/{id:int}")]
public class PowerController : Controller
{
private readonly ServerService ServerService;
public PowerController(ServerService serverService)
{
ServerService = serverService;
}
[HttpPost("start")]
public async Task<ActionResult> Start([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Start))
return Problem("Cannot fire start trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Start);
return NoContent();
}
[HttpPost("stop")]
public async Task<ActionResult> Stop([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Stop))
return Problem("Cannot fire stop trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Stop);
return NoContent();
}
[HttpPost("kill")]
public async Task<ActionResult> Kill([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Kill))
return Problem("Cannot fire kill trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Kill);
return NoContent();
}
[HttpPost("install")]
public async Task<ActionResult> Install([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
if (!server.StateMachine.CanFire(ServerTrigger.Install))
return Problem("Cannot fire install trigger in this state");
await server.StateMachine.FireAsync(ServerTrigger.Install);
return NoContent();
}
}

View File

@@ -0,0 +1,68 @@
using Microsoft.AspNetCore.Mvc;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
using MoonlightServers.DaemonShared.Enums;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[ApiController]
[Route("api/servers/{id:int}")]
public class ServersController : Controller
{
private readonly ServerService ServerService;
private readonly ServerConfigurationMapper ConfigurationMapper;
public ServersController(ServerService serverService, ServerConfigurationMapper configurationMapper)
{
ServerService = serverService;
ConfigurationMapper = configurationMapper;
}
[HttpPost("sync")]
public async Task<ActionResult> Sync([FromRoute] int id)
{
await ServerService.InitializeById(id);
return NoContent();
}
[HttpGet("status")]
public async Task<ActionResult<ServerStatusResponse>> Status([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
return new ServerStatusResponse()
{
State = (ServerState)server.StateMachine.State
};
}
[HttpGet("logs")]
public async Task<ActionResult<ServerLogsResponse>> Logs([FromRoute] int id)
{
var server = ServerService.GetById(id);
if (server == null)
return Problem("No server with this id found", statusCode: 404);
var messages = await server.Console.GetCacheAsync();
return new ServerLogsResponse()
{
Messages = messages.ToArray()
};
}
[HttpGet("stats")]
public async Task<ServerStatsResponse> GetStats([FromRoute] int id)
{
return new ServerStatsResponse()
{
};
}
}

View File

@@ -35,7 +35,7 @@ public class ServerConfigurationMapper
Cpu = response.Cpu,
Disk = response.Disk,
Memory = response.Memory,
StopCommand = response.StopCommand
StopCommand = response.StopCommand,
};
}

View File

@@ -17,7 +17,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Http\Controllers\Servers\" />
<Folder Include="Http\Middleware\" />
</ItemGroup>

View File

@@ -0,0 +1,39 @@
using Microsoft.AspNetCore.SignalR;
using MoonlightServers.Daemon.Http.Hubs;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
namespace MoonlightServers.Daemon.ServerSystem.Implementations;
public class ConsoleSignalRComponent : IServerComponent
{
private readonly IHubContext<ServerWebSocketHub> Hub;
private readonly ServerContext Context;
private IAsyncDisposable? StdOutSubscription;
private string HubGroup;
public ConsoleSignalRComponent(IHubContext<ServerWebSocketHub> hub, ServerContext context)
{
Hub = hub;
Context = context;
}
public async Task InitializeAsync()
{
HubGroup = Context.Configuration.Id.ToString();
StdOutSubscription = await Context.Server.Console.SubscribeStdOutAsync(OnStdOut);
}
private async ValueTask OnStdOut(string output)
{
await Hub.Clients.Group(HubGroup).SendAsync("ConsoleOutput", output);
}
public async ValueTask DisposeAsync()
{
if (StdOutSubscription != null)
await StdOutSubscription.DisposeAsync();
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections;
using MoonlightServers.Daemon.ServerSystem.Enums;
using MoonlightServers.Daemon.ServerSystem.Interfaces;
using MoonlightServers.Daemon.ServerSystem.Models;
@@ -39,7 +40,8 @@ public partial class Server : IAsyncDisposable
IRestorer restorer,
IRuntime runtime,
IStatistics statistics,
IServerStateHandler[] handlers
IEnumerable<IServerStateHandler> handlers,
IEnumerable<IServerComponent> additionalComponents
)
{
Logger = logger;
@@ -54,13 +56,15 @@ public partial class Server : IAsyncDisposable
Runtime = runtime;
Statistics = statistics;
AllComponents =
IEnumerable<IServerComponent> defaultComponents =
[
Console, RuntimeFileSystem, InstallationFileSystem, Installation, OnlineDetector, Reporter, Restorer,
Runtime, Statistics
];
Handlers = handlers;
AllComponents = defaultComponents.Concat(additionalComponents).ToArray();
Handlers = handlers.ToArray();
}
private void ConfigureStateMachine(ServerState initialState)

View File

@@ -64,6 +64,11 @@ public class ServerFactory
handlers.Add(ActivatorUtilities.CreateInstance<InstallationHandler>(scope.ServiceProvider));
handlers.Add(ActivatorUtilities.CreateInstance<DebugHandler>(scope.ServiceProvider));
// Resolve additional components
var components = new List<IServerComponent>();
components.Add(ActivatorUtilities.CreateInstance<ConsoleSignalRComponent>(scope.ServiceProvider));
// TODO: Add a plugin hook for dynamically resolving components and checking if any is unset
// Resolve server from di
@@ -81,7 +86,8 @@ public class ServerFactory
runtime,
statistics,
// And now all the handlers
handlers.ToArray()
handlers,
components
);
context.Server = server;

View File

@@ -0,0 +1,112 @@
using System.Collections.Concurrent;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.Daemon.ServerSystem;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
namespace MoonlightServers.Daemon.Services;
public class ServerService : IHostedLifecycleService
{
private readonly ConcurrentDictionary<int, Server> Servers = new();
private readonly ILogger<ServerService> Logger;
private readonly ServerFactory ServerFactory;
private readonly RemoteService RemoteService;
private readonly ServerConfigurationMapper ConfigurationMapper;
public ServerService(
ILogger<ServerService> logger,
ServerFactory serverFactory,
RemoteService remoteService,
ServerConfigurationMapper configurationMapper
)
{
Logger = logger;
ServerFactory = serverFactory;
RemoteService = remoteService;
ConfigurationMapper = configurationMapper;
}
public Server? GetById(int id)
=> Servers.GetValueOrDefault(id);
public async Task Initialize(ServerConfiguration configuration)
{
var existingServer = Servers.GetValueOrDefault(configuration.Id);
if (existingServer != null)
{
existingServer.Context.Configuration = configuration;
// TODO: Implement a way for components to get notified
}
else
{
var server = await ServerFactory.CreateAsync(configuration);
Servers[configuration.Id] = server;
await server.InitializeAsync();
}
}
public async Task InitializeById(int id)
{
var serverData = await RemoteService.GetServer(id);
var config = ConfigurationMapper.FromServerDataResponse(serverData);
await Initialize(config);
}
private async Task InitializeAll()
{
Logger.LogDebug("Initialing servers from panel");
var servers = await PagedData<ServerDataResponse>.All(async (page, pageSize) =>
await RemoteService.GetServers(page, pageSize)
);
foreach (var serverData in servers)
{
try
{
var config = ConfigurationMapper.FromServerDataResponse(serverData);
await Initialize(config);
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while initializing server {id}", serverData.Id);
}
}
}
#region Lifetime handlers
public Task StartAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public async Task StartedAsync(CancellationToken cancellationToken)
{
await InitializeAll();
}
public Task StartingAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public async Task StoppingAsync(CancellationToken cancellationToken)
{
Logger.LogDebug("Stopping server service. Disposing servers");
foreach (var server in Servers.Values)
await server.DisposeAsync();
}
#endregion
}

View File

@@ -329,6 +329,9 @@ public class Startup
WebApplicationBuilder.Services.AddSingleton<ServerFactory>();
WebApplicationBuilder.Services.AddSingleton<ServerConfigurationMapper>();
WebApplicationBuilder.Services.AddSingleton<ServerService>();
WebApplicationBuilder.Services.AddHostedService(sp => sp.GetRequiredService<ServerService>());
return Task.CompletedTask;
}

View File

@@ -6,5 +6,6 @@ public enum ServerState
Starting = 1,
Online = 2,
Stopping = 3,
Installing = 4
Installing = 4,
Locked = 5
}