Implemented server console streaming in the frontend with xterm. Added logs endpoint for servers
This commit is contained in:
@@ -125,7 +125,7 @@ public class ServersController : Controller
|
||||
{
|
||||
var server = await GetServerWithPermCheck(serverId);
|
||||
|
||||
// TODO: Handle transparent proxy
|
||||
// TODO: Handle transparent node proxy
|
||||
|
||||
var accessToken = NodeService.CreateAccessToken(server.Node, parameters =>
|
||||
{
|
||||
@@ -148,6 +148,31 @@ public class ServersController : Controller
|
||||
AccessToken = accessToken
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("{serverId:int}/logs")]
|
||||
[RequirePermission("meta.authenticated")]
|
||||
public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId)
|
||||
{
|
||||
var server = await GetServerWithPermCheck(serverId);
|
||||
|
||||
var apiClient = await NodeService.CreateApiClient(server.Node);
|
||||
|
||||
try
|
||||
{
|
||||
var data = await apiClient.GetJson<DaemonShared.DaemonSide.Http.Responses.Servers.ServerLogsResponse>(
|
||||
$"api/servers/{server.Id}/logs"
|
||||
);
|
||||
|
||||
return new ServerLogsResponse()
|
||||
{
|
||||
Messages = data.Messages
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
throw new HttpApiException("Unable to access the node the server is running on", 502);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Server> GetServerWithPermCheck(int serverId,
|
||||
Func<IQueryable<Server>, IQueryable<Server>>? queryModifier = null)
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"MOONLIGHT_APP_PUBLICURL": "http://localhost:5269"
|
||||
}
|
||||
},
|
||||
"commandLineArgs": "--frontend-asset js/XtermBlazor.min.js --frontend-asset js/moonlightServers.js --frontend-asset js/addon-fit.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public class PluginStartup : IAppStartup
|
||||
builder.Services.AutoAddServices<PluginStartup>();
|
||||
|
||||
BundleService.BundleCss("css/MoonlightServers.min.css");
|
||||
BundleService.BundleCss("css/XtermBlazor.min.css");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
||||
|
||||
public class ServerLogsResponse
|
||||
{
|
||||
public string[] Messages { get; set; }
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
<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"/>
|
||||
<PackageReference Include="XtermBlazor" Version="2.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -21,7 +22,6 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="Helpers\"/>
|
||||
<Folder Include="Interfaces\"/>
|
||||
<Folder Include="wwwroot\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
1
MoonlightServers.Frontend/Styles/additions/fonts.css
Normal file
1
MoonlightServers.Frontend/Styles/additions/fonts.css
Normal file
@@ -0,0 +1 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Mono,wght@0,200..900;1,200..900&display=swap');
|
||||
@@ -4,6 +4,7 @@
|
||||
@import "additions/animations.css";
|
||||
@import "additions/buttons.css";
|
||||
@import "additions/cards.css";
|
||||
@import "additions/fonts.css";
|
||||
@import "additions/progress.css";
|
||||
@import "additions/scrollbar.css";
|
||||
@import "additions/loaders.css";
|
||||
|
||||
97
MoonlightServers.Frontend/UI/Components/XtermConsole.razor
Normal file
97
MoonlightServers.Frontend/UI/Components/XtermConsole.razor
Normal file
@@ -0,0 +1,97 @@
|
||||
@using XtermBlazor
|
||||
|
||||
@inject IJSRuntime JsRuntime
|
||||
@inject ILogger<XtermConsole> Logger
|
||||
|
||||
<div class="bg-black rounded-lg p-2">
|
||||
@if (IsInitialized)
|
||||
{
|
||||
<Xterm @ref="Terminal"
|
||||
Addons="Addons"
|
||||
Options="Options"
|
||||
OnFirstRender="HandleFirstRender"/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public Func<Task>? OnAfterInitialized { get; set; }
|
||||
[Parameter] public Func<Task>? OnFirstRender { get; set; }
|
||||
|
||||
private Xterm Terminal;
|
||||
private bool IsInitialized = false;
|
||||
private bool HadFirstRender = false;
|
||||
|
||||
private readonly Queue<string> MessageCache = new();
|
||||
private readonly HashSet<string> Addons = ["addon-fit"];
|
||||
private readonly TerminalOptions Options = new()
|
||||
{
|
||||
CursorBlink = false,
|
||||
CursorStyle = CursorStyle.Bar,
|
||||
CursorWidth = 1,
|
||||
FontFamily = "Space Mono, monospace",
|
||||
DisableStdin = true,
|
||||
CursorInactiveStyle = CursorInactiveStyle.None,
|
||||
Theme =
|
||||
{
|
||||
Background = "#000000"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if(!firstRender)
|
||||
return;
|
||||
|
||||
// Initialize addons
|
||||
|
||||
try
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("moonlightServers.loadAddons");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("An error occured while initializing addons: {e}", e);
|
||||
}
|
||||
|
||||
IsInitialized = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
if(OnAfterInitialized != null)
|
||||
await OnAfterInitialized.Invoke();
|
||||
}
|
||||
|
||||
private async Task HandleFirstRender()
|
||||
{
|
||||
try
|
||||
{
|
||||
await Terminal.Addon("addon-fit").InvokeVoidAsync("fit");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("An error occured while calling addons: {e}", e);
|
||||
}
|
||||
|
||||
HadFirstRender = true;
|
||||
|
||||
// Write out cache
|
||||
while (MessageCache.Count > 0)
|
||||
{
|
||||
await Terminal.Write(MessageCache.Dequeue());
|
||||
}
|
||||
|
||||
if (OnFirstRender != null)
|
||||
await OnFirstRender.Invoke();
|
||||
}
|
||||
|
||||
public async Task Write(string content)
|
||||
{
|
||||
// We cache messages here as there is the chance that the console isn't ready for input while receiving write tasks
|
||||
|
||||
if (HadFirstRender)
|
||||
await Terminal.Write(content);
|
||||
else
|
||||
MessageCache.Enqueue(content);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
@using MoonCore.Exceptions
|
||||
@using MoonCore.Helpers
|
||||
@using MoonlightServers.Shared.Enums
|
||||
@using MoonlightServers.Frontend.UI.Components
|
||||
|
||||
@inject HttpApiClient ApiClient
|
||||
|
||||
@@ -67,11 +68,11 @@
|
||||
}
|
||||
|
||||
<div class="flex gap-x-1.5">
|
||||
<WButton CssClasses="btn btn-primary" OnClick="_ => DoSmth()">
|
||||
<button class="btn btn-success">
|
||||
<i class="icon-play me-1 align-middle"></i>
|
||||
<span class="align-middle">Start</span>
|
||||
</WButton>
|
||||
<button type="button" class="btn btn-tertiary">
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary">
|
||||
<i class="icon-rotate-ccw me-1 align-middle"></i>
|
||||
<span class="align-middle">Restart</span>
|
||||
</button>
|
||||
@@ -85,27 +86,33 @@
|
||||
|
||||
<div class="mt-5 mx-2 relative">
|
||||
<ul class="relative text-sm font-medium flex flex-nowrap -mx-4 sm:-mx-6 lg:-mx-8 overflow-x-scroll no-scrollbar">
|
||||
|
||||
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8"><a
|
||||
href="/admin/servers"
|
||||
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">Console</a>
|
||||
href="/admin/servers/all"
|
||||
class="block pb-3 text-white whitespace-nowrap border-b-2 border-primary-500">Console</a></li>
|
||||
|
||||
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8"><a
|
||||
href="/admin/servers"
|
||||
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">Files</a>
|
||||
</li>
|
||||
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8"><a
|
||||
href="/admin/servers/all"
|
||||
class="block pb-3 text-white whitespace-nowrap border-b-2 border-primary-500">Files</a></li>
|
||||
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8"><a
|
||||
href="/admin/servers/nodes"
|
||||
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">Backups</a>
|
||||
href="/admin/servers/nodes"
|
||||
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">Backups</a>
|
||||
</li>
|
||||
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8"><a
|
||||
href="/admin/servers/stars"
|
||||
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">Networking</a>
|
||||
href="/admin/servers/stars"
|
||||
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">Networking</a>
|
||||
</li>
|
||||
<li class="mr-6 last:mr-0 first:pl-4 sm:first:pl-6 lg:first:pl-8 last:pr-4 sm:last:pr-6 lg:last:pr-8"><a
|
||||
href="/admin/servers/manager"
|
||||
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">Variables</a>
|
||||
href="/admin/servers/manager"
|
||||
class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">Variables</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 h-44">
|
||||
<XtermConsole @ref="XtermConsole" OnAfterInitialized="OnAfterConsoleInitialized" />
|
||||
</div>
|
||||
}
|
||||
</LazyLoader>
|
||||
|
||||
@@ -116,8 +123,11 @@
|
||||
private ServerDetailResponse Server;
|
||||
private bool NotFound = false;
|
||||
private ServerPowerState PowerState;
|
||||
private string InitialConsoleMessage; // TODO: When moving to a single component, fail safe when failed to load
|
||||
|
||||
private string CurrentTask = "";
|
||||
private XtermConsole? XtermConsole;
|
||||
|
||||
private HubConnection ConsoleConnection;
|
||||
|
||||
private async Task Load(LazyLoader _)
|
||||
@@ -136,6 +146,16 @@
|
||||
|
||||
PowerState = status.PowerState;
|
||||
|
||||
// Load initial messages
|
||||
var initialLogs = await ApiClient.GetJson<ServerLogsResponse>(
|
||||
$"api/servers/{ServerId}/logs"
|
||||
);
|
||||
|
||||
InitialConsoleMessage = "";
|
||||
|
||||
foreach (var message in initialLogs.Messages)
|
||||
InitialConsoleMessage += message;
|
||||
|
||||
// Load console meta
|
||||
var consoleDetails = await ApiClient.GetJson<ServerConsoleResponse>(
|
||||
$"api/servers/{ServerId}/console"
|
||||
@@ -161,6 +181,14 @@
|
||||
await AddTask(Formatter.ConvertCamelCaseToSpaces(task));
|
||||
});
|
||||
|
||||
ConsoleConnection.On<string>("ConsoleOutput", async content =>
|
||||
{
|
||||
if (XtermConsole != null)
|
||||
await XtermConsole.Write(content);
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
|
||||
// Connect
|
||||
await ConsoleConnection.StartAsync();
|
||||
|
||||
@@ -175,23 +203,10 @@
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DoSmth()
|
||||
|
||||
private async Task OnAfterConsoleInitialized()
|
||||
{
|
||||
await AddTask("Creating storage");
|
||||
await Task.Delay(1500);
|
||||
|
||||
await AddTask("Pulling docker image");
|
||||
await Task.Delay(1500);
|
||||
|
||||
await AddTask("Removing container");
|
||||
await Task.Delay(1500);
|
||||
|
||||
await AddTask("Creating container");
|
||||
await Task.Delay(1500);
|
||||
|
||||
await AddTask("Starting container");
|
||||
await Task.Delay(1500);
|
||||
await XtermConsole!.Write(InitialConsoleMessage);
|
||||
}
|
||||
|
||||
private async Task AddTask(string message)
|
||||
@@ -201,7 +216,7 @@
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
await Task.Delay(3000);
|
||||
|
||||
if (CurrentTask != message)
|
||||
return;
|
||||
|
||||
1
MoonlightServers.Frontend/wwwroot/css/XtermBlazor.min.css
vendored
Normal file
1
MoonlightServers.Frontend/wwwroot/css/XtermBlazor.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{border:0;height:0;left:-9999em;margin:0;opacity:0;overflow:hidden;padding:0;position:absolute;resize:none;top:0;white-space:nowrap;width:0;z-index:-5}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;bottom:0;cursor:default;left:0;overflow-y:scroll;position:absolute;right:0;top:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{left:0;position:absolute;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;left:-9999em;line-height:normal;position:absolute;top:0;visibility:hidden}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{bottom:0;color:transparent;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:10}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{height:1px;left:-9999px;overflow:hidden;position:absolute;width:1px}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{position:absolute;z-index:6}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{pointer-events:none;position:absolute;right:0;top:0;z-index:8}.xterm-decoration-top{position:relative;z-index:2}
|
||||
9
MoonlightServers.Frontend/wwwroot/js/XtermBlazor.min.js
vendored
Normal file
9
MoonlightServers.Frontend/wwwroot/js/XtermBlazor.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
MoonlightServers.Frontend/wwwroot/js/addon-fit.js
Normal file
2
MoonlightServers.Frontend/wwwroot/js/addon-fit.js
Normal file
@@ -0,0 +1,2 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
|
||||
//# sourceMappingURL=addon-fit.js.map
|
||||
11
MoonlightServers.Frontend/wwwroot/js/moonlightServers.js
Normal file
11
MoonlightServers.Frontend/wwwroot/js/moonlightServers.js
Normal file
@@ -0,0 +1,11 @@
|
||||
window.moonlightServers = {
|
||||
loadAddons: function () {
|
||||
if(window.moonlightServers.consoleAddonsLoaded)
|
||||
return;
|
||||
|
||||
window.moonlightServers.consoleAddonsLoaded = true;
|
||||
XtermBlazor.registerAddons({"addon-fit": new FitAddon.FitAddon()});
|
||||
}
|
||||
}
|
||||
|
||||
window.moonlightServers.consoleAddonsLoaded = false;
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MoonlightServers.Shared.Http.Responses.Users.Servers;
|
||||
|
||||
public class ServerLogsResponse
|
||||
{
|
||||
public string[] Messages { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user