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

@@ -125,7 +125,7 @@ public class ServersController : Controller
{ {
var server = await GetServerWithPermCheck(serverId); var server = await GetServerWithPermCheck(serverId);
// TODO: Handle transparent proxy // TODO: Handle transparent node proxy
var accessToken = NodeService.CreateAccessToken(server.Node, parameters => var accessToken = NodeService.CreateAccessToken(server.Node, parameters =>
{ {
@@ -149,6 +149,31 @@ public class ServersController : Controller
}; };
} }
[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, private async Task<Server> GetServerWithPermCheck(int serverId,
Func<IQueryable<Server>, IQueryable<Server>>? queryModifier = null) Func<IQueryable<Server>, IQueryable<Server>>? queryModifier = null)
{ {

View File

@@ -10,7 +10,8 @@
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_ENVIRONMENT": "Development",
"MOONLIGHT_APP_PUBLICURL": "http://localhost:5269" "MOONLIGHT_APP_PUBLICURL": "http://localhost:5269"
} },
"commandLineArgs": "--frontend-asset js/XtermBlazor.min.js --frontend-asset js/moonlightServers.js --frontend-asset js/addon-fit.js"
} }
} }
} }

View File

@@ -19,6 +19,7 @@ public class PluginStartup : IAppStartup
builder.Services.AutoAddServices<PluginStartup>(); builder.Services.AutoAddServices<PluginStartup>();
BundleService.BundleCss("css/MoonlightServers.min.css"); BundleService.BundleCss("css/MoonlightServers.min.css");
BundleService.BundleCss("css/XtermBlazor.min.css");
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -51,6 +51,10 @@ public static class ServerConsoleExtensions
{ {
// Ignored // Ignored
} }
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception e) catch (Exception e)
{ {
server.Logger.LogWarning("An unhandled error occured while reading from container stream: {e}", 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; namespace MoonlightServers.Daemon.Helpers;
public class ServerConsoleConnection public class ServerConsoleConnection
{ {
public DateTime AuthenticatedUntil { get; set; } private readonly ServerService ServerService;
public int ServerId { get; set; } 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")] [HttpPost("{serverId:int}/start")]
public async Task Start(int serverId) public async Task Start(int serverId)
{ {

View File

@@ -17,12 +17,24 @@ public class ServerConsoleHub : Hub
ConsoleService = consoleService; 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")] [HubMethodName("Authenticate")]
public async Task Authenticate(string accessToken) public async Task Authenticate(string accessToken)
{ {
try try
{ {
await ConsoleService.Authenticate(Context, accessToken); await ConsoleService.AuthenticateClient(Context, accessToken);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -30,6 +42,5 @@ public class ServerConsoleHub : Hub
} }
} }
public override async Task OnDisconnectedAsync(Exception? exception) #endregion
=> await ConsoleService.OnClientDisconnected(Context);
} }

View File

@@ -8,152 +8,61 @@ namespace MoonlightServers.Daemon.Services;
[Singleton] [Singleton]
public class ServerConsoleService public class ServerConsoleService
{ {
private readonly Dictionary<int, ServerConsoleMonitor> Monitors = new();
private readonly Dictionary<string, ServerConsoleConnection> Connections = new();
private readonly ILogger<ServerConsoleService> Logger; private readonly ILogger<ServerConsoleService> Logger;
private readonly AccessTokenHelper AccessTokenHelper; private readonly IServiceProvider ServiceProvider;
private readonly ServerService ServerService;
private readonly IHubContext<ServerConsoleHub> HubContext; private readonly Dictionary<string, ServerConsoleConnection> Connections = new();
public ServerConsoleService( public ServerConsoleService(
ILogger<ServerConsoleService> logger, ILogger<ServerConsoleService> logger,
AccessTokenHelper accessTokenHelper, IServiceProvider serviceProvider
ServerService serverService, IHubContext<ServerConsoleHub> hubContext) )
{ {
Logger = logger; Logger = logger;
AccessTokenHelper = accessTokenHelper; ServiceProvider = serviceProvider;
ServerService = serverService;
HubContext = hubContext;
} }
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) lock (Connections)
{ Connections[context.ConnectionId] = connection;
removedConnection = Connections.GetValueOrDefault(context.ConnectionId);
if(removedConnection != null) await connection.Initialize(context);
Connections.Remove(context.ConnectionId);
} }
// Client never authenticated themselves, nothing to do public async Task AuthenticateClient(HubCallerContext context, string accessToken)
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; ServerConsoleConnection? connection;
lock (Connections) lock (Connections)
{ connection = Connections.GetValueOrDefault(context.ConnectionId);
connection = Connections
.GetValueOrDefault(context.ConnectionId); if(connection == null)
return;
await connection.Authenticate(context, accessToken);
} }
if (connection == null) // If no existing connection has been found, we create a new one public async Task DestroyClient(HubCallerContext context)
{ {
connection = new() ServerConsoleConnection? connection;
{
ServerId = server.Configuration.Id,
AuthenticatedUntil = DateTime.UtcNow.AddMinutes(10)
};
lock (Connections) lock (Connections)
Connections.Add(context.ConnectionId, connection); connection = Connections.GetValueOrDefault(context.ConnectionId);
Logger.LogDebug("Connection {id} authenticated successfully", context.ConnectionId); if(connection == null)
} return;
else
Logger.LogDebug("Connection {id} re-authenticated successfully", context.ConnectionId);
ServerConsoleMonitor? monitor; await connection.Destroy(context);
lock (Monitors) lock (Connections)
monitor = Monitors.GetValueOrDefault(server.Configuration.Id); Connections.Remove(context.ConnectionId);
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

@@ -0,0 +1,6 @@
namespace MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
public class ServerLogsResponse
{
public string[] Messages { get; set; }
}

View File

@@ -11,6 +11,7 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11" PrivateAssets="all"/> <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="Microsoft.AspNetCore.SignalR.Client" Version="8.0.11" />
<PackageReference Include="Moonlight.Client" Version="2.1.0"/> <PackageReference Include="Moonlight.Client" Version="2.1.0"/>
<PackageReference Include="XtermBlazor" Version="2.1.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -21,7 +22,6 @@
<ItemGroup> <ItemGroup>
<Folder Include="Helpers\"/> <Folder Include="Helpers\"/>
<Folder Include="Interfaces\"/> <Folder Include="Interfaces\"/>
<Folder Include="wwwroot\"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1 @@
@import url('https://fonts.googleapis.com/css2?family=Space+Mono,wght@0,200..900;1,200..900&display=swap');

View File

@@ -4,6 +4,7 @@
@import "additions/animations.css"; @import "additions/animations.css";
@import "additions/buttons.css"; @import "additions/buttons.css";
@import "additions/cards.css"; @import "additions/cards.css";
@import "additions/fonts.css";
@import "additions/progress.css"; @import "additions/progress.css";
@import "additions/scrollbar.css"; @import "additions/scrollbar.css";
@import "additions/loaders.css"; @import "additions/loaders.css";

View 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);
}
}

View File

@@ -6,6 +6,7 @@
@using MoonCore.Exceptions @using MoonCore.Exceptions
@using MoonCore.Helpers @using MoonCore.Helpers
@using MoonlightServers.Shared.Enums @using MoonlightServers.Shared.Enums
@using MoonlightServers.Frontend.UI.Components
@inject HttpApiClient ApiClient @inject HttpApiClient ApiClient
@@ -67,11 +68,11 @@
} }
<div class="flex gap-x-1.5"> <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> <i class="icon-play me-1 align-middle"></i>
<span class="align-middle">Start</span> <span class="align-middle">Start</span>
</WButton> </button>
<button type="button" class="btn btn-tertiary"> <button type="button" class="btn btn-primary">
<i class="icon-rotate-ccw me-1 align-middle"></i> <i class="icon-rotate-ccw me-1 align-middle"></i>
<span class="align-middle">Restart</span> <span class="align-middle">Restart</span>
</button> </button>
@@ -85,13 +86,15 @@
<div class="mt-5 mx-2 relative"> <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"> <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>
</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 <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" href="/admin/servers/all"
class="block pb-3 text-white whitespace-nowrap border-b-2 border-primary-500">Files</a></li> 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 <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" 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> class="block pb-3 text-gray-400 hover:text-white whitespace-nowrap hover:border-b-2 hover:border-primary-500">Backups</a>
@@ -106,6 +109,10 @@
</li> </li>
</ul> </ul>
</div> </div>
<div class="mt-3 h-44">
<XtermConsole @ref="XtermConsole" OnAfterInitialized="OnAfterConsoleInitialized" />
</div>
} }
</LazyLoader> </LazyLoader>
@@ -116,8 +123,11 @@
private ServerDetailResponse Server; private ServerDetailResponse Server;
private bool NotFound = false; private bool NotFound = false;
private ServerPowerState PowerState; private ServerPowerState PowerState;
private string InitialConsoleMessage; // TODO: When moving to a single component, fail safe when failed to load
private string CurrentTask = ""; private string CurrentTask = "";
private XtermConsole? XtermConsole;
private HubConnection ConsoleConnection; private HubConnection ConsoleConnection;
private async Task Load(LazyLoader _) private async Task Load(LazyLoader _)
@@ -136,6 +146,16 @@
PowerState = status.PowerState; 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 // Load console meta
var consoleDetails = await ApiClient.GetJson<ServerConsoleResponse>( var consoleDetails = await ApiClient.GetJson<ServerConsoleResponse>(
$"api/servers/{ServerId}/console" $"api/servers/{ServerId}/console"
@@ -161,6 +181,14 @@
await AddTask(Formatter.ConvertCamelCaseToSpaces(task)); await AddTask(Formatter.ConvertCamelCaseToSpaces(task));
}); });
ConsoleConnection.On<string>("ConsoleOutput", async content =>
{
if (XtermConsole != null)
await XtermConsole.Write(content);
await InvokeAsync(StateHasChanged);
});
// Connect // Connect
await ConsoleConnection.StartAsync(); await ConsoleConnection.StartAsync();
@@ -176,22 +204,9 @@
} }
} }
private async Task DoSmth() private async Task OnAfterConsoleInitialized()
{ {
await AddTask("Creating storage"); await XtermConsole!.Write(InitialConsoleMessage);
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);
} }
private async Task AddTask(string message) private async Task AddTask(string message)
@@ -201,7 +216,7 @@
Task.Run(async () => Task.Run(async () =>
{ {
await Task.Delay(2000); await Task.Delay(3000);
if (CurrentTask != message) if (CurrentTask != message)
return; return;

View 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}

File diff suppressed because one or more lines are too long

View 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

View 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;

View File

@@ -0,0 +1,6 @@
namespace MoonlightServers.Shared.Http.Responses.Users.Servers;
public class ServerLogsResponse
{
public string[] Messages { get; set; }
}