Implemented installation handling. Added crash handling. Refactored tasks reset/cancel functions

This commit is contained in:
2025-02-14 21:15:03 +01:00
parent 761ab455f0
commit 1fbf1ae9ec
12 changed files with 264 additions and 44 deletions

View File

@@ -72,7 +72,7 @@ public partial class Server
private async Task LogToConsole(string message) private async Task LogToConsole(string message)
{ {
await Console.WriteToOutput($"\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m {message}\n\r"); await Console.WriteToOutput($"\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m\x1b[38;5;250m\x1b[3m {message}\x1b[0m\n\r");
} }
public Task<string[]> GetConsoleMessages() public Task<string[]> GetConsoleMessages()

View File

@@ -0,0 +1,34 @@
using Docker.DotNet;
using Docker.DotNet.Models;
namespace MoonlightServers.Daemon.Abstractions;
public partial class Server
{
public async Task InternalCrash()
{
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
ContainerInspectResponse? container;
try
{
container = await dockerClient.Containers.InspectContainerAsync(RuntimeContainerId);
}
catch (DockerContainerNotFoundException)
{
container = null;
}
if(container == null)
return;
var exitCode = container.State.ExitCode;
// TODO: Report to panel
await LogToConsole($"Server crashed. Exit code: {exitCode}");
await Destroy();
}
}

View File

@@ -35,10 +35,16 @@ public partial class Server
} }
catch (DockerContainerNotFoundException){} catch (DockerContainerNotFoundException){}
// Canceling server tasks & listeners // Canceling server tasks & listeners and start new ones
await ResetTasks();
}
public async Task ResetTasks()
{
// Note: This will keep the docker container running, it will just cancel the server cancellation token
// and recreate the token
await CancelTasks(); await CancelTasks();
// and recreating cancellation token
Cancellation = new(); Cancellation = new();
} }

View File

@@ -1,6 +1,7 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Docker.DotNet.Models; using Docker.DotNet.Models;
using MoonlightServers.Daemon.Enums; using MoonlightServers.Daemon.Enums;
using MoonlightServers.Daemon.Extensions;
using Stateless; using Stateless;
namespace MoonlightServers.Daemon.Abstractions; namespace MoonlightServers.Daemon.Abstractions;
@@ -38,23 +39,27 @@ public partial class Server
.Permit(ServerTrigger.Reinstall, ServerState.Installing); .Permit(ServerTrigger.Reinstall, ServerState.Installing);
StateMachine.Configure(ServerState.Starting) StateMachine.Configure(ServerState.Starting)
.Permit(ServerTrigger.NotifyContainerDied, ServerState.Offline) .Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline)
.Permit(ServerTrigger.NotifyOnline, ServerState.Online) .Permit(ServerTrigger.NotifyOnline, ServerState.Online)
.Permit(ServerTrigger.Stop, ServerState.Stopping) .Permit(ServerTrigger.Stop, ServerState.Stopping)
.OnEntryAsync(InternalStart); .OnEntryAsync(InternalStart)
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalCrash);
StateMachine.Configure(ServerState.Online) StateMachine.Configure(ServerState.Online)
.Permit(ServerTrigger.NotifyContainerDied, ServerState.Offline) .Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline)
.Permit(ServerTrigger.Stop, ServerState.Stopping); .Permit(ServerTrigger.Stop, ServerState.Stopping)
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalCrash);
StateMachine.Configure(ServerState.Stopping) StateMachine.Configure(ServerState.Stopping)
.Permit(ServerTrigger.NotifyContainerDied, ServerState.Offline) .Permit(ServerTrigger.NotifyRuntimeContainerDied, ServerState.Offline)
.Permit(ServerTrigger.Kill, ServerState.Offline) .Permit(ServerTrigger.Kill, ServerState.Offline)
.OnEntryAsync(InternalStop); .OnEntryAsync(InternalStop)
.OnExitFromAsync(ServerTrigger.NotifyRuntimeContainerDied, InternalFinishStop);
StateMachine.Configure(ServerState.Installing) StateMachine.Configure(ServerState.Installing)
.Permit(ServerTrigger.NotifyContainerDied, ServerState.Offline) .Permit(ServerTrigger.NotifyInstallationContainerDied, ServerState.Offline)
.OnEntryAsync(InternalInstall); .OnEntryAsync(InternalInstall)
.OnExitAsync(InternalFinishInstall);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -1,4 +1,5 @@
using Docker.DotNet; using Docker.DotNet;
using Docker.DotNet.Models;
using MoonCore.Helpers; using MoonCore.Helpers;
using MoonlightServers.Daemon.Enums; using MoonlightServers.Daemon.Enums;
using MoonlightServers.Daemon.Extensions; using MoonlightServers.Daemon.Extensions;
@@ -36,7 +37,8 @@ public partial class Server
var runtimeHostPath = await EnsureRuntimeVolume(); var runtimeHostPath = await EnsureRuntimeVolume();
// Write installation script to path // Write installation script to path
await File.WriteAllTextAsync(PathBuilder.File(installationHostPath, "install.sh"), installData.Script.Replace("\n\r", "\n") + "\n\n"); var content = installData.Script.Replace("\r\n", "\n");
await File.WriteAllTextAsync(PathBuilder.File(installationHostPath, "install.sh"), content);
// Creating container configuration // Creating container configuration
var parameters = Configuration.ToInstallationCreateParameters( var parameters = Configuration.ToInstallationCreateParameters(
@@ -49,6 +51,26 @@ public partial class Server
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>(); var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
// Ensure we can actually spawn the container
try
{
var existingContainer = await dockerClient.Containers.InspectContainerAsync(InstallationContainerName);
// Perform automatic cleanup / restore
if (existingContainer.State.Running)
await dockerClient.Containers.KillContainerAsync(existingContainer.ID, new());
await dockerClient.Containers.RemoveContainerAsync(existingContainer.ID, new());
}
catch (DockerContainerNotFoundException)
{
// Ignored
}
// Spawn the container
var container = await dockerClient.Containers.CreateContainerAsync(parameters); var container = await dockerClient.Containers.CreateContainerAsync(parameters);
InstallationContainerId = container.ID; InstallationContainerId = container.ID;
@@ -56,4 +78,40 @@ public partial class Server
await dockerClient.Containers.StartContainerAsync(InstallationContainerId, new()); await dockerClient.Containers.StartContainerAsync(InstallationContainerId, new());
} }
private async Task InternalFinishInstall()
{
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
ContainerInspectResponse? container;
try
{
container = await dockerClient.Containers.InspectContainerAsync(InstallationContainerId, CancellationToken.None);
}
catch (DockerContainerNotFoundException)
{
container = null;
}
if(container == null)
return;
var exitCode = container.State.ExitCode;
await LogToConsole($"Installation finished with exit code: {exitCode}");
if (exitCode != 0)
{
// TODO: Report installation failure
}
await LogToConsole("Removing container");
//await dockerClient.Containers.RemoveContainerAsync(InstallationContainerId, new());
InstallationContainerId = null;
await ResetTasks();
await RemoveInstallationVolume();
}
} }

