Implemented power state and task streaming over signalr

This commit is contained in:
2024-12-30 01:16:23 +01:00
parent 394d8b05ed
commit 0bd9074494
14 changed files with 436 additions and 4 deletions

View File

@@ -8,5 +8,6 @@ public static class ServerMetaExtensions
public static async Task NotifyTask(this Server server, ServerTask task)
{
server.Logger.LogInformation("Task: {task}", task);
await server.InvokeTaskAdded(task.ToString());
}
}

View File

@@ -0,0 +1,22 @@
using System.Text.Json;
using MoonCore.Attributes;
using MoonCore.Extended.Helpers;
using MoonlightServers.Daemon.Configuration;
namespace MoonlightServers.Daemon.Helpers;
[Singleton]
public class AccessTokenHelper
{
private readonly AppConfiguration Configuration;
public AccessTokenHelper(AppConfiguration configuration)
{
Configuration = configuration;
}
public bool Process(string accessToken, out Dictionary<string, JsonElement> data)
{
return JwtHelper.TryVerifyAndDecodePayload(Configuration.Security.Token, accessToken, out data);
}
}

View File

@@ -0,0 +1,7 @@
namespace MoonlightServers.Daemon.Helpers;
public class ServerConsoleConnection
{
public DateTime AuthenticatedUntil { get; set; }
public int ServerId { get; set; }
}

View File

@@ -0,0 +1,44 @@
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

@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.SignalR;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.Enums;
namespace MoonlightServers.Daemon.Http.Hubs;
public class ServerConsoleHub : Hub
{
private readonly ILogger<ServerConsoleHub> Logger;
private readonly ServerConsoleService ConsoleService;
public ServerConsoleHub(ILogger<ServerConsoleHub> logger, ServerConsoleService consoleService)
{
Logger = logger;
ConsoleService = consoleService;
}
[HubMethodName("Authenticate")]
public async Task Authenticate(string accessToken)
{
try
{
await ConsoleService.Authenticate(Context, accessToken);
}
catch (Exception e)
{
Logger.LogError("An unhandled error occured in the Authenticate method: {e}", e);
}
}
public override async Task OnDisconnectedAsync(Exception? exception)
=> await ConsoleService.OnClientDisconnected(Context);
}

View File

@@ -15,6 +15,7 @@ public class Server
public StateMachine<ServerState> StateMachine { get; set; }
public ServerConfiguration Configuration { get; set; }
public string? ContainerId { get; set; }
public event Func<string, Task> OnTaskAdded;
// This can be used to stop streaming when the server gets destroyed or something
public CancellationTokenSource Cancellation { get; set; }
@@ -59,4 +60,16 @@ public class Server
}
#endregion
#region Event invokers
public async Task InvokeTaskAdded(string task)
{
if(OnTaskAdded == null)
return;
await OnTaskAdded.Invoke(task).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
}
#endregion
}

View File

@@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
<PackageReference Include="MoonCore" Version="1.8.1" />
<PackageReference Include="MoonCore.Extended" Version="1.2.4" />
<PackageReference Include="MoonCore.Unix" Version="1.0.0" />

View File

