Files
Servers/MoonlightServers.Daemon/ServerSystem/Server.cs

183 lines
5.9 KiB
C#

using Microsoft.AspNetCore.SignalR;
using MoonCore.Exceptions;
using MoonlightServers.Daemon.Http.Hubs;
using MoonlightServers.Daemon.Models.Cache;
using Stateless;
namespace MoonlightServers.Daemon.ServerSystem;
public class Server : IAsyncDisposable
{
public ServerConfiguration Configuration { get; set; }
public CancellationToken TaskCancellation => TaskCancellationSource.Token;
internal StateMachine<ServerState, ServerTrigger> StateMachine { get; private set; }
private CancellationTokenSource TaskCancellationSource;
private Dictionary<Type, ServerSubSystem> SubSystems = new();
private ServerState InternalState = ServerState.Offline;
private readonly IHubContext<ServerWebSocketHub> HubContext;
private readonly IServiceScope ServiceScope;
private readonly ILoggerFactory LoggerFactory;
private readonly ILogger Logger;
public Server(
ServerConfiguration configuration,
IServiceScope serviceScope,
IHubContext<ServerWebSocketHub> hubContext
)
{
Configuration = configuration;
ServiceScope = serviceScope;
HubContext = hubContext;
TaskCancellationSource = new CancellationTokenSource();
LoggerFactory = serviceScope.ServiceProvider.GetRequiredService<ILoggerFactory>();
Logger = LoggerFactory.CreateLogger($"Server {Configuration.Id}");
StateMachine = new StateMachine<ServerState, ServerTrigger>(
() => InternalState,
state => InternalState = state,
FiringMode.Queued
);
// Configure basic state machine flow
StateMachine.Configure(ServerState.Offline)
.Permit(ServerTrigger.Start, ServerState.Starting)
.Permit(ServerTrigger.Install, ServerState.Installing)
.PermitReentry(ServerTrigger.FailSafe);
StateMachine.Configure(ServerState.Starting)
.Permit(ServerTrigger.OnlineDetected, ServerState.Online)
.Permit(ServerTrigger.FailSafe, 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.FailSafe)
.PermitReentry(ServerTrigger.Kill)
.Permit(ServerTrigger.Exited, ServerState.Offline);
StateMachine.Configure(ServerState.Installing)
.Permit(ServerTrigger.FailSafe, ServerState.Offline)
.Permit(ServerTrigger.Exited, ServerState.Offline);
// Configure task reset when server goes offline
StateMachine.Configure(ServerState.Offline)
.OnEntryAsync(async () =>
{
if (!TaskCancellationSource.IsCancellationRequested)
await TaskCancellationSource.CancelAsync();
TaskCancellationSource = new();
});
// Setup websocket notify for state changes
StateMachine.OnTransitionedAsync(async transition =>
{
await HubContext.Clients
.Group(Configuration.Id.ToString())
.SendAsync("StateChanged", transition.Destination.ToString());
});
}
public async Task Initialize(Type[] subSystemTypes)
{
foreach (var type in subSystemTypes)
{
var logger = LoggerFactory.CreateLogger($"Server {Configuration.Id} - {type.Name}");
var subSystem = ActivatorUtilities.CreateInstance(
ServiceScope.ServiceProvider,
type,
this,
logger
) as ServerSubSystem;
if (subSystem == null)
{
Logger.LogError("Unable to construct server sub system: {name}", type.Name);
continue;
}
SubSystems.Add(type, subSystem);
}
foreach (var type in SubSystems.Keys)
{
try
{
await SubSystems[type].Initialize();
}
catch (Exception e)
{
Logger.LogError("An unhandled error occured while initializing sub system {name}: {e}", type.Name, e);
}
}
}
public async Task Trigger(ServerTrigger trigger)
{
if (!StateMachine.CanFire(trigger))
throw new HttpApiException($"The trigger {trigger} is not supported during the state {StateMachine.State}", 400);
await StateMachine.FireAsync(trigger);
}
public async Task Delete()
{
foreach (var subSystem in SubSystems.Values)
await subSystem.Delete();
}
// This method completely bypasses the state machine.
// Using this method without any checks will lead to
// broken server states. Use with caution
public void OverrideState(ServerState state)
{
InternalState = state;
}
public T? GetSubSystem<T>() where T : ServerSubSystem
{
var type = typeof(T);
var subSystem = SubSystems.GetValueOrDefault(type);
if (subSystem == null)
return null;
return subSystem as T;
}
public T GetRequiredSubSystem<T>() where T : ServerSubSystem
{
var subSystem = GetSubSystem<T>();
if (subSystem == null)
throw new AggregateException("Unable to resolve requested sub system");
return subSystem;
}
public async ValueTask DisposeAsync()
{
if (!TaskCancellationSource.IsCancellationRequested)
await TaskCancellationSource.CancelAsync();
foreach (var subSystem in SubSystems.Values)
await subSystem.DisposeAsync();
ServiceScope.Dispose();
}
}