Added state streaming. Started working on server console

This commit is contained in:
Marcel Baumgartner
2024-02-02 22:21:31 +01:00
parent 95507fd41f
commit 9e515d9ed7
8 changed files with 108 additions and 23 deletions

View File

@@ -1,6 +1,6 @@
using Moonlight.Features.Servers.UI.Layouts; using Moonlight.Features.Servers.UI.Layouts;
using Moonlight.Features.ServiceManagement.Models.Abstractions; using Moonlight.Features.ServiceManagement.Models.Abstractions;
using Console = Moonlight.Features.Servers.UI.UserViews.Console;
namespace Moonlight.Features.Servers.Actions; namespace Moonlight.Features.Servers.Actions;
@@ -8,9 +8,12 @@ public class ServerServiceDefinition : ServiceDefinition
{ {
public override ServiceActions Actions => new ServerActions(); public override ServiceActions Actions => new ServerActions();
public override Type ConfigType => typeof(ServerConfig); public override Type ConfigType => typeof(ServerConfig);
public override async Task BuildUserView(ServiceViewContext context) public override async Task BuildUserView(ServiceViewContext context)
{ {
context.Layout = typeof(UserLayout); context.Layout = typeof(UserLayout);
await context.AddPage<Console>("Console", "/console", "bx bx-sm bxs-terminal");
} }
public override Task BuildAdminView(ServiceViewContext context) public override Task BuildAdminView(ServiceViewContext context)

View File

@@ -24,4 +24,15 @@ public class MetaCache<T>
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<T> Get(int id)
{
lock (Cache)
{
if(!Cache.ContainsKey(id))
Cache.Add(id, Activator.CreateInstance<T>());
return Task.FromResult(Cache[id]);
}
}
} }

View File

@@ -1,7 +1,6 @@
using System.Net.WebSockets; using System.Net.WebSockets;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MoonCore.Helpers; using MoonCore.Helpers;
using Moonlight.Features.Servers.Entities; using Moonlight.Features.Servers.Entities;
using Moonlight.Features.Servers.Extensions.Attributes; using Moonlight.Features.Servers.Extensions.Attributes;
using Moonlight.Features.Servers.Models.Packets; using Moonlight.Features.Servers.Models.Packets;
@@ -29,24 +28,18 @@ public class NodeController : Controller
// Load node from request context // Load node from request context
var node = (HttpContext.Items["Node"] as ServerNode)!; var node = (HttpContext.Items["Node"] as ServerNode)!;
await NodeService.Meta.Update(node.Id, meta => await NodeService.Meta.Update(node.Id, meta => { meta.IsBooting = true; });
{
meta.IsBooting = true;
});
return Ok(); return Ok();
} }
[HttpPost("notify/finish")] [HttpPost("notify/finish")]
public async Task<ActionResult> NotifyBootFinish() public async Task<ActionResult> NotifyBootFinish()
{ {
// Load node from request context // Load node from request context
var node = (HttpContext.Items["Node"] as ServerNode)!; var node = (HttpContext.Items["Node"] as ServerNode)!;
await NodeService.Meta.Update(node.Id, meta => await NodeService.Meta.Update(node.Id, meta => { meta.IsBooting = false; });
{
meta.IsBooting = false;
});
return Ok(); return Ok();
} }
@@ -55,15 +48,15 @@ public class NodeController : Controller
public async Task<ActionResult> Ws() public async Task<ActionResult> Ws()
{ {
// Validate if it is even a websocket connection // Validate if it is even a websocket connection
if (HttpContext.WebSockets.IsWebSocketRequest) if (!HttpContext.WebSockets.IsWebSocketRequest)
return BadRequest("This endpoint is only available for websockets"); return BadRequest("This endpoint is only available for websockets");
// Accept websocket connection // Accept websocket connection
var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
// Build connection wrapper // Build connection wrapper
var wsPacketConnection = new WsPacketConnection(websocket); var wsPacketConnection = new WsPacketConnection(websocket);
// Register packets // Register packets
await wsPacketConnection.RegisterPacket<ServerStateUpdate>("serverStateUpdate"); await wsPacketConnection.RegisterPacket<ServerStateUpdate>("serverStateUpdate");
await wsPacketConnection.RegisterPacket<ServerOutputMessage>("serverOutputMessage"); await wsPacketConnection.RegisterPacket<ServerOutputMessage>("serverOutputMessage");
@@ -79,6 +72,22 @@ public class NodeController : Controller
meta.State = serverStateUpdate.State; meta.State = serverStateUpdate.State;
meta.LastChangeTimestamp = DateTime.UtcNow; meta.LastChangeTimestamp = DateTime.UtcNow;
}); });
await (await ServerService.Meta.Get(serverStateUpdate.Id)).OnStateChanged.Invoke();
}
if (packet is ServerOutputMessage serverOutputMessage)
{
await ServerService.Meta.Update(serverOutputMessage.Id, meta =>
{
lock (meta.ConsoleMessages)
meta.ConsoleMessages.Add(serverOutputMessage.Message);
meta.LastChangeTimestamp = DateTime.UtcNow;
});
await (await ServerService.Meta.Get(serverOutputMessage.Id)).OnConsoleMessage.Invoke(serverOutputMessage
.Message);
} }
} }

View File

