using MoonCore.Observability; using MoonlightServers.Daemon.Extensions; using MoonlightServers.Daemon.Helpers; using MoonlightServers.Daemon.ServerSystem; 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 StateMachine StateMachine { get; private set; } public ServerContext Context { get; } public IAsyncObservable OnState => OnStateSubject; private readonly EventSubject OnStateSubject = new(); private readonly ILogger Logger; private IAsyncDisposable? ProvisionExitSubscription; private IAsyncDisposable? InstallerExitSubscription; public Server( ILogger logger, IConsole console, IFileSystem fileSystem, IInstaller installer, IProvisioner provisioner, IRestorer restorer, IStatistics statistics, ServerContext context ) { Logger = logger; Console = console; FileSystem = fileSystem; Installer = installer; Provisioner = provisioner; Restorer = restorer; Statistics = statistics; Context = context; } public async Task Initialize() { Logger.LogDebug("Initializing server components"); IServerComponent[] components = [Console, Restorer, FileSystem, Installer, Provisioner, Statistics]; 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); // 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) ); } private void CreateStateMachine(ServerState initialState) { StateMachine = new StateMachine(initialState, FiringMode.Queued); StateMachine.OnTransitionedAsync(async transition => await OnStateSubject.OnNextAsync(transition.Destination) ); // Configure basic state machine flow StateMachine.Configure(ServerState.Offline) .Permit(ServerTrigger.Start, ServerState.Starting) .Permit(ServerTrigger.Install, ServerState.Installing) .PermitReentry(ServerTrigger.FailSafe); StateMachine.Configure(ServerState.Starting) .Permit(ServerTrigger.OnlineDetected, ServerState.Online) .Permit(ServerTrigger.FailSafe, ServerState.Offline) .Permit(ServerTrigger.Exited, ServerState.Offline) .Permit(ServerTrigger.Stop, ServerState.Stopping) .Permit(ServerTrigger.Kill, ServerState.Stopping); StateMachine.Configure(ServerState.Online) .Permit(ServerTrigger.Stop, ServerState.Stopping) .Permit(ServerTrigger.Kill, ServerState.Stopping) .Permit(ServerTrigger.Exited, ServerState.Offline); StateMachine.Configure(ServerState.Stopping) .PermitReentry(ServerTrigger.FailSafe) .PermitReentry(ServerTrigger.Kill) .Permit(ServerTrigger.Exited, ServerState.Offline); StateMachine.Configure(ServerState.Installing) .Permit(ServerTrigger.FailSafe, ServerState.Offline) // TODO: Add kill .Permit(ServerTrigger.Exited, ServerState.Offline); // Handle transitions StateMachine.Configure(ServerState.Starting) .OnEntryAsync(HandleStart) .OnExitFromAsync(ServerTrigger.Exited, HandleRuntimeExit); StateMachine.Configure(ServerState.Installing) .OnEntryAsync(HandleInstall) .OnExitFromAsync(ServerTrigger.Exited, HandleInstallExit); StateMachine.Configure(ServerState.Online) .OnExitFromAsync(ServerTrigger.Exited, HandleRuntimeExit); StateMachine.Configure(ServerState.Stopping) .OnEntryFromAsync(ServerTrigger.Stop, HandleStop) .OnExitFromAsync(ServerTrigger.Exited, HandleRuntimeExit); } #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 // TODO: Implement // 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"); await Console.WriteToMoonlight("Failed to start the server. More details can be found in the daemon logs"); await StateMachine.FireAsync(ServerTrigger.FailSafe); } } private async Task HandleStop() { await Provisioner.Stop(); } private async Task HandleRuntimeExit() { Logger.LogDebug("Deprovisioning"); await Console.WriteToMoonlight("Deprovisioning"); await Provisioner.Deprovision(); } private async Task HandleInstall() { try { Logger.LogDebug("Installing"); Logger.LogDebug("Setting up"); await Console.WriteToMoonlight("Setting up installation"); // TODO: Extract to service 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" }; await Installer.Setup(); await Console.AttachToInstallation(); await Installer.Start(); } catch (Exception e) { Logger.LogError(e, "An error occured while starting installation"); await Console.WriteToMoonlight("An error occured while starting installation"); await StateMachine.FireAsync(ServerTrigger.FailSafe); } } private async Task HandleInstallExit() { Logger.LogDebug("Installation done"); 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(); await Console.DisposeAsync(); await FileSystem.DisposeAsync(); await Installer.DisposeAsync(); await Provisioner.DisposeAsync(); await Restorer.DisposeAsync(); await Statistics.DisposeAsync(); } }