View File

@@ -4,5 +4,6 @@ namespace MoonlightServers.Daemon.Abstractions;
public partial class Server public partial class Server
{ {
public async Task NotifyContainerDied() => await StateMachine.FireAsync(ServerTrigger.NotifyContainerDied); public async Task NotifyRuntimeContainerDied() => await StateMachine.FireAsync(ServerTrigger.NotifyRuntimeContainerDied);
public async Task NotifyInstallationContainerDied() => await StateMachine.FireAsync(ServerTrigger.NotifyInstallationContainerDied);
} }

View File

@@ -10,4 +10,9 @@ public partial class Server
{ {
await Console.WriteToInput($"{Configuration.StopCommand}\n\r"); await Console.WriteToInput($"{Configuration.StopCommand}\n\r");
} }
private async Task InternalFinishStop()
{
await Destroy();
}
} }

View File

@@ -6,6 +6,21 @@ namespace MoonlightServers.Daemon.Abstractions;
public partial class Server public partial class Server
{ {
private async Task<string> EnsureRuntimeVolume() private async Task<string> EnsureRuntimeVolume()
{
var hostPath = GetRuntimeVolumePath();
await LogToConsole("Creating storage");
// Create volume if missing
if (!Directory.Exists(hostPath))
Directory.CreateDirectory(hostPath);
// TODO: Virtual disk
return hostPath;
}
private string GetRuntimeVolumePath()
{ {
var appConfiguration = ServiceProvider.GetRequiredService<AppConfiguration>(); var appConfiguration = ServiceProvider.GetRequiredService<AppConfiguration>();
@@ -14,23 +29,39 @@ public partial class Server
Configuration.Id.ToString() Configuration.Id.ToString()
); );
await LogToConsole("Creating storage"); if (hostPath.StartsWith("/"))
return hostPath;
else
return PathBuilder.JoinPaths(Directory.GetCurrentDirectory(), hostPath);
}
public async Task RemoveRuntimeVolume()
{
var hostPath = GetRuntimeVolumePath();
await LogToConsole("Removing storage");
// Remove volume if existing
if (Directory.Exists(hostPath))
Directory.Delete(hostPath, true);
// TODO: Virtual disk // TODO: Virtual disk
}
private async Task<string> EnsureInstallationVolume()
{
var hostPath = GetInstallationVolumePath();
await LogToConsole("Creating installation storage");
// Create volume if missing // Create volume if missing
if (!Directory.Exists(hostPath)) if (!Directory.Exists(hostPath))
Directory.CreateDirectory(hostPath); Directory.CreateDirectory(hostPath);
if (hostPath.StartsWith("/"))
return hostPath;
else
return PathBuilder.JoinPaths(Directory.GetCurrentDirectory(), hostPath);
return hostPath; return hostPath;
} }
private async Task<string> EnsureInstallationVolume() private string GetInstallationVolumePath()
{ {
var appConfiguration = ServiceProvider.GetRequiredService<AppConfiguration>(); var appConfiguration = ServiceProvider.GetRequiredService<AppConfiguration>();
@@ -39,17 +70,20 @@ public partial class Server
Configuration.Id.ToString() Configuration.Id.ToString()
); );
await LogToConsole("Creating installation storage");
// Create volume if missing
if (!Directory.Exists(hostPath))
Directory.CreateDirectory(hostPath);
if (hostPath.StartsWith("/")) if (hostPath.StartsWith("/"))
return hostPath; return hostPath;
else else
return PathBuilder.JoinPaths(Directory.GetCurrentDirectory(), hostPath); return PathBuilder.JoinPaths(Directory.GetCurrentDirectory(), hostPath);
}
return hostPath; public async Task RemoveInstallationVolume()
{
var hostPath = GetInstallationVolumePath();
await LogToConsole("Removing installation storage");
// Remove volume if existing
if (Directory.Exists(hostPath))
Directory.Delete(hostPath, true);
} }
} }