@@ -1,3 +1,4 @@
using MoonCore.Helpers;
using Moonlight.Features.Servers.Models.Enums; using Moonlight.Features.Servers.Models.Enums;
namespace Moonlight.Features.Servers.Models.Abstractions; namespace Moonlight.Features.Servers.Models.Abstractions;
@@ -5,5 +6,8 @@ namespace Moonlight.Features.Servers.Models.Abstractions;
public class ServerMeta public class ServerMeta
{ {
public ServerState State { get; set; } public ServerState State { get; set; }
public DateTime LastChangeTimestamp { get; set; } public DateTime LastChangeTimestamp { get; set; } = DateTime.UtcNow;
public SmartEventHandler OnStateChanged { get; set; } = new();
public SmartEventHandler<string> OnConsoleMessage { get; set; } = new();
public List<string> ConsoleMessages { get; set; } = new();
} }

View File

@@ -41,6 +41,12 @@ public class ServerService
await httpClient.Post($"servers/{server.Id}/power/{powerAction.ToString().ToLower()}"); await httpClient.Post($"servers/{server.Id}/power/{powerAction.ToString().ToLower()}");
} }
public async Task SubscribeToConsole(Server server)
{
using var httpClient = CreateHttpClient(server);
await httpClient.Post($"servers/{server.Id}/subscribe");
}
private HttpApiClient<NodeException> CreateHttpClient(Server server) private HttpApiClient<NodeException> CreateHttpClient(Server server)
{ {
using var scope = ServiceProvider.CreateScope(); using var scope = ServiceProvider.CreateScope();

View File

@@ -211,11 +211,9 @@
.Include(x => x.Allocations) .Include(x => x.Allocations)
.Include(x => x.MainAllocation) .Include(x => x.MainAllocation)
.First(x => x.Service.Id == Service.Id); .First(x => x.Service.Id == Service.Id);
Meta = new ServerMeta();
/*
// Load meta and setup event handlers // Load meta and setup event handlers
Meta = await ServerService.Meta.Get(Server); Meta = await ServerService.Meta.Get(Server.Id);
Meta.OnStateChanged += async Task () => Meta.OnStateChanged += async Task () =>
{ {
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
@@ -247,7 +245,7 @@
}; };
// Send console subscription and add auto resubscribe for it // Send console subscription and add auto resubscribe for it
await ServerService.Console.Subscribe(Server); await ServerService.SubscribeToConsole(Server);
// We need this to revalidate to the daemon that we are still interested // We need this to revalidate to the daemon that we are still interested
// in the console logs. By default the expiration time is 15 minutes from last // in the console logs. By default the expiration time is 15 minutes from last
@@ -257,7 +255,7 @@
while (!BackgroundCancel.IsCancellationRequested) while (!BackgroundCancel.IsCancellationRequested)
{ {
await Task.Delay(TimeSpan.FromMinutes(10)); await Task.Delay(TimeSpan.FromMinutes(10));
await ServerService.Console.Subscribe(Server); await ServerService.SubscribeToConsole(Server);
} }
}); });
@@ -270,8 +268,6 @@
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
}); });
*/
} }
private async Task OnInstallConsoleMessage(string message) private async Task OnInstallConsoleMessage(string message)

View File

@@ -0,0 +1,49 @@
@using Moonlight.Features.Servers.Models.Abstractions
@using Moonlight.Features.Servers.UI.Components
@implements IDisposable
<div class="card card-body bg-black p-3">
<Terminal @ref="Terminal" />
<div class="mt-3">
<div class="input-group">
<input class="form-control form-control-transparent text-white" placeholder="Enter command"/>
<button class="btn btn-secondary rounded-start">Execute</button>
</div>
</div>
</div>
@code
{
[CascadingParameter]
public ServerMeta Meta { get; set; }
private Terminal Terminal;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
string[] messages;
lock (Meta.ConsoleMessages)
messages = Meta.ConsoleMessages.TakeLast(100).ToArray();
foreach (var message in messages)
await Terminal.WriteLine(message);
Meta.OnConsoleMessage += OnConsoleMessage;
}
}
private async Task OnConsoleMessage(string message)
{
await Terminal.WriteLine(message);
}
public void Dispose()
{
if(Meta != null)
Meta.OnConsoleMessage -= OnConsoleMessage;
}
}

View File

@@ -37,6 +37,8 @@
<link href="/css/sweetalert2dark.css" rel="stylesheet" type="text/css"/> <link href="/css/sweetalert2dark.css" rel="stylesheet" type="text/css"/>
<link href="/css/interfont.css" rel="stylesheet" type="text/css"/> <link href="/css/interfont.css" rel="stylesheet" type="text/css"/>
@* <link href="https://fonts.googleapis.com/css?family=Inter:300,400,500,600,700" rel="stylesheet" type="text/css"> *@ @* <link href="https://fonts.googleapis.com/css?family=Inter:300,400,500,600,700" rel="stylesheet" type="text/css"> *@
<link href="/_content/XtermBlazor/XtermBlazor.css" rel="stylesheet" type="text/css" />
</head> </head>
<body data-kt-app-header-fixed="true" <body data-kt-app-header-fixed="true"
data-kt-app-header-fixed-mobile="true" data-kt-app-header-fixed-mobile="true"
@@ -57,6 +59,11 @@
<script src="/js/sweetalert2.js"></script> <script src="/js/sweetalert2.js"></script>
<script src="/js/ckeditor.js"></script> <script src="/js/ckeditor.js"></script>
<script src="/_content/XtermBlazor/XtermBlazor.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-search@0.13.0/lib/xterm-addon-search.min.js"></script>
@foreach (var theme in themes) @foreach (var theme in themes)
{ {
if (!string.IsNullOrEmpty(theme.JsUrl)) if (!string.IsNullOrEmpty(theme.JsUrl))