|
|
|
|
@@ -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();
|
|
|
|
|
|