183 lines
5.9 KiB
C#
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();
|
|
}
|
|
} |