@@ -0,0 +1,159 @@
using Microsoft.AspNetCore.SignalR;
using MoonCore.Attributes;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.Daemon.Http.Hubs;
namespace MoonlightServers.Daemon.Services;
[Singleton]
public class ServerConsoleService
{
private readonly Dictionary<int, ServerConsoleMonitor> Monitors = new();
private readonly Dictionary<string, ServerConsoleConnection> Connections = new();
private readonly ILogger<ServerConsoleService> Logger;
private readonly AccessTokenHelper AccessTokenHelper;
private readonly ServerService ServerService;
private readonly IHubContext<ServerConsoleHub> HubContext;
public ServerConsoleService(
ILogger<ServerConsoleService> logger,
AccessTokenHelper accessTokenHelper,
ServerService serverService, IHubContext<ServerConsoleHub> hubContext)
{
Logger = logger;
AccessTokenHelper = accessTokenHelper;
ServerService = serverService;
HubContext = hubContext;
}
public Task OnClientDisconnected(HubCallerContext context)
{
ServerConsoleConnection? removedConnection;
lock (Connections)
{
removedConnection = Connections.GetValueOrDefault(context.ConnectionId);
if(removedConnection != null)
Connections.Remove(context.ConnectionId);
}
// Client never authenticated themselves, nothing to do
if(removedConnection == null)
return Task.CompletedTask;
Logger.LogDebug("Authenticated client {id} disconnected", context.ConnectionId);
// Count remaining clients requesting the same resource
int count;
lock (Connections)
{
count = Connections
.Values
.Count(x => x.ServerId == removedConnection.ServerId);
}
if(count > 0)
return Task.CompletedTask;
ServerConsoleMonitor? monitor;
lock (Monitors)
monitor = Monitors.GetValueOrDefault(removedConnection.ServerId);
if(monitor == null)
return Task.CompletedTask;
Logger.LogDebug("Destroying console monitor for server {id}", removedConnection.ServerId);
monitor.Destroy();
lock (Monitors)
Monitors.Remove(removedConnection.ServerId);
return Task.CompletedTask;
}
public async Task Authenticate(HubCallerContext context, string accessToken)
{
// Validate access token
if (!AccessTokenHelper.Process(accessToken, out var accessData))
{
Logger.LogDebug("Received invalid or expired access token. Closing connection");
context.Abort();
return;
}
// Validate access token data
if (!accessData.ContainsKey("type") || !accessData.ContainsKey("serverId"))
{
Logger.LogDebug("Received invalid access token: Required parameters are missing. Closing connection");
context.Abort();
return;
}
// Validate access token type
var type = accessData["type"].GetString()!;
if (type != "console")
{
Logger.LogDebug("Received invalid access token: Invalid type '{type}'. Closing connection", type);
context.Abort();
return;
}
var serverId = accessData["serverId"].GetInt32();
var server = ServerService.GetServer(serverId);
if (server == null)
{
Logger.LogDebug("Received invalid access token: No server found with the requested id. Closing connection");
context.Abort();
return;
}
ServerConsoleConnection? connection;
lock (Connections)
{
connection = Connections
.GetValueOrDefault(context.ConnectionId);
}
if (connection == null) // If no existing connection has been found, we create a new one
{
connection = new()
{
ServerId = server.Configuration.Id,
AuthenticatedUntil = DateTime.UtcNow.AddMinutes(10)
};
lock (Connections)
Connections.Add(context.ConnectionId, connection);
Logger.LogDebug("Connection {id} authenticated successfully", context.ConnectionId);
}
else
Logger.LogDebug("Connection {id} re-authenticated successfully", context.ConnectionId);
ServerConsoleMonitor? monitor;
lock (Monitors)
monitor = Monitors.GetValueOrDefault(server.Configuration.Id);
if (monitor == null)
{
Logger.LogDebug("Initializing console monitor for server {id}", server.Configuration.Id);
monitor = new ServerConsoleMonitor(server, HubContext.Clients);
monitor.Initialize();
lock (Monitors)
Monitors.Add(server.Configuration.Id, monitor);
}
await HubContext.Groups.AddToGroupAsync(context.ConnectionId, $"server-{server.Configuration.Id}");
}
}

View File

@@ -6,6 +6,7 @@ using MoonCore.Extensions;
using MoonCore.Helpers;
using MoonCore.Services;
using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Http.Hubs;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon;
@@ -43,13 +44,17 @@ public class Startup
await RegisterBase();
await RegisterDocker();
await RegisterServers();
await RegisterSignalR();
await RegisterCors();
await BuildWebApplication();
await UseBase();
await UseCors();
await UseBaseMiddleware();
await MapBase();
await MapHubs();
await WebApplication.RunAsync();
}
@@ -71,7 +76,6 @@ public class Startup
WebApplicationBuilder.Services.AddControllers();
WebApplicationBuilder.Services.AddApiExceptionHandler();
WebApplicationBuilder.Services.AddSignalR();
return Task.CompletedTask;
}
@@ -248,7 +252,7 @@ public class Startup
WebApplicationBuilder.Services.AddHostedService<ApplicationStateService>(
sp => sp.GetRequiredService<ApplicationStateService>()
);
return Task.CompletedTask;
}
@@ -258,4 +262,43 @@ public class Startup
}
#endregion
#region Maps
private Task RegisterSignalR()
{
WebApplicationBuilder.Services.AddSignalR();
return Task.CompletedTask;
}
private Task MapHubs()
{
WebApplication.MapHub<ServerConsoleHub>("api/servers/console");
return Task.CompletedTask;
}
#endregion
#region Cors
private Task RegisterCors()
{
//TODO: IMPORTANT: CHANGE !!!
WebApplicationBuilder.Services.AddCors(x =>
x.AddDefaultPolicy(builder =>
builder.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod().Build()
)
);
return Task.CompletedTask;
}
private Task UseCors()
{
WebApplication.UseCors();
return Task.CompletedTask;
}
#endregion
}