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.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonlightServers.Daemon.Configuration;
|
using MoonlightServers.Daemon.ServerSystem.SubSystems;
|
||||||
using MoonlightServers.Daemon.Services;
|
using MoonlightServers.Daemon.Services;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
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 serverId = int.Parse(User.Claims.First(x => x.Type == "serverId").Value);
|
||||||
var path = User.Claims.First(x => x.Type == "path").Value;
|
var path = User.Claims.First(x => x.Type == "path").Value;
|
||||||
|
|
||||||
var server = ServerService.GetServer(serverId);
|
var server = ServerService.Find(serverId);
|
||||||
|
|
||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
await server.FileSystem.Read(path,
|
var storageSubSystem = server.GetRequiredSubSystem<StorageSubSystem>();
|
||||||
async dataStream => { await Results.File(dataStream).ExecuteAsync(HttpContext); });
|
|
||||||
|
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.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
|
using MoonlightServers.Daemon.Helpers;
|
||||||
|
using MoonlightServers.Daemon.ServerSystem.SubSystems;
|
||||||
using MoonlightServers.Daemon.Services;
|
using MoonlightServers.Daemon.Services;
|
||||||
using MoonlightServers.DaemonShared.DaemonSide.Http.Requests;
|
using MoonlightServers.DaemonShared.DaemonSide.Http.Requests;
|
||||||
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
||||||
@@ -22,56 +24,41 @@ public class ServerFileSystemController : Controller
|
|||||||
[HttpGet("{id:int}/files/list")]
|
[HttpGet("{id:int}/files/list")]
|
||||||
public async Task<ServerFileSystemResponse[]> List([FromRoute] int id, [FromQuery] string path = "")
|
public async Task<ServerFileSystemResponse[]> List([FromRoute] int id, [FromQuery] string path = "")
|
||||||
{
|
{
|
||||||
var server = ServerService.GetServer(id);
|
var fileSystem = await GetFileSystemById(id);
|
||||||
|
|
||||||
if (server == null)
|
return await fileSystem.List(path);
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
|
||||||
|
|
||||||
return await server.FileSystem.List(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id:int}/files/move")]
|
[HttpPost("{id:int}/files/move")]
|
||||||
public async Task Move([FromRoute] int id, [FromQuery] string oldPath, [FromQuery] string newPath)
|
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)
|
await fileSystem.Move(oldPath, newPath);
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
|
||||||
|
|
||||||
await server.FileSystem.Move(oldPath, newPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:int}/files/delete")]
|
[HttpDelete("{id:int}/files/delete")]
|
||||||
public async Task Delete([FromRoute] int id, [FromQuery] string path)
|
public async Task Delete([FromRoute] int id, [FromQuery] string path)
|
||||||
{
|
{
|
||||||
var server = ServerService.GetServer(id);
|
var fileSystem = await GetFileSystemById(id);
|
||||||
|
|
||||||
if (server == null)
|
await fileSystem.Delete(path);
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
|
||||||
|
|
||||||
await server.FileSystem.Delete(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id:int}/files/mkdir")]
|
[HttpPost("{id:int}/files/mkdir")]
|
||||||
public async Task Mkdir([FromRoute] int id, [FromQuery] string path)
|
public async Task Mkdir([FromRoute] int id, [FromQuery] string path)
|
||||||
{
|
{
|
||||||
var server = ServerService.GetServer(id);
|
var fileSystem = await GetFileSystemById(id);
|
||||||
|
|
||||||
if (server == null)
|
await fileSystem.Mkdir(path);
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
|
||||||
|
|
||||||
await server.FileSystem.Mkdir(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id:int}/files/compress")]
|
[HttpPost("{id:int}/files/compress")]
|
||||||
public async Task Compress([FromRoute] int id, [FromBody] ServerFilesCompressRequest request)
|
public async Task Compress([FromRoute] int id, [FromBody] ServerFilesCompressRequest request)
|
||||||
{
|
{
|
||||||
var server = ServerService.GetServer(id);
|
var fileSystem = await GetFileSystemById(id);
|
||||||
|
|
||||||
if (server == null)
|
await fileSystem.Compress(
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
|
||||||
|
|
||||||
await server.FileSystem.Compress(
|
|
||||||
request.Items,
|
request.Items,
|
||||||
request.Destination,
|
request.Destination,
|
||||||
request.Type
|
request.Type
|
||||||
@@ -81,15 +68,24 @@ public class ServerFileSystemController : Controller
|
|||||||
[HttpPost("{id:int}/files/decompress")]
|
[HttpPost("{id:int}/files/decompress")]
|
||||||
public async Task Decompress([FromRoute] int id, [FromBody] ServerFilesDecompressRequest request)
|
public async Task Decompress([FromRoute] int id, [FromBody] ServerFilesDecompressRequest request)
|
||||||
{
|
{
|
||||||
var server = ServerService.GetServer(id);
|
var fileSystem = await GetFileSystemById(id);
|
||||||
|
|
||||||
if (server == null)
|
await fileSystem.Decompress(
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
|
||||||
|
|
||||||
await server.FileSystem.Decompress(
|
|
||||||
request.Path,
|
request.Path,
|
||||||
request.Destination,
|
request.Destination,
|
||||||
request.Type
|
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 MoonCore.Exceptions;
|
||||||
using MoonlightServers.Daemon.Enums;
|
using MoonlightServers.Daemon.Enums;
|
||||||
using MoonlightServers.Daemon.Services;
|
using MoonlightServers.Daemon.Services;
|
||||||
|
using ServerTrigger = MoonlightServers.Daemon.ServerSystem.ServerTrigger;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
||||||
|
|
||||||
@@ -21,44 +22,44 @@ public class ServerPowerController : Controller
|
|||||||
[HttpPost("{serverId:int}/start")]
|
[HttpPost("{serverId:int}/start")]
|
||||||
public async Task Start(int serverId)
|
public async Task Start(int serverId)
|
||||||
{
|
{
|
||||||
var server = ServerService.GetServer(serverId);
|
var server = ServerService.Find(serverId);
|
||||||
|
|
||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
await server.Start();
|
await server.Trigger(ServerTrigger.Start);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{serverId:int}/stop")]
|
[HttpPost("{serverId:int}/stop")]
|
||||||
public async Task Stop(int serverId)
|
public async Task Stop(int serverId)
|
||||||
{
|
{
|
||||||
var server = ServerService.GetServer(serverId);
|
var server = ServerService.Find(serverId);
|
||||||
|
|
||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
await server.Stop();
|
await server.Trigger(ServerTrigger.Stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{serverId:int}/install")]
|
[HttpPost("{serverId:int}/install")]
|
||||||
public async Task Install(int serverId)
|
public async Task Install(int serverId)
|
||||||
{
|
{
|
||||||
var server = ServerService.GetServer(serverId);
|
var server = ServerService.Find(serverId);
|
||||||
|
|
||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
await server.Install();
|
await server.Trigger(ServerTrigger.Install);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{serverId:int}/kill")]
|
[HttpPost("{serverId:int}/kill")]
|
||||||
public async Task Kill(int serverId)
|
public async Task Kill(int serverId)
|
||||||
{
|
{
|
||||||
var server = ServerService.GetServer(serverId);
|
var server = ServerService.Find(serverId);
|
||||||
|
|
||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
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.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
|
using MoonlightServers.Daemon.ServerSystem.SubSystems;
|
||||||
using MoonlightServers.Daemon.Services;
|
using MoonlightServers.Daemon.Services;
|
||||||
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
||||||
using MoonlightServers.DaemonShared.Enums;
|
using MoonlightServers.DaemonShared.Enums;
|
||||||
@@ -28,20 +29,20 @@ public class ServersController : Controller
|
|||||||
[HttpDelete("{serverId:int}")]
|
[HttpDelete("{serverId:int}")]
|
||||||
public async Task Delete([FromRoute] int serverId)
|
public async Task Delete([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
await ServerService.Delete(serverId);
|
//await ServerService.Delete(serverId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{serverId:int}/status")]
|
[HttpGet("{serverId:int}/status")]
|
||||||
public Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
|
public Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = ServerService.GetServer(serverId);
|
var server = ServerService.Find(serverId);
|
||||||
|
|
||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
var result = new ServerStatusResponse()
|
var result = new ServerStatusResponse()
|
||||||
{
|
{
|
||||||
State = (ServerState)server.State
|
State = (ServerState)server.StateMachine.State
|
||||||
};
|
};
|
||||||
|
|
||||||
return Task.FromResult(result);
|
return Task.FromResult(result);
|
||||||
@@ -50,14 +51,17 @@ public class ServersController : Controller
|
|||||||
[HttpGet("{serverId:int}/logs")]
|
[HttpGet("{serverId:int}/logs")]
|
||||||
public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId)
|
public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId)
|
||||||
{
|
{
|
||||||
var server = ServerService.GetServer(serverId);
|
var server = ServerService.Find(serverId);
|
||||||
|
|
||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
|
var consoleSubSystem = server.GetRequiredSubSystem<ConsoleSubSystem>();
|
||||||
|
var messages = await consoleSubSystem.RetrieveCache();
|
||||||
|
|
||||||
return new ServerLogsResponse()
|
return new ServerLogsResponse()
|
||||||
{
|
{
|
||||||
Messages = await server.GetConsoleMessages()
|
Messages = messages
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonlightServers.Daemon.Configuration;
|
using MoonlightServers.Daemon.Configuration;
|
||||||
using MoonlightServers.Daemon.Helpers;
|
using MoonlightServers.Daemon.ServerSystem.SubSystems;
|
||||||
using MoonlightServers.Daemon.Services;
|
using MoonlightServers.Daemon.Services;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
||||||
@@ -64,14 +64,18 @@ public class UploadController : Controller
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
var server = ServerService.GetServer(serverId);
|
var server = ServerService.Find(serverId);
|
||||||
|
|
||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
|
var storageSubSystem = server.GetRequiredSubSystem<StorageSubSystem>();
|
||||||
|
|
||||||
|
var fileSystem = await storageSubSystem.GetFileSystem();
|
||||||
|
|
||||||
var dataStream = file.OpenReadStream();
|
var dataStream = file.OpenReadStream();
|
||||||
|
|
||||||
await server.FileSystem.CreateChunk(
|
await fileSystem.CreateChunk(
|
||||||
path,
|
path,
|
||||||
totalSize,
|
totalSize,
|
||||||
positionToSkipTo,
|
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 AppConfiguration Configuration;
|
||||||
private readonly ILogger<DockerImageService> Logger;
|
private readonly ILogger<DockerImageService> Logger;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, TaskCompletionSource> PendingDownloads = new();
|
||||||
|
|
||||||
public DockerImageService(
|
public DockerImageService(
|
||||||
DockerClient dockerClient,
|
DockerClient dockerClient,
|
||||||
ILogger<DockerImageService> logger,
|
ILogger<DockerImageService> logger,
|
||||||
@@ -23,55 +25,84 @@ public class DockerImageService
|
|||||||
Logger = logger;
|
Logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Ensure(string name, Action<string>? onProgressUpdated)
|
public async Task Download(string name, Action<string>? onProgressUpdated = null)
|
||||||
{
|
{
|
||||||
// Figure out if and which credentials to use by checking for the domain
|
// If there is already a download for this image occuring, we want to wait for this to complete instead
|
||||||
AuthConfig credentials = new();
|
// of calling docker to download it again
|
||||||
|
if (PendingDownloads.TryGetValue(name, out var downloadTaskCompletion))
|
||||||
var domain = GetDomainFromDockerImageName(name);
|
|
||||||
|
|
||||||
var configuredCredentials = Configuration.Docker.Credentials
|
|
||||||
.FirstOrDefault(x => x.Domain.Equals(domain, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
|
|
||||||
if (configuredCredentials != null)
|
|
||||||
{
|
{
|
||||||
credentials.Username = configuredCredentials.Username;
|
await downloadTaskCompletion.Task;
|
||||||
credentials.Password = configuredCredentials.Password;
|
return;
|
||||||
credentials.Email = configuredCredentials.Email;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we want to pull the image
|
var tsc = new TaskCompletionSource();
|
||||||
await DockerClient.Images.CreateImageAsync(new()
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply credentials configuration if specified
|
||||||
|
if (configuredCredentials != null)
|
||||||
{
|
{
|
||||||
FromImage = name
|
credentials.Username = configuredCredentials.Username;
|
||||||
},
|
credentials.Password = configuredCredentials.Password;
|
||||||
credentials,
|
credentials.Email = configuredCredentials.Email;
|
||||||
new Progress<JSONMessage>(async message =>
|
}
|
||||||
{
|
|
||||||
if (message.Progress == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var line = $"[{message.ID}] {message.ProgressMessage}";
|
// Now we want to pull the image
|
||||||
|
await DockerClient.Images.CreateImageAsync(new()
|
||||||
|
{
|
||||||
|
FromImage = name
|
||||||
|
},
|
||||||
|
credentials,
|
||||||
|
new Progress<JSONMessage>(async message =>
|
||||||
|
{
|
||||||
|
if (message.Progress == null)
|
||||||
|
return;
|
||||||
|
|
||||||
Logger.LogDebug("{line}", line);
|
var line = $"[{message.ID}] {message.ProgressMessage}";
|
||||||
|
|
||||||
if (onProgressUpdated != null)
|
Logger.LogDebug("{line}", line);
|
||||||
onProgressUpdated.Invoke(line);
|
|
||||||
})
|
if (onProgressUpdated != null)
|
||||||
);
|
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 ;)
|
private string GetDomainFromDockerImageName(string name) // Method names are my passion ;)
|
||||||
{
|
{
|
||||||
var nameParts = name.Split("/");
|
var nameParts = name.Split("/");
|
||||||
|
|
||||||
// If it has 1 part -> just the image name (e.g., "ubuntu")
|
// If it has 1 part -> just the image name (e.g., "ubuntu")
|
||||||
// If it has 2 parts -> usually "user/image" (e.g., "library/ubuntu")
|
// 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 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 nameParts[0]; // Registry domain is explicitly specified
|
||||||
|
|
||||||
return "docker.io"; // Default Docker registry
|
return "docker.io"; // Default Docker registry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
using Docker.DotNet;
|
using Docker.DotNet;
|
||||||
using Docker.DotNet.Models;
|
using Docker.DotNet.Models;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using MoonCore.Attributes;
|
using MoonCore.Attributes;
|
||||||
using MoonCore.Exceptions;
|
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using MoonlightServers.Daemon.Abstractions;
|
|
||||||
using MoonlightServers.Daemon.Configuration;
|
|
||||||
using MoonlightServers.Daemon.Enums;
|
|
||||||
using MoonlightServers.Daemon.Extensions;
|
using MoonlightServers.Daemon.Extensions;
|
||||||
using MoonlightServers.Daemon.Http.Hubs;
|
using MoonlightServers.Daemon.Http.Hubs;
|
||||||
using MoonlightServers.Daemon.Models.Cache;
|
using MoonlightServers.Daemon.Models.Cache;
|
||||||
|
using MoonlightServers.Daemon.ServerSystem;
|
||||||
|
using MoonlightServers.Daemon.ServerSystem.SubSystems;
|
||||||
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
|
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Services;
|
namespace MoonlightServers.Daemon.Services;
|
||||||
@@ -17,271 +16,222 @@ namespace MoonlightServers.Daemon.Services;
|
|||||||
[Singleton]
|
[Singleton]
|
||||||
public class ServerService : IHostedLifecycleService
|
public class ServerService : IHostedLifecycleService
|
||||||
{
|
{
|
||||||
private readonly List<Server> Servers = new();
|
private readonly Dictionary<int, Server> Servers = new();
|
||||||
private readonly ILogger<ServerService> Logger;
|
|
||||||
private readonly RemoteService RemoteService;
|
private readonly RemoteService RemoteService;
|
||||||
|
private readonly DockerClient DockerClient;
|
||||||
private readonly IServiceProvider ServiceProvider;
|
private readonly IServiceProvider ServiceProvider;
|
||||||
private readonly ILoggerFactory LoggerFactory;
|
private readonly CancellationTokenSource TaskCancellation;
|
||||||
private readonly IHubContext<ServerWebSocketHub> WebSocketHub;
|
private readonly ILogger<ServerService> Logger;
|
||||||
private readonly AppConfiguration Configuration;
|
private readonly IHubContext<ServerWebSocketHub> HubContext;
|
||||||
private CancellationTokenSource Cancellation = new();
|
|
||||||
private bool IsInitialized = false;
|
|
||||||
|
|
||||||
public ServerService(
|
public ServerService(
|
||||||
RemoteService remoteService,
|
RemoteService remoteService,
|
||||||
ILogger<ServerService> logger,
|
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
ILoggerFactory loggerFactory,
|
DockerClient dockerClient,
|
||||||
IHubContext<ServerWebSocketHub> webSocketHub,
|
ILogger<ServerService> logger,
|
||||||
AppConfiguration configuration
|
IHubContext<ServerWebSocketHub> hubContext
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
RemoteService = remoteService;
|
RemoteService = remoteService;
|
||||||
Logger = logger;
|
|
||||||
ServiceProvider = serviceProvider;
|
ServiceProvider = serviceProvider;
|
||||||
LoggerFactory = loggerFactory;
|
DockerClient = dockerClient;
|
||||||
WebSocketHub = webSocketHub;
|
Logger = logger;
|
||||||
Configuration = configuration;
|
HubContext = hubContext;
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Initialize() //TODO: Add initialize call from panel
|
TaskCancellation = new CancellationTokenSource();
|
||||||
{
|
|
||||||
if (IsInitialized)
|
|
||||||
{
|
|
||||||
Logger.LogWarning("Ignoring initialize call: Already initialized");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
IsInitialized = true;
|
|
||||||
|
|
||||||
// Loading models and converting them
|
|
||||||
Logger.LogInformation("Fetching servers from panel");
|
|
||||||
|
|
||||||
var servers = await PagedData<ServerDataResponse>.All(async (page, pageSize) =>
|
|
||||||
await RemoteService.GetServers(page, pageSize)
|
|
||||||
);
|
|
||||||
|
|
||||||
var configurations = servers
|
|
||||||
.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)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await server.CancelTasks();
|
|
||||||
await server.DestroyStorage();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogCritical(
|
|
||||||
"An unhandled error occured while stopping the server management for server {id}: {e}",
|
|
||||||
server.Id,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
Logger.LogTrace("Canceling own tasks");
|
|
||||||
await Cancellation.CancelAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task AttachToDockerEvents()
|
|
||||||
{
|
|
||||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
|
||||||
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
|
||||||
// This lets the event monitor restart
|
|
||||||
while (!Cancellation.Token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Logger.LogTrace("Attached to docker events");
|
|
||||||
|
|
||||||
await dockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(),
|
|
||||||
new Progress<Message>(async message =>
|
|
||||||
{
|
|
||||||
if (message.Action != "die")
|
|
||||||
return;
|
|
||||||
|
|
||||||
Server? server;
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
{
|
|
||||||
await server.NotifyRuntimeContainerDied();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's an installation container
|
|
||||||
lock (Servers)
|
|
||||||
server = Servers.FirstOrDefault(x => x.InstallationContainerId == message.ID);
|
|
||||||
|
|
||||||
if (server != null)
|
|
||||||
{
|
|
||||||
await server.NotifyInstallationContainerDied();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}), Cancellation.Token);
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
} // Can be ignored
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogError("An unhandled error occured while attaching to docker events: {e}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InitializeServerRange(ServerConfiguration[] serverConfigurations)
|
|
||||||
{
|
|
||||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
|
||||||
|
|
||||||
var existingContainers = await dockerClient.Containers.ListContainersAsync(new()
|
|
||||||
{
|
|
||||||
All = true,
|
|
||||||
Limit = null,
|
|
||||||
Filters = new Dictionary<string, IDictionary<string, bool>>()
|
|
||||||
{
|
|
||||||
{
|
|
||||||
"label",
|
|
||||||
new Dictionary<string, bool>()
|
|
||||||
{
|
|
||||||
{
|
|
||||||
"Software=Moonlight-Panel",
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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)
|
public async Task Sync(int serverId)
|
||||||
|
{
|
||||||
|
if (Servers.TryGetValue(serverId, out var server))
|
||||||
|
{
|
||||||
|
var serverData = await RemoteService.GetServer(serverId);
|
||||||
|
var configuration = serverData.ToServerConfiguration();
|
||||||
|
|
||||||
|
server.Configuration = configuration;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await Initialize(serverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Sync(int serverId, ServerConfiguration configuration)
|
||||||
|
{
|
||||||
|
if (Servers.TryGetValue(serverId, out var server))
|
||||||
|
server.Configuration = configuration;
|
||||||
|
else
|
||||||
|
await Initialize(serverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
foreach (var configuration in configurations)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Initialize(configuration);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(
|
||||||
|
"An unhandled error occured while initializing server {id}: {e}",
|
||||||
|
configuration.Id,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Initialize(int serverId)
|
||||||
{
|
{
|
||||||
var serverData = await RemoteService.GetServer(serverId);
|
var serverData = await RemoteService.GetServer(serverId);
|
||||||
var serverConfiguration = serverData.ToServerConfiguration();
|
var configuration = serverData.ToServerConfiguration();
|
||||||
|
|
||||||
var server = GetServer(serverId);
|
await Initialize(configuration);
|
||||||
|
|
||||||
if (server == null)
|
|
||||||
await InitializeServer(serverConfiguration, []);
|
|
||||||
else
|
|
||||||
server.UpdateConfiguration(serverConfiguration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Delete(int serverId)
|
public Server? Find(int serverId)
|
||||||
|
=> Servers.GetValueOrDefault(serverId);
|
||||||
|
|
||||||
|
public async Task Initialize(ServerConfiguration configuration)
|
||||||
{
|
{
|
||||||
var server = GetServer(serverId);
|
var serverScope = ServiceProvider.CreateScope();
|
||||||
|
|
||||||
// If a server with this id doesn't exist we can just exit
|
var server = new Server(configuration, serverScope, HubContext);
|
||||||
if (server == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (server.State == ServerState.Installing)
|
Type[] subSystems =
|
||||||
throw new HttpApiException("Unable to delete a server while it is installing", 400);
|
[
|
||||||
|
typeof(ProvisionSubSystem),
|
||||||
|
typeof(StorageSubSystem),
|
||||||
|
typeof(DebugSubSystem),
|
||||||
|
typeof(ShutdownSubSystem),
|
||||||
|
typeof(ConsoleSubSystem),
|
||||||
|
typeof(RestoreSubSystem),
|
||||||
|
typeof(OnlineDetectionService),
|
||||||
|
typeof(InstallationSubSystem)
|
||||||
|
];
|
||||||
|
|
||||||
#region Callbacks
|
await server.Initialize(subSystems);
|
||||||
|
|
||||||
var deleteCompletion = new TaskCompletionSource();
|
Servers[configuration.Id] = server;
|
||||||
|
|
||||||
async Task HandleStateChange(ServerState state)
|
|
||||||
{
|
|
||||||
if (state == ServerState.Offline)
|
|
||||||
await DeleteServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task DeleteServer()
|
|
||||||
{
|
|
||||||
await server.CancelTasks();
|
|
||||||
await server.DestroyStorage();
|
|
||||||
await server.RemoveInstallationVolume();
|
|
||||||
await server.RemoveRuntimeVolume();
|
|
||||||
|
|
||||||
deleteCompletion.SetResult();
|
|
||||||
|
|
||||||
lock (Servers)
|
|
||||||
Servers.Remove(server);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
// If the server is still online, we are killing it and then
|
|
||||||
// waiting for the callback to trigger notifying us that the server is now offline
|
|
||||||
// so we can delete it. The request will pause until then using the deleteCompletion task
|
|
||||||
if (server.State != ServerState.Offline)
|
|
||||||
{
|
|
||||||
server.OnStateChanged += HandleStateChange;
|
|
||||||
await server.Kill();
|
|
||||||
|
|
||||||
await deleteCompletion.Task;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
await DeleteServer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Server? GetServer(int id)
|
#region Docker Monitoring
|
||||||
|
|
||||||
|
private async Task MonitorContainers()
|
||||||
{
|
{
|
||||||
lock (Servers)
|
Task.Run(async () =>
|
||||||
return Servers.FirstOrDefault(x => x.Id == id);
|
{
|
||||||
|
// Restart unless shutdown is requested
|
||||||
|
while (!TaskCancellation.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Starting to monitor events");
|
||||||
|
|
||||||
|
await DockerClient.System.MonitorEventsAsync(new(),
|
||||||
|
new Progress<Message>(async message =>
|
||||||
|
{
|
||||||
|
// Filter out unwanted events
|
||||||
|
if (message.Action != "die")
|
||||||
|
return;
|
||||||
|
|
||||||
|
// TODO: Implement a cached lookup using a shared dictionary by the sub system
|
||||||
|
|
||||||
|
var server = Servers.Values.FirstOrDefault(serverToCheck =>
|
||||||
|
{
|
||||||
|
var provisionSubSystem = serverToCheck.GetRequiredSubSystem<ProvisionSubSystem>();
|
||||||
|
|
||||||
|
if (provisionSubSystem.CurrentContainerId == message.ID)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
await server.StateMachine.FireAsync(ServerTrigger.Exited);
|
||||||
|
}), TaskCancellation.Token);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// Can be ignored
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError("An unhandled error occured while monitoring events: {e}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Lifecycle
|
#endregion
|
||||||
|
|
||||||
|
#region Lifetime
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
=> Task.CompletedTask;
|
=> Task.CompletedTask;
|
||||||
@@ -291,33 +241,97 @@ public class ServerService : IHostedLifecycleService
|
|||||||
|
|
||||||
public async Task StartedAsync(CancellationToken cancellationToken)
|
public async Task StartedAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
await MonitorContainers();
|
||||||
{
|
|
||||||
await Initialize();
|
await InitializeAll();
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogCritical("Unable to initialize servers. Is the panel online? Error: {e}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartingAsync(CancellationToken cancellationToken)
|
public Task StartingAsync(CancellationToken cancellationToken)
|
||||||
=> Task.CompletedTask;
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
public Task StoppedAsync(CancellationToken cancellationToken)
|
public async Task StoppedAsync(CancellationToken cancellationToken)
|
||||||
=> Task.CompletedTask;
|
|
||||||
|
|
||||||
public async Task StoppingAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
try
|
foreach (var server in Servers.Values)
|
||||||
{
|
await server.DisposeAsync();
|
||||||
await Stop();
|
|
||||||
}
|
await TaskCancellation.CancelAsync();
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogCritical("Unable to stop server handling: {e}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task StoppingAsync(CancellationToken cancellationToken)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
/*
|
||||||
|
*var existingContainers = await dockerClient.Containers.ListContainersAsync(new()
|
||||||
|
{
|
||||||
|
All = true,
|
||||||
|
Limit = null,
|
||||||
|
Filters = new Dictionary<string, IDictionary<string, bool>>()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"label",
|
||||||
|
new Dictionary<string, bool>()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"Software=Moonlight-Panel",
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*public async Task Delete(int serverId)
|
||||||
|
{
|
||||||
|
var server = GetServer(serverId);
|
||||||
|
|
||||||
|
// If a server with this id doesn't exist we can just exit
|
||||||
|
if (server == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (server.State == ServerState.Installing)
|
||||||
|
throw new HttpApiException("Unable to delete a server while it is installing", 400);
|
||||||
|
|
||||||
|
#region Callbacks
|
||||||
|
|
||||||
|
var deleteCompletion = new TaskCompletionSource();
|
||||||
|
|
||||||
|
async Task HandleStateChange(ServerState state)
|
||||||
|
{
|
||||||
|
if (state == ServerState.Offline)
|
||||||
|
await DeleteServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task DeleteServer()
|
||||||
|
{
|
||||||
|
await server.CancelTasks();
|
||||||
|
await server.DestroyStorage();
|
||||||
|
await server.RemoveInstallationVolume();
|
||||||
|
await server.RemoveRuntimeVolume();
|
||||||
|
|
||||||
|
deleteCompletion.SetResult();
|
||||||
|
|
||||||
|
lock (Servers)
|
||||||
|
Servers.Remove(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
// If the server is still online, we are killing it and then
|
||||||
|
// waiting for the callback to trigger notifying us that the server is now offline
|
||||||
|
// so we can delete it. The request will pause until then using the deleteCompletion task
|
||||||
|
if (server.State != ServerState.Offline)
|
||||||
|
{
|
||||||
|
server.OnStateChanged += HandleStateChange;
|
||||||
|
await server.Kill();
|
||||||
|
|
||||||
|
await deleteCompletion.Task;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await DeleteServer();
|
||||||
|
}
|
||||||
|
*
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ using MoonCore.Helpers;
|
|||||||
using MoonlightServers.Daemon.Configuration;
|
using MoonlightServers.Daemon.Configuration;
|
||||||
using MoonlightServers.Daemon.Helpers;
|
using MoonlightServers.Daemon.Helpers;
|
||||||
using MoonlightServers.Daemon.Http.Hubs;
|
using MoonlightServers.Daemon.Http.Hubs;
|
||||||
|
using MoonlightServers.Daemon.ServerSystem;
|
||||||
using MoonlightServers.Daemon.Services;
|
using MoonlightServers.Daemon.Services;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon;
|
namespace MoonlightServers.Daemon;
|
||||||
|
|||||||
Reference in New Issue
Block a user