Recreated plugin with new project template. Started implementing server system daemon
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IInstallConsole
|
||||
{
|
||||
public event Func<string, Task>? OnOutput;
|
||||
|
||||
public Task AttachAsync();
|
||||
|
||||
public Task WriteInputAsync(string value);
|
||||
|
||||
public Task ClearCacheAsync();
|
||||
public Task<string[]> GetCacheAsync();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IInstallEnvironment : IAsyncDisposable
|
||||
{
|
||||
public IInstallStatistics Statistics { get; }
|
||||
public IInstallConsole Console { get; }
|
||||
|
||||
public event Func<Task>? OnExited;
|
||||
|
||||
public Task<bool> IsRunningAsync();
|
||||
|
||||
public Task StartAsync();
|
||||
public Task KillAsync();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using MoonlightServers.Daemon.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IInstallEnvironmentService
|
||||
{
|
||||
public Task<IInstallEnvironment?> FindAsync(string id);
|
||||
|
||||
public Task<IInstallEnvironment> CreateAsync(
|
||||
string id,
|
||||
RuntimeConfiguration runtimeConfiguration,
|
||||
InstallConfiguration installConfiguration,
|
||||
IInstallStorage installStorage,
|
||||
IRuntimeStorage runtimeStorage
|
||||
);
|
||||
|
||||
public Task DeleteAsync(IInstallEnvironment environment);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IInstallStatistics
|
||||
{
|
||||
public event Func<ServerStatistics, Task>? OnStatisticsReceived;
|
||||
|
||||
public Task AttachAsync();
|
||||
|
||||
public Task ClearCacheAsync();
|
||||
public Task<ServerStatistics[]> GetCacheAsync();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IInstallStorage
|
||||
{
|
||||
public Task<string> GetHostPathAsync();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using MoonlightServers.Daemon.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IInstallStorageService
|
||||
{
|
||||
public Task<IInstallStorage?> FindAsync(string id);
|
||||
public Task<IInstallStorage> CreateAsync(string id, RuntimeConfiguration runtimeConfiguration, InstallConfiguration installConfiguration);
|
||||
public Task DeleteAsync(IInstallStorage installStorage);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IRuntimeConsole
|
||||
{
|
||||
public event Func<string, Task>? OnOutput;
|
||||
|
||||
public Task AttachAsync();
|
||||
|
||||
public Task WriteInputAsync(string value);
|
||||
|
||||
public Task ClearCacheAsync();
|
||||
public Task<string[]> GetCacheAsync();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IRuntimeEnvironment : IAsyncDisposable
|
||||
{
|
||||
public IRuntimeStatistics Statistics { get; }
|
||||
public IRuntimeConsole Console { get; }
|
||||
|
||||
public event Func<Task>? OnExited;
|
||||
|
||||
public Task<bool> IsRunningAsync();
|
||||
|
||||
public Task StartAsync();
|
||||
public Task KillAsync();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using MoonlightServers.Daemon.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IRuntimeEnvironmentService
|
||||
{
|
||||
public Task<IRuntimeEnvironment?> FindAsync(string id);
|
||||
public Task<IRuntimeEnvironment> CreateAsync(string id, RuntimeConfiguration configuration, IRuntimeStorage runtimeStorage);
|
||||
public Task UpdateAsync(IRuntimeEnvironment environment, RuntimeConfiguration configuration);
|
||||
public Task DeleteAsync(IRuntimeEnvironment environment);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IRuntimeStatistics
|
||||
{
|
||||
public event Func<ServerStatistics, Task>? OnStatisticsReceived;
|
||||
|
||||
public Task AttachAsync();
|
||||
|
||||
public Task ClearCacheAsync();
|
||||
public Task<ServerStatistics[]> GetCacheAsync();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IRuntimeStorage
|
||||
{
|
||||
public Task<string> GetHostPathAsync();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using MoonlightServers.Daemon.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
public interface IRuntimeStorageService
|
||||
{
|
||||
public Task<IRuntimeStorage?> FindAsync(string id);
|
||||
public Task<IRuntimeStorage> CreateAsync(string id, RuntimeConfiguration configuration);
|
||||
public Task UpdateAsync(IRuntimeStorage runtimeStorage, RuntimeConfiguration configuration);
|
||||
public Task DeleteAsync(IRuntimeStorage runtimeStorage);
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
using System.Text;
|
||||
using Docker.DotNet;
|
||||
using MoonCore.Events;
|
||||
using MoonCore.Helpers;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Docker;
|
||||
|
||||
public class DockerConsole : IConsole
|
||||
{
|
||||
private readonly EventSource<string> StdOutEventSource = new();
|
||||
private readonly ConcurrentList<string> StdOutCache = new();
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly ServerContext Context;
|
||||
private readonly ILogger Logger;
|
||||
|
||||
private MultiplexedStream? CurrentStream;
|
||||
private CancellationTokenSource Cts = new();
|
||||
|
||||
public DockerConsole(DockerClient dockerClient, ServerContext context)
|
||||
{
|
||||
DockerClient = dockerClient;
|
||||
Context = context;
|
||||
Logger = Context.Logger;
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public async Task WriteStdInAsync(string content)
|
||||
{
|
||||
if (CurrentStream == null)
|
||||
{
|
||||
Logger.LogWarning("Unable to write to stdin as no stream is connected");
|
||||
return;
|
||||
}
|
||||
|
||||
var contextBuffer = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
await CurrentStream.WriteAsync(contextBuffer, 0, contextBuffer.Length, Cts.Token);
|
||||
}
|
||||
|
||||
public async Task WriteStdOutAsync(string content)
|
||||
{
|
||||
// Add output cache
|
||||
if (StdOutCache.Count > 250) // TODO: Config
|
||||
StdOutCache.RemoveRange(0, 100);
|
||||
|
||||
StdOutCache.Add(content);
|
||||
|
||||
// Fire event
|
||||
await StdOutEventSource.InvokeAsync(content);
|
||||
}
|
||||
|
||||
public async Task AttachRuntimeAsync()
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
|
||||
|
||||
await AttachToContainerAsync(containerName);
|
||||
}
|
||||
|
||||
public async Task AttachInstallationAsync()
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id);
|
||||
|
||||
await AttachToContainerAsync(containerName);
|
||||
}
|
||||
|
||||
private async Task AttachToContainerAsync(string containerName)
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
|
||||
// Cancels previous active read task if it exists
|
||||
if (!Cts.IsCancellationRequested)
|
||||
await Cts.CancelAsync();
|
||||
|
||||
// Update the current cancellation token
|
||||
Cts = cts;
|
||||
|
||||
// Start reading task
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// This loop is here to reconnect to the stream when connection is lost.
|
||||
// This can occur when docker restarts for example
|
||||
|
||||
while (!cts.IsCancellationRequested)
|
||||
{
|
||||
MultiplexedStream? innerStream = null;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogTrace("Attaching");
|
||||
|
||||
innerStream = await DockerClient.Containers.AttachContainerAsync(
|
||||
containerName,
|
||||
true,
|
||||
new()
|
||||
{
|
||||
Stderr = true,
|
||||
Stdin = true,
|
||||
Stdout = true,
|
||||
Stream = true
|
||||
},
|
||||
cts.Token
|
||||
);
|
||||
|
||||
CurrentStream = innerStream;
|
||||
|
||||
var buffer = new byte[1024];
|
||||
|
||||
try
|
||||
{
|
||||
// Read while server tasks are not canceled
|
||||
while (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
var readResult = await innerStream.ReadOutputAsync(
|
||||
buffer,
|
||||
0,
|
||||
buffer.Length,
|
||||
cts.Token
|
||||
);
|
||||
|
||||
if (readResult.EOF)
|
||||
await cts.CancelAsync();
|
||||
|
||||
var decodedText = Encoding.UTF8.GetString(buffer, 0, readResult.Count);
|
||||
|
||||
await WriteStdOutAsync(decodedText);
|
||||
}
|
||||
|
||||
Logger.LogTrace("Read loop exited");
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogWarning(e, "An unhandled error occured while reading from container stream");
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Container got removed. Stop the reconnect loop
|
||||
|
||||
Logger.LogDebug("Container '{name}' got removed. Stopping reconnect stream for console", containerName);
|
||||
await cts.CancelAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e, "An error occured while attaching to container");
|
||||
}
|
||||
|
||||
innerStream?.Dispose();
|
||||
}
|
||||
|
||||
Logger.LogDebug("Disconnected from container stream");
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchRuntimeAsync()
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
|
||||
|
||||
await FetchFromContainerAsync(containerName);
|
||||
}
|
||||
|
||||
public async Task FetchInstallationAsync()
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id);
|
||||
|
||||
await FetchFromContainerAsync(containerName);
|
||||
}
|
||||
|
||||
private async Task FetchFromContainerAsync(string containerName)
|
||||
{
|
||||
var logStream = await DockerClient.Containers.GetContainerLogsAsync(containerName, true, new()
|
||||
{
|
||||
Follow = false,
|
||||
ShowStderr = true,
|
||||
ShowStdout = true
|
||||
});
|
||||
|
||||
var combinedOutput = await logStream.ReadOutputToEndAsync(Cts.Token);
|
||||
var contentToAdd = combinedOutput.stdout + combinedOutput.stderr;
|
||||
|
||||
await WriteStdOutAsync(contentToAdd);
|
||||
}
|
||||
|
||||
public Task ClearCacheAsync()
|
||||
{
|
||||
StdOutCache.Clear();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IEnumerable<string>> GetCacheAsync()
|
||||
{
|
||||
return Task.FromResult<IEnumerable<string>>(StdOutCache);
|
||||
}
|
||||
|
||||
public async Task<IAsyncDisposable> SubscribeStdOutAsync(Func<string, ValueTask> callback)
|
||||
=> await StdOutEventSource.SubscribeAsync(callback);
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!Cts.IsCancellationRequested)
|
||||
await Cts.CancelAsync();
|
||||
|
||||
if (CurrentStream != null)
|
||||
CurrentStream.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Docker;
|
||||
|
||||
public static class DockerConstants
|
||||
{
|
||||
public const string RuntimeNameTemplate = "moonlight-runtime-{0}";
|
||||
public const string InstallationNameTemplate = "moonlight-installation-{0}";
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
using Docker.DotNet;
|
||||
using Docker.DotNet.Models;
|
||||
using MoonCore.Events;
|
||||
using MoonlightServers.Daemon.Mappers;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
using MoonlightServers.Daemon.Services;
|
||||
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Docker;
|
||||
|
||||
public class DockerInstallation : IInstallation
|
||||
{
|
||||
private readonly DockerEventService DockerEventService;
|
||||
private readonly ServerConfigurationMapper Mapper;
|
||||
private readonly DockerImageService ImageService;
|
||||
private readonly ServerContext ServerContext;
|
||||
private readonly DockerClient DockerClient;
|
||||
private IReporter Reporter => ServerContext.Server.Reporter;
|
||||
|
||||
private readonly EventSource<int> ExitEventSource = new();
|
||||
|
||||
private IAsyncDisposable ContainerEventSubscription;
|
||||
private string ContainerId;
|
||||
|
||||
public DockerInstallation(
|
||||
DockerClient dockerClient,
|
||||
ServerContext serverContext,
|
||||
ServerConfigurationMapper mapper,
|
||||
DockerImageService imageService,
|
||||
DockerEventService dockerEventService
|
||||
)
|
||||
{
|
||||
DockerClient = dockerClient;
|
||||
ServerContext = serverContext;
|
||||
Mapper = mapper;
|
||||
ImageService = imageService;
|
||||
DockerEventService = dockerEventService;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
ContainerEventSubscription = await DockerEventService.SubscribeContainerAsync(OnContainerEvent);
|
||||
}
|
||||
|
||||
private async ValueTask OnContainerEvent(Message message)
|
||||
{
|
||||
// Only handle events for our own container
|
||||
if (message.ID != ContainerId)
|
||||
return;
|
||||
|
||||
// Only handle die events
|
||||
if (message.Action != "die")
|
||||
return;
|
||||
|
||||
int exitCode;
|
||||
|
||||
if (message.Actor.Attributes.TryGetValue("exitCode", out var exitCodeStr))
|
||||
{
|
||||
if (!int.TryParse(exitCodeStr, out exitCode))
|
||||
exitCode = 0;
|
||||
}
|
||||
else
|
||||
exitCode = 0;
|
||||
|
||||
|
||||
await ExitEventSource.InvokeAsync(exitCode);
|
||||
}
|
||||
|
||||
public async Task<bool> CheckExistsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
|
||||
|
||||
await DockerClient.Containers.InspectContainerAsync(
|
||||
containerName
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateAsync(
|
||||
string runtimePath,
|
||||
string hostPath,
|
||||
ServerInstallDataResponse data
|
||||
)
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
|
||||
|
||||
var parameters = Mapper.ToInstallParameters(
|
||||
ServerContext.Configuration,
|
||||
data,
|
||||
runtimePath,
|
||||
hostPath,
|
||||
containerName
|
||||
);
|
||||
|
||||
// Docker image
|
||||
await Reporter.StatusAsync("Downloading docker image");
|
||||
|
||||
await ImageService.DownloadAsync(data.DockerImage, async status => { await Reporter.StatusAsync(status); });
|
||||
|
||||
await Reporter.StatusAsync("Downloaded docker image");
|
||||
|
||||
// Write install script to install fs
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(hostPath, "install.sh"),
|
||||
data.Script
|
||||
);
|
||||
|
||||
//
|
||||
|
||||
var response = await DockerClient.Containers.CreateContainerAsync(parameters);
|
||||
ContainerId = response.ID;
|
||||
|
||||
await Reporter.StatusAsync("Created container");
|
||||
}
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
|
||||
|
||||
await DockerClient.Containers.StartContainerAsync(containerName, new());
|
||||
}
|
||||
|
||||
public async Task KillAsync()
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
|
||||
|
||||
await DockerClient.Containers.KillContainerAsync(containerName, new());
|
||||
}
|
||||
|
||||
public async Task DestroyAsync()
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
|
||||
|
||||
try
|
||||
{
|
||||
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
|
||||
|
||||
if (container.State.Running)
|
||||
await DockerClient.Containers.KillContainerAsync(containerName, new());
|
||||
|
||||
await DockerClient.Containers.RemoveContainerAsync(containerName, new()
|
||||
{
|
||||
Force = true
|
||||
});
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> callback)
|
||||
=> await ExitEventSource.SubscribeAsync(callback);
|
||||
|
||||
public async Task RestoreAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.InstallationNameTemplate, ServerContext.Configuration.Id);
|
||||
|
||||
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
|
||||
ContainerId = container.ID;
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await ContainerEventSubscription.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Docker;
|
||||
|
||||
public class DockerRestorer : IRestorer
|
||||
{
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly ServerContext Context;
|
||||
|
||||
public DockerRestorer(DockerClient dockerClient, ServerContext context)
|
||||
{
|
||||
DockerClient = dockerClient;
|
||||
Context = context;
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public async Task<bool> HandleRuntimeAsync()
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
|
||||
|
||||
try
|
||||
{
|
||||
var container = await DockerClient.Containers.InspectContainerAsync(
|
||||
containerName
|
||||
);
|
||||
|
||||
return container.State.Running;
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> HandleInstallationAsync()
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.InstallationNameTemplate, Context.Configuration.Id);
|
||||
|
||||
try
|
||||
{
|
||||
var container = await DockerClient.Containers.InspectContainerAsync(
|
||||
containerName
|
||||
);
|
||||
|
||||
return container.State.Running;
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
=> ValueTask.CompletedTask;
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
using Docker.DotNet;
|
||||
using Docker.DotNet.Models;
|
||||
using MoonCore.Events;
|
||||
using MoonlightServers.Daemon.Mappers;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
using MoonlightServers.Daemon.Services;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Docker;
|
||||
|
||||
public class DockerRuntime : IRuntime
|
||||
{
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly ServerContext Context;
|
||||
private readonly ServerConfigurationMapper Mapper;
|
||||
private readonly DockerEventService DockerEventService;
|
||||
private readonly DockerImageService ImageService;
|
||||
private readonly EventSource<int> ExitEventSource = new();
|
||||
|
||||
private IReporter Reporter => Context.Server.Reporter;
|
||||
private IAsyncDisposable ContainerEventSubscription;
|
||||
private string ContainerId;
|
||||
|
||||
public DockerRuntime(
|
||||
DockerClient dockerClient,
|
||||
ServerContext context,
|
||||
ServerConfigurationMapper mapper,
|
||||
DockerEventService dockerEventService,
|
||||
DockerImageService imageService
|
||||
)
|
||||
{
|
||||
DockerClient = dockerClient;
|
||||
Context = context;
|
||||
Mapper = mapper;
|
||||
DockerEventService = dockerEventService;
|
||||
ImageService = imageService;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
ContainerEventSubscription = await DockerEventService.SubscribeContainerAsync(OnContainerEvent);
|
||||
}
|
||||
|
||||
private async ValueTask OnContainerEvent(Message message)
|
||||
{
|
||||
// Only handle events for our own container
|
||||
if (message.ID != ContainerId)
|
||||
return;
|
||||
|
||||
// Only handle die events
|
||||
if (message.Action != "die")
|
||||
return;
|
||||
|
||||
int exitCode;
|
||||
|
||||
if (message.Actor.Attributes.TryGetValue("exitCode", out var exitCodeStr))
|
||||
{
|
||||
if (!int.TryParse(exitCodeStr, out exitCode))
|
||||
exitCode = 0;
|
||||
}
|
||||
else
|
||||
exitCode = 0;
|
||||
|
||||
|
||||
await ExitEventSource.InvokeAsync(exitCode);
|
||||
}
|
||||
|
||||
public async Task<bool> CheckExistsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
|
||||
|
||||
await DockerClient.Containers.InspectContainerAsync(
|
||||
containerName
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateAsync(string path)
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
|
||||
|
||||
var parameters = Mapper.ToRuntimeParameters(
|
||||
Context.Configuration,
|
||||
path,
|
||||
containerName
|
||||
);
|
||||
|
||||
// Docker image
|
||||
await Reporter.StatusAsync("Downloading docker image");
|
||||
|
||||
await ImageService.DownloadAsync(
|
||||
Context.Configuration.DockerImage,
|
||||
async status => { await Reporter.StatusAsync(status); }
|
||||
);
|
||||
|
||||
await Reporter.StatusAsync("Downloaded docker image");
|
||||
|
||||
//
|
||||
|
||||
var response = await DockerClient.Containers.CreateContainerAsync(parameters);
|
||||
ContainerId = response.ID;
|
||||
|
||||
await Reporter.StatusAsync("Created container");
|
||||
}
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
|
||||
|
||||
await DockerClient.Containers.StartContainerAsync(containerName, new());
|
||||
}
|
||||
|
||||
public Task UpdateAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task KillAsync()
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
|
||||
|
||||
await DockerClient.Containers.KillContainerAsync(containerName, new());
|
||||
}
|
||||
|
||||
public async Task DestroyAsync()
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
|
||||
|
||||
try
|
||||
{
|
||||
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
|
||||
|
||||
if (container.State.Running)
|
||||
await DockerClient.Containers.KillContainerAsync(containerName, new());
|
||||
|
||||
await DockerClient.Containers.RemoveContainerAsync(containerName, new()
|
||||
{
|
||||
Force = true
|
||||
});
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> callback)
|
||||
=> await ExitEventSource.SubscribeAsync(callback);
|
||||
|
||||
public async Task RestoreAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var containerName = string.Format(DockerConstants.RuntimeNameTemplate, Context.Configuration.Id);
|
||||
|
||||
var container = await DockerClient.Containers.InspectContainerAsync(containerName);
|
||||
ContainerId = container.ID;
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await ContainerEventSubscription.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Docker;
|
||||
|
||||
public class DockerStatistics : IStatistics
|
||||
{
|
||||
public Task InitializeAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task AttachRuntimeAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task AttachInstallationAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task ClearCacheAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task<IEnumerable<StatisticsData>> GetCacheAsync()
|
||||
=> Task.FromResult<IEnumerable<StatisticsData>>([]);
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
=> ValueTask.CompletedTask;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
|
||||
public enum ServerTrigger
|
||||
{
|
||||
Start = 0,
|
||||
Stop = 1,
|
||||
Kill = 2,
|
||||
DetectOnline = 3,
|
||||
Install = 4,
|
||||
Fail = 5,
|
||||
Exited = 6
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.FileSystems;
|
||||
|
||||
public class RawInstallationFs : IFileSystem
|
||||
{
|
||||
private readonly string BaseDirectory;
|
||||
|
||||
public RawInstallationFs(ServerContext context)
|
||||
{
|
||||
BaseDirectory = Path.Combine(
|
||||
Directory.GetCurrentDirectory(),
|
||||
"storage",
|
||||
"install",
|
||||
context.Configuration.Id.ToString()
|
||||
);
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task<string> GetPathAsync()
|
||||
=> Task.FromResult(BaseDirectory);
|
||||
|
||||
public Task<bool> CheckExistsAsync()
|
||||
{
|
||||
var exists = Directory.Exists(BaseDirectory);
|
||||
return Task.FromResult(exists);
|
||||
}
|
||||
|
||||
public Task<bool> CheckMountedAsync()
|
||||
=> Task.FromResult(true);
|
||||
|
||||
public Task CreateAsync()
|
||||
{
|
||||
Directory.CreateDirectory(BaseDirectory);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task PerformChecksAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task MountAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task UnmountAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task DestroyAsync()
|
||||
{
|
||||
Directory.Delete(BaseDirectory, true);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
=> ValueTask.CompletedTask;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.FileSystems;
|
||||
|
||||
public class RawRuntimeFs : IFileSystem
|
||||
{
|
||||
private readonly string BaseDirectory;
|
||||
|
||||
public RawRuntimeFs(ServerContext context)
|
||||
{
|
||||
BaseDirectory = Path.Combine(
|
||||
Directory.GetCurrentDirectory(),
|
||||
"storage",
|
||||
"volumes",
|
||||
context.Configuration.Id.ToString()
|
||||
);
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task<string> GetPathAsync()
|
||||
=> Task.FromResult(BaseDirectory);
|
||||
|
||||
public Task<bool> CheckExistsAsync()
|
||||
{
|
||||
var exists = Directory.Exists(BaseDirectory);
|
||||
return Task.FromResult(exists);
|
||||
}
|
||||
|
||||
public Task<bool> CheckMountedAsync()
|
||||
=> Task.FromResult(true);
|
||||
|
||||
public Task CreateAsync()
|
||||
{
|
||||
Directory.CreateDirectory(BaseDirectory);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task PerformChecksAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task MountAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task UnmountAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task DestroyAsync()
|
||||
{
|
||||
Directory.Delete(BaseDirectory, true);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
=> ValueTask.CompletedTask;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
using Stateless;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
|
||||
|
||||
public class DebugHandler : IServerStateHandler
|
||||
{
|
||||
private readonly ServerContext Context;
|
||||
private IAsyncDisposable? StdOutSubscription;
|
||||
|
||||
public DebugHandler(ServerContext context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
|
||||
{
|
||||
if(StdOutSubscription != null)
|
||||
return;
|
||||
|
||||
StdOutSubscription = await Context.Server.Console.SubscribeStdOutAsync(line =>
|
||||
{
|
||||
Console.WriteLine($"STD OUT: {line}");
|
||||
return ValueTask.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (StdOutSubscription != null)
|
||||
await StdOutSubscription.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
|
||||
using Stateless;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
|
||||
|
||||
public class InstallationHandler : IServerStateHandler
|
||||
{
|
||||
private readonly ServerContext Context;
|
||||
private Server Server => Context.Server;
|
||||
|
||||
private IAsyncDisposable? ExitSubscription;
|
||||
|
||||
public InstallationHandler(ServerContext context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
|
||||
{
|
||||
if (transition is
|
||||
{ Source: ServerState.Offline, Destination: ServerState.Installing, Trigger: ServerTrigger.Install })
|
||||
{
|
||||
await StartAsync();
|
||||
}
|
||||
else if (transition is
|
||||
{ Source: ServerState.Installing, Destination: ServerState.Offline, Trigger: ServerTrigger.Exited })
|
||||
{
|
||||
await CompleteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartAsync()
|
||||
{
|
||||
// Plan:
|
||||
// 1. Fetch latest configuration
|
||||
// 2. Check if both file systems exists
|
||||
// 3. Check if both file systems are mounted
|
||||
// 4. Run file system checks
|
||||
// 5. Create installation container
|
||||
// 6. Attach console
|
||||
// 7. Start installation container
|
||||
|
||||
// 1. Fetch latest configuration
|
||||
var installData = new ServerInstallDataResponse()
|
||||
{
|
||||
Script = await File.ReadAllTextAsync(Path.Combine("storage", "install.sh")),
|
||||
Shell = "/bin/ash",
|
||||
DockerImage = "ghcr.io/parkervcp/installers:alpine"
|
||||
};
|
||||
|
||||
// 2. Check if file system exists
|
||||
if (!await Server.RuntimeFileSystem.CheckExistsAsync())
|
||||
await Server.RuntimeFileSystem.CreateAsync();
|
||||
|
||||
if (!await Server.InstallationFileSystem.CheckExistsAsync())
|
||||
await Server.InstallationFileSystem.CreateAsync();
|
||||
|
||||
// 3. Check if both file systems are mounted
|
||||
if (!await Server.RuntimeFileSystem.CheckMountedAsync())
|
||||
await Server.RuntimeFileSystem.MountAsync();
|
||||
|
||||
if (!await Server.InstallationFileSystem.CheckMountedAsync())
|
||||
await Server.InstallationFileSystem.MountAsync();
|
||||
|
||||
// 4. Run file system checks
|
||||
await Server.RuntimeFileSystem.PerformChecksAsync();
|
||||
await Server.InstallationFileSystem.PerformChecksAsync();
|
||||
|
||||
// 5. Create installation
|
||||
|
||||
var runtimePath = await Server.RuntimeFileSystem.GetPathAsync();
|
||||
var installationPath = await Server.InstallationFileSystem.GetPathAsync();
|
||||
|
||||
if (await Server.Installation.CheckExistsAsync())
|
||||
await Server.Installation.DestroyAsync();
|
||||
|
||||
await Server.Installation.CreateAsync(runtimePath, installationPath, installData);
|
||||
|
||||
if (ExitSubscription == null)
|
||||
ExitSubscription = await Server.Installation.SubscribeExitedAsync(OnInstallationExited);
|
||||
|
||||
// 6. Attach console
|
||||
|
||||
await Server.Console.AttachInstallationAsync();
|
||||
|
||||
// 7. Start installation container
|
||||
await Server.Installation.StartAsync();
|
||||
}
|
||||
|
||||
private async ValueTask OnInstallationExited(int exitCode)
|
||||
{
|
||||
// TODO: Notify the crash handler component of the exit code
|
||||
|
||||
await Server.StateMachine.FireAsync(ServerTrigger.Exited);
|
||||
}
|
||||
|
||||
private async Task CompleteAsync()
|
||||
{
|
||||
// Plan:
|
||||
// 1. Handle possible crash
|
||||
// 2. Remove installation container
|
||||
// 3. Remove installation file system
|
||||
|
||||
// 1. Handle possible crash
|
||||
// TODO
|
||||
|
||||
// 2. Remove installation container
|
||||
await Server.Installation.DestroyAsync();
|
||||
|
||||
// 3. Remove installation file system
|
||||
await Server.InstallationFileSystem.UnmountAsync();
|
||||
await Server.InstallationFileSystem.DestroyAsync();
|
||||
|
||||
Context.Logger.LogDebug("Completed installation");
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (ExitSubscription != null)
|
||||
await ExitSubscription.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
using Stateless;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
|
||||
|
||||
public class OnlineDetectionHandler : IServerStateHandler
|
||||
{
|
||||
private readonly ServerContext Context;
|
||||
private IOnlineDetector OnlineDetector => Context.Server.OnlineDetector;
|
||||
private ILogger Logger => Context.Logger;
|
||||
|
||||
private IAsyncDisposable? ConsoleSubscription;
|
||||
private bool IsActive = false;
|
||||
|
||||
public OnlineDetectionHandler(ServerContext context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
|
||||
{
|
||||
if (
|
||||
transition is
|
||||
{ Source: ServerState.Offline, Destination: ServerState.Starting, Trigger: ServerTrigger.Start } && !IsActive
|
||||
)
|
||||
{
|
||||
await StartAsync();
|
||||
}
|
||||
else if (transition is { Source: not ServerState.Installing, Destination: ServerState.Offline } && IsActive)
|
||||
{
|
||||
await StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartAsync()
|
||||
{
|
||||
IsActive = true;
|
||||
|
||||
await OnlineDetector.CreateAsync();
|
||||
|
||||
ConsoleSubscription = await Context.Server.Console.SubscribeStdOutAsync(OnHandleOutput);
|
||||
|
||||
Logger.LogTrace("Created online detector. Created console subscription");
|
||||
}
|
||||
|
||||
private async ValueTask OnHandleOutput(string line)
|
||||
{
|
||||
if(!IsActive)
|
||||
return;
|
||||
|
||||
if(!await OnlineDetector.HandleOutputAsync(line))
|
||||
return;
|
||||
|
||||
if(!Context.Server.StateMachine.CanFire(ServerTrigger.DetectOnline))
|
||||
return;
|
||||
|
||||
Logger.LogTrace("Detected server as online. Destroying online detector");
|
||||
|
||||
await Context.Server.StateMachine.FireAsync(ServerTrigger.DetectOnline);
|
||||
await StopAsync();
|
||||
}
|
||||
|
||||
private async Task StopAsync()
|
||||
{
|
||||
IsActive = false;
|
||||
|
||||
if (ConsoleSubscription != null)
|
||||
{
|
||||
await ConsoleSubscription.DisposeAsync();
|
||||
ConsoleSubscription = null;
|
||||
}
|
||||
|
||||
await OnlineDetector.DestroyAsync();
|
||||
|
||||
Logger.LogTrace("Destroyed online detector. Revoked console subscription");
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (ConsoleSubscription != null)
|
||||
await ConsoleSubscription.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
using Stateless;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
|
||||
|
||||
public class ShutdownHandler : IServerStateHandler
|
||||
{
|
||||
private readonly ServerContext ServerContext;
|
||||
|
||||
public ShutdownHandler(ServerContext serverContext)
|
||||
{
|
||||
ServerContext = serverContext;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
|
||||
{
|
||||
// Filter (we only want to handle exists from the runtime, so we filter out the installing state)
|
||||
if (transition is not
|
||||
{
|
||||
Destination: ServerState.Offline,
|
||||
Source: not ServerState.Installing,
|
||||
Trigger: ServerTrigger.Exited // We don't want to handle the fail event here
|
||||
})
|
||||
return;
|
||||
|
||||
// Plan:
|
||||
// 1. Handle possible crash
|
||||
// 2. Remove runtime
|
||||
|
||||
// 1. Handle possible crash
|
||||
// TODO: Handle crash here
|
||||
|
||||
// 2. Remove runtime
|
||||
|
||||
await ServerContext.Server.Runtime.DestroyAsync();
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
=> ValueTask.CompletedTask;
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
using Stateless;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Handlers;
|
||||
|
||||
public class StartupHandler : IServerStateHandler
|
||||
{
|
||||
private IAsyncDisposable? ExitSubscription;
|
||||
|
||||
private readonly ServerContext Context;
|
||||
private Server Server => Context.Server;
|
||||
|
||||
public StartupHandler(ServerContext context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition)
|
||||
{
|
||||
// Filter
|
||||
if (transition is not {Source: ServerState.Offline, Destination: ServerState.Starting, Trigger: ServerTrigger.Start})
|
||||
return;
|
||||
|
||||
// Plan:
|
||||
// 1. Fetch latest configuration
|
||||
// 2. Check if file system exists
|
||||
// 3. Check if file system is mounted
|
||||
// 4. Run file system checks
|
||||
// 5. Create runtime
|
||||
// 6. Attach console
|
||||
// 7. Start runtime
|
||||
|
||||
// 1. Fetch latest configuration
|
||||
// TODO
|
||||
// Consider moving it out of the startup handler, as other handlers might need
|
||||
// the updated config as well or add sorting into the handler registration to ensure they are executing in the correct order.
|
||||
// Sort when building server, not when executing handlers
|
||||
|
||||
// 2. Check if file system exists
|
||||
if (!await Server.RuntimeFileSystem.CheckExistsAsync())
|
||||
await Server.RuntimeFileSystem.CreateAsync();
|
||||
|
||||
// 3. Check if file system is mounted
|
||||
if (!await Server.RuntimeFileSystem.CheckMountedAsync())
|
||||
await Server.RuntimeFileSystem.CheckMountedAsync();
|
||||
|
||||
// 4. Run file system checks
|
||||
await Server.RuntimeFileSystem.PerformChecksAsync();
|
||||
|
||||
// 5. Create runtime
|
||||
var hostPath = await Server.RuntimeFileSystem.GetPathAsync();
|
||||
|
||||
if (await Server.Runtime.CheckExistsAsync())
|
||||
await Server.Runtime.DestroyAsync();
|
||||
|
||||
await Server.Runtime.CreateAsync(hostPath);
|
||||
|
||||
if (ExitSubscription == null)
|
||||
ExitSubscription = await Server.Runtime.SubscribeExitedAsync(OnRuntimeExited);
|
||||
|
||||
// 6. Attach console
|
||||
|
||||
await Server.Console.AttachRuntimeAsync();
|
||||
|
||||
// 7. Start runtime
|
||||
|
||||
await Server.Runtime.StartAsync();
|
||||
}
|
||||
|
||||
private async ValueTask OnRuntimeExited(int exitCode)
|
||||
{
|
||||
// TODO: Notify the crash handler component of the exit code
|
||||
|
||||
await Server.StateMachine.FireAsync(ServerTrigger.Exited);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (ExitSubscription != null)
|
||||
await ExitSubscription.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using MoonlightServers.Daemon.Http.Hubs;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations;
|
||||
|
||||
public class ConsoleSignalRComponent : IServerComponent
|
||||
{
|
||||
private readonly IHubContext<ServerWebSocketHub> Hub;
|
||||
private readonly ServerContext Context;
|
||||
|
||||
private IAsyncDisposable? StdOutSubscription;
|
||||
private string HubGroup;
|
||||
|
||||
public ConsoleSignalRComponent(IHubContext<ServerWebSocketHub> hub, ServerContext context)
|
||||
{
|
||||
Hub = hub;
|
||||
Context = context;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
HubGroup = Context.Configuration.Id.ToString();
|
||||
|
||||
StdOutSubscription = await Context.Server.Console.SubscribeStdOutAsync(OnStdOut);
|
||||
}
|
||||
|
||||
private async ValueTask OnStdOut(string output)
|
||||
{
|
||||
await Hub.Clients.Group(HubGroup).SendAsync("ConsoleOutput", output);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (StdOutSubscription != null)
|
||||
await StdOutSubscription.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
using System.ComponentModel;
|
||||
using Docker.DotNet.Models;
|
||||
using MoonlightServers.Daemon.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public static class ConfigMapper
|
||||
{
|
||||
public static CreateContainerParameters GetRuntimeConfig(
|
||||
string uuid,
|
||||
string name,
|
||||
RuntimeConfiguration configuration,
|
||||
string runtimeStoragePath
|
||||
)
|
||||
{
|
||||
var parameters = new CreateContainerParameters()
|
||||
{
|
||||
HostConfig = new()
|
||||
};
|
||||
|
||||
ApplySharedOptions(parameters, configuration);
|
||||
|
||||
// Limits
|
||||
|
||||
if (configuration.Limits.CpuPercent.HasValue)
|
||||
{
|
||||
parameters.HostConfig.CPUQuota = configuration.Limits.CpuPercent.Value * 1000;
|
||||
parameters.HostConfig.CPUPeriod = 100000;
|
||||
parameters.HostConfig.CPUShares = 1024;
|
||||
}
|
||||
|
||||
if (configuration.Limits.MemoryMb.HasValue)
|
||||
{
|
||||
var memoryLimit = configuration.Limits.MemoryMb.Value;
|
||||
|
||||
// The overhead multiplier gives the container a little bit more memory to prevent crashes
|
||||
var memoryOverhead = memoryLimit + memoryLimit * 0.05f;
|
||||
|
||||
parameters.HostConfig.Memory = (long)memoryOverhead * 1024L * 1024L;
|
||||
parameters.HostConfig.MemoryReservation = (long)memoryLimit * 1024L * 1024L;
|
||||
|
||||
if (configuration.Limits.SwapMb.HasValue)
|
||||
{
|
||||
var rawSwap = configuration.Limits.SwapMb.Value * 1024L * 1024L;
|
||||
parameters.HostConfig.MemorySwap = rawSwap + (long)memoryOverhead;
|
||||
}
|
||||
}
|
||||
|
||||
parameters.HostConfig.BlkioWeight = 100;
|
||||
parameters.HostConfig.OomKillDisable = true;
|
||||
|
||||
// Storage
|
||||
|
||||
parameters.HostConfig.Tmpfs = new Dictionary<string, string>()
|
||||
{
|
||||
{ "/tmp", "rw,exec,nosuid,size=100M" } // TODO: Config
|
||||
};
|
||||
|
||||
parameters.WorkingDir = "/home/container";
|
||||
|
||||
parameters.HostConfig.Mounts = new List<Mount>();
|
||||
parameters.HostConfig.Mounts.Add(new Mount()
|
||||
{
|
||||
Source = runtimeStoragePath,
|
||||
Target = "/home/container",
|
||||
Type = "bind",
|
||||
ReadOnly = false
|
||||
});
|
||||
|
||||
// Labels
|
||||
parameters.Labels = new Dictionary<string, string>()
|
||||
{
|
||||
{ "dev.moonlightpanel", "true" },
|
||||
{ "dev.moonlightpanel.id", uuid }
|
||||
};
|
||||
|
||||
foreach (var label in configuration.Environment.Labels)
|
||||
parameters.Labels.Add(label.Key, label.Value);
|
||||
|
||||
// Security
|
||||
|
||||
parameters.HostConfig.CapDrop = new List<string>()
|
||||
{
|
||||
"setpcap", "mknod", "audit_write", "net_raw", "dac_override",
|
||||
"fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap"
|
||||
};
|
||||
|
||||
parameters.HostConfig.ReadonlyRootfs = true;
|
||||
parameters.HostConfig.SecurityOpt = new List<string>()
|
||||
{
|
||||
"no-new-privileges"
|
||||
};
|
||||
|
||||
// Name
|
||||
|
||||
parameters.Name = name;
|
||||
|
||||
// Docker Image
|
||||
parameters.Image = configuration.Template.DockerImage;
|
||||
|
||||
// Networking
|
||||
|
||||
if (configuration.Network.Ports.Length > 0 && !string.IsNullOrWhiteSpace(configuration.Network.FriendlyName))
|
||||
parameters.Hostname = configuration.Network.FriendlyName;
|
||||
|
||||
parameters.ExposedPorts = new Dictionary<string, EmptyStruct>();
|
||||
parameters.HostConfig.PortBindings = new Dictionary<string, IList<PortBinding>>();
|
||||
|
||||
foreach (var port in configuration.Network.Ports)
|
||||
{
|
||||
parameters.ExposedPorts.Add($"{port.Port}/tcp", new());
|
||||
parameters.ExposedPorts.Add($"{port.Port}/udp", new());
|
||||
|
||||
parameters.HostConfig.PortBindings.Add($"{port.Port}/tcp", new List<PortBinding>
|
||||
{
|
||||
new()
|
||||
{
|
||||
HostPort = port.Port.ToString(),
|
||||
HostIP = port.IpAddress
|
||||
}
|
||||
});
|
||||
|
||||
parameters.HostConfig.PortBindings.Add($"{port.Port}/udp", new List<PortBinding>
|
||||
{
|
||||
new()
|
||||
{
|
||||
HostPort = port.Port.ToString(),
|
||||
HostIP = port.IpAddress
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Force outgoing ip stuff
|
||||
|
||||
// User
|
||||
parameters.User = "1000:1000";
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public static CreateContainerParameters GetInstallConfig(
|
||||
string uuid,
|
||||
string name,
|
||||
RuntimeConfiguration runtimeConfiguration,
|
||||
InstallConfiguration installConfiguration,
|
||||
string runtimeStoragePath,
|
||||
string installStoragePath
|
||||
)
|
||||
{
|
||||
var parameters = new CreateContainerParameters()
|
||||
{
|
||||
HostConfig = new()
|
||||
};
|
||||
|
||||
ApplySharedOptions(parameters, runtimeConfiguration);
|
||||
|
||||
// Labels
|
||||
parameters.Labels = new Dictionary<string, string>()
|
||||
{
|
||||
{ "dev.moonlightpanel", "true" },
|
||||
{ "dev.moonlightpanel.id", uuid }
|
||||
};
|
||||
|
||||
foreach (var label in runtimeConfiguration.Environment.Labels)
|
||||
parameters.Labels.Add(label.Key, label.Value);
|
||||
|
||||
// Name
|
||||
|
||||
parameters.Name = name;
|
||||
|
||||
// Docker Image
|
||||
parameters.Image = installConfiguration.DockerImage;
|
||||
|
||||
// User
|
||||
parameters.User = "1000:1000";
|
||||
|
||||
// Storage
|
||||
parameters.WorkingDir = "/mnt/server";
|
||||
|
||||
parameters.HostConfig.Mounts = new List<Mount>();
|
||||
|
||||
parameters.HostConfig.Mounts.Add(new Mount()
|
||||
{
|
||||
Source = runtimeStoragePath,
|
||||
Target = "/mnt/server",
|
||||
ReadOnly = false,
|
||||
Type = "bind"
|
||||
});
|
||||
|
||||
parameters.HostConfig.Mounts.Add(new Mount()
|
||||
{
|
||||
Source = installStoragePath,
|
||||
Target = "/mnt/install",
|
||||
ReadOnly = false,
|
||||
Type = "bind"
|
||||
});
|
||||
|
||||
// Command
|
||||
parameters.Cmd = [installConfiguration.Shell, "/mnt/install/install.sh"];
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private static void ApplySharedOptions(
|
||||
CreateContainerParameters parameters,
|
||||
RuntimeConfiguration configuration
|
||||
)
|
||||
{
|
||||
// Input, output & error streams and TTY
|
||||
|
||||
parameters.Tty = true;
|
||||
parameters.AttachStderr = true;
|
||||
parameters.AttachStdin = true;
|
||||
parameters.AttachStdout = true;
|
||||
parameters.OpenStdin = true;
|
||||
|
||||
// Logging
|
||||
|
||||
parameters.HostConfig.LogConfig = new()
|
||||
{
|
||||
Type = "json-file", // We need to use this provider, as the GetLogs endpoint needs it
|
||||
Config = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
// Environment variables
|
||||
|
||||
parameters.Env = new List<string>()
|
||||
{
|
||||
$"STARTUP={configuration.Template.StartupCommand}",
|
||||
//TODO: Add timezone, add server ip
|
||||
};
|
||||
|
||||
if (configuration.Limits.MemoryMb.HasValue)
|
||||
parameters.Env.Add($"SERVER_MEMORY={configuration.Limits.MemoryMb.Value}");
|
||||
|
||||
if (configuration.Network.MainPort != null)
|
||||
{
|
||||
parameters.Env.Add($"SERVER_IP={configuration.Network.MainPort.IpAddress}");
|
||||
parameters.Env.Add($"SERVER_PORT={configuration.Network.MainPort.Port}");
|
||||
}
|
||||
|
||||
// Handle port variables
|
||||
var i = 1;
|
||||
foreach (var port in configuration.Network.Ports)
|
||||
{
|
||||
parameters.Env.Add($"ML_PORT_{i}={port.Port}");
|
||||
i++;
|
||||
}
|
||||
|
||||
// Copy variables as env vars
|
||||
foreach (var variable in configuration.Environment.Variables)
|
||||
parameters.Env.Add($"{variable.Key}={variable.Value}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using System.Buffers;
|
||||
using System.Text;
|
||||
using Docker.DotNet;
|
||||
using Docker.DotNet.Models;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public class DockerConsole : IRuntimeConsole, IInstallConsole, IAsyncDisposable
|
||||
{
|
||||
public event Func<string, Task>? OnOutput;
|
||||
|
||||
private MultiplexedStream? Stream;
|
||||
|
||||
private readonly string ContainerId;
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly ILogger Logger;
|
||||
|
||||
private readonly List<string> Cache = new(302);
|
||||
private readonly SemaphoreSlim CacheLock = new(1, 1);
|
||||
private readonly CancellationTokenSource Cts = new();
|
||||
|
||||
public DockerConsole(
|
||||
string containerId,
|
||||
DockerClient dockerClient,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
ContainerId = containerId;
|
||||
DockerClient = dockerClient;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public async Task AttachAsync()
|
||||
{
|
||||
// Fetch initial logs
|
||||
Logger.LogTrace("Fetching pre-existing logs from container");
|
||||
|
||||
var logResponse = await DockerClient.Containers.GetContainerLogsAsync(
|
||||
ContainerId,
|
||||
new()
|
||||
{
|
||||
Follow = false,
|
||||
ShowStderr = true,
|
||||
ShowStdout = true
|
||||
}
|
||||
);
|
||||
|
||||
// Append to cache
|
||||
var logs = await logResponse.ReadOutputToEndAsync(Cts.Token);
|
||||
|
||||
await CacheLock.WaitAsync(Cts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
Cache.Add(logs.stdout);
|
||||
Cache.Add(logs.stderr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CacheLock.Release();
|
||||
}
|
||||
|
||||
// Stream new logs
|
||||
Logger.LogTrace("Starting log streaming");
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var capturedCt = Cts.Token;
|
||||
|
||||
Logger.LogTrace("Starting attach loop");
|
||||
|
||||
while (!capturedCt.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = await DockerClient.Containers.AttachContainerAsync(
|
||||
ContainerId,
|
||||
new ContainerAttachParameters()
|
||||
{
|
||||
Stderr = true,
|
||||
Stdin = true,
|
||||
Stdout = true,
|
||||
Stream = true
|
||||
},
|
||||
capturedCt
|
||||
);
|
||||
|
||||
// Make stream accessible from the outside
|
||||
Stream = stream;
|
||||
|
||||
const int bufferSize = 1024;
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
|
||||
|
||||
while (!capturedCt.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var readResult = await stream.ReadOutputAsync(buffer, 0, bufferSize, capturedCt);
|
||||
|
||||
if (readResult.Count > 0)
|
||||
{
|
||||
var decodedBuffer = Encoding.UTF8.GetString(buffer, 0, readResult.Count);
|
||||
|
||||
await CacheLock.WaitAsync(capturedCt);
|
||||
|
||||
try
|
||||
{
|
||||
if (Cache.Count > 300)
|
||||
Cache.RemoveRange(0, 50);
|
||||
|
||||
Cache.Add(decodedBuffer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CacheLock.Release();
|
||||
}
|
||||
|
||||
if (OnOutput != null)
|
||||
await OnOutput.Invoke(decodedBuffer);
|
||||
}
|
||||
|
||||
if (readResult.EOF)
|
||||
break;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e, "An unhandled error occured while processing container stream");
|
||||
}
|
||||
}
|
||||
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e, "An unhandled error occured while handling container attaching");
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogTrace("Attach loop exited");
|
||||
});
|
||||
}
|
||||
|
||||
public async Task WriteInputAsync(string value)
|
||||
{
|
||||
if (Stream == null)
|
||||
throw new AggregateException("Stream is not available. Container might not be attached");
|
||||
|
||||
var buffer = Encoding.UTF8.GetBytes(value);
|
||||
await Stream.WriteAsync(buffer, 0, buffer.Length, Cts.Token);
|
||||
}
|
||||
|
||||
public async Task ClearCacheAsync()
|
||||
{
|
||||
await CacheLock.WaitAsync(Cts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
Cache.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
CacheLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string[]> GetCacheAsync()
|
||||
{
|
||||
await CacheLock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
return Cache.ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
CacheLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await Cts.CancelAsync();
|
||||
Stream?.Dispose();
|
||||
CacheLock.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using Docker.DotNet;
|
||||
using Docker.DotNet.Models;
|
||||
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public class DockerEventService : BackgroundService
|
||||
{
|
||||
public event Func<ContainerDieEvent, Task>? OnContainerDied;
|
||||
|
||||
private readonly ILogger<DockerEventService> Logger;
|
||||
private readonly DockerClient DockerClient;
|
||||
|
||||
public DockerEventService(
|
||||
ILogger<DockerEventService> logger,
|
||||
DockerClient dockerClient
|
||||
)
|
||||
{
|
||||
Logger = logger;
|
||||
DockerClient = dockerClient;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
Logger.LogTrace("Starting up docker event monitor");
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogTrace("Monitoring events");
|
||||
|
||||
await DockerClient.System.MonitorEventsAsync(
|
||||
new ContainerEventsParameters(),
|
||||
new Progress<Message>(OnEventAsync),
|
||||
stoppingToken
|
||||
);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e, "An error occured while processing container event monitoring");
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogTrace("Closed docker event monitor");
|
||||
}
|
||||
|
||||
private async void OnEventAsync(Message message)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (message.Type)
|
||||
{
|
||||
case "container":
|
||||
|
||||
var containerId = message.Actor.ID;
|
||||
|
||||
switch (message.Action)
|
||||
{
|
||||
case "die":
|
||||
|
||||
if (
|
||||
!message.Actor.Attributes.TryGetValue("exitCode", out var exitCodeStr) ||
|
||||
!int.TryParse(exitCodeStr, out var exitCode)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (OnContainerDied != null)
|
||||
await OnContainerDied.Invoke(new ContainerDieEvent(containerId, exitCode));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(
|
||||
e,
|
||||
"An error occured while handling event {type} for {action}",
|
||||
message.Type,
|
||||
message.Action
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public class DockerInstallEnv : IInstallEnvironment
|
||||
{
|
||||
public IInstallStatistics Statistics => InnerStatistics;
|
||||
public IInstallConsole Console => InnerConsole;
|
||||
|
||||
public event Func<Task>? OnExited;
|
||||
|
||||
public string ContainerId { get; }
|
||||
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly ILogger Logger;
|
||||
private readonly DockerEventService EventService;
|
||||
|
||||
private readonly DockerStatistics InnerStatistics;
|
||||
private readonly DockerConsole InnerConsole;
|
||||
|
||||
public DockerInstallEnv(string containerId, DockerClient dockerClient, ILogger logger, DockerEventService eventService)
|
||||
{
|
||||
ContainerId = containerId;
|
||||
DockerClient = dockerClient;
|
||||
Logger = logger;
|
||||
EventService = eventService;
|
||||
|
||||
InnerStatistics = new DockerStatistics();
|
||||
InnerConsole = new DockerConsole(containerId, dockerClient, logger);
|
||||
|
||||
EventService.OnContainerDied += HandleDieEventAsync;
|
||||
}
|
||||
|
||||
public async Task<bool> IsRunningAsync()
|
||||
{
|
||||
var container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
|
||||
return container.State.Running;
|
||||
}
|
||||
|
||||
public async Task StartAsync()
|
||||
=> await DockerClient.Containers.StartContainerAsync(ContainerId, new());
|
||||
|
||||
public async Task KillAsync()
|
||||
=> await DockerClient.Containers.KillContainerAsync(ContainerId, new());
|
||||
|
||||
private async Task HandleDieEventAsync(ContainerDieEvent dieEvent)
|
||||
{
|
||||
if(dieEvent.ContainerId != ContainerId)
|
||||
return;
|
||||
|
||||
if(OnExited != null)
|
||||
await OnExited.Invoke();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
EventService.OnContainerDied -= HandleDieEventAsync;
|
||||
|
||||
await InnerConsole.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.Models;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public class DockerInstallEnvService : IInstallEnvironmentService
|
||||
{
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly ILoggerFactory LoggerFactory;
|
||||
private readonly DockerEventService DockerEventService;
|
||||
|
||||
private const string NameTemplate = "ml-install-{0}";
|
||||
|
||||
public DockerInstallEnvService(DockerClient dockerClient, ILoggerFactory loggerFactory,
|
||||
DockerEventService dockerEventService)
|
||||
{
|
||||
DockerClient = dockerClient;
|
||||
LoggerFactory = loggerFactory;
|
||||
DockerEventService = dockerEventService;
|
||||
}
|
||||
|
||||
public async Task<IInstallEnvironment?> FindAsync(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
|
||||
string.Format(NameTemplate, id)
|
||||
);
|
||||
|
||||
var logger =
|
||||
LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
|
||||
|
||||
return new DockerInstallEnv(dockerInspect.ID, DockerClient, logger, DockerEventService);
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IInstallEnvironment> CreateAsync(
|
||||
string id,
|
||||
RuntimeConfiguration runtimeConfiguration,
|
||||
InstallConfiguration installConfiguration,
|
||||
IInstallStorage installStorage,
|
||||
IRuntimeStorage runtimeStorage
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
|
||||
string.Format(NameTemplate, id)
|
||||
);
|
||||
|
||||
if (dockerInspect.State.Running)
|
||||
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
|
||||
|
||||
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
var runtimeStoragePath = await runtimeStorage.GetHostPathAsync();
|
||||
var installStoragePath = await installStorage.GetHostPathAsync();
|
||||
|
||||
var parameters = ConfigMapper.GetInstallConfig(
|
||||
id,
|
||||
string.Format(NameTemplate, id),
|
||||
runtimeConfiguration,
|
||||
installConfiguration,
|
||||
runtimeStoragePath,
|
||||
installStoragePath
|
||||
);
|
||||
|
||||
var container = await DockerClient.Containers.CreateContainerAsync(parameters);
|
||||
|
||||
var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
|
||||
|
||||
return new DockerInstallEnv(container.ID, DockerClient, logger, DockerEventService);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(IInstallEnvironment environment)
|
||||
{
|
||||
if (environment is not DockerInstallEnv dockerInstallEnv)
|
||||
throw new ArgumentException(
|
||||
$"You cannot delete runtime environments which haven't been created by {nameof(DockerInstallEnv)}");
|
||||
|
||||
await dockerInstallEnv.DisposeAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
|
||||
dockerInstallEnv.ContainerId
|
||||
);
|
||||
|
||||
if (dockerInspect.State.Running)
|
||||
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
|
||||
|
||||
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public class DockerRuntimeEnv : IRuntimeEnvironment
|
||||
{
|
||||
public IRuntimeStatistics Statistics => InnerStatistics;
|
||||
public IRuntimeConsole Console => InnerConsole;
|
||||
|
||||
public string ContainerId { get; }
|
||||
|
||||
public event Func<Task>? OnExited;
|
||||
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly DockerEventService EventService;
|
||||
|
||||
private readonly DockerConsole InnerConsole;
|
||||
private readonly DockerStatistics InnerStatistics;
|
||||
|
||||
public DockerRuntimeEnv(string containerId, DockerClient dockerClient, ILogger logger, DockerEventService eventService)
|
||||
{
|
||||
ContainerId = containerId;
|
||||
DockerClient = dockerClient;
|
||||
EventService = eventService;
|
||||
|
||||
InnerStatistics = new DockerStatistics();
|
||||
InnerConsole = new DockerConsole(containerId, dockerClient, logger);
|
||||
|
||||
EventService.OnContainerDied += HandleDieEventAsync;
|
||||
}
|
||||
|
||||
public async Task<bool> IsRunningAsync()
|
||||
{
|
||||
var container = await DockerClient.Containers.InspectContainerAsync(ContainerId);
|
||||
return container.State.Running;
|
||||
}
|
||||
|
||||
public async Task StartAsync()
|
||||
=> await DockerClient.Containers.StartContainerAsync(ContainerId, new());
|
||||
|
||||
public async Task KillAsync()
|
||||
=> await DockerClient.Containers.KillContainerAsync(ContainerId, new());
|
||||
|
||||
private async Task HandleDieEventAsync(ContainerDieEvent dieEvent)
|
||||
{
|
||||
if(dieEvent.ContainerId != ContainerId)
|
||||
return;
|
||||
|
||||
if(OnExited != null)
|
||||
await OnExited.Invoke();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
EventService.OnContainerDied -= HandleDieEventAsync;
|
||||
|
||||
await InnerConsole.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.Models;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public class DockerRuntimeEnvService : IRuntimeEnvironmentService
|
||||
{
|
||||
private readonly DockerClient DockerClient;
|
||||
private readonly ILoggerFactory LoggerFactory;
|
||||
private readonly DockerEventService DockerEventService;
|
||||
|
||||
private const string NameTemplate = "ml-runtime-{0}";
|
||||
|
||||
public DockerRuntimeEnvService(DockerClient dockerClient, ILoggerFactory loggerFactory,
|
||||
DockerEventService dockerEventService)
|
||||
{
|
||||
DockerClient = dockerClient;
|
||||
LoggerFactory = loggerFactory;
|
||||
DockerEventService = dockerEventService;
|
||||
}
|
||||
|
||||
public async Task<IRuntimeEnvironment?> FindAsync(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
|
||||
string.Format(NameTemplate, id)
|
||||
);
|
||||
|
||||
var logger =
|
||||
LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
|
||||
|
||||
return new DockerRuntimeEnv(dockerInspect.ID, DockerClient, logger, DockerEventService);
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IRuntimeEnvironment> CreateAsync(
|
||||
string id,
|
||||
RuntimeConfiguration configuration,
|
||||
IRuntimeStorage storage
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
|
||||
string.Format(NameTemplate, id)
|
||||
);
|
||||
|
||||
if (dockerInspect.State.Running)
|
||||
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
|
||||
|
||||
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
var storagePath = await storage.GetHostPathAsync();
|
||||
|
||||
var parameters = ConfigMapper.GetRuntimeConfig(
|
||||
id,
|
||||
string.Format(NameTemplate, id),
|
||||
configuration,
|
||||
storagePath
|
||||
);
|
||||
|
||||
var container = await DockerClient.Containers.CreateContainerAsync(parameters);
|
||||
|
||||
var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Implementations.Docker({id})");
|
||||
|
||||
return new DockerRuntimeEnv(container.ID, DockerClient, logger, DockerEventService);
|
||||
}
|
||||
|
||||
public Task UpdateAsync(IRuntimeEnvironment environment, RuntimeConfiguration configuration)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(IRuntimeEnvironment environment)
|
||||
{
|
||||
if (environment is not DockerRuntimeEnv dockerRuntimeEnv)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"You cannot delete runtime environments which haven't been created by {nameof(DockerRuntimeEnvService)}"
|
||||
);
|
||||
}
|
||||
|
||||
await dockerRuntimeEnv.DisposeAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var dockerInspect = await DockerClient.Containers.InspectContainerAsync(
|
||||
dockerRuntimeEnv.ContainerId
|
||||
);
|
||||
|
||||
if (dockerInspect.State.Running)
|
||||
await DockerClient.Containers.KillContainerAsync(dockerInspect.ID, new());
|
||||
|
||||
await DockerClient.Containers.RemoveContainerAsync(dockerInspect.ID, new());
|
||||
}
|
||||
catch (DockerContainerNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public class DockerStatistics : IRuntimeStatistics, IInstallStatistics
|
||||
{
|
||||
public event Func<ServerStatistics, Task>? OnStatisticsReceived;
|
||||
|
||||
public Task AttachAsync() => Task.CompletedTask;
|
||||
|
||||
public Task ClearCacheAsync() => Task.CompletedTask;
|
||||
|
||||
public Task<ServerStatistics[]> GetCacheAsync() => Task.FromResult<ServerStatistics[]>([]);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker.Events;
|
||||
|
||||
public record ContainerDieEvent(string ContainerId, int ExitCode);
|
||||
@@ -0,0 +1,22 @@
|
||||
using Docker.DotNet;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static void AddDockerServices(this IServiceCollection collection)
|
||||
{
|
||||
var client = new DockerClientBuilder()
|
||||
.WithEndpoint(new Uri("unix:///var/run/docker.sock"))
|
||||
.Build();
|
||||
|
||||
collection.AddSingleton(client);
|
||||
|
||||
collection.AddSingleton<DockerEventService>();
|
||||
collection.AddHostedService(sp => sp.GetRequiredService<DockerEventService>());
|
||||
|
||||
collection.AddSingleton<IRuntimeEnvironmentService, DockerRuntimeEnvService>();
|
||||
collection.AddSingleton<IInstallEnvironmentService, DockerInstallEnvService>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static void AddLocalServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IRuntimeStorageService, LocalRuntimeStorageService>();
|
||||
services.AddSingleton<IInstallStorageService, LocalInstallStorageService>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
|
||||
|
||||
public class LocalInstallStorage : IInstallStorage
|
||||
{
|
||||
public string HostPath { get; }
|
||||
|
||||
public LocalInstallStorage(string hostPath)
|
||||
{
|
||||
HostPath = hostPath;
|
||||
}
|
||||
|
||||
public Task<string> GetHostPathAsync() => Task.FromResult(HostPath);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using MoonlightServers.Daemon.Models;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
|
||||
|
||||
public class LocalInstallStorageService : IInstallStorageService
|
||||
{
|
||||
private const string HostPathTemplate = "./mldaemon/install/{0}";
|
||||
|
||||
public Task<IInstallStorage?> FindAsync(string id)
|
||||
{
|
||||
var path = string.Format(HostPathTemplate, id);
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
return Task.FromResult<IInstallStorage?>(null);
|
||||
|
||||
return Task.FromResult<IInstallStorage?>(new LocalInstallStorage(path));
|
||||
}
|
||||
|
||||
public Task<IInstallStorage> CreateAsync(string id, RuntimeConfiguration runtimeConfiguration, InstallConfiguration installConfiguration)
|
||||
{
|
||||
var path = string.Format(HostPathTemplate, id);
|
||||
|
||||
Directory.CreateDirectory(path);
|
||||
|
||||
return Task.FromResult<IInstallStorage>(new LocalInstallStorage(path));
|
||||
}
|
||||
|
||||
public Task DeleteAsync(IInstallStorage installStorage)
|
||||
{
|
||||
if (installStorage is not LocalInstallStorage localInstallStorage)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"You cannot delete install storages which haven't been created by {nameof(LocalInstallStorageService)}"
|
||||
);
|
||||
}
|
||||
|
||||
if(Directory.Exists(localInstallStorage.HostPath))
|
||||
Directory.Delete(localInstallStorage.HostPath, true);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
|
||||
|
||||
public class LocalRuntimeStorage : IRuntimeStorage
|
||||
{
|
||||
public string HostPath { get; }
|
||||
|
||||
public LocalRuntimeStorage(string hostPath)
|
||||
{
|
||||
HostPath = hostPath;
|
||||
}
|
||||
|
||||
public Task<string> GetHostPathAsync() => Task.FromResult(HostPath);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using MoonlightServers.Daemon.Models;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations.Local;
|
||||
|
||||
public class LocalRuntimeStorageService : IRuntimeStorageService
|
||||
{
|
||||
private const string HostPathTemplate = "./mldaemon/runtime/{0}";
|
||||
|
||||
public Task<IRuntimeStorage?> FindAsync(string id)
|
||||
{
|
||||
var path = string.Format(HostPathTemplate, id);
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
return Task.FromResult<IRuntimeStorage?>(null);
|
||||
|
||||
return Task.FromResult<IRuntimeStorage?>(new LocalRuntimeStorage(path));
|
||||
}
|
||||
|
||||
public Task<IRuntimeStorage> CreateAsync(string id, RuntimeConfiguration configuration)
|
||||
{
|
||||
var path = string.Format(HostPathTemplate, id);
|
||||
|
||||
Directory.CreateDirectory(path);
|
||||
|
||||
return Task.FromResult<IRuntimeStorage>(new LocalRuntimeStorage(path));
|
||||
}
|
||||
|
||||
public Task UpdateAsync(IRuntimeStorage runtimeStorage, RuntimeConfiguration configuration)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task DeleteAsync(IRuntimeStorage runtimeStorage)
|
||||
{
|
||||
if (runtimeStorage is not LocalRuntimeStorage localRuntimeStorage)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"You cannot delete runtime storages which haven't been created by {nameof(LocalRuntimeStorageService)}"
|
||||
);
|
||||
}
|
||||
|
||||
if(Directory.Exists(localRuntimeStorage.HostPath))
|
||||
Directory.Delete(localRuntimeStorage.HostPath, true);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations;
|
||||
|
||||
public class RegexOnlineDetector : IOnlineDetector
|
||||
{
|
||||
private readonly ServerContext Context;
|
||||
|
||||
private Regex? Expression;
|
||||
|
||||
public RegexOnlineDetector(ServerContext context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task CreateAsync()
|
||||
{
|
||||
if(string.IsNullOrEmpty(Context.Configuration.OnlineDetection))
|
||||
return Task.CompletedTask;
|
||||
|
||||
Expression = new Regex(Context.Configuration.OnlineDetection, RegexOptions.Compiled);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<bool> HandleOutputAsync(string line)
|
||||
{
|
||||
if (Expression == null)
|
||||
return Task.FromResult(false);
|
||||
|
||||
var result = Expression.Matches(line).Count > 0;
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task DestroyAsync()
|
||||
{
|
||||
Expression = null;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
Expression = null;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Implementations;
|
||||
|
||||
public class ServerReporter : IReporter
|
||||
{
|
||||
private readonly ServerContext Context;
|
||||
|
||||
private const string StatusTemplate =
|
||||
"\x1b[1;38;2;200;90;200mM\x1b[1;38;2;204;110;230mo\x1b[1;38;2;170;130;245mo\x1b[1;38;2;140;150;255mn\x1b[1;38;2;110;180;255ml\x1b[1;38;2;100;200;255mi\x1b[1;38;2;100;220;255mg\x1b[1;38;2;120;235;255mh\x1b[1;38;2;140;250;255mt\x1b[0m \x1b[3;38;2;200;200;200m{0}\x1b[0m\n\r";
|
||||
|
||||
private const string ErrorTemplate =
|
||||
"\x1b[1;38;2;200;90;200mM\x1b[1;38;2;204;110;230mo\x1b[1;38;2;170;130;245mo\x1b[1;38;2;140;150;255mn\x1b[1;38;2;110;180;255ml\x1b[1;38;2;100;200;255mi\x1b[1;38;2;100;220;255mg\x1b[1;38;2;120;235;255mh\x1b[1;38;2;140;250;255mt\x1b[0m \x1b[1;38;2;255;0;0m{0}\x1b[0m\n\r";
|
||||
|
||||
public ServerReporter(ServerContext context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public async Task StatusAsync(string message)
|
||||
{
|
||||
Context.Logger.LogInformation("Status: {message}", message);
|
||||
|
||||
await Context.Server.Console.WriteStdOutAsync(
|
||||
string.Format(StatusTemplate, message)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task ErrorAsync(string message)
|
||||
{
|
||||
Context.Logger.LogError("Error: {message}", message);
|
||||
|
||||
await Context.Server.Console.WriteStdOutAsync(
|
||||
string.Format(ErrorTemplate, message)
|
||||
);
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
=> ValueTask.CompletedTask;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IConsole : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes to the standard input of the console. If attached to the runtime when using docker for example this
|
||||
/// would write into the containers standard input.
|
||||
/// <remarks>This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required</remarks>
|
||||
/// </summary>
|
||||
/// <param name="content">Content to write</param>
|
||||
/// <returns></returns>
|
||||
public Task WriteStdInAsync(string content);
|
||||
/// <summary>
|
||||
/// Writes to the standard output of the console. If attached to the runtime when using docker for example this
|
||||
/// would write into the containers standard output.
|
||||
/// <remarks>This method does not add a newline separator at the end of the content. The caller needs to add this themselves if required</remarks>
|
||||
/// </summary>
|
||||
/// <param name="content">Content to write</param>
|
||||
/// <returns></returns>
|
||||
public Task WriteStdOutAsync(string content);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches the console to the runtime environment
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task AttachRuntimeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Attaches the console to the installation environment
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task AttachInstallationAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Fetches all output from the runtime environment and write them into the cache without triggering any events
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task FetchRuntimeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Fetches all output from the installation environment and write them into the cache without triggering any events
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task FetchInstallationAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Clears the cache of the standard output received by the environments
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task ClearCacheAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content from the standard output cache
|
||||
/// </summary>
|
||||
/// <returns>Content from the cache</returns>
|
||||
public Task<IEnumerable<string>> GetCacheAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to standard output receive events
|
||||
/// </summary>
|
||||
/// <param name="callback">Callback which will be invoked whenever a new line is received</param>
|
||||
/// <returns>Subscription disposable to unsubscribe from the event</returns>
|
||||
public Task<IAsyncDisposable> SubscribeStdOutAsync(Func<string, ValueTask> callback);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IFileSystem : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the path of the file system on the host operating system to be reused by other components
|
||||
/// </summary>
|
||||
/// <returns>Path to the file systems storage location</returns>
|
||||
public Task<string> GetPathAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the file system exists
|
||||
/// </summary>
|
||||
/// <returns>True if it does exist. False if it doesn't exist</returns>
|
||||
public Task<bool> CheckExistsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the file system is mounted
|
||||
/// </summary>
|
||||
/// <returns>True if its mounted, False if it is not mounted</returns>
|
||||
public Task<bool> CheckMountedAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the file system. E.g. Creating a virtual disk, formatting it
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task CreateAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Performs checks and optimisations on the file system.
|
||||
/// E.g. checking for corrupted files, resizing a virtual disk or adjusting file permissions
|
||||
/// <remarks>Requires <see cref="MountAsync"/> to be called before or the file system to be in a mounted state</remarks>
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task PerformChecksAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Mounts the file system
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task MountAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Unmounts the file system
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task UnmountAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the file system and its contents
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task DestroyAsync();
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IInstallation : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the installation environment exists. It doesn't matter if it is currently running or not
|
||||
/// </summary>
|
||||
/// <returns>True if it exists, False if it doesn't</returns>
|
||||
public Task<bool> CheckExistsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the installation environment
|
||||
/// </summary>
|
||||
/// <param name="runtimePath">Host path of the runtime storage location</param>
|
||||
/// <param name="hostPath">Host path of the installation file system</param>
|
||||
/// <param name="data">Installation data for the server</param>
|
||||
/// <returns></returns>
|
||||
public Task CreateAsync(string runtimePath, string hostPath, ServerInstallDataResponse data);
|
||||
|
||||
/// <summary>
|
||||
/// Starts the installation
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task StartAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Kills the current installation immediately
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task KillAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Removes the installation. E.g. removes the docker container
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task DestroyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the event when the installation exists
|
||||
/// </summary>
|
||||
/// <param name="callback">Callback to invoke whenever the installation exists</param>
|
||||
/// <returns>Subscription disposable to unsubscribe from the event</returns>
|
||||
public Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> callback);
|
||||
|
||||
/// <summary>
|
||||
/// Connects an existing installation to this abstraction in order to restore it.
|
||||
/// E.g. fetching the container id and using it for exit events
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task RestoreAsync();
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IOnlineDetector : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the detection engine for the online state
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task CreateAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Handles the detection of the online state based on the received output
|
||||
/// </summary>
|
||||
/// <param name="line">Excerpt of the output</param>
|
||||
/// <returns>True if the detection showed that the server is online. False if the detection didnt find anything</returns>
|
||||
public Task<bool> HandleOutputAsync(string line);
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the detection engine for the online state
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task DestroyAsync();
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IReporter : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes both in the server logs as well in the server console the provided message as a status update
|
||||
/// </summary>
|
||||
/// <param name="message">Message to write</param>
|
||||
/// <returns></returns>
|
||||
public Task StatusAsync(string message);
|
||||
|
||||
/// <summary>
|
||||
/// Writes both in the server logs as well in the server console the provided message as an error
|
||||
/// </summary>
|
||||
/// <param name="message">Message to write</param>
|
||||
/// <returns></returns>
|
||||
public Task ErrorAsync(string message);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IRestorer : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks for any running runtime environment from which the state can be restored from
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task<bool> HandleRuntimeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Checks for any running installation environment from which the state can be restored from
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task<bool> HandleInstallationAsync();
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IRuntime : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the runtime does exist. This includes already running instances
|
||||
/// </summary>
|
||||
/// <returns>True if it exists, False if it doesn't</returns>
|
||||
public Task<bool> CheckExistsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the runtime with the specified path as the storage path where the server files should be stored in
|
||||
/// </summary>
|
||||
/// <param name="path">Path where the server files are located</param>
|
||||
/// <returns></returns>
|
||||
public Task CreateAsync(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Starts the runtime. This requires <see cref="CreateAsync"/> to be called before this function
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task StartAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Performs a live update on the runtime. When this method is called the current server configuration has already been updated
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task UpdateAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Kills the current runtime immediately
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task KillAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the runtime. When implemented using docker this would remove the container used for hosting the runtime
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task DestroyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// This subscribes to the exited event of the runtime
|
||||
/// </summary>
|
||||
/// <param name="callback">Callback gets invoked whenever the runtime exites</param>
|
||||
/// <returns>Subscription disposable to unsubscribe from the event</returns>
|
||||
public Task<IAsyncDisposable> SubscribeExitedAsync(Func<int, ValueTask> callback);
|
||||
|
||||
/// <summary>
|
||||
/// Connects an existing runtime to this abstraction in order to restore it.
|
||||
/// E.g. fetching the container id and using it for exit events
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task RestoreAsync();
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IServerComponent : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the server component
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task InitializeAsync();
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
using Stateless;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IServerStateHandler : IAsyncDisposable
|
||||
{
|
||||
public Task ExecuteAsync(StateMachine<ServerState, ServerTrigger>.Transition transition);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
|
||||
public interface IStatistics : IServerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Attaches the statistics collector to the currently running runtime
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task AttachRuntimeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Attaches the statistics collector to the currently running installation
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task AttachInstallationAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Clears the statistics cache
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task ClearCacheAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the statistics data from the cache
|
||||
/// </summary>
|
||||
/// <returns>All data from the cache</returns>
|
||||
public Task<IEnumerable<StatisticsData>> GetCacheAsync();
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using MoonlightServers.Daemon.Models.Cache;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
public class ServerContext
|
||||
{
|
||||
public ServerConfiguration Configuration { get; set; }
|
||||
public int Identifier { get; set; }
|
||||
public AsyncServiceScope ServiceScope { get; set; }
|
||||
public Server Server { get; set; }
|
||||
public ILogger Logger { get; set; }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Models;
|
||||
|
||||
public class StatisticsData
|
||||
{
|
||||
|
||||
}
|
||||
37
MoonlightServers.Daemon/ServerSystem/Server.Delete.cs
Normal file
37
MoonlightServers.Daemon/ServerSystem/Server.Delete.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
public async Task DeleteAsync()
|
||||
{
|
||||
await Lock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
if(State != ServerState.Offline)
|
||||
throw new InvalidOperationException("Server is not offline");
|
||||
|
||||
Logger.LogTrace("Deleting");
|
||||
|
||||
InstallStorage ??= await InstallStorageService.FindAsync(Uuid);
|
||||
|
||||
if (InstallStorage != null)
|
||||
{
|
||||
Logger.LogTrace("Deleting install storage");
|
||||
await InstallStorageService.DeleteAsync(InstallStorage);
|
||||
}
|
||||
|
||||
RuntimeStorage ??= await RuntimeStorageService.FindAsync(Uuid);
|
||||
|
||||
if (RuntimeStorage != null)
|
||||
{
|
||||
Logger.LogTrace("Deleting runtime storage");
|
||||
await RuntimeStorageService.DeleteAsync(RuntimeStorage);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Lock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
157
MoonlightServers.Daemon/ServerSystem/Server.Install.cs
Normal file
157
MoonlightServers.Daemon/ServerSystem/Server.Install.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
public async Task InstallAsync()
|
||||
{
|
||||
await Lock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
if (State != ServerState.Offline)
|
||||
throw new InvalidOperationException("Server is not offline");
|
||||
|
||||
// Check if any pre-existing install env exists, if we don't have a reference to it already
|
||||
InstallEnvironment ??= await InstallEnvironmentService.FindAsync(Uuid);
|
||||
|
||||
// Check if storages exist
|
||||
InstallStorage ??= await InstallStorageService.FindAsync(Uuid);
|
||||
RuntimeStorage ??= await RuntimeStorageService.FindAsync(Uuid);
|
||||
|
||||
// Remove any pre-existing installation env
|
||||
if (InstallEnvironment != null)
|
||||
{
|
||||
Logger.LogTrace("Destroying pre-existing install environment");
|
||||
|
||||
if (await InstallEnvironment.IsRunningAsync())
|
||||
{
|
||||
Logger.LogTrace("Pre-existing install environment is still running, killing it");
|
||||
await InstallEnvironment.KillAsync();
|
||||
}
|
||||
|
||||
// Remove any event handlers if existing
|
||||
InstallEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
|
||||
InstallEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
|
||||
InstallEnvironment.OnExited -= OnInstallExitedAsync;
|
||||
|
||||
// Now remove it
|
||||
// Finally remove it
|
||||
await InstallEnvironmentService.DeleteAsync(InstallEnvironment);
|
||||
InstallEnvironment = null;
|
||||
|
||||
Logger.LogTrace("Pre-existing install environment destroyed");
|
||||
}
|
||||
|
||||
// Remove pre-existing installation storage
|
||||
if (InstallStorage != null)
|
||||
{
|
||||
Logger.LogTrace("Destroying pre-existing installation storage");
|
||||
|
||||
await InstallStorageService.DeleteAsync(InstallStorage);
|
||||
InstallStorage = null;
|
||||
}
|
||||
|
||||
// Fetch the latest configuration
|
||||
Logger.LogTrace("Fetching latest configuration");
|
||||
|
||||
RuntimeConfiguration = await ConfigurationService.GetRuntimeConfigurationAsync(Uuid);
|
||||
InstallConfiguration = await ConfigurationService.GetInstallConfigurationAsync(Uuid);
|
||||
|
||||
// Ensure runtime storage
|
||||
if (RuntimeStorage == null)
|
||||
{
|
||||
Logger.LogTrace("Creating runtime storage");
|
||||
RuntimeStorage = await RuntimeStorageService.CreateAsync(Uuid, RuntimeConfiguration);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogTrace("Updating runtime storage");
|
||||
await RuntimeStorageService.UpdateAsync(RuntimeStorage, RuntimeConfiguration);
|
||||
}
|
||||
|
||||
// Create installation storage
|
||||
Logger.LogTrace("Creating installation storage");
|
||||
InstallStorage = await InstallStorageService.CreateAsync(Uuid, RuntimeConfiguration, InstallConfiguration);
|
||||
|
||||
// Write install script
|
||||
var installStoragePath = await InstallStorage.GetHostPathAsync();
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(installStoragePath, "install.sh"),
|
||||
InstallConfiguration.Script
|
||||
);
|
||||
|
||||
// Create env
|
||||
Logger.LogTrace("Creating install environment");
|
||||
|
||||
InstallEnvironment = await InstallEnvironmentService.CreateAsync(
|
||||
Uuid,
|
||||
RuntimeConfiguration,
|
||||
InstallConfiguration,
|
||||
InstallStorage,
|
||||
RuntimeStorage
|
||||
);
|
||||
|
||||
// Add event handlers
|
||||
Logger.LogTrace("Attaching to install environment");
|
||||
|
||||
InstallEnvironment.Console.OnOutput += OnConsoleMessageAsync;
|
||||
InstallEnvironment.Statistics.OnStatisticsReceived += OnStatisticsReceivedAsync;
|
||||
InstallEnvironment.OnExited += OnInstallExitedAsync;
|
||||
|
||||
// Attach console and statistics
|
||||
await InstallEnvironment.Console.AttachAsync();
|
||||
await InstallEnvironment.Statistics.AttachAsync();
|
||||
|
||||
// Finally start the env
|
||||
Logger.LogTrace("Starting install environment");
|
||||
|
||||
await InstallEnvironment.StartAsync();
|
||||
|
||||
await ChangeStateAsync(ServerState.Installing);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnInstallExitedAsync()
|
||||
{
|
||||
Logger.LogTrace("Install environment exited, checking result and cleaning up");
|
||||
|
||||
await Lock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: Handle crash
|
||||
|
||||
if (InstallEnvironment == null)
|
||||
throw new InvalidOperationException("Install environment is not set");
|
||||
|
||||
// Make sure no event handler is there
|
||||
InstallEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
|
||||
InstallEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
|
||||
InstallEnvironment.OnExited -= OnInstallExitedAsync;
|
||||
|
||||
// Remove env
|
||||
await InstallEnvironmentService.DeleteAsync(InstallEnvironment);
|
||||
InstallEnvironment = null;
|
||||
|
||||
Logger.LogTrace("Install environment cleaned up");
|
||||
|
||||
if(InstallStorage == null)
|
||||
throw new InvalidOperationException("Install storage is not set");
|
||||
|
||||
Logger.LogTrace("Cleaned up install storage");
|
||||
await InstallStorageService.DeleteAsync(InstallStorage);
|
||||
InstallStorage = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Lock.Release();
|
||||
}
|
||||
|
||||
await ChangeStateAsync(ServerState.Offline);
|
||||
}
|
||||
}
|
||||
163
MoonlightServers.Daemon/ServerSystem/Server.Power.cs
Normal file
163
MoonlightServers.Daemon/ServerSystem/Server.Power.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
public async Task StartAsync()
|
||||
{
|
||||
await Lock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
if (State != ServerState.Offline)
|
||||
throw new InvalidOperationException("Server is not offline");
|
||||
|
||||
// Check for any pre-existing runtime environment, if we don't have a reference already
|
||||
RuntimeEnvironment ??= await RuntimeEnvironmentService.FindAsync(Uuid);
|
||||
RuntimeStorage ??= await RuntimeStorageService.FindAsync(Uuid);
|
||||
|
||||
// Remove any pre-existing environment
|
||||
if (RuntimeEnvironment != null)
|
||||
{
|
||||
Logger.LogTrace("Destroying pre-existing runtime environment");
|
||||
|
||||
if (await RuntimeEnvironment.IsRunningAsync())
|
||||
{
|
||||
Logger.LogTrace("Pre-existing runtime environment is still running, killing it");
|
||||
await RuntimeEnvironment.KillAsync();
|
||||
}
|
||||
|
||||
// Make sure no event handler is there anymore
|
||||
RuntimeEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
|
||||
RuntimeEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
|
||||
RuntimeEnvironment.OnExited -= OnRuntimeExitedAsync;
|
||||
|
||||
// Finally remove it
|
||||
await RuntimeEnvironmentService.DeleteAsync(RuntimeEnvironment);
|
||||
RuntimeEnvironment = null;
|
||||
|
||||
Logger.LogTrace("Pre-existing runtime environment destroyed");
|
||||
}
|
||||
|
||||
// Fetch the latest config
|
||||
Logger.LogTrace("Fetching latest configuration");
|
||||
RuntimeConfiguration = await ConfigurationService.GetRuntimeConfigurationAsync(Uuid);
|
||||
|
||||
// Ensure runtime storage
|
||||
if (RuntimeStorage == null)
|
||||
{
|
||||
Logger.LogTrace("Creating runtime storage");
|
||||
RuntimeStorage = await RuntimeStorageService.CreateAsync(Uuid, RuntimeConfiguration);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogTrace("Updating runtime storage");
|
||||
await RuntimeStorageService.UpdateAsync(RuntimeStorage, RuntimeConfiguration);
|
||||
}
|
||||
|
||||
// Create the environment
|
||||
Logger.LogTrace("Creating runtime environment");
|
||||
|
||||
RuntimeEnvironment = await RuntimeEnvironmentService.CreateAsync(Uuid, RuntimeConfiguration, RuntimeStorage);
|
||||
|
||||
// Set event handlers
|
||||
Logger.LogTrace("Attaching to runtime environment");
|
||||
|
||||
RuntimeEnvironment.Console.OnOutput += OnConsoleMessageAsync;
|
||||
RuntimeEnvironment.Statistics.OnStatisticsReceived += OnStatisticsReceivedAsync;
|
||||
RuntimeEnvironment.OnExited += OnRuntimeExitedAsync;
|
||||
|
||||
// Attach console & statistics
|
||||
await RuntimeEnvironment.Console.AttachAsync();
|
||||
await RuntimeEnvironment.Statistics.AttachAsync();
|
||||
|
||||
// Start up
|
||||
Logger.LogTrace("Starting runtime environment");
|
||||
|
||||
await RuntimeEnvironment.StartAsync();
|
||||
|
||||
await ChangeStateAsync(ServerState.Starting);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync()
|
||||
{
|
||||
await Lock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
if (State is not (ServerState.Starting or ServerState.Online))
|
||||
throw new InvalidOperationException("Server is not starting or online");
|
||||
|
||||
if (RuntimeEnvironment == null)
|
||||
throw new InvalidOperationException("Runtime environment is not set");
|
||||
|
||||
Logger.LogTrace("Sending stop command to runtime environment");
|
||||
await RuntimeEnvironment.Console.WriteInputAsync("stop\n\r");
|
||||
|
||||
await ChangeStateAsync(ServerState.Stopping);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task KillAsync()
|
||||
{
|
||||
await Lock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
if (State is not (ServerState.Starting or ServerState.Online or ServerState.Stopping))
|
||||
throw new InvalidOperationException("Server is not starting, stopping or online");
|
||||
|
||||
if (RuntimeEnvironment == null)
|
||||
throw new InvalidOperationException("Runtime environment is not set");
|
||||
|
||||
Logger.LogTrace("Killing runtime environment");
|
||||
await RuntimeEnvironment.KillAsync();
|
||||
|
||||
await ChangeStateAsync(ServerState.Stopping);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnRuntimeExitedAsync()
|
||||
{
|
||||
Logger.LogTrace("Runtime environment exited, checking result and cleaning up");
|
||||
|
||||
await Lock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: Handle crash
|
||||
|
||||
if (RuntimeEnvironment == null)
|
||||
throw new InvalidOperationException("Runtime environment is not set");
|
||||
|
||||
// Make sure no event handler is there anymore
|
||||
RuntimeEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
|
||||
RuntimeEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
|
||||
RuntimeEnvironment.OnExited -= OnRuntimeExitedAsync;
|
||||
|
||||
// Finally remove it
|
||||
await RuntimeEnvironmentService.DeleteAsync(RuntimeEnvironment);
|
||||
RuntimeEnvironment = null;
|
||||
|
||||
Logger.LogTrace("Runtime environment cleaned up");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Lock.Release();
|
||||
}
|
||||
|
||||
await ChangeStateAsync(ServerState.Offline);
|
||||
}
|
||||
}
|
||||
69
MoonlightServers.Daemon/ServerSystem/Server.Restore.cs
Normal file
69
MoonlightServers.Daemon/ServerSystem/Server.Restore.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public partial class Server
|
||||
{
|
||||
// Attempts to reattach to any running install or runtime environment that survived a daemon restart.
|
||||
// Returns the appropriate state based on what was found, or Offline if nothing is running.
|
||||
private async Task<ServerState> RestoreAsync()
|
||||
{
|
||||
// Install
|
||||
Logger.LogTrace("Checking for existing install environment");
|
||||
|
||||
InstallEnvironment = await InstallEnvironmentService.FindAsync(Uuid);
|
||||
InstallStorage = await InstallStorageService.FindAsync(Uuid);
|
||||
|
||||
if (InstallEnvironment != null)
|
||||
{
|
||||
var isRunning = await InstallEnvironment.IsRunningAsync();
|
||||
|
||||
if (isRunning)
|
||||
{
|
||||
Logger.LogTrace("Found running install environment, reattaching");
|
||||
|
||||
InstallEnvironment.Console.OnOutput += OnConsoleMessageAsync;
|
||||
InstallEnvironment.Statistics.OnStatisticsReceived += OnStatisticsReceivedAsync;
|
||||
InstallEnvironment.OnExited += OnInstallExitedAsync;
|
||||
|
||||
await InstallEnvironment.Console.AttachAsync();
|
||||
await InstallEnvironment.Statistics.AttachAsync();
|
||||
|
||||
return ServerState.Installing;
|
||||
}
|
||||
|
||||
Logger.LogTrace("Install environment exists but is not running, ignoring");
|
||||
}
|
||||
|
||||
// Runtime
|
||||
Logger.LogTrace("Checking for existing runtime environment");
|
||||
|
||||
RuntimeEnvironment = await RuntimeEnvironmentService.FindAsync(Uuid);
|
||||
RuntimeStorage = await RuntimeStorageService.FindAsync(Uuid);
|
||||
|
||||
if (RuntimeEnvironment != null)
|
||||
{
|
||||
var isRunning = await RuntimeEnvironment.IsRunningAsync();
|
||||
|
||||
if (isRunning)
|
||||
{
|
||||
Logger.LogTrace("Found running runtime environment, reattaching");
|
||||
|
||||
RuntimeEnvironment.Console.OnOutput += OnConsoleMessageAsync;
|
||||
RuntimeEnvironment.Statistics.OnStatisticsReceived += OnStatisticsReceivedAsync;
|
||||
RuntimeEnvironment.OnExited += OnRuntimeExitedAsync;
|
||||
|
||||
await RuntimeEnvironment.Console.AttachAsync();
|
||||
await RuntimeEnvironment.Statistics.AttachAsync();
|
||||
|
||||
// TODO: Use string online check here
|
||||
|
||||
return ServerState.Online;
|
||||
}
|
||||
|
||||
Logger.LogTrace("Runtime environment exists but is not running, ignoring");
|
||||
}
|
||||
|
||||
Logger.LogTrace("No running environments found");
|
||||
|
||||
return ServerState.Offline;
|
||||
}
|
||||
}
|
||||
@@ -1,161 +1,111 @@
|
||||
using MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
using Stateless;
|
||||
using MoonlightServers.Daemon.Models;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
using MoonlightServers.Daemon.Services;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public partial class Server : IAsyncDisposable
|
||||
{
|
||||
public int Identifier => InnerContext.Identifier;
|
||||
public ServerContext Context => InnerContext;
|
||||
public ServerState State { get; private set; }
|
||||
|
||||
public IConsole Console { get; }
|
||||
public IFileSystem RuntimeFileSystem { get; }
|
||||
public IFileSystem InstallationFileSystem { get; }
|
||||
public IInstallation Installation { get; }
|
||||
public IOnlineDetector OnlineDetector { get; }
|
||||
public IReporter Reporter { get; }
|
||||
public IRestorer Restorer { get; }
|
||||
public IRuntime Runtime { get; }
|
||||
public IStatistics Statistics { get; }
|
||||
public StateMachine<ServerState, ServerTrigger> StateMachine { get; private set; }
|
||||
private IRuntimeEnvironment? RuntimeEnvironment;
|
||||
private RuntimeConfiguration RuntimeConfiguration;
|
||||
private IRuntimeStorage? RuntimeStorage;
|
||||
|
||||
private readonly IServerStateHandler[] Handlers;
|
||||
private IInstallEnvironment? InstallEnvironment;
|
||||
private InstallConfiguration InstallConfiguration;
|
||||
private IInstallStorage? InstallStorage;
|
||||
|
||||
private readonly IServerComponent[] AllComponents;
|
||||
private readonly ServerContext InnerContext;
|
||||
private readonly IRuntimeEnvironmentService RuntimeEnvironmentService;
|
||||
private readonly IInstallEnvironmentService InstallEnvironmentService;
|
||||
private readonly IRuntimeStorageService RuntimeStorageService;
|
||||
private readonly IInstallStorageService InstallStorageService;
|
||||
|
||||
private readonly ServerConfigurationService ConfigurationService;
|
||||
private readonly string Uuid;
|
||||
private readonly ILogger Logger;
|
||||
|
||||
private readonly SemaphoreSlim Lock = new(1, 1);
|
||||
|
||||
public Server(
|
||||
ILogger logger,
|
||||
ServerContext context,
|
||||
IConsole console,
|
||||
IFileSystem runtimeFileSystem,
|
||||
IFileSystem installationFileSystem,
|
||||
IInstallation installation,
|
||||
IOnlineDetector onlineDetector,
|
||||
IReporter reporter,
|
||||
IRestorer restorer,
|
||||
IRuntime runtime,
|
||||
IStatistics statistics,
|
||||
IEnumerable<IServerStateHandler> handlers,
|
||||
IEnumerable<IServerComponent> additionalComponents
|
||||
string uuid,
|
||||
IRuntimeEnvironmentService runtimeEnvironmentService,
|
||||
IInstallEnvironmentService installEnvironmentService,
|
||||
IRuntimeStorageService runtimeStorageService,
|
||||
IInstallStorageService installStorageService,
|
||||
ServerConfigurationService configurationService,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
Uuid = uuid;
|
||||
RuntimeEnvironmentService = runtimeEnvironmentService;
|
||||
InstallEnvironmentService = installEnvironmentService;
|
||||
RuntimeStorageService = runtimeStorageService;
|
||||
InstallStorageService = installStorageService;
|
||||
ConfigurationService = configurationService;
|
||||
Logger = logger;
|
||||
InnerContext = context;
|
||||
Console = console;
|
||||
RuntimeFileSystem = runtimeFileSystem;
|
||||
InstallationFileSystem = installationFileSystem;
|
||||
Installation = installation;
|
||||
OnlineDetector = onlineDetector;
|
||||
Reporter = reporter;
|
||||
Restorer = restorer;
|
||||
Runtime = runtime;
|
||||
Statistics = statistics;
|
||||
|
||||
IEnumerable<IServerComponent> defaultComponents =
|
||||
[
|
||||
Console, RuntimeFileSystem, InstallationFileSystem, Installation, OnlineDetector, Reporter, Restorer,
|
||||
Runtime, Statistics
|
||||
];
|
||||
|
||||
AllComponents = defaultComponents.Concat(additionalComponents).ToArray();
|
||||
|
||||
Handlers = handlers.ToArray();
|
||||
}
|
||||
|
||||
private void ConfigureStateMachine(ServerState initialState)
|
||||
{
|
||||
StateMachine = new StateMachine<ServerState, ServerTrigger>(
|
||||
initialState, FiringMode.Queued
|
||||
);
|
||||
|
||||
StateMachine.Configure(ServerState.Offline)
|
||||
.Permit(ServerTrigger.Start, ServerState.Starting)
|
||||
.Permit(ServerTrigger.Install, ServerState.Installing)
|
||||
.PermitReentry(ServerTrigger.Fail);
|
||||
|
||||
StateMachine.Configure(ServerState.Starting)
|
||||
.Permit(ServerTrigger.DetectOnline, ServerState.Online)
|
||||
.Permit(ServerTrigger.Fail, 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.Fail)
|
||||
.PermitReentry(ServerTrigger.Kill)
|
||||
.Permit(ServerTrigger.Exited, ServerState.Offline);
|
||||
|
||||
StateMachine.Configure(ServerState.Installing)
|
||||
.Permit(ServerTrigger.Fail, ServerState.Offline) // TODO: Add kill
|
||||
.Permit(ServerTrigger.Exited, ServerState.Offline);
|
||||
}
|
||||
|
||||
private void ConfigureStateMachineEvents()
|
||||
{
|
||||
// Configure the calling of the handlers
|
||||
StateMachine.OnTransitionedAsync(async transition =>
|
||||
{
|
||||
var hasFailed = false;
|
||||
|
||||
foreach (var handler in Handlers)
|
||||
{
|
||||
try
|
||||
{
|
||||
await handler.ExecuteAsync(transition);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(
|
||||
e,
|
||||
"Handler {name} has thrown an unexpected exception",
|
||||
handler.GetType().FullName
|
||||
);
|
||||
|
||||
hasFailed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!hasFailed)
|
||||
return; // Everything went fine, we can exit now
|
||||
|
||||
// Something has failed, lets check if we can handle the error
|
||||
// via a fail trigger
|
||||
|
||||
if(!StateMachine.CanFire(ServerTrigger.Fail))
|
||||
return;
|
||||
|
||||
// Trigger the fail so the server gets a chance to handle the error softly
|
||||
await StateMachine.FireAsync(ServerTrigger.Fail);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
foreach (var component in AllComponents)
|
||||
await component.InitializeAsync();
|
||||
Logger.LogTrace("Initializing");
|
||||
|
||||
var restoredState = ServerState.Offline;
|
||||
await Lock.WaitAsync();
|
||||
|
||||
ConfigureStateMachine(restoredState);
|
||||
ConfigureStateMachineEvents();
|
||||
try
|
||||
{
|
||||
// Restore state
|
||||
State = await RestoreAsync();
|
||||
|
||||
Logger.LogTrace("Initialization complete, restored to state {State}", State);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnConsoleMessageAsync(string message)
|
||||
{
|
||||
Console.WriteLine($"Console: {message}");
|
||||
}
|
||||
|
||||
private async Task OnStatisticsReceivedAsync(ServerStatistics statistics)
|
||||
{
|
||||
}
|
||||
|
||||
private Task ChangeStateAsync(ServerState newState)
|
||||
{
|
||||
Logger.LogTrace("State changed from {OldState} to {NewState}", State, newState);
|
||||
|
||||
State = newState;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
foreach (var handler in Handlers)
|
||||
await handler.DisposeAsync();
|
||||
|
||||
foreach (var component in AllComponents)
|
||||
await component.DisposeAsync();
|
||||
Logger.LogTrace("Disposing");
|
||||
|
||||
if (RuntimeEnvironment != null)
|
||||
{
|
||||
Logger.LogTrace("Detaching and disposing runtime environment");
|
||||
|
||||
RuntimeEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
|
||||
RuntimeEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
|
||||
RuntimeEnvironment.OnExited -= OnRuntimeExitedAsync;
|
||||
|
||||
await RuntimeEnvironment.DisposeAsync();
|
||||
}
|
||||
|
||||
if (InstallEnvironment != null)
|
||||
{
|
||||
Logger.LogTrace("Detaching and disposing install environment");
|
||||
|
||||
InstallEnvironment.Console.OnOutput -= OnConsoleMessageAsync;
|
||||
InstallEnvironment.Statistics.OnStatisticsReceived -= OnStatisticsReceivedAsync;
|
||||
InstallEnvironment.OnExited -= OnInstallExitedAsync;
|
||||
|
||||
await InstallEnvironment.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,97 +1,46 @@
|
||||
using MoonlightServers.Daemon.Models.Cache;
|
||||
using MoonlightServers.Daemon.ServerSystem.Docker;
|
||||
using MoonlightServers.Daemon.ServerSystem.FileSystems;
|
||||
using MoonlightServers.Daemon.ServerSystem.Handlers;
|
||||
using MoonlightServers.Daemon.ServerSystem.Implementations;
|
||||
using MoonlightServers.Daemon.ServerSystem.Interfaces;
|
||||
using MoonlightServers.Daemon.ServerSystem.Models;
|
||||
using MoonlightServers.Daemon.ServerSystem.Abstractions;
|
||||
using MoonlightServers.Daemon.Services;
|
||||
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public class ServerFactory
|
||||
{
|
||||
private readonly IServiceProvider ServiceProvider;
|
||||
private readonly IRuntimeEnvironmentService RuntimeEnvironmentService;
|
||||
private readonly IInstallEnvironmentService InstallEnvironmentService;
|
||||
private readonly IRuntimeStorageService RuntimeStorageService;
|
||||
private readonly IInstallStorageService InstallStorageService;
|
||||
private readonly ServerConfigurationService ConfigurationService;
|
||||
private readonly ILoggerFactory LoggerFactory;
|
||||
|
||||
public ServerFactory(IServiceProvider serviceProvider)
|
||||
public ServerFactory(
|
||||
IRuntimeEnvironmentService runtimeEnvironmentService,
|
||||
IInstallEnvironmentService installEnvironmentService,
|
||||
IRuntimeStorageService runtimeStorageService,
|
||||
IInstallStorageService installStorageService,
|
||||
ServerConfigurationService configurationService,
|
||||
ILoggerFactory loggerFactory
|
||||
)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
RuntimeEnvironmentService = runtimeEnvironmentService;
|
||||
InstallEnvironmentService = installEnvironmentService;
|
||||
RuntimeStorageService = runtimeStorageService;
|
||||
InstallStorageService = installStorageService;
|
||||
ConfigurationService = configurationService;
|
||||
LoggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public async Task<Server> CreateAsync(ServerConfiguration configuration)
|
||||
public async Task<Server> CreateAsync(string uuid)
|
||||
{
|
||||
var scope = ServiceProvider.CreateAsyncScope();
|
||||
var logger = LoggerFactory.CreateLogger($"MoonlightServers.Daemon.ServerSystem.Server({uuid})");
|
||||
|
||||
var loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger($"Servers.Instance.{configuration.Id}.{nameof(Server)}");
|
||||
|
||||
var context = scope.ServiceProvider.GetRequiredService<ServerContext>();
|
||||
|
||||
context.Identifier = configuration.Id;
|
||||
context.Configuration = configuration;
|
||||
context.ServiceScope = scope;
|
||||
context.Logger = logger;
|
||||
|
||||
// Define all required components
|
||||
|
||||
IConsole console;
|
||||
IFileSystem runtimeFs;
|
||||
IFileSystem installFs;
|
||||
IInstallation installation;
|
||||
IOnlineDetector onlineDetector;
|
||||
IReporter reporter;
|
||||
IRestorer restorer;
|
||||
IRuntime runtime;
|
||||
IStatistics statistics;
|
||||
|
||||
// Resolve the components
|
||||
|
||||
console = ActivatorUtilities.CreateInstance<DockerConsole>(scope.ServiceProvider);
|
||||
reporter = ActivatorUtilities.CreateInstance<ServerReporter>(scope.ServiceProvider);
|
||||
runtimeFs = ActivatorUtilities.CreateInstance<RawRuntimeFs>(scope.ServiceProvider);
|
||||
installFs = ActivatorUtilities.CreateInstance<RawInstallationFs>(scope.ServiceProvider);
|
||||
installation = ActivatorUtilities.CreateInstance<DockerInstallation>(scope.ServiceProvider);
|
||||
onlineDetector = ActivatorUtilities.CreateInstance<RegexOnlineDetector>(scope.ServiceProvider);
|
||||
restorer = ActivatorUtilities.CreateInstance<DockerRestorer>(scope.ServiceProvider);
|
||||
runtime = ActivatorUtilities.CreateInstance<DockerRuntime>(scope.ServiceProvider);
|
||||
statistics = ActivatorUtilities.CreateInstance<DockerStatistics>(scope.ServiceProvider);
|
||||
|
||||
// Resolve handlers
|
||||
var handlers = new List<IServerStateHandler>();
|
||||
|
||||
handlers.Add(ActivatorUtilities.CreateInstance<OnlineDetectionHandler>(scope.ServiceProvider));
|
||||
handlers.Add(ActivatorUtilities.CreateInstance<StartupHandler>(scope.ServiceProvider));
|
||||
handlers.Add(ActivatorUtilities.CreateInstance<ShutdownHandler>(scope.ServiceProvider));
|
||||
handlers.Add(ActivatorUtilities.CreateInstance<InstallationHandler>(scope.ServiceProvider));
|
||||
handlers.Add(ActivatorUtilities.CreateInstance<DebugHandler>(scope.ServiceProvider));
|
||||
|
||||
// Resolve additional components
|
||||
var components = new List<IServerComponent>();
|
||||
|
||||
components.Add(ActivatorUtilities.CreateInstance<ConsoleSignalRComponent>(scope.ServiceProvider));
|
||||
|
||||
// TODO: Add a plugin hook for dynamically resolving components and checking if any is unset
|
||||
|
||||
// Resolve server from di
|
||||
var server = new Server(
|
||||
logger,
|
||||
context,
|
||||
// Now all components
|
||||
console,
|
||||
runtimeFs,
|
||||
installFs,
|
||||
installation,
|
||||
onlineDetector,
|
||||
reporter,
|
||||
restorer,
|
||||
runtime,
|
||||
statistics,
|
||||
// And now all the handlers
|
||||
handlers,
|
||||
components
|
||||
return new Server(
|
||||
uuid,
|
||||
RuntimeEnvironmentService,
|
||||
InstallEnvironmentService,
|
||||
RuntimeStorageService,
|
||||
InstallStorageService,
|
||||
ConfigurationService,
|
||||
logger
|
||||
);
|
||||
|
||||
context.Server = server;
|
||||
|
||||
return server;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem.Enums;
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public enum ServerState
|
||||
{
|
||||
3
MoonlightServers.Daemon/ServerSystem/ServerStatistics.cs
Normal file
3
MoonlightServers.Daemon/ServerSystem/ServerStatistics.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace MoonlightServers.Daemon.ServerSystem;
|
||||
|
||||
public record ServerStatistics();
|
||||
Reference in New Issue
Block a user