View File

@@ -8,5 +8,6 @@ public enum ServerTrigger
Kill = 3, Kill = 3,
Reinstall = 4, Reinstall = 4,
NotifyOnline = 5, NotifyOnline = 5,
NotifyContainerDied = 6 NotifyRuntimeContainerDied = 6,
NotifyInstallationContainerDied = 7
} }

View File

@@ -176,8 +176,6 @@ public static class ServerConfigurationExtensions
parameters.Cmd = [installShell, "/mnt/install/install.sh"]; parameters.Cmd = [installShell, "/mnt/install/install.sh"];
parameters.HostConfig.AutoRemove = true;
return parameters; return parameters;
} }

View File

@@ -0,0 +1,66 @@
using Stateless;
namespace MoonlightServers.Daemon.Extensions;
public static class StateConfigurationExtensions
{
public static StateMachine<TState, TTrigger>.StateConfiguration OnExitFrom<TState, TTrigger>(
this StateMachine<TState, TTrigger>.StateConfiguration configuration, TTrigger trigger, Action entryAction
)
{
configuration.OnExit(transition =>
{
if(!transition.Trigger!.Equals(trigger))
return;
entryAction.Invoke();
});
return configuration;
}
public static StateMachine<TState, TTrigger>.StateConfiguration OnExitFrom<TState, TTrigger>(
this StateMachine<TState, TTrigger>.StateConfiguration configuration, TTrigger trigger, Action<StateMachine<TState, TTrigger>.Transition> entryAction
)
{
configuration.OnExit(transition =>
{
if(!transition.Trigger!.Equals(trigger))
return;
entryAction.Invoke(transition);
});
return configuration;
}
public static StateMachine<TState, TTrigger>.StateConfiguration OnExitFromAsync<TState, TTrigger>(
this StateMachine<TState, TTrigger>.StateConfiguration configuration, TTrigger trigger, Func<Task> entryAction
)
{
configuration.OnExitAsync(transition =>
{
if(!transition.Trigger!.Equals(trigger))
return Task.CompletedTask;
return entryAction.Invoke();
});
return configuration;
}
public static StateMachine<TState, TTrigger>.StateConfiguration OnExitFromAsync<TState, TTrigger>(
this StateMachine<TState, TTrigger>.StateConfiguration configuration, TTrigger trigger, Func<StateMachine<TState, TTrigger>.Transition, Task> entryAction
)
{
configuration.OnExitAsync(transition =>
{
if(!transition.Trigger!.Equals(trigger))
return Task.CompletedTask;
return entryAction.Invoke(transition);
});
return configuration;
}
}

View File

@@ -115,15 +115,27 @@ public class ServerService : IHostedLifecycleService
Server? server; Server? server;
lock (Servers)
server = Servers.FirstOrDefault(x => x.RuntimeContainerId == message.ID || x.InstallationContainerId == message.ID);
// TODO: Maybe implement a lookup for containers which id isn't set in the cache // TODO: Maybe implement a lookup for containers which id isn't set in the cache
if (server == null) // Check if it's a runtime container
return; lock (Servers)
server = Servers.FirstOrDefault(x => x.RuntimeContainerId == message.ID);
await server.NotifyContainerDied(); if (server != null)
{
await server.NotifyRuntimeContainerDied();
return;
}
// Check if it's an installation container
lock (Servers)
server = Servers.FirstOrDefault(x => x.InstallationContainerId == message.ID);
if (server != null)
{
await server.NotifyInstallationContainerDied();
return;
}
}), Cancellation.Token); }), Cancellation.Token);
} }
catch (TaskCanceledException) catch (TaskCanceledException)