Re-implemented server state machine. Cleaned up code

TODO: Handle trigger errors
This commit is contained in:
2025-02-12 23:02:00 +01:00
parent 4088bfaef5
commit f45699f300
44 changed files with 913 additions and 831 deletions

View File

@@ -1,119 +0,0 @@
using Docker.DotNet;
using Docker.DotNet.Models;
using MoonCore.Helpers;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Models;
using MoonlightServers.DaemonShared.Enums;
namespace MoonlightServers.Daemon.Helpers;
public class ServerActionHelper
{
public static async Task Start(Server server, IServiceProvider serviceProvider)
{
await EnsureStorage(server, serviceProvider);
await EnsureDockerImage(server, serviceProvider);
await CreateRuntimeContainer(server, serviceProvider);
await StartRuntimeContainer(server, serviceProvider);
}
private static async Task EnsureStorage(Server server, IServiceProvider serviceProvider)
{
await NotifyTask(server, serviceProvider, ServerTask.CreatingStorage);
// Build paths
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
var volumePath = PathBuilder.Dir(
configuration.Storage.Volumes,
server.Configuration.Id.ToString()
);
// Create volume if missing
if (!Directory.Exists(volumePath))
Directory.CreateDirectory(volumePath);
// TODO: Virtual disk
}
private static async Task EnsureDockerImage(Server server, IServiceProvider serviceProvider)
{
await NotifyTask(server, serviceProvider, ServerTask.PullingDockerImage);
var dockerClient = serviceProvider.GetRequiredService<DockerClient>();
await dockerClient.Images.CreateImageAsync(new()
{
FromImage = server.Configuration.DockerImage
},
new AuthConfig(),
new Progress<JSONMessage>(async message =>
{
//var percentage = (int)(message.Progress.Current / message.Progress.Total);
//await UpdateProgress(server, serviceProvider, percentage);
})
);
}
private static async Task CreateRuntimeContainer(Server server, IServiceProvider serviceProvider)
{
var dockerClient = serviceProvider.GetRequiredService<DockerClient>();
try
{
var existingContainer = await dockerClient.Containers.InspectContainerAsync(
$"moonlight-runtime-{server.Configuration.Id}"
);
await NotifyTask(server, serviceProvider, ServerTask.RemovingContainer);
if (existingContainer.State.Running) // Stop already running container
{
await dockerClient.Containers.StopContainerAsync(existingContainer.ID, new()
{
WaitBeforeKillSeconds = 30 // TODO: Config
});
}
await dockerClient.Containers.RemoveContainerAsync(existingContainer.ID, new());
}
catch (DockerContainerNotFoundException)
{
}
await NotifyTask(server, serviceProvider, ServerTask.CreatingContainer);
// Create a new container
var parameters = new CreateContainerParameters();
ServerConfigurationHelper.ApplyRuntimeOptions(
parameters,
server.Configuration,
serviceProvider.GetRequiredService<AppConfiguration>()
);
var container = await dockerClient.Containers.CreateContainerAsync(parameters);
server.ContainerId = container.ID;
}
private static async Task StartRuntimeContainer(Server server, IServiceProvider serviceProvider)
{
await NotifyTask(server, serviceProvider, ServerTask.StartingContainer);
var dockerClient = serviceProvider.GetRequiredService<DockerClient>();
await dockerClient.Containers.StartContainerAsync(server.ContainerId, new());
}
private static async Task NotifyTask(Server server, IServiceProvider serviceProvider, ServerTask task)
{
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger($"Server {server.Configuration.Id}");
logger.LogInformation("Task: {task}", task);
}
private static async Task UpdateProgress(Server server, IServiceProvider serviceProvider, int progress)
{
}
}

View File

@@ -1,44 +0,0 @@
using Microsoft.AspNetCore.SignalR;
using MoonlightServers.Daemon.Models;
using MoonlightServers.DaemonShared.Enums;
namespace MoonlightServers.Daemon.Helpers;
public class ServerConsoleMonitor
{
private readonly Server Server;
private readonly IHubClients Clients;
public ServerConsoleMonitor(Server server, IHubClients clients)
{
Server = server;
Clients = clients;
}
public void Initialize()
{
Server.StateMachine.OnTransitioned += OnPowerStateChanged;
Server.OnTaskAdded += OnTaskNotify;
}
public void Destroy()
{
Server.StateMachine.OnTransitioned -= OnPowerStateChanged;
}
private async Task OnTaskNotify(string task)
{
await Clients.Group($"server-{Server.Configuration.Id}").SendAsync(
"TaskNotify",
task
);
}
private async Task OnPowerStateChanged(ServerState serverState)
{
await Clients.Group($"server-{Server.Configuration.Id}").SendAsync(
"PowerStateChanged",
serverState.ToString()
);
}
}

View File

@@ -1,28 +1,29 @@
using Microsoft.AspNetCore.SignalR;
using MoonlightServers.Daemon.Abstractions;
using MoonlightServers.Daemon.Enums;
using MoonlightServers.Daemon.Http.Hubs;
using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.Enums;
namespace MoonlightServers.Daemon.Helpers;
public class ServerConsoleConnection
public class ServerWebSocketConnection
{
private readonly ServerService ServerService;
private readonly ILogger<ServerConsoleConnection> Logger;
private readonly ILogger<ServerWebSocketConnection> Logger;
private readonly AccessTokenHelper AccessTokenHelper;
private readonly IHubContext<ServerConsoleHub> HubContext;
private readonly IHubContext<ServerWebSocketHub> HubContext;
private int ServerId = -1;
private Server Server;
private bool IsInitialized = false;
private string ConnectionId;
public ServerConsoleConnection(
public ServerWebSocketConnection(
ServerService serverService,
ILogger<ServerConsoleConnection> logger,
ILogger<ServerWebSocketConnection> logger,
AccessTokenHelper accessTokenHelper,
IHubContext<ServerConsoleHub> hubContext
IHubContext<ServerWebSocketHub> hubContext
)
{
ServerService = serverService;
@@ -64,7 +65,7 @@ public class ServerConsoleConnection
// Validate access token type
var type = accessData["type"].GetString()!;
if (type != "console")
if (type != "websocket")
{
Logger.LogDebug("Received invalid access token: Invalid type '{type}'", type);
@@ -78,7 +79,7 @@ public class ServerConsoleConnection
var serverId = accessData["serverId"].GetInt32();
// Check that the access token isn't or another server
// Check that the access token isn't for another server
if (ServerId != -1 && ServerId == serverId)
{
Logger.LogDebug("Received invalid access token: Server id not valid for this session. Current server id: {serverId}", ServerId);
@@ -117,31 +118,27 @@ public class ServerConsoleConnection
IsInitialized = true;
// Setup event handlers
Server.StateMachine.OnTransitioned += HandlePowerStateChange;
Server.OnTaskAdded += HandleTaskAdded;
Server.Console.OnOutput += HandleConsoleOutput;
Server.OnConsoleOutput += HandleConsoleOutput;
Server.OnStateChanged += HandleStateChange;
Logger.LogTrace("Authenticated and initialized server console connection '{id}'", context.ConnectionId);
}
public Task Destroy(HubCallerContext context)
{
Server.StateMachine.OnTransitioned -= HandlePowerStateChange;
Server.OnTaskAdded -= HandleTaskAdded;
Logger.LogTrace("Destroyed server console connection '{id}'", context.ConnectionId);
Server.OnConsoleOutput -= HandleConsoleOutput;
Server.OnStateChanged -= HandleStateChange;
return Task.CompletedTask;
}
#region Event Handlers
private async Task HandlePowerStateChange(ServerState serverState)
=> await HubContext.Clients.Client(ConnectionId).SendAsync("PowerStateChanged", serverState.ToString());
private async Task HandleTaskAdded(string task)
=> await HubContext.Clients.Client(ConnectionId).SendAsync("TaskNotify", task);
private async Task HandleStateChange(ServerState state)
=> await HubContext.Clients.Client(ConnectionId).SendAsync("StateChanged", state.ToString());
private async Task HandleConsoleOutput(string line)
=> await HubContext.Clients.Client(ConnectionId).SendAsync("ConsoleOutput", line);

View File

@@ -1,74 +0,0 @@
namespace MoonlightServers.Daemon.Helpers;
public class StateMachine<T> where T : struct, Enum
{
private readonly List<StateMachineTransition> Transitions = new();
private readonly object Lock = new();
public T CurrentState { get; private set; }
public event Func<T, Task> OnTransitioned;
public event Action<T, Exception> OnError;
public StateMachine(T initialState)
{
CurrentState = initialState;
}
public void AddTransition(T from, T to, T? onError, Func<Task>? fun)
{
Transitions.Add(new()
{
From = from,
To = to,
OnError = onError,
OnTransitioning = fun
});
}
public void AddTransition(T from, T to, Func<Task> fun) => AddTransition(from, to, null, fun);
public void AddTransition(T from, T to) => AddTransition(from, to, null, null);
public async Task TransitionTo(T to)
{
lock (Lock)
{
var transition = Transitions.FirstOrDefault(x =>
x.From.Equals(CurrentState) &&
x.To.Equals(to)
);
if (transition == null)
throw new InvalidOperationException("Unable to transition to the request state: No transition found");
try
{
if(transition.OnTransitioning != null)
transition.OnTransitioning.Invoke().Wait();
// Successfully executed => update state
CurrentState = transition.To;
}
catch (Exception e)
{
if(OnError != null)
OnError.Invoke(to, e);
if (transition.OnError.HasValue)
CurrentState = transition.OnError.Value;
else
throw new AggregateException("An error occured while transitioning to a state", e);
}
}
if(OnTransitioned != null)
await OnTransitioned.Invoke(CurrentState);
}
public class StateMachineTransition
{
public T From { get; set; }
public T To { get; set; }
public T? OnError { get; set; }
public Func<Task>? OnTransitioning { get; set; }
}
}