Implemented power state and task streaming over signalr
This commit is contained in:
@@ -118,6 +118,36 @@ public class ServersController : Controller
|
||||
throw new HttpApiException("Unable to access the node the server is running on", 502);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{serverId:int}/console")]
|
||||
[RequirePermission("meta.authenticated")]
|
||||
public async Task<ServerConsoleResponse> GetConsole([FromRoute] int serverId)
|
||||
{
|
||||
var server = await GetServerWithPermCheck(serverId);
|
||||
|
||||
// TODO: Handle transparent proxy
|
||||
|
||||
var accessToken = NodeService.CreateAccessToken(server.Node, parameters =>
|
||||
{
|
||||
parameters.Add("type", "console");
|
||||
parameters.Add("serverId", server.Id);
|
||||
}, TimeSpan.FromMinutes(10));
|
||||
|
||||
var url = "";
|
||||
|
||||
if (server.Node.UseSsl)
|
||||
url += "https://";
|
||||
else
|
||||
url += "http://";
|
||||
|
||||
url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/api/servers/console";
|
||||
|
||||
return new ServerConsoleResponse()
|
||||
{
|
||||
Target = url,
|
||||
AccessToken = accessToken
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Server> GetServerWithPermCheck(int serverId,
|
||||
Func<IQueryable<Server>, IQueryable<Server>>? queryModifier = null)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MoonCore.Attributes;
|
||||
using MoonCore.Extended.Helpers;
|
||||
using MoonCore.Helpers;
|
||||
using MoonlightServers.ApiServer.Database.Entities;
|
||||
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics;
|
||||
@@ -11,7 +12,7 @@ public class NodeService
|
||||
{
|
||||
public async Task<HttpApiClient> CreateApiClient(Node node)
|
||||
{
|
||||
string url = "";
|
||||
var url = "";
|
||||
|
||||
if (node.UseSsl)
|
||||
url += "https://";
|
||||
@@ -29,6 +30,9 @@ public class NodeService
|
||||
|
||||
return new HttpApiClient(httpClient);
|
||||
}
|
||||
|
||||
public string CreateAccessToken(Node node, Action<Dictionary<string, object>> parameters, TimeSpan duration)
|
||||
=> JwtHelper.Encode(node.Token, parameters, duration);
|
||||
|
||||
public async Task<SystemStatusResponse> GetSystemStatus(Node node)
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
22
MoonlightServers.Daemon/Helpers/AccessTokenHelper.cs
Normal file
22
MoonlightServers.Daemon/Helpers/AccessTokenHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace MoonlightServers.Daemon.Helpers;
|
||||
|
||||
public class ServerConsoleConnection
|
||||
{
|
||||
public DateTime AuthenticatedUntil { get; set; }
|
||||
public int ServerId { get; set; }
|
||||
}
|
||||
44
MoonlightServers.Daemon/Helpers/ServerConsoleMonitor.cs
Normal file
44
MoonlightServers.Daemon/Helpers/ServerConsoleMonitor.cs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
35
MoonlightServers.Daemon/Http/Hubs/ServerConsoleHub.cs
Normal file
35
MoonlightServers.Daemon/Http/Hubs/ServerConsoleHub.cs
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
159
MoonlightServers.Daemon/Services/ServerConsoleService.cs
Normal file
159
MoonlightServers.Daemon/Services/ServerConsoleService.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.11"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11" PrivateAssets="all"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.11" />
|
||||
<PackageReference Include="Moonlight.Client" Version="2.1.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
@page "/servers/{ServerId:int}"
|
||||
|
||||
@using Microsoft.AspNetCore.SignalR.Client
|
||||
@using MoonlightServers.Shared.Http.Responses.Users.Servers
|
||||
@using MoonCore.Blazor.Tailwind.Components
|
||||
@using MoonCore.Exceptions
|
||||
@using MoonCore.Helpers
|
||||
@using MoonlightServers.Shared.Enums
|
||||
|
||||
@inject HttpApiClient ApiClient
|
||||
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<LazyLoader Load="Load">
|
||||
@if (NotFound)
|
||||
{
|
||||
@@ -22,7 +26,19 @@
|
||||
{
|
||||
<div class="card card-body justify-between py-2.5 px-5 flex-row">
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="p-2.5 rounded-full bg-success-400 me-3"></div>
|
||||
@{
|
||||
var bgColor = PowerState switch
|
||||
{
|
||||
ServerPowerState.Installing => "bg-primary-500",
|
||||
ServerPowerState.Offline => "bg-danger-500",
|
||||
ServerPowerState.Starting => "bg-warning-500",
|
||||
ServerPowerState.Stopping => "bg-warning-500",
|
||||
ServerPowerState.Online => "bg-success-500",
|
||||
_ => "bg-gray-500"
|
||||
};
|
||||
}
|
||||
|
||||
<div class="p-2.5 rounded-full @bgColor me-3"></div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="hidden sm:flex text-lg font-semibold">@Server.Name</div>
|
||||
@@ -99,16 +115,57 @@
|
||||
|
||||
private ServerDetailResponse Server;
|
||||
private bool NotFound = false;
|
||||
private ServerPowerState PowerState;
|
||||
|
||||
private string CurrentTask = "";
|
||||
private HubConnection ConsoleConnection;
|
||||
|
||||
private async Task Load(LazyLoader _)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load meta data
|
||||
Server = await ApiClient.GetJson<ServerDetailResponse>(
|
||||
$"api/servers/{ServerId}"
|
||||
);
|
||||
|
||||
// Load initial status for first render
|
||||
var status = await ApiClient.GetJson<ServerStatusResponse>(
|
||||
$"api/servers/{ServerId}/status"
|
||||
);
|
||||
|
||||
PowerState = status.PowerState;
|
||||
|
||||
// Load console meta
|
||||
var consoleDetails = await ApiClient.GetJson<ServerConsoleResponse>(
|
||||
$"api/servers/{ServerId}/console"
|
||||
);
|
||||
|
||||
// Build signal r
|
||||
ConsoleConnection = new HubConnectionBuilder()
|
||||
.WithUrl(consoleDetails.Target)
|
||||
.Build();
|
||||
|
||||
// Define handlers
|
||||
ConsoleConnection.On<string>("PowerStateChanged", async powerStateStr =>
|
||||
{
|
||||
if(!Enum.TryParse(powerStateStr, out ServerPowerState receivedState))
|
||||
return;
|
||||
|
||||
PowerState = receivedState;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
|
||||
ConsoleConnection.On<string>("TaskNotify", async task =>
|
||||
{
|
||||
await AddTask(Formatter.ConvertCamelCaseToSpaces(task));
|
||||
});
|
||||
|
||||
// Connect
|
||||
await ConsoleConnection.StartAsync();
|
||||
|
||||
// Authenticate
|
||||
await ConsoleConnection.SendAsync("Authenticate", consoleDetails.AccessToken);
|
||||
}
|
||||
catch (HttpApiException e)
|
||||
{
|
||||
@@ -153,4 +210,12 @@
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (ConsoleConnection.State == HubConnectionState.Connected)
|
||||
await ConsoleConnection.StopAsync();
|
||||
|
||||
await ConsoleConnection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace MoonlightServers.Shared.Http.Responses.Users.Servers;
|
||||
|
||||
public class ServerConsoleResponse
|
||||
{
|
||||
public string Target { get; set; }
|
||||
public string AccessToken { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user