Implemented server console streaming in the frontend with xterm. Added logs endpoint for servers

This commit is contained in:
2024-12-31 17:57:39 +01:00
parent 6d674e153a
commit f652945a3f
19 changed files with 419 additions and 163 deletions

View File

@@ -51,6 +51,10 @@ public static class ServerConsoleExtensions
{
// Ignored
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception e)
{
server.Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", e);

View File

@@ -1,7 +1,149 @@
using Microsoft.AspNetCore.SignalR;
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 DateTime AuthenticatedUntil { get; set; }
public int ServerId { get; set; }
private readonly ServerService ServerService;
private readonly ILogger<ServerConsoleConnection> Logger;
private readonly AccessTokenHelper AccessTokenHelper;
private readonly IHubContext<ServerConsoleHub> HubContext;
private int ServerId = -1;
private Server Server;
private bool IsInitialized = false;
private string ConnectionId;
public ServerConsoleConnection(
ServerService serverService,
ILogger<ServerConsoleConnection> logger,
AccessTokenHelper accessTokenHelper,
IHubContext<ServerConsoleHub> hubContext
)
{
ServerService = serverService;
Logger = logger;
AccessTokenHelper = accessTokenHelper;
HubContext = hubContext;
}
public Task Initialize(HubCallerContext context) => 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");
await HubContext.Clients.Client(context.ConnectionId).SendAsync(
"Error",
"Received invalid or expired access token"
);
return;
}
// Validate access token data
if (!accessData.ContainsKey("type") || !accessData.ContainsKey("serverId"))
{
Logger.LogDebug("Received invalid access token: Required parameters are missing");
await HubContext.Clients.Client(context.ConnectionId).SendAsync(
"Error",
"Received invalid access token: Required parameters are missing"
);
return;
}
// Validate access token type
var type = accessData["type"].GetString()!;
if (type != "console")
{
Logger.LogDebug("Received invalid access token: Invalid type '{type}'", type);
await HubContext.Clients.Client(context.ConnectionId).SendAsync(
"Error",
$"Received invalid access token: Invalid type '{type}'"
);
return;
}
var serverId = accessData["serverId"].GetInt32();
// Check that the access token isn't or 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);
await HubContext.Clients.Client(context.ConnectionId).SendAsync(
"Error",
$"Received invalid access token: Server id not valid for this session. Current server id: {ServerId}"
);
return;
}
var server = ServerService.GetServer(serverId);
// Check i the server actually exists
if (server == null)
{
Logger.LogDebug("Received invalid access token: No server found with the requested id");
await HubContext.Clients.Client(context.ConnectionId).SendAsync(
"Error",
"Received invalid access token: No server found with the requested id"
);
return;
}
// Set values
Server = server;
ServerId = serverId;
ConnectionId = context.ConnectionId;
if(IsInitialized)
return;
IsInitialized = true;
// Setup event handlers
Server.StateMachine.OnTransitioned += HandlePowerStateChange;
Server.OnTaskAdded += HandleTaskAdded;
Server.Console.OnOutput += HandleConsoleOutput;
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);
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 HandleConsoleOutput(string line)
=> await HubContext.Clients.Client(ConnectionId).SendAsync("ConsoleOutput", line);
#endregion
}

View File

@@ -32,6 +32,20 @@ public class ServersController : Controller
};
}
[HttpGet("{serverId:int}/logs")]
public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId)
{
var server = ServerService.GetServer(serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
return new ServerLogsResponse()
{
Messages = server.Console.Messages
};
}
[HttpPost("{serverId:int}/start")]
public async Task Start(int serverId)
{

View File

@@ -17,12 +17,24 @@ public class ServerConsoleHub : Hub
ConsoleService = consoleService;
}
#region Connection Handlers
public override async Task OnConnectedAsync()
=> await ConsoleService.InitializeClient(Context);
public override async Task OnDisconnectedAsync(Exception? exception)
=> await ConsoleService.DestroyClient(Context);
#endregion
#region Methods
[HubMethodName("Authenticate")]
public async Task Authenticate(string accessToken)
{
try
{
await ConsoleService.Authenticate(Context, accessToken);
await ConsoleService.AuthenticateClient(Context, accessToken);
}
catch (Exception e)
{
@@ -30,6 +42,5 @@ public class ServerConsoleHub : Hub
}
}
public override async Task OnDisconnectedAsync(Exception? exception)
=> await ConsoleService.OnClientDisconnected(Context);
#endregion
}

View File

@@ -8,152 +8,61 @@ 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 IServiceProvider ServiceProvider;
private readonly IHubContext<ServerConsoleHub> HubContext;
private readonly Dictionary<string, ServerConsoleConnection> Connections = new();
public ServerConsoleService(
ILogger<ServerConsoleService> logger,
AccessTokenHelper accessTokenHelper,
ServerService serverService, IHubContext<ServerConsoleHub> hubContext)
IServiceProvider serviceProvider
)
{
Logger = logger;
AccessTokenHelper = accessTokenHelper;
ServerService = serverService;
HubContext = hubContext;
ServiceProvider = serviceProvider;
}
public Task OnClientDisconnected(HubCallerContext context)
public async Task InitializeClient(HubCallerContext context)
{
ServerConsoleConnection? removedConnection;
var connection = new ServerConsoleConnection(
ServiceProvider.GetRequiredService<ServerService>(),
ServiceProvider.GetRequiredService<ILogger<ServerConsoleConnection>>(),
ServiceProvider.GetRequiredService<AccessTokenHelper>(),
ServiceProvider.GetRequiredService<IHubContext<ServerConsoleHub>>()
);
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;
Connections[context.ConnectionId] = connection;
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;
await connection.Initialize(context);
}
public async Task Authenticate(HubCallerContext context, string accessToken)
public async Task AuthenticateClient(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;
connection = Connections.GetValueOrDefault(context.ConnectionId);
lock (Monitors)
monitor = Monitors.GetValueOrDefault(server.Configuration.Id);
if(connection == null)
return;
if (monitor == null)
{
Logger.LogDebug("Initializing console monitor for server {id}", server.Configuration.Id);
monitor = new ServerConsoleMonitor(server, HubContext.Clients);
monitor.Initialize();
await connection.Authenticate(context, accessToken);
}
lock (Monitors)
Monitors.Add(server.Configuration.Id, monitor);
}
public async Task DestroyClient(HubCallerContext context)
{
ServerConsoleConnection? connection;
await HubContext.Groups.AddToGroupAsync(context.ConnectionId, $"server-{server.Configuration.Id}");
lock (Connections)
connection = Connections.GetValueOrDefault(context.ConnectionId);
if(connection == null)
return;
await connection.Destroy(context);
lock (Connections)
Connections.Remove(context.ConnectionId);
}
}