Refactored/recreated server system. Seperated into sub systems. Still wip
This commit is contained in:
@@ -1,80 +0,0 @@
|
||||
using System.Text;
|
||||
using Docker.DotNet;
|
||||
using Docker.DotNet.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.Abstractions;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
private async Task AttachConsole(string containerId)
|
||||
{
|
||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||
|
||||
var stream = await dockerClient.Containers.AttachContainerAsync(containerId, true,
|
||||
new ContainerAttachParameters()
|
||||
{
|
||||
Stderr = true,
|
||||
Stdin = true,
|
||||
Stdout = true,
|
||||
Stream = true
|
||||
},
|
||||
Cancellation.Token
|
||||
);
|
||||
|
||||
// Reading
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (!Cancellation.Token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var buffer = new byte[1024];
|
||||
|
||||
var readResult = await stream.ReadOutputAsync(
|
||||
buffer,
|
||||
0,
|
||||
buffer.Length,
|
||||
Cancellation.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 Console.WriteToOutput(decodedText);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Writing
|
||||
Console.OnInput += async content =>
|
||||
{
|
||||
var contentBuffer = Encoding.UTF8.GetBytes(content);
|
||||
await stream.WriteAsync(contentBuffer, 0, contentBuffer.Length, Cancellation.Token);
|
||||
};
|
||||
}
|
||||
|
||||
private async Task LogToConsole(string message)
|
||||
{
|
||||
await Console.WriteToOutput($"\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m\x1b[38;5;250m\x1b[3m {message}\x1b[0m\n\r");
|
||||
}
|
||||
|
||||
public Task<string[]> GetConsoleMessages()
|
||||
=> Task.FromResult(Console.Messages);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using Docker.DotNet;
|
||||
using Docker.DotNet.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.Abstractions;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
public async Task InternalCrash()
|
||||
{
|
||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||
|
||||
ContainerInspectResponse? container;
|
||||
|
||||
try
|
||||
{
|
||||
container = await dockerClient.Containers.InspectContainerAsync(RuntimeContainerId);
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
container = null;
|
||||
}
|
||||
|
||||
if(container == null)
|
||||
return;
|
||||
|
||||
var exitCode = container.State.ExitCode;
|
||||
|
||||
// TODO: Report to panel
|
||||
|
||||
await LogToConsole($"Server crashed. Exit code: {exitCode}");
|
||||
|
||||
await Destroy();
|
||||
}
|
||||
|
||||
public async Task InternalError()
|
||||
{
|
||||
await LogToConsole("An unhandled error occured performing action");
|
||||
// TODO:
|
||||
Logger.LogInformation("Reporting or smth");
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.Extensions;
|
||||
using MoonlightServers.Daemon.Services;
|
||||
|
||||
namespace MoonlightServers.Daemon.Abstractions;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
private async Task Create()
|
||||
{
|
||||
var dockerImageService = ServiceProvider.GetRequiredService<DockerImageService>();
|
||||
|
||||
// We call an external service for that, as we want to have a central management point of images
|
||||
// for analytics and automatic deletion
|
||||
await dockerImageService.Ensure(Configuration.DockerImage, async message => { await LogToConsole(message); });
|
||||
|
||||
await EnsureRuntimeVolume();
|
||||
|
||||
await LogToConsole("Creating container");
|
||||
|
||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||
|
||||
var parameters = Configuration.ToRuntimeCreateParameters(
|
||||
appConfiguration: AppConfiguration,
|
||||
hostPath: RuntimeVolumePath,
|
||||
containerName: RuntimeContainerName
|
||||
);
|
||||
|
||||
var container = await dockerClient.Containers.CreateContainerAsync(parameters);
|
||||
RuntimeContainerId = container.ID;
|
||||
}
|
||||
|
||||
private async Task ReCreate()
|
||||
{
|
||||
await Destroy();
|
||||
await Create();
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.Configuration;
|
||||
|
||||
namespace MoonlightServers.Daemon.Abstractions;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
private async Task Destroy()
|
||||
{
|
||||
// Note: This only destroys the container, it doesn't delete any data
|
||||
|
||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||
|
||||
try
|
||||
{
|
||||
var container = await dockerClient.Containers.InspectContainerAsync(
|
||||
RuntimeContainerName
|
||||
);
|
||||
|
||||
if (container.State.Running)
|
||||
{
|
||||
// Stop container when running
|
||||
await LogToConsole("Stopping container");
|
||||
|
||||
await dockerClient.Containers.StopContainerAsync(container.ID, new()
|
||||
{
|
||||
WaitBeforeKillSeconds = (uint)AppConfiguration.Server.WaitBeforeKillSeconds
|
||||
});
|
||||
}
|
||||
|
||||
await LogToConsole("Removing container");
|
||||
await dockerClient.Containers.RemoveContainerAsync(container.ID, new());
|
||||
|
||||
RuntimeContainerId = null;
|
||||
}
|
||||
catch (DockerContainerNotFoundException){}
|
||||
|
||||
// Canceling server tasks & listeners and start new ones
|
||||
await ResetTasks();
|
||||
}
|
||||
|
||||
public async Task ResetTasks()
|
||||
{
|
||||
// Note: This will keep the docker container running, it will just cancel the server cancellation token
|
||||
// and recreate the token
|
||||
await CancelTasks();
|
||||
|
||||
Cancellation = new();
|
||||
}
|
||||
|
||||
public async Task CancelTasks()
|
||||
{
|
||||
// Note: This will keep the docker container running, it will just cancel the server cancellation token
|
||||
|
||||
if (!Cancellation.IsCancellationRequested)
|
||||
await Cancellation.CancelAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Docker.DotNet.Models;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using MoonlightServers.Daemon.Enums;
|
||||
using MoonlightServers.Daemon.Extensions;
|
||||
using Stateless;
|
||||
|
||||
namespace MoonlightServers.Daemon.Abstractions;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
// We are expecting a list of running containers, as we don't wont to inspect every possible container just to check if it exists.
|
||||
// If none are provided, we skip the checks. Use this overload if you are creating a new server which didn't exist before
|
||||
public async Task Initialize(IList<ContainerListResponse>? runningContainers = null)
|
||||
{
|
||||
if (runningContainers != null)
|
||||
{
|
||||
var reAttachSuccessful = await ReAttach(runningContainers);
|
||||
|
||||
// If we weren't able to reattach with the current running containers, we initialize the
|
||||
// state machine as offline
|
||||
if(!reAttachSuccessful)
|
||||
await InitializeStateMachine(ServerState.Offline);
|
||||
}
|
||||
else
|
||||
await InitializeStateMachine(ServerState.Offline);
|
||||
|
||||
// Now we initialize all events, so we can react to certain state changes and outputs.
|
||||
// We need to do this regardless if the server was reattached or not, as it hasn't been initialized yet
|
||||
await InitializeEvents();
|
||||
|
||||
// Load storage configuration
|
||||
await InitializeStorage();
|
||||
}
|
||||
|
||||
private Task InitializeStateMachine(ServerState initialState)
|
||||
{
|
||||
StateMachine = new StateMachine<ServerState, ServerTrigger>(initialState);
|
||||
|
||||
// Setup transitions
|
||||
StateMachine.Configure(ServerState.Offline)
|
||||
.Permit(ServerTrigger.Start, ServerState.Starting) // Allow to start
|
||||
.Permit(ServerTrigger.Reinstall, ServerState.Installing) // Allow to install
|
||||
.OnEntryFromAsync(ServerTrigger.NotifyInternalError, InternalError); // Handle unhandled errors
|
||||
|
||||
StateMachine.Configure(ServerState.Starting)
|
||||
.Permit(ServerTrigger.Stop, ServerState.Stopping) // Allow stopping
|
||||
.Permit(ServerTrigger.Kill, ServerState.Stopping) // Allow killing while starting
|
||||
.Permit(ServerTrigger.NotifyOnline, ServerState.Online) // Allow the server to report as online
|
||||
.Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline) // Allow server to handle container death
|
||||
.Permit(ServerTrigger.NotifyInternalError, ServerState.Offline) // Error handling for the action below
|
||||
.OnEntryAsync(InternalStart) // Perform start action
|
||||
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalCrash); // Define a runtime container death as a crash
|
||||
|
||||
StateMachine.Configure(ServerState.Online)
|
||||
.Permit(ServerTrigger.Stop, ServerState.Stopping) // Allow stopping
|
||||
.Permit(ServerTrigger.Kill, ServerState.Stopping) // Allows killing
|
||||
.Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline) // Allow server to handle container death
|
||||
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalCrash); // Define a runtime container death as a crash
|
||||
|
||||
StateMachine.Configure(ServerState.Stopping)
|
||||
.PermitReentry(ServerTrigger.Kill) // Allow killing, will return to stopping to trigger kill and handle the death correctly
|
||||
.Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline) // Allow server to handle container death
|
||||
.Permit(ServerTrigger.NotifyInternalError, ServerState.Offline) // Error handling for the actions below
|
||||
.OnEntryFromAsync(ServerTrigger.Stop, InternalStop) // Perform stop action
|
||||
.OnEntryFromAsync(ServerTrigger.Kill, InternalKill) // Perform kill action
|
||||
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalFinishStop); // Define a runtime container death as a successful stop
|
||||
|
||||
StateMachine.Configure(ServerState.Installing)
|
||||
.Permit(ServerTrigger.NotifyInstallationContainerDied, ServerState.Offline) // Allow server to handle container death
|
||||
.Permit(ServerTrigger.NotifyInternalError, ServerState.Offline) // Error handling for the action below
|
||||
.OnEntryAsync(InternalInstall) // Perform install action
|
||||
.OnExitFromAsync(ServerTrigger.NotifyInstallationContainerDied, InternalFinishInstall); // Define the death of the installation container as successful
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task InitializeEvents()
|
||||
{
|
||||
Console.OnOutput += async content =>
|
||||
{
|
||||
if (StateMachine.State == ServerState.Starting)
|
||||
{
|
||||
if (Regex.Matches(content, Configuration.OnlineDetection).Count > 0)
|
||||
await StateMachine.FireAsync(ServerTrigger.NotifyOnline);
|
||||
}
|
||||
};
|
||||
|
||||
StateMachine.OnTransitioned(transition =>
|
||||
{
|
||||
Logger.LogDebug(
|
||||
"{source} => {destination} ({trigger})",
|
||||
transition.Source,
|
||||
transition.Destination,
|
||||
transition.Trigger
|
||||
);
|
||||
});
|
||||
|
||||
StateMachine.OnTransitionCompleted(transition =>
|
||||
{
|
||||
Logger.LogDebug("State: {state}", transition.Destination);
|
||||
});
|
||||
|
||||
// Proxy the events so outside subscribes can react to it and notify websockets
|
||||
StateMachine.OnTransitionCompletedAsync(async transition =>
|
||||
{
|
||||
// Notify all clients interested in the server
|
||||
await WebSocketHub.Clients
|
||||
.Group(Id.ToString()) //TODO: Consider saving the string value in memory
|
||||
.SendAsync("StateChanged", transition.Destination.ToString());
|
||||
|
||||
// Notify all external listeners
|
||||
if (OnStateChanged != null)
|
||||
await OnStateChanged(transition.Destination);
|
||||
});
|
||||
|
||||
Console.OnOutput += (async message =>
|
||||
{
|
||||
// Notify all clients interested in the server
|
||||
await WebSocketHub.Clients
|
||||
.Group(Id.ToString()) //TODO: Consider saving the string value in memory
|
||||
.SendAsync("ConsoleOutput", message);
|
||||
|
||||
if (OnConsoleOutput != null)
|
||||
await OnConsoleOutput(message);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#region Reattaching & reattach strategies
|
||||
|
||||
private async Task<bool> ReAttach(IList<ContainerListResponse> runningContainers)
|
||||
{
|
||||
// Docker container names are starting with a / when returned in the docker container list api endpoint,
|
||||
// so we trim it from the name when searching
|
||||
|
||||
var existingRuntimeContainer = runningContainers.FirstOrDefault(
|
||||
x => x.Names.Any(y => y.TrimStart('/') == RuntimeContainerName)
|
||||
);
|
||||
|
||||
if (existingRuntimeContainer != null)
|
||||
{
|
||||
await ReAttachToRuntime(existingRuntimeContainer);
|
||||
return true;
|
||||
}
|
||||
|
||||
var existingInstallContainer = runningContainers.FirstOrDefault(
|
||||
x => x.Names.Any(y => y.TrimStart('/') == InstallationContainerName)
|
||||
);
|
||||
|
||||
if (existingInstallContainer != null)
|
||||
{
|
||||
await ReAttachToInstallation(existingInstallContainer);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task ReAttachToRuntime(ContainerListResponse runtimeContainer)
|
||||
{
|
||||
if (runtimeContainer.State == "running")
|
||||
{
|
||||
RuntimeContainerId = runtimeContainer.ID;
|
||||
|
||||
await InitializeStateMachine(ServerState.Online);
|
||||
|
||||
await AttachConsole(runtimeContainer.ID);
|
||||
}
|
||||
else
|
||||
await InitializeStateMachine(ServerState.Offline);
|
||||
}
|
||||
|
||||
private async Task ReAttachToInstallation(ContainerListResponse installationContainer)
|
||||
{
|
||||
if (installationContainer.State == "running")
|
||||
{
|
||||
InstallationContainerId = installationContainer.ID;
|
||||
|
||||
await InitializeStateMachine(ServerState.Installing);
|
||||
|
||||
await AttachConsole(installationContainer.ID);
|
||||
}
|
||||
else
|
||||
await InitializeStateMachine(ServerState.Offline);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
using Docker.DotNet;
|
||||
using Docker.DotNet.Models;
|
||||
using MoonCore.Helpers;
|
||||
using MoonlightServers.Daemon.Enums;
|
||||
using MoonlightServers.Daemon.Extensions;
|
||||
using MoonlightServers.Daemon.Services;
|
||||
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
|
||||
|
||||
namespace MoonlightServers.Daemon.Abstractions;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
public async Task Install() => await StateMachine.FireAsync(ServerTrigger.Reinstall);
|
||||
|
||||
private async Task InternalInstall()
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: Consider if checking for existing install containers is actually useful, because
|
||||
// when the daemon is starting and a installation is still ongoing it will reattach anyways
|
||||
// and the container has the auto remove flag enabled by default (maybe also consider this for the normal runtime container)
|
||||
|
||||
await LogToConsole("Fetching installation configuration");
|
||||
|
||||
// Fetching remote configuration and install config
|
||||
var remoteService = ServiceProvider.GetRequiredService<RemoteService>();
|
||||
|
||||
var installData = await remoteService.GetServerInstallation(Configuration.Id);
|
||||
var serverData = await remoteService.GetServer(Configuration.Id);
|
||||
|
||||
// We are updating the regular server config here as well
|
||||
// as changes to variables and other settings wouldn't sync otherwise
|
||||
// because they won't trigger a sync
|
||||
var serverConfiguration = serverData.ToServerConfiguration();
|
||||
UpdateConfiguration(serverConfiguration);
|
||||
|
||||
var dockerImageService = ServiceProvider.GetRequiredService<DockerImageService>();
|
||||
|
||||
// We call an external service for that, as we want to have a central management point of images
|
||||
// for analytics and automatic deletion
|
||||
await dockerImageService.Ensure(installData.DockerImage, async message => { await LogToConsole(message); });
|
||||
|
||||
// Ensuring storage
|
||||
await EnsureInstallationVolume();
|
||||
await EnsureRuntimeVolume();
|
||||
|
||||
// Write installation script to path
|
||||
var content = installData.Script.Replace("\r\n", "\n");
|
||||
await File.WriteAllTextAsync(PathBuilder.File(InstallationVolumePath, "install.sh"), content);
|
||||
|
||||
// Creating container configuration
|
||||
var parameters = Configuration.ToInstallationCreateParameters(
|
||||
appConfiguration: AppConfiguration,
|
||||
RuntimeVolumePath,
|
||||
InstallationVolumePath,
|
||||
InstallationContainerName,
|
||||
installData.DockerImage,
|
||||
installData.Shell
|
||||
);
|
||||
|
||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||
|
||||
// Ensure we can actually spawn the container
|
||||
|
||||
try
|
||||
{
|
||||
var existingContainer = await dockerClient.Containers.InspectContainerAsync(InstallationContainerName);
|
||||
|
||||
// Perform automatic cleanup / restore
|
||||
|
||||
if (existingContainer.State.Running)
|
||||
await dockerClient.Containers.KillContainerAsync(existingContainer.ID, new());
|
||||
|
||||
await dockerClient.Containers.RemoveContainerAsync(existingContainer.ID, new());
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
// Spawn the container
|
||||
|
||||
var container = await dockerClient.Containers.CreateContainerAsync(parameters);
|
||||
InstallationContainerId = container.ID;
|
||||
|
||||
await AttachConsole(InstallationContainerId);
|
||||
|
||||
await dockerClient.Containers.StartContainerAsync(InstallationContainerId, new());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("An error occured while performing install trigger: {e}", e);
|
||||
await StateMachine.FireAsync(ServerTrigger.NotifyInternalError);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InternalFinishInstall()
|
||||
{
|
||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||
|
||||
ContainerInspectResponse? container;
|
||||
|
||||
try
|
||||
{
|
||||
container = await dockerClient.Containers.InspectContainerAsync(InstallationContainerId, CancellationToken.None);
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
container = null;
|
||||
}
|
||||
|
||||
if(container == null)
|
||||
return;
|
||||
|
||||
var exitCode = container.State.ExitCode;
|
||||
|
||||
await LogToConsole($"Installation finished with exit code: {exitCode}");
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
// TODO: Report installation failure
|
||||
}
|
||||
|
||||
await LogToConsole("Removing container");
|
||||
//await dockerClient.Containers.RemoveContainerAsync(InstallationContainerId, new());
|
||||
InstallationContainerId = null;
|
||||
|
||||
await ResetTasks();
|
||||
|
||||
await RemoveInstallationVolume();
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.Enums;
|
||||
|
||||
namespace MoonlightServers.Daemon.Abstractions;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
public async Task Kill() => await StateMachine.FireAsync(ServerTrigger.Kill);
|
||||
|
||||
private async Task InternalKill()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (RuntimeContainerId == null)
|
||||
return;
|
||||
|
||||
await LogToConsole("Killing container");
|
||||
|
||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||
await dockerClient.Containers.KillContainerAsync(RuntimeContainerId, new());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("An error occured while performing stop trigger: {e}", e);
|
||||
await StateMachine.FireAsync(ServerTrigger.NotifyInternalError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using MoonlightServers.Daemon.Enums;
|
||||
|
||||
namespace MoonlightServers.Daemon.Abstractions;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
public async Task NotifyRuntimeContainerDied() => await StateMachine.FireAsync(ServerTrigger.NotifyRuntimeContainerDied);
|
||||
public async Task NotifyInstallationContainerDied() => await StateMachine.FireAsync(ServerTrigger.NotifyInstallationContainerDied);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.Enums;
|
||||
using MoonlightServers.Daemon.Extensions;
|
||||
using MoonlightServers.Daemon.Services;
|
||||
|
||||
namespace MoonlightServers.Daemon.Abstractions;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
public async Task Start() => await StateMachine.FireAsync(ServerTrigger.Start);
|
||||
|
||||
private async Task InternalStart()
|
||||
{
|
||||
try
|
||||
{
|
||||
await LogToConsole("Fetching configuration");
|
||||
|
||||
var remoteService = ServiceProvider.GetRequiredService<RemoteService>();
|
||||
var serverData = await remoteService.GetServer(Configuration.Id);
|
||||
|
||||
// We are updating the server config here
|
||||
// as changes to variables and other settings wouldn't sync otherwise
|
||||
// because they won't trigger a sync
|
||||
var serverConfiguration = serverData.ToServerConfiguration();
|
||||
UpdateConfiguration(serverConfiguration);
|
||||
|
||||
await ReCreate();
|
||||
|
||||
await LogToConsole("Starting container");
|
||||
|
||||
// We can disable the null check for the runtime container id, as we set it by calling ReCreate();
|
||||
await AttachConsole(RuntimeContainerId!);
|
||||
|
||||
// Start container
|
||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||
await dockerClient.Containers.StartContainerAsync(RuntimeContainerId, new());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("An error occured while performing start trigger: {e}", e);
|
||||
await StateMachine.FireAsync(ServerTrigger.NotifyInternalError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using MoonlightServers.Daemon.Enums;
|
||||
|
||||
namespace MoonlightServers.Daemon.Abstractions;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
public async Task Stop() => await StateMachine.FireAsync(ServerTrigger.Stop);
|
||||
|
||||
private async Task InternalStop()
|
||||
{
|
||||
try
|
||||
{
|
||||
await Console.WriteToInput($"{Configuration.StopCommand}\n\r");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("An error occured while performing stop trigger: {e}", e);
|
||||
await StateMachine.FireAsync(ServerTrigger.NotifyInternalError);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InternalFinishStop()
|
||||
{
|
||||
await Destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
using MoonCore.Helpers;
|
||||
using MoonCore.Unix.SecureFs;
|
||||
using MoonlightServers.Daemon.Configuration;
|
||||
using MoonlightServers.Daemon.Helpers;
|
||||
|
||||
namespace MoonlightServers.Daemon.Abstractions;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
public ServerFileSystem FileSystem { get; private set; }
|
||||
|
||||
private SpinLock FsLock = new();
|
||||
|
||||
private SecureFileSystem? InternalFileSystem;
|
||||
|
||||
private string RuntimeVolumePath;
|
||||
private string InstallationVolumePath;
|
||||
|
||||
private async Task InitializeStorage()
|
||||
{
|
||||
#region Configure paths
|
||||
|
||||
var appConfiguration = ServiceProvider.GetRequiredService<AppConfiguration>();
|
||||
|
||||
// Runtime
|
||||
var runtimePath = PathBuilder.Dir(appConfiguration.Storage.Volumes, Configuration.Id.ToString());
|
||||
|
||||
if (appConfiguration.Storage.Volumes.StartsWith("/"))
|
||||
RuntimeVolumePath = runtimePath;
|
||||
else
|
||||
RuntimeVolumePath = PathBuilder.Dir(Directory.GetCurrentDirectory(), runtimePath);
|
||||
|
||||
// Installation
|
||||
var installationPath = PathBuilder.Dir(appConfiguration.Storage.Install, Configuration.Id.ToString());
|
||||
|
||||
if (appConfiguration.Storage.Install.StartsWith("/"))
|
||||
InstallationVolumePath = installationPath;
|
||||
else
|
||||
InstallationVolumePath = PathBuilder.Dir(Directory.GetCurrentDirectory(), installationPath);
|
||||
|
||||
#endregion
|
||||
|
||||
await ConnectRuntimeVolume();
|
||||
}
|
||||
|
||||
public async Task DestroyStorage()
|
||||
{
|
||||
await DisconnectRuntimeVolume();
|
||||
}
|
||||
|
||||
private async Task ConnectRuntimeVolume()
|
||||
{
|
||||
var gotLock = false;
|
||||
|
||||
try
|
||||
{
|
||||
FsLock.Enter(ref gotLock);
|
||||
|
||||
// We want to dispose the old fs if existing, to make sure we wont leave any file descriptors open
|
||||
if(InternalFileSystem != null && !InternalFileSystem.IsDisposed)
|
||||
InternalFileSystem.Dispose();
|
||||
|
||||
await EnsureRuntimeVolume();
|
||||
|
||||
InternalFileSystem = new SecureFileSystem(RuntimeVolumePath);
|
||||
FileSystem = new ServerFileSystem(InternalFileSystem);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if(gotLock)
|
||||
FsLock.Exit();
|
||||
}
|
||||
}
|
||||
|
||||
private Task DisconnectRuntimeVolume()
|
||||
{
|
||||
if(InternalFileSystem != null && !InternalFileSystem.IsDisposed)
|
||||
InternalFileSystem.Dispose();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task EnsureRuntimeVolume()
|
||||
{
|
||||
if (!Directory.Exists(RuntimeVolumePath))
|
||||
Directory.CreateDirectory(RuntimeVolumePath);
|
||||
|
||||
// TODO: Virtual disk
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task RemoveRuntimeVolume()
|
||||
{
|
||||
// Remove volume if existing
|
||||
if (Directory.Exists(RuntimeVolumePath))
|
||||
Directory.Delete(RuntimeVolumePath, true);
|
||||
|
||||
// TODO: Virtual disk
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task EnsureInstallationVolume()
|
||||
{
|
||||
// Create volume if missing
|
||||
if (!Directory.Exists(InstallationVolumePath))
|
||||
Directory.CreateDirectory(InstallationVolumePath);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task RemoveInstallationVolume()
|
||||
{
|
||||
// Remove install volume if existing
|
||||
if (Directory.Exists(InstallationVolumePath))
|
||||
Directory.Delete(InstallationVolumePath, true);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using Docker.DotNet.Models;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using MoonlightServers.Daemon.Configuration;
|
||||
using MoonlightServers.Daemon.Enums;
|
||||
using MoonlightServers.Daemon.Http.Hubs;
|
||||
using MoonlightServers.Daemon.Models;
|
||||
using MoonlightServers.Daemon.Models.Cache;
|
||||
using Stateless;
|
||||
|
||||
namespace MoonlightServers.Daemon.Abstractions;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
// Exposed configuration/state values
|
||||
public int Id => Configuration.Id;
|
||||
public ServerState State => StateMachine.State;
|
||||
|
||||
// Exposed container names and ids
|
||||
public string RuntimeContainerName { get; private set; }
|
||||
public string? RuntimeContainerId { get; private set; }
|
||||
|
||||
public string InstallationContainerName { get; private set; }
|
||||
public string? InstallationContainerId { get; private set; }
|
||||
|
||||
// Events
|
||||
public event Func<ServerState, Task>? OnStateChanged;
|
||||
public event Func<string, Task>? OnConsoleOutput;
|
||||
|
||||
// Private stuff
|
||||
|
||||
private readonly ILogger Logger;
|
||||
private readonly IServiceProvider ServiceProvider;
|
||||
private readonly ServerConsole Console;
|
||||
|
||||
private readonly IHubContext<ServerWebSocketHub> WebSocketHub;
|
||||
|
||||
private StateMachine<ServerState, ServerTrigger> StateMachine;
|
||||
private ServerConfiguration Configuration;
|
||||
private CancellationTokenSource Cancellation;
|
||||
private AppConfiguration AppConfiguration;
|
||||
|
||||
public Server(
|
||||
ILogger logger,
|
||||
IServiceProvider serviceProvider,
|
||||
ServerConfiguration configuration,
|
||||
IHubContext<ServerWebSocketHub> webSocketHub,
|
||||
AppConfiguration appConfiguration
|
||||
)
|
||||
{
|
||||
Logger = logger;
|
||||
ServiceProvider = serviceProvider;
|
||||
Configuration = configuration;
|
||||
WebSocketHub = webSocketHub;
|
||||
AppConfiguration = appConfiguration;
|
||||
|
||||
Console = new(AppConfiguration.Server.ConsoleMessageCacheLimit);
|
||||
Cancellation = new();
|
||||
|
||||
RuntimeContainerName = $"moonlight-runtime-{Configuration.Id}";
|
||||
InstallationContainerName = $"moonlight-install-{Configuration.Id}";
|
||||
}
|
||||
|
||||
public void UpdateConfiguration(ServerConfiguration configuration)
|
||||
=> Configuration = configuration;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MoonCore.Exceptions;
|
||||
using MoonlightServers.Daemon.Configuration;
|
||||
using MoonlightServers.Daemon.ServerSystem.SubSystems;
|
||||
using MoonlightServers.Daemon.Services;
|
||||
|
||||
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
||||
@@ -24,12 +24,21 @@ public class DownloadController : Controller
|
||||
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.GetServer(serverId);
|
||||
var server = ServerService.Find(serverId);
|
||||
|
||||
if (server == null)
|
||||
throw new HttpApiException("No server with this id found", 404);
|
||||
|
||||
await server.FileSystem.Read(path,
|
||||
async dataStream => { await Results.File(dataStream).ExecuteAsync(HttpContext); });
|
||||
var storageSubSystem = server.GetRequiredSubSystem<StorageSubSystem>();
|
||||
|
||||
var fileSystem = await storageSubSystem.GetFileSystem();
|
||||
|
||||
await fileSystem.Read(
|
||||
path,
|
||||
async dataStream =>
|
||||
{
|
||||
await Results.File(dataStream).ExecuteAsync(HttpContext);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
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;
|
||||
@@ -22,56 +24,41 @@ public class ServerFileSystemController : Controller
|
||||
[HttpGet("{id:int}/files/list")]
|
||||
public async Task<ServerFileSystemResponse[]> List([FromRoute] int id, [FromQuery] string path = "")
|
||||
{
|
||||
var server = ServerService.GetServer(id);
|
||||
var fileSystem = await GetFileSystemById(id);
|
||||
|
||||
if (server == null)
|
||||
throw new HttpApiException("No server with this id found", 404);
|
||||
|
||||
return await server.FileSystem.List(path);
|
||||
return await fileSystem.List(path);
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/files/move")]
|
||||
public async Task Move([FromRoute] int id, [FromQuery] string oldPath, [FromQuery] string newPath)
|
||||
{
|
||||
var server = ServerService.GetServer(id);
|
||||
var fileSystem = await GetFileSystemById(id);
|
||||
|
||||
if (server == null)
|
||||
throw new HttpApiException("No server with this id found", 404);
|
||||
|
||||
await server.FileSystem.Move(oldPath, newPath);
|
||||
await fileSystem.Move(oldPath, newPath);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}/files/delete")]
|
||||
public async Task Delete([FromRoute] int id, [FromQuery] string path)
|
||||
{
|
||||
var server = ServerService.GetServer(id);
|
||||
var fileSystem = await GetFileSystemById(id);
|
||||
|
||||
if (server == null)
|
||||
throw new HttpApiException("No server with this id found", 404);
|
||||
|
||||
await server.FileSystem.Delete(path);
|
||||
await fileSystem.Delete(path);
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/files/mkdir")]
|
||||
public async Task Mkdir([FromRoute] int id, [FromQuery] string path)
|
||||
{
|
||||
var server = ServerService.GetServer(id);
|
||||
var fileSystem = await GetFileSystemById(id);
|
||||
|
||||
if (server == null)
|
||||
throw new HttpApiException("No server with this id found", 404);
|
||||
|
||||
await server.FileSystem.Mkdir(path);
|
||||
await fileSystem.Mkdir(path);
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/files/compress")]
|
||||
public async Task Compress([FromRoute] int id, [FromBody] ServerFilesCompressRequest request)
|
||||
{
|
||||
var server = ServerService.GetServer(id);
|
||||
var fileSystem = await GetFileSystemById(id);
|
||||
|
||||
if (server == null)
|
||||
throw new HttpApiException("No server with this id found", 404);
|
||||
|
||||
await server.FileSystem.Compress(
|
||||
await fileSystem.Compress(
|
||||
request.Items,
|
||||
request.Destination,
|
||||
request.Type
|
||||
@@ -81,15 +68,24 @@ public class ServerFileSystemController : Controller
|
||||
[HttpPost("{id:int}/files/decompress")]
|
||||
public async Task Decompress([FromRoute] int id, [FromBody] ServerFilesDecompressRequest request)
|
||||
{
|
||||
var server = ServerService.GetServer(id);
|
||||
var fileSystem = await GetFileSystemById(id);
|
||||
|
||||
if (server == null)
|
||||
throw new HttpApiException("No server with this id found", 404);
|
||||
|
||||
await server.FileSystem.Decompress(
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
|
||||
@@ -21,44 +22,44 @@ public class ServerPowerController : Controller
|
||||
[HttpPost("{serverId:int}/start")]
|
||||
public async Task Start(int serverId)
|
||||
{
|
||||
var server = ServerService.GetServer(serverId);
|
||||
var server = ServerService.Find(serverId);
|
||||
|
||||
if (server == null)
|
||||
throw new HttpApiException("No server with this id found", 404);
|
||||
|
||||
await server.Start();
|
||||
await server.Trigger(ServerTrigger.Start);
|
||||
}
|
||||
|
||||
[HttpPost("{serverId:int}/stop")]
|
||||
public async Task Stop(int serverId)
|
||||
{
|
||||
var server = ServerService.GetServer(serverId);
|
||||
var server = ServerService.Find(serverId);
|
||||
|
||||
if (server == null)
|
||||
throw new HttpApiException("No server with this id found", 404);
|
||||
|
||||
await server.Stop();
|
||||
await server.Trigger(ServerTrigger.Stop);
|
||||
}
|
||||
|
||||
[HttpPost("{serverId:int}/install")]
|
||||
public async Task Install(int serverId)
|
||||
{
|
||||
var server = ServerService.GetServer(serverId);
|
||||
var server = ServerService.Find(serverId);
|
||||
|
||||
if (server == null)
|
||||
throw new HttpApiException("No server with this id found", 404);
|
||||
|
||||
await server.Install();
|
||||
await server.Trigger(ServerTrigger.Install);
|
||||
}
|
||||
|
||||
[HttpPost("{serverId:int}/kill")]
|
||||
public async Task Kill(int serverId)
|
||||
{
|
||||
var server = ServerService.GetServer(serverId);
|
||||
var server = ServerService.Find(serverId);
|
||||
|
||||
if (server == null)
|
||||
throw new HttpApiException("No server with this id found", 404);
|
||||
|
||||
await server.Kill();
|
||||
await server.Trigger(ServerTrigger.Kill);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
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.Responses.Servers;
|
||||
using MoonlightServers.DaemonShared.Enums;
|
||||
@@ -28,20 +29,20 @@ public class ServersController : Controller
|
||||
[HttpDelete("{serverId:int}")]
|
||||
public async Task Delete([FromRoute] int serverId)
|
||||
{
|
||||
await ServerService.Delete(serverId);
|
||||
//await ServerService.Delete(serverId);
|
||||
}
|
||||
|
||||
[HttpGet("{serverId:int}/status")]
|
||||
public Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
|
||||
{
|
||||
var server = ServerService.GetServer(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.State
|
||||
State = (ServerState)server.StateMachine.State
|
||||
};
|
||||
|
||||
return Task.FromResult(result);
|
||||
@@ -50,14 +51,17 @@ public class ServersController : Controller
|
||||
[HttpGet("{serverId:int}/logs")]
|
||||
public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId)
|
||||
{
|
||||
var server = ServerService.GetServer(serverId);
|
||||
var server = ServerService.Find(serverId);
|
||||
|
||||
if (server == null)
|
||||
throw new HttpApiException("No server with this id found", 404);
|
||||
|
||||
var consoleSubSystem = server.GetRequiredSubSystem<ConsoleSubSystem>();
|
||||
var messages = await consoleSubSystem.RetrieveCache();
|
||||
|
||||
return new ServerLogsResponse()
|
||||
{
|
||||
Messages = await server.GetConsoleMessages()
|
||||
Messages = messages
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using MoonCore.Exceptions;
|
||||
using MoonCore.Helpers;
|
||||
using MoonlightServers.Daemon.Configuration;
|
||||
using MoonlightServers.Daemon.Helpers;
|
||||
using MoonlightServers.Daemon.ServerSystem.SubSystems;
|
||||
using MoonlightServers.Daemon.Services;
|
||||
|
||||
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
||||
@@ -64,14 +64,18 @@ public class UploadController : Controller
|
||||
|
||||
#endregion
|
||||
|
||||
var server = ServerService.GetServer(serverId);
|
||||
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 server.FileSystem.CreateChunk(
|
||||
await fileSystem.CreateChunk(
|
||||
path,
|
||||
totalSize,
|
||||
positionToSkipTo,
|
||||
|
||||
183
MoonlightServers.Daemon/ServerSystem/Server.cs
Normal file
183
MoonlightServers.Daemon/ServerSystem/Server.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using MoonCore.Exceptions;
|
||||
using MoonlightServers.Daemon.Http.Hubs;
|
||||
using MoonlightServers.Daemon.Models.Cache;
|
||||
using Stateless;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public class Server : IAsyncDisposable
|
||||
{
|
||||
public ServerConfiguration Configuration { get; set; }
|
||||
public CancellationToken TaskCancellation => TaskCancellationSource.Token;
|
||||
internal StateMachine<ServerState, ServerTrigger> StateMachine { get; private set; }
|
||||
private CancellationTokenSource TaskCancellationSource;
|
||||
|
||||
private Dictionary<Type, ServerSubSystem> SubSystems = new();
|
||||
private ServerState InternalState = ServerState.Offline;
|
||||
|
||||
private readonly IHubContext<ServerWebSocketHub> HubContext;
|
||||
private readonly IServiceScope ServiceScope;
|
||||
private readonly ILoggerFactory LoggerFactory;
|
||||
private readonly ILogger Logger;
|
||||
|
||||
|
||||
public Server(
|
||||
ServerConfiguration configuration,
|
||||
IServiceScope serviceScope,
|
||||
IHubContext<ServerWebSocketHub> hubContext
|
||||
)
|
||||
{
|
||||
Configuration = configuration;
|
||||
ServiceScope = serviceScope;
|
||||
HubContext = hubContext;
|
||||
|
||||
TaskCancellationSource = new CancellationTokenSource();
|
||||
|
||||
LoggerFactory = serviceScope.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
Logger = LoggerFactory.CreateLogger($"Server {Configuration.Id}");
|
||||
|
||||
StateMachine = new StateMachine<ServerState, ServerTrigger>(
|
||||
() => InternalState,
|
||||
state => InternalState = state,
|
||||
FiringMode.Queued
|
||||
);
|
||||
|
||||
// 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)
|
||||
.Permit(ServerTrigger.Exited, ServerState.Offline);
|
||||
|
||||
// Configure task reset when server goes offline
|
||||
|
||||
StateMachine.Configure(ServerState.Offline)
|
||||
.OnEntryAsync(async () =>
|
||||
{
|
||||
if (!TaskCancellationSource.IsCancellationRequested)
|
||||
await TaskCancellationSource.CancelAsync();
|
||||
|
||||
TaskCancellationSource = new();
|
||||
});
|
||||
|
||||
// Setup websocket notify for state changes
|
||||
|
||||
StateMachine.OnTransitionedAsync(async transition =>
|
||||
{
|
||||
await HubContext.Clients
|
||||
.Group(Configuration.Id.ToString())
|
||||
.SendAsync("StateChanged", transition.Destination.ToString());
|
||||
});
|
||||
}
|
||||
|
||||
public async Task Initialize(Type[] subSystemTypes)
|
||||
{
|
||||
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
|
||||
{
|
||||
await SubSystems[type].Initialize();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("An unhandled error occured while initializing sub system {name}: {e}", type.Name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Trigger(ServerTrigger trigger)
|
||||
{
|
||||
if (!StateMachine.CanFire(trigger))
|
||||
throw new HttpApiException($"The trigger {trigger} is not supported during the state {StateMachine.State}", 400);
|
||||
|
||||
await StateMachine.FireAsync(trigger);
|
||||
}
|
||||
|
||||
public async Task Delete()
|
||||
{
|
||||
foreach (var subSystem in SubSystems.Values)
|
||||
await subSystem.Delete();
|
||||
}
|
||||
|
||||
// This method completely bypasses the state machine.
|
||||
// Using this method without any checks will lead to
|
||||
// broken server states. Use with caution
|
||||
public void OverrideState(ServerState state)
|
||||
{
|
||||
InternalState = state;
|
||||
}
|
||||
|
||||
public T? GetSubSystem<T>() where T : ServerSubSystem
|
||||
{
|
||||
var type = typeof(T);
|
||||
var subSystem = SubSystems.GetValueOrDefault(type);
|
||||
|
||||
if (subSystem == null)
|
||||
return null;
|
||||
|
||||
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()
|
||||
{
|
||||
if (!TaskCancellationSource.IsCancellationRequested)
|
||||
await TaskCancellationSource.CancelAsync();
|
||||
|
||||
foreach (var subSystem in SubSystems.Values)
|
||||
await subSystem.DisposeAsync();
|
||||
|
||||
ServiceScope.Dispose();
|
||||
}
|
||||
}
|
||||
10
MoonlightServers.Daemon/ServerSystem/ServerState.cs
Normal file
10
MoonlightServers.Daemon/ServerSystem/ServerState.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public enum ServerState
|
||||
{
|
||||
Offline = 0,
|
||||
Starting = 1,
|
||||
Online = 2,
|
||||
Stopping = 3,
|
||||
Installing = 4
|
||||
}
|
||||
27
MoonlightServers.Daemon/ServerSystem/ServerSubSystem.cs
Normal file
27
MoonlightServers.Daemon/ServerSystem/ServerSubSystem.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
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;
|
||||
}
|
||||
12
MoonlightServers.Daemon/ServerSystem/ServerTrigger.cs
Normal file
12
MoonlightServers.Daemon/ServerSystem/ServerTrigger.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public enum ServerTrigger
|
||||
{
|
||||
Start = 0,
|
||||
Stop = 1,
|
||||
Kill = 2,
|
||||
Install = 3,
|
||||
Exited = 4,
|
||||
OnlineDetected = 5,
|
||||
FailSafe = 6
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
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 async Task Attach(string containerId)
|
||||
{
|
||||
Stream = await DockerClient.Containers.AttachContainerAsync(containerId,
|
||||
true,
|
||||
new ContainerAttachParameters()
|
||||
{
|
||||
Stderr = true,
|
||||
Stdin = true,
|
||||
Stdout = true,
|
||||
Stream = true
|
||||
},
|
||||
Server.TaskCancellation
|
||||
);
|
||||
|
||||
// Reading
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (!Server.TaskCancellation.IsCancellationRequested)
|
||||
{
|
||||
var buffer = new byte[1024];
|
||||
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset stream so no further inputs will be piped to it
|
||||
Stream = null;
|
||||
|
||||
Logger.LogDebug("Disconnected from container stream");
|
||||
});
|
||||
}
|
||||
|
||||
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[38;5;16;48;5;135m\x1b[39m\x1b[1m 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
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 location exists
|
||||
|
||||
Logger.LogDebug("Ensuring storage");
|
||||
|
||||
var storageSubSystem = Server.GetRequiredSubSystem<StorageSubSystem>();
|
||||
|
||||
if (!await storageSubSystem.IsRuntimeVolumeReady())
|
||||
{
|
||||
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 = await storageSubSystem.GetRuntimeHostPath();
|
||||
|
||||
var installPath = await storageSubSystem.EnsureInstallVolume();
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
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. 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.IsRuntimeVolumeReady())
|
||||
{
|
||||
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 = await storageSubSystem.GetRuntimeHostPath();
|
||||
|
||||
// 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. 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
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using MoonCore.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 bool IsInitialized = false;
|
||||
|
||||
public StorageSubSystem(
|
||||
Server server,
|
||||
ILogger logger,
|
||||
AppConfiguration appConfiguration
|
||||
) : base(server, logger)
|
||||
{
|
||||
AppConfiguration = appConfiguration;
|
||||
}
|
||||
|
||||
public override async Task Initialize()
|
||||
{
|
||||
Logger.LogDebug("Lazy initializing server file system");
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await EnsureRuntimeVolume();
|
||||
var hostPath = await GetRuntimeHostPath();
|
||||
|
||||
SecureFileSystem = new(hostPath);
|
||||
ServerFileSystem = new(SecureFileSystem);
|
||||
|
||||
IsInitialized = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("An unhandled error occured while lazy initializing server file system: {e}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#region Runtime
|
||||
|
||||
public Task<ServerFileSystem> GetFileSystem()
|
||||
{
|
||||
if (!IsInitialized)
|
||||
throw new HttpApiException("The file system is still initializing. Please try again later", 503);
|
||||
|
||||
return Task.FromResult(ServerFileSystem);
|
||||
}
|
||||
|
||||
public Task<bool> IsRuntimeVolumeReady()
|
||||
{
|
||||
return Task.FromResult(IsInitialized);
|
||||
}
|
||||
|
||||
private async Task EnsureRuntimeVolume()
|
||||
{
|
||||
var path = await GetRuntimeHostPath();
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
Directory.CreateDirectory(path);
|
||||
|
||||
var consoleSubSystem = Server.GetRequiredSubSystem<ConsoleSubSystem>();
|
||||
|
||||
await consoleSubSystem.WriteMoonlight("Creating virtual disk file. Please be patient");
|
||||
await Task.Delay(TimeSpan.FromSeconds(8));
|
||||
|
||||
await consoleSubSystem.WriteMoonlight("Formatting virtual disk. This can take a bit");
|
||||
await Task.Delay(TimeSpan.FromSeconds(8));
|
||||
|
||||
await consoleSubSystem.WriteMoonlight("Mounting virtual disk. Please be patient");
|
||||
await Task.Delay(TimeSpan.FromSeconds(3));
|
||||
|
||||
await consoleSubSystem.WriteMoonlight("Virtual disk ready");
|
||||
|
||||
// TODO: Implement virtual disk
|
||||
}
|
||||
|
||||
public Task<string> GetRuntimeHostPath()
|
||||
{
|
||||
var path = Path.Combine(
|
||||
AppConfiguration.Storage.Volumes,
|
||||
Configuration.Id.ToString()
|
||||
);
|
||||
|
||||
if (!path.StartsWith('/'))
|
||||
path = Path.Combine(Directory.GetCurrentDirectory(), path);
|
||||
|
||||
return Task.FromResult(path);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Installation
|
||||
|
||||
public async Task<string> EnsureInstallVolume()
|
||||
{
|
||||
var path = await GetInstallHostPath();
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
Directory.CreateDirectory(path);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public Task<string> GetInstallHostPath()
|
||||
{
|
||||
var path = Path.Combine(
|
||||
AppConfiguration.Storage.Install,
|
||||
Configuration.Id.ToString()
|
||||
);
|
||||
|
||||
if (!path.StartsWith('/'))
|
||||
path = Path.Combine(Directory.GetCurrentDirectory(), path);
|
||||
|
||||
return Task.FromResult(path);
|
||||
}
|
||||
|
||||
public async Task DeleteInstallVolume()
|
||||
{
|
||||
var path = await GetInstallHostPath();
|
||||
|
||||
if(!Directory.Exists(path))
|
||||
return;
|
||||
|
||||
Directory.Delete(path, true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override ValueTask DisposeAsync()
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
if(!SecureFileSystem.IsDisposed)
|
||||
SecureFileSystem.Dispose();
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ public class DockerImageService
|
||||
private readonly AppConfiguration Configuration;
|
||||
private readonly ILogger<DockerImageService> Logger;
|
||||
|
||||
private readonly Dictionary<string, TaskCompletionSource> PendingDownloads = new();
|
||||
|
||||
public DockerImageService(
|
||||
DockerClient dockerClient,
|
||||
ILogger<DockerImageService> logger,
|
||||
@@ -23,16 +25,31 @@ public class DockerImageService
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public async Task Ensure(string name, Action<string>? onProgressUpdated)
|
||||
public async Task Download(string name, Action<string>? onProgressUpdated = null)
|
||||
{
|
||||
// If there is already a download for this image occuring, we want to wait for this to complete instead
|
||||
// of calling docker to download it again
|
||||
if (PendingDownloads.TryGetValue(name, out var downloadTaskCompletion))
|
||||
{
|
||||
await downloadTaskCompletion.Task;
|
||||
return;
|
||||
}
|
||||
|
||||
var tsc = new TaskCompletionSource();
|
||||
PendingDownloads.Add(name, tsc);
|
||||
|
||||
try
|
||||
{
|
||||
// Figure out if and which credentials to use by checking for the domain
|
||||
AuthConfig credentials = new();
|
||||
|
||||
var domain = GetDomainFromDockerImageName(name);
|
||||
|
||||
var configuredCredentials = Configuration.Docker.Credentials
|
||||
.FirstOrDefault(x => x.Domain.Equals(domain, StringComparison.InvariantCultureIgnoreCase));
|
||||
var configuredCredentials = Configuration.Docker.Credentials.FirstOrDefault(x =>
|
||||
x.Domain.Equals(domain, StringComparison.InvariantCultureIgnoreCase)
|
||||
);
|
||||
|
||||
// Apply credentials configuration if specified
|
||||
if (configuredCredentials != null)
|
||||
{
|
||||
credentials.Username = configuredCredentials.Username;
|
||||
@@ -59,6 +76,19 @@ public class DockerImageService
|
||||
onProgressUpdated.Invoke(line);
|
||||
})
|
||||
);
|
||||
|
||||
tsc.SetResult();
|
||||
PendingDownloads.Remove(name);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("An error occured while download image {name}: {e}", name, e);
|
||||
|
||||
tsc.SetException(e);
|
||||
PendingDownloads.Remove(name);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetDomainFromDockerImageName(string name) // Method names are my passion ;)
|
||||
@@ -69,7 +99,8 @@ public class DockerImageService
|
||||
// If it has 2 parts -> usually "user/image" (e.g., "library/ubuntu")
|
||||
// If it has 3 or more -> assume first part is the registry domain
|
||||
|
||||
if (nameParts.Length >= 3 || (nameParts.Length >= 2 && nameParts[0].Contains('.') || nameParts[0].Contains(':')))
|
||||
if (nameParts.Length >= 3 ||
|
||||
(nameParts.Length >= 2 && nameParts[0].Contains('.') || nameParts[0].Contains(':')))
|
||||
return nameParts[0]; // Registry domain is explicitly specified
|
||||
|
||||
return "docker.io"; // Default Docker registry
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
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.Abstractions;
|
||||
using MoonlightServers.Daemon.Configuration;
|
||||
using MoonlightServers.Daemon.Enums;
|
||||
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;
|
||||
@@ -17,156 +16,254 @@ namespace MoonlightServers.Daemon.Services;
|
||||
[Singleton]
|
||||
public class ServerService : IHostedLifecycleService
|
||||
{
|
||||
private readonly List<Server> Servers = new();
|
||||
private readonly ILogger<ServerService> Logger;
|
||||
private readonly Dictionary<int, Server> Servers = new();
|
||||
|
||||
private readonly RemoteService RemoteService;
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly IServiceProvider ServiceProvider;
|
||||
private readonly ILoggerFactory LoggerFactory;
|
||||
private readonly IHubContext<ServerWebSocketHub> WebSocketHub;
|
||||
private readonly AppConfiguration Configuration;
|
||||
private CancellationTokenSource Cancellation = new();
|
||||
private bool IsInitialized = false;
|
||||
private readonly CancellationTokenSource TaskCancellation;
|
||||
private readonly ILogger<ServerService> Logger;
|
||||
private readonly IHubContext<ServerWebSocketHub> HubContext;
|
||||
|
||||
public ServerService(
|
||||
RemoteService remoteService,
|
||||
ILogger<ServerService> logger,
|
||||
IServiceProvider serviceProvider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IHubContext<ServerWebSocketHub> webSocketHub,
|
||||
AppConfiguration configuration
|
||||
DockerClient dockerClient,
|
||||
ILogger<ServerService> logger,
|
||||
IHubContext<ServerWebSocketHub> hubContext
|
||||
)
|
||||
{
|
||||
RemoteService = remoteService;
|
||||
Logger = logger;
|
||||
ServiceProvider = serviceProvider;
|
||||
LoggerFactory = loggerFactory;
|
||||
WebSocketHub = webSocketHub;
|
||||
Configuration = configuration;
|
||||
DockerClient = dockerClient;
|
||||
Logger = logger;
|
||||
HubContext = hubContext;
|
||||
|
||||
TaskCancellation = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
public async Task Initialize() //TODO: Add initialize call from panel
|
||||
public async Task Sync(int serverId)
|
||||
{
|
||||
if (IsInitialized)
|
||||
if (Servers.TryGetValue(serverId, out var server))
|
||||
{
|
||||
Logger.LogWarning("Ignoring initialize call: Already initialized");
|
||||
return;
|
||||
var serverData = await RemoteService.GetServer(serverId);
|
||||
var configuration = serverData.ToServerConfiguration();
|
||||
|
||||
server.Configuration = configuration;
|
||||
}
|
||||
else
|
||||
IsInitialized = true;
|
||||
await Initialize(serverId);
|
||||
}
|
||||
|
||||
// Loading models and converting them
|
||||
Logger.LogInformation("Fetching servers from panel");
|
||||
public async Task Sync(int serverId, ServerConfiguration configuration)
|
||||
{
|
||||
if (Servers.TryGetValue(serverId, out var server))
|
||||
server.Configuration = configuration;
|
||||
else
|
||||
await Initialize(serverId);
|
||||
}
|
||||
|
||||
var servers = await PagedData<ServerDataResponse>.All(async (page, pageSize) =>
|
||||
await RemoteService.GetServers(page, pageSize)
|
||||
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
|
||||
);
|
||||
|
||||
var configurations = servers
|
||||
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();
|
||||
|
||||
Logger.LogInformation("Initializing {count} servers", servers.Length);
|
||||
|
||||
await InitializeServerRange(configurations); // TODO: Initialize them multi threaded (maybe)
|
||||
|
||||
// Attach to docker events
|
||||
await AttachToDockerEvents();
|
||||
}
|
||||
|
||||
public async Task Stop()
|
||||
{
|
||||
Server[] servers;
|
||||
|
||||
lock (Servers)
|
||||
servers = Servers.ToArray();
|
||||
|
||||
//
|
||||
Logger.LogTrace("Canceling server tasks and disconnecting storage");
|
||||
|
||||
foreach (var server in servers)
|
||||
foreach (var configuration in configurations)
|
||||
{
|
||||
try
|
||||
{
|
||||
await server.CancelTasks();
|
||||
await server.DestroyStorage();
|
||||
await Initialize(configuration);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogCritical(
|
||||
"An unhandled error occured while stopping the server management for server {id}: {e}",
|
||||
server.Id,
|
||||
Logger.LogError(
|
||||
"An unhandled error occured while initializing server {id}: {e}",
|
||||
configuration.Id,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
Logger.LogTrace("Canceling own tasks");
|
||||
await Cancellation.CancelAsync();
|
||||
}
|
||||
|
||||
private Task AttachToDockerEvents()
|
||||
public async Task Initialize(int serverId)
|
||||
{
|
||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||
var serverData = await RemoteService.GetServer(serverId);
|
||||
var configuration = serverData.ToServerConfiguration();
|
||||
|
||||
await Initialize(configuration);
|
||||
}
|
||||
|
||||
public Server? Find(int serverId)
|
||||
=> Servers.GetValueOrDefault(serverId);
|
||||
|
||||
public async Task Initialize(ServerConfiguration configuration)
|
||||
{
|
||||
var serverScope = ServiceProvider.CreateScope();
|
||||
|
||||
var server = new Server(configuration, serverScope, HubContext);
|
||||
|
||||
Type[] subSystems =
|
||||
[
|
||||
typeof(ProvisionSubSystem),
|
||||
typeof(StorageSubSystem),
|
||||
typeof(DebugSubSystem),
|
||||
typeof(ShutdownSubSystem),
|
||||
typeof(ConsoleSubSystem),
|
||||
typeof(RestoreSubSystem),
|
||||
typeof(OnlineDetectionService),
|
||||
typeof(InstallationSubSystem)
|
||||
];
|
||||
|
||||
await server.Initialize(subSystems);
|
||||
|
||||
Servers[configuration.Id] = server;
|
||||
}
|
||||
|
||||
#region Docker Monitoring
|
||||
|
||||
private async Task MonitorContainers()
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// This lets the event monitor restart
|
||||
while (!Cancellation.Token.IsCancellationRequested)
|
||||
// Restart unless shutdown is requested
|
||||
while (!TaskCancellation.Token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogTrace("Attached to docker events");
|
||||
Logger.LogTrace("Starting to monitor events");
|
||||
|
||||
await dockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(),
|
||||
await DockerClient.System.MonitorEventsAsync(new(),
|
||||
new Progress<Message>(async message =>
|
||||
{
|
||||
// Filter out unwanted events
|
||||
if (message.Action != "die")
|
||||
return;
|
||||
|
||||
Server? server;
|
||||
// TODO: Implement a cached lookup using a shared dictionary by the sub system
|
||||
|
||||
// TODO: Maybe implement a lookup for containers which id isn't set in the cache
|
||||
|
||||
// Check if it's a runtime container
|
||||
lock (Servers)
|
||||
server = Servers.FirstOrDefault(x => x.RuntimeContainerId == message.ID);
|
||||
|
||||
if (server != null)
|
||||
var server = Servers.Values.FirstOrDefault(serverToCheck =>
|
||||
{
|
||||
await server.NotifyRuntimeContainerDied();
|
||||
return;
|
||||
}
|
||||
var provisionSubSystem = serverToCheck.GetRequiredSubSystem<ProvisionSubSystem>();
|
||||
|
||||
// Check if it's an installation container
|
||||
lock (Servers)
|
||||
server = Servers.FirstOrDefault(x => x.InstallationContainerId == message.ID);
|
||||
if (provisionSubSystem.CurrentContainerId == message.ID)
|
||||
return true;
|
||||
|
||||
if (server != null)
|
||||
{
|
||||
await server.NotifyInstallationContainerDied();
|
||||
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;
|
||||
}
|
||||
}), Cancellation.Token);
|
||||
|
||||
await server.StateMachine.FireAsync(ServerTrigger.Exited);
|
||||
}), TaskCancellation.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
} // Can be ignored
|
||||
// Can be ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("An unhandled error occured while attaching to docker events: {e}", e);
|
||||
Logger.LogError("An unhandled error occured while monitoring events: {e}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task InitializeServerRange(ServerConfiguration[] serverConfigurations)
|
||||
{
|
||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||
#endregion
|
||||
|
||||
var existingContainers = await dockerClient.Containers.ListContainersAsync(new()
|
||||
#region Lifetime
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public async Task StartedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await MonitorContainers();
|
||||
|
||||
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,
|
||||
@@ -184,48 +281,9 @@ public class ServerService : IHostedLifecycleService
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var configuration in serverConfigurations)
|
||||
await InitializeServer(configuration, existingContainers);
|
||||
}
|
||||
|
||||
public async Task<Server> InitializeServer(
|
||||
ServerConfiguration serverConfiguration,
|
||||
IList<ContainerListResponse> existingContainers
|
||||
)
|
||||
{
|
||||
Logger.LogTrace("Initializing server '{id}'", serverConfiguration.Id);
|
||||
|
||||
var server = new Server(
|
||||
LoggerFactory.CreateLogger($"Server {serverConfiguration.Id}"),
|
||||
ServiceProvider,
|
||||
serverConfiguration,
|
||||
WebSocketHub,
|
||||
Configuration
|
||||
);
|
||||
|
||||
await server.Initialize(existingContainers);
|
||||
|
||||
lock (Servers)
|
||||
Servers.Add(server);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
public async Task Sync(int serverId)
|
||||
{
|
||||
var serverData = await RemoteService.GetServer(serverId);
|
||||
var serverConfiguration = serverData.ToServerConfiguration();
|
||||
|
||||
var server = GetServer(serverId);
|
||||
|
||||
if (server == null)
|
||||
await InitializeServer(serverConfiguration, []);
|
||||
else
|
||||
server.UpdateConfiguration(serverConfiguration);
|
||||
}
|
||||
|
||||
public async Task Delete(int serverId)
|
||||
*
|
||||
*
|
||||
*public async Task Delete(int serverId)
|
||||
{
|
||||
var server = GetServer(serverId);
|
||||
|
||||
@@ -274,50 +332,6 @@ public class ServerService : IHostedLifecycleService
|
||||
else
|
||||
await DeleteServer();
|
||||
}
|
||||
|
||||
public Server? GetServer(int id)
|
||||
{
|
||||
lock (Servers)
|
||||
return Servers.FirstOrDefault(x => x.Id == id);
|
||||
}
|
||||
|
||||
#region Lifecycle
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public async Task StartedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Initialize();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogCritical("Unable to initialize servers. Is the panel online? Error: {e}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartingAsync(CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task StoppedAsync(CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public async Task StoppingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Stop();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogCritical("Unable to stop server handling: {e}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
*
|
||||
*/
|
||||
}
|
||||
@@ -11,6 +11,7 @@ using MoonCore.Helpers;
|
||||
using MoonlightServers.Daemon.Configuration;
|
||||
using MoonlightServers.Daemon.Helpers;
|
||||
using MoonlightServers.Daemon.Http.Hubs;
|
||||
using MoonlightServers.Daemon.ServerSystem;
|
||||
using MoonlightServers.Daemon.Services;
|
||||
|
||||
namespace MoonlightServers.Daemon;
|
||||
|
||||
Reference in New Issue
Block a user