Implemented online detection. Extended ServerContext to include self reference so sub components can subscribe to the state. Improved console module detach handling. Implemented new server service to replace the old one. Added log restore when restoring

This commit is contained in:
2025-07-30 20:52:24 +02:00
parent eaf8c36f7f
commit 5c170935b4
15 changed files with 419 additions and 70 deletions

View File

@@ -1,7 +1,10 @@
using Microsoft.AspNetCore.SignalR;
using MoonCore.Observability;
using MoonlightServers.Daemon.Extensions;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.Daemon.Http.Hubs;
using MoonlightServers.Daemon.Mappers;
using MoonlightServers.Daemon.ServerSystem;
using MoonlightServers.Daemon.Services;
using Stateless;
namespace MoonlightServers.Daemon.ServerSys.Abstractions;
@@ -14,15 +17,20 @@ public class Server : IAsyncDisposable
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<Server> 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(
ILogger<Server> logger,
@@ -32,8 +40,11 @@ public class Server : IAsyncDisposable
IProvisioner provisioner,
IRestorer restorer,
IStatistics statistics,
ServerContext context
)
IOnlineDetection onlineDetection,
ServerContext context,
RemoteService remoteService,
ServerConfigurationMapper mapper,
IHubContext<ServerWebSocketHub> hubContext)
{
Logger = logger;
Console = console;
@@ -43,13 +54,17 @@ public class Server : IAsyncDisposable
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];
IServerComponent[] components = [Console, Restorer, FileSystem, Installer, Provisioner, Statistics, OnlineDetection];
foreach (var serverComponent in components)
{
@@ -79,6 +94,8 @@ public class Server : IAsyncDisposable
CreateStateMachine(restoredState);
await SetupHubEvents();
// Setup event handling
ProvisionExitSubscription = await Provisioner.OnExited.SubscribeEventAsync(async _ =>
await StateMachine.FireAsync(ServerTrigger.Exited)
@@ -89,6 +106,14 @@ public class Server : IAsyncDisposable
);
}
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);
@@ -140,9 +165,31 @@ public class Server : IAsyncDisposable
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()
);
});
}
#region State machine handlers
private async Task HandleStart()
@@ -158,7 +205,11 @@ public class Server : IAsyncDisposable
// 6. Start the container
// 1. Fetch latest configuration from panel
// TODO: Implement
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)
@@ -187,9 +238,6 @@ public class Server : IAsyncDisposable
catch (Exception e)
{
Logger.LogError(e, "An error occured while starting the server");
await Console.WriteToMoonlight("Failed to start the server. More details can be found in the daemon logs");
await StateMachine.FireAsync(ServerTrigger.FailSafe);
}
}
@@ -198,53 +246,61 @@ public class Server : IAsyncDisposable
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()
{
try
{
Logger.LogDebug("Installing");
// Plan:
// 1. Fetch the latest installation data
// 2. Setup installation environment
// 3. Attach console to installation
// 4. Start the installation
Logger.LogDebug("Setting up");
await Console.WriteToMoonlight("Setting up installation");
Logger.LogDebug("Installing");
// TODO: Extract to service
Logger.LogDebug("Setting up");
await Console.WriteToMoonlight("Setting up installation");
Context.InstallConfiguration = new()
{
Shell = "/bin/ash",
DockerImage = "ghcr.io/parkervcp/installers:alpine",
Script =
"#!/bin/ash\n# Paper Installation Script\n#\n# Server Files: /mnt/server\nPROJECT=paper\n\nif [ -n \"${DL_PATH}\" ]; then\n\techo -e \"Using supplied download url: ${DL_PATH}\"\n\tDOWNLOAD_URL=`eval echo $(echo ${DL_PATH} | sed -e 's/{{/${/g' -e 's/}}/}/g')`\nelse\n\tVER_EXISTS=`curl -s https://api.papermc.io/v2/projects/${PROJECT} | jq -r --arg VERSION $MINECRAFT_VERSION '.versions[] | contains($VERSION)' | grep -m1 true`\n\tLATEST_VERSION=`curl -s https://api.papermc.io/v2/projects/${PROJECT} | jq -r '.versions' | jq -r '.[-1]'`\n\n\tif [ \"${VER_EXISTS}\" == \"true\" ]; then\n\t\techo -e \"Version is valid. Using version ${MINECRAFT_VERSION}\"\n\telse\n\t\techo -e \"Specified version not found. Defaulting to the latest ${PROJECT} version\"\n\t\tMINECRAFT_VERSION=${LATEST_VERSION}\n\tfi\n\n\tBUILD_EXISTS=`curl -s https://api.papermc.io/v2/projects/${PROJECT}/versions/${MINECRAFT_VERSION} | jq -r --arg BUILD ${BUILD_NUMBER} '.builds[] | tostring | contains($BUILD)' | grep -m1 true`\n\tLATEST_BUILD=`curl -s https://api.papermc.io/v2/projects/${PROJECT}/versions/${MINECRAFT_VERSION} | jq -r '.builds' | jq -r '.[-1]'`\n\n\tif [ \"${BUILD_EXISTS}\" == \"true\" ]; then\n\t\techo -e \"Build is valid for version ${MINECRAFT_VERSION}. Using build ${BUILD_NUMBER}\"\n\telse\n\t\techo -e \"Using the latest ${PROJECT} build for version ${MINECRAFT_VERSION}\"\n\t\tBUILD_NUMBER=${LATEST_BUILD}\n\tfi\n\n\tJAR_NAME=${PROJECT}-${MINECRAFT_VERSION}-${BUILD_NUMBER}.jar\n\n\techo \"Version being downloaded\"\n\techo -e \"MC Version: ${MINECRAFT_VERSION}\"\n\techo -e \"Build: ${BUILD_NUMBER}\"\n\techo -e \"JAR Name of Build: ${JAR_NAME}\"\n\tDOWNLOAD_URL=https://api.papermc.io/v2/projects/${PROJECT}/versions/${MINECRAFT_VERSION}/builds/${BUILD_NUMBER}/downloads/${JAR_NAME}\nfi\n\ncd /mnt/server\n\necho -e \"Running curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\"\n\nif [ -f ${SERVER_JARFILE} ]; then\n\tmv ${SERVER_JARFILE} ${SERVER_JARFILE}.old\nfi\n\ncurl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\n\nif [ ! -f server.properties ]; then\n echo -e \"Downloading MC server.properties\"\n curl -o server.properties https://raw.githubusercontent.com/parkervcp/eggs/master/minecraft/java/server.properties\nfi"
};
// 1. Fetch the latest installation data
Logger.LogDebug("Fetching installation data");
await Console.WriteToMoonlight("Fetching installation data");
await Installer.Setup();
Context.InstallConfiguration = await RemoteService.GetServerInstallation(Context.Configuration.Id);
await Console.AttachToInstallation();
// 2. Setup installation environment
await Installer.Setup();
await Installer.Start();
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while starting installation");
await Console.WriteToMoonlight("An error occured while starting installation");
// 3. Attach console to installation
await Console.AttachToInstallation();
await StateMachine.FireAsync(ServerTrigger.FailSafe);
}
// 4. Start the installation
await Installer.Start();
}
private async Task HandleInstallExit()
{
Logger.LogDebug("Installation done");
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");
@@ -260,6 +316,9 @@ public class Server : IAsyncDisposable
if (InstallerExitSubscription != null)
await InstallerExitSubscription.DisposeAsync();
if (ConsoleSubscription != null)
await ConsoleSubscription.DisposeAsync();
await Console.DisposeAsync();
await FileSystem.DisposeAsync();
await Installer.DisposeAsync();