Implemented server console streaming in the frontend with xterm. Added logs endpoint for servers
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user