Re-implemented server state machine. Cleaned up code
TODO: Handle trigger errors
This commit is contained in:
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user