Implemented online detection. Extended ServerContext to include self reference so sub components can subscribe to the state. Improved console module detach handling. Implemented new server service to replace the old one. Added log restore when restoring

This commit is contained in:
2025-07-30 20:52:24 +02:00
parent eaf8c36f7f
commit 5c170935b4
15 changed files with 419 additions and 70 deletions

View File

@@ -38,10 +38,9 @@ public class DefaultRestorer : IRestorer
{
Logger.LogDebug("Detected runtime to restore");
await Console.CollectFromRuntime();
await Console.AttachToRuntime();
await Statistics.SubscribeToRuntime();
// TODO: Read out existing container log in order to search if the server is online
return ServerState.Online;
}
@@ -50,6 +49,7 @@ public class DefaultRestorer : IRestorer
{
Logger.LogDebug("Detected installation to restore");
await Console.CollectFromInstallation();
await Console.AttachToInstallation();
await Statistics.SubscribeToInstallation();

View File

@@ -1,11 +1,8 @@
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text;
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonCore.Helpers;
using MoonCore.Observability;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.Daemon.ServerSys.Abstractions;
namespace MoonlightServers.Daemon.ServerSys.Implementations;
@@ -26,11 +23,12 @@ public class DockerConsole : IConsole
private MultiplexedStream? CurrentStream;
private CancellationTokenSource Cts = new();
private const string MlPrefix = "\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";
public DockerConsole(
ServerContext context,
DockerClient dockerClient,
ILogger<DockerConsole> logger
)
ILogger<DockerConsole> logger)
{
Context = context;
DockerClient = dockerClient;
@@ -55,8 +53,45 @@ public class DockerConsole : IConsole
await AttachStream(containerName);
}
private Task AttachStream(string containerName)
public async Task Detach()
{
Logger.LogDebug("Detaching stream");
if (!Cts.IsCancellationRequested)
await Cts.CancelAsync();
}
public async Task CollectFromRuntime()
=> await CollectFromContainer($"moonlight-runtime-{Context.Configuration.Id}");
public async Task CollectFromInstallation()
=> await CollectFromContainer($"moonlight-install-{Context.Configuration.Id}");
private async Task CollectFromContainer(string containerName)
{
var logStream = await DockerClient.Containers.GetContainerLogsAsync(containerName, true, new()
{
Follow = false,
ShowStderr = true,
ShowStdout = true
});
var combinedOutput = await logStream.ReadOutputToEndAsync(CancellationToken.None);
var contentToAdd = combinedOutput.stdout + combinedOutput.stderr;
await WriteToOutput(contentToAdd);
}
private async Task AttachStream(string containerName)
{
// This stops any previously existing stream reading if
// any is currently running
if (!Cts.IsCancellationRequested)
await Cts.CancelAsync();
// Reset
Cts = new();
Task.Run(async () =>
{
// This loop is here to reconnect to the container if for some reason the container
@@ -138,8 +173,6 @@ public class DockerConsole : IConsole
Logger.LogDebug("Disconnected from container stream");
}, Cts.Token);
return Task.CompletedTask;
}
public async Task WriteToOutput(string content)
@@ -165,13 +198,12 @@ public class DockerConsole : IConsole
contentBuffer.Length,
Cts.Token
);
await OnInputSubject.OnNextAsync(content);
}
public async Task WriteToMoonlight(string content)
=> await WriteToOutput(
$"\x1b[0;38;2;255;255;255;48;2;124;28;230m Moonlight \x1b[0m\x1b[38;5;250m\x1b[3m {content}\x1b[0m\n\r");
=> await WriteToOutput(string.Format(MlPrefix, content));
public Task ClearOutput()
{

View File

@@ -170,13 +170,13 @@ public class DockerInstaller : IInstaller
ContainerId = createdContainer.ID;
Logger.LogInformation("Created container");
Logger.LogDebug("Created container");
await Console.WriteToMoonlight("Created container");
}
public async Task Start()
{
Logger.LogInformation("Starting container");
Logger.LogDebug("Starting container");
await Console.WriteToMoonlight("Starting container");
await DockerClient.Containers.StartContainerAsync(ContainerId, new());
}

View File

@@ -115,7 +115,7 @@ public class DockerProvisioner : IProvisioner
await EnsureContainerOffline(possibleContainer);
Logger.LogInformation("Removing orphan container");
Logger.LogDebug("Removing orphan container");
await DockerClient.Containers.RemoveContainerAsync(ContainerName, new());
}
catch (DockerContainerNotFoundException)
@@ -151,7 +151,7 @@ public class DockerProvisioner : IProvisioner
ContainerId = createdContainer.ID;
Logger.LogInformation("Created container");
Logger.LogDebug("Created container");
await Console.WriteToMoonlight("Created container");
}
@@ -214,7 +214,7 @@ public class DockerProvisioner : IProvisioner
await EnsureContainerOffline(container);
// 3. Remove the container
Logger.LogInformation("Removing container");
Logger.LogDebug("Removing container");
await Console.WriteToMoonlight("Removing container");
await DockerClient.Containers.RemoveContainerAsync(container.ID, new());

View File

@@ -0,0 +1,87 @@
using System.Text.RegularExpressions;
using MoonCore.Observability;
using MoonlightServers.Daemon.ServerSys.Abstractions;
using MoonlightServers.Daemon.ServerSystem;
namespace MoonlightServers.Daemon.ServerSys.Implementations;
public class RegexOnlineDetection : IOnlineDetection
{
private readonly ServerContext Context;
private readonly IConsole Console;
private readonly ILogger<RegexOnlineDetection> Logger;
private Regex? Regex;
private IAsyncDisposable? ConsoleSubscription;
private IAsyncDisposable? StateSubscription;
public RegexOnlineDetection(ServerContext context, IConsole console, ILogger<RegexOnlineDetection> logger)
{
Context = context;
Console = console;
Logger = logger;
}
public async Task Initialize()
{
Logger.LogInformation("Subscribing to state changes");
StateSubscription = await Context.Self.OnState.SubscribeAsync(async state =>
{
if (state == ServerState.Starting) // Subscribe to console when starting
{
Logger.LogInformation("Detected state change to online. Subscribing to console in order to check for the regex matches");
if(ConsoleSubscription != null)
await ConsoleSubscription.DisposeAsync();
try
{
Regex = new(Context.Configuration.OnlineDetection, RegexOptions.Compiled);
}
catch (Exception e)
{
Logger.LogError(e, "An error occured while building regex expression. Please make sure the regex is valid");
}
ConsoleSubscription = await Console.OnOutput.SubscribeEventAsync(HandleOutput);
}
else if (ConsoleSubscription != null) // Unsubscribe from console when any other state and not already unsubscribed
{
Logger.LogInformation("Detected state change to {state}. Unsubscribing from console", state);
await ConsoleSubscription.DisposeAsync();
ConsoleSubscription = null;
}
});
}
private async ValueTask HandleOutput(string line)
{
// Handle here just to make sure. Shouldn't be required as we
// unsubscribe from the console, as soon as we go online (or any other state).
// The regex should also not be null as we initialize it in the handler above but whatevers
if(Context.Self.StateMachine.State != ServerState.Starting || Regex == null)
return;
if(Regex.Matches(line).Count == 0)
return;
await Context.Self.StateMachine.FireAsync(ServerTrigger.OnlineDetected);
}
public Task Sync()
{
Regex = new(Context.Configuration.OnlineDetection, RegexOptions.Compiled);
return Task.CompletedTask;
}
public async ValueTask DisposeAsync()
{
if(ConsoleSubscription != null)
await ConsoleSubscription.DisposeAsync();
if(StateSubscription != null)
await StateSubscription.DisposeAsync();
}
}