From cd4d278ceb9f0770cbbe19d52f67a746c085d60a Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Tue, 23 May 2023 21:03:09 +0200 Subject: [PATCH 01/15] Added external discord bot api --- .../Api/Moonlight/DiscordBotController.cs | 149 ++++++++++++++++++ .../DiscordBot/Requests/SetPowerSignal.cs | 8 + Moonlight/defaultstorage/configs/config.json | 4 + 3 files changed, 161 insertions(+) create mode 100644 Moonlight/App/Http/Controllers/Api/Moonlight/DiscordBotController.cs create mode 100644 Moonlight/App/Http/Requests/DiscordBot/Requests/SetPowerSignal.cs diff --git a/Moonlight/App/Http/Controllers/Api/Moonlight/DiscordBotController.cs b/Moonlight/App/Http/Controllers/Api/Moonlight/DiscordBotController.cs new file mode 100644 index 00000000..d2c4adb4 --- /dev/null +++ b/Moonlight/App/Http/Controllers/Api/Moonlight/DiscordBotController.cs @@ -0,0 +1,149 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Moonlight.App.ApiClients.Wings; +using Moonlight.App.ApiClients.Wings.Resources; +using Moonlight.App.Database.Entities; +using Moonlight.App.Http.Requests.DiscordBot.Requests; +using Moonlight.App.Repositories; +using Moonlight.App.Services; + +namespace Moonlight.App.Http.Controllers.Api.Moonlight; + +[ApiController] +[Route("api/moonlight/discordbot")] +public class DiscordBotController : Controller +{ + private readonly Repository UserRepository; + private readonly Repository ServerRepository; + private readonly ServerService ServerService; + private readonly string Token = ""; + private readonly bool Enable; + + public DiscordBotController( + Repository userRepository, + Repository serverRepository, + ServerService serverService, + ConfigService configService) + { + UserRepository = userRepository; + ServerRepository = serverRepository; + ServerService = serverService; + + var config = configService + .GetSection("Moonlight") + .GetSection("DiscordBotApi"); + + Enable = config.GetValue("Enable"); + + if (Enable) + { + Token = config.GetValue("Token"); + } + } + + [HttpGet("{id}/link")] + public async Task GetLink(ulong id) + { + if (!await IsAuth(Request)) + return StatusCode(403); + + if (await GetUserFromDiscordId(id) == null) + { + return BadRequest(); + } + + return Ok(); + } + + [HttpGet("{id}/servers")] + public async Task> GetServers(ulong id) + { + if (!await IsAuth(Request)) + return StatusCode(403); + + var user = await GetUserFromDiscordId(id); + + if (user == null) + return BadRequest(); + + return ServerRepository + .Get() + .Include(x => x.Owner) + .Include(x => x.Image) + .Where(x => x.Owner.Id == user.Id) + .ToArray(); + } + + [HttpPost("{id}/servers/{uuid}")] + public async Task SetPowerState(ulong id, Guid uuid, [FromBody] SetPowerSignal signal) + { + if (!await IsAuth(Request)) + return StatusCode(403); + + var user = await GetUserFromDiscordId(id); + + if (user == null) + return BadRequest(); + + var server = ServerRepository + .Get() + .Include(x => x.Owner) + .FirstOrDefault(x => x.Owner.Id == user.Id && x.Uuid == uuid); + + if (server == null) + return NotFound(); + + if (Enum.TryParse(signal.Signal, true, out PowerSignal powerSignal)) + { + await ServerService.SetPowerState(server, powerSignal); + return Ok(); + } + else + return BadRequest(); + } + + [HttpGet("{id}/servers/{uuid}")] + public async Task> GetServerDetails(ulong id, Guid uuid) + { + if (!await IsAuth(Request)) + return StatusCode(403); + + var user = await GetUserFromDiscordId(id); + + if (user == null) + return BadRequest(); + + var server = ServerRepository + .Get() + .Include(x => x.Owner) + .FirstOrDefault(x => x.Owner.Id == user.Id && x.Uuid == uuid); + + if (server == null) + return NotFound(); + + return await ServerService.GetDetails(server); + } + + private Task GetUserFromDiscordId(ulong discordId) + { + var user = UserRepository + .Get() + .FirstOrDefault(x => x.DiscordId == discordId); + + return Task.FromResult(user); + } + + private Task IsAuth(HttpRequest request) + { + if (!Enable) + return Task.FromResult(false); + + if (string.IsNullOrEmpty(request.Headers.Authorization)) + return Task.FromResult(false); + + if(request.Headers.Authorization == Token) + return Task.FromResult(true); + + return Task.FromResult(false); + } +} \ No newline at end of file diff --git a/Moonlight/App/Http/Requests/DiscordBot/Requests/SetPowerSignal.cs b/Moonlight/App/Http/Requests/DiscordBot/Requests/SetPowerSignal.cs new file mode 100644 index 00000000..1d364053 --- /dev/null +++ b/Moonlight/App/Http/Requests/DiscordBot/Requests/SetPowerSignal.cs @@ -0,0 +1,8 @@ +using Moonlight.App.ApiClients.Wings; + +namespace Moonlight.App.Http.Requests.DiscordBot.Requests; + +public class SetPowerSignal +{ + public string Signal { get; set; } +} \ No newline at end of file diff --git a/Moonlight/defaultstorage/configs/config.json b/Moonlight/defaultstorage/configs/config.json index 19537828..bc1bc396 100644 --- a/Moonlight/defaultstorage/configs/config.json +++ b/Moonlight/defaultstorage/configs/config.json @@ -8,6 +8,10 @@ "Port": "10324", "Username": "user_name" }, + "DiscordBotApi": { + "Enable": false, + "Token": "you api key here" + }, "DiscordBot": { "Enable": false, "Token": "Discord.Token.Here", From 800f9fbb500ab0507bafcfc944ccc76ef711d321 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Fri, 26 May 2023 15:18:59 +0200 Subject: [PATCH 02/15] Added new beta server list ui and added days to uptime formatter --- Moonlight/App/Helpers/Formatter.cs | 2 +- Moonlight/Shared/Views/Servers/Index.razor | 173 ++++++++++++++++++--- 2 files changed, 150 insertions(+), 25 deletions(-) diff --git a/Moonlight/App/Helpers/Formatter.cs b/Moonlight/App/Helpers/Formatter.cs index 78fbfc55..d23d3b5e 100644 --- a/Moonlight/App/Helpers/Formatter.cs +++ b/Moonlight/App/Helpers/Formatter.cs @@ -8,7 +8,7 @@ public static class Formatter { TimeSpan t = TimeSpan.FromMilliseconds(uptime); - return $"{t.Hours}h {t.Minutes}m {t.Seconds}s"; + return $"{t.Days}d {t.Hours}h {t.Minutes}m {t.Seconds}s"; } private static double Round(this double d, int decimals) diff --git a/Moonlight/Shared/Views/Servers/Index.razor b/Moonlight/Shared/Views/Servers/Index.razor index b0131db7..dcc2405d 100644 --- a/Moonlight/Shared/Views/Servers/Index.razor +++ b/Moonlight/Shared/Views/Servers/Index.razor @@ -1,5 +1,4 @@ @page "/servers" -@using Moonlight.App.Services.Sessions @using Moonlight.App.Repositories.Servers @using Microsoft.EntityFrameworkCore @using Moonlight.App.Database.Entities @@ -9,9 +8,97 @@ @inject IServiceScopeFactory ServiceScopeFactory - @if (AllServers.Any()) +@if (AllServers.Any()) +{ + if (UseSortedServerView) { - @foreach (var server in AllServers) + var groupedServers = AllServers + .OrderBy(x => x.Name) + .GroupBy(x => x.Image.Name); + + foreach (var groupedServer in groupedServers) + { +
@(groupedServer.Key)
+
+ @foreach (var server in groupedServer) + { +
+ +
+
+
+
+ +
+
+ + @(server.Name) + + + @(Math.Round(server.Memory / 1024D, 2)) GB / @(Math.Round(server.Disk / 1024D, 2)) GB / @(server.Node.Name) - @(server.Image.Name) + +
+
+
+
+ @(server.Node.Fqdn):@(server.MainAllocation.Port) +
+
+ @if (StatusCache.ContainsKey(server)) + { + var status = StatusCache[server]; + + switch (status) + { + case "offline": + + Offline + + break; + case "stopping": + + Stopping + + break; + case "starting": + + Starting + + break; + case "running": + + Running + + break; + case "failed": + + Failed + + break; + default: + + Offline + + break; + } + } + else + { + + Loading + + } +
+
+ +
+ } +
+ } + } + else + { + foreach (var server in AllServers) { @@ -71,29 +172,53 @@ } } - else - { -
-
-

- You have no servers -

- - We were not able to find any servers associated with your account - +} +else +{ +
+
+

+ You have no servers +

+ + We were not able to find any servers associated with your account + +
+
+} + +
+
+
+
+ Beta
- } +
+
+ +
+
+ + +
+
+
+
+
+
@code { [CascadingParameter] public User User { get; set; } - + private Server[] AllServers; private readonly Dictionary StatusCache = new(); + private bool UseSortedServerView = false; + private Task Load(LazyLoader arg) { AllServers = ServerRepository @@ -123,11 +248,11 @@ } }); } - + return Task.CompletedTask; } - private void AddStatus(App.Database.Entities.Server server, string status) + private void AddStatus(Server server, string status) { lock (StatusCache) { From ac3bdba3e8abab172f00f1eeb1af4b3b1279198e Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Fri, 26 May 2023 17:14:06 +0200 Subject: [PATCH 03/15] Implemented new discord linking system --- .../Api/Moonlight/OAuth2Controller.cs | 44 ++++++++++- Moonlight/App/OAuth2/OAuth2Provider.cs | 2 + .../OAuth2/Providers/DiscordOAuth2Provider.cs | 75 ++++++++++++++++++ .../OAuth2/Providers/GoogleOAuth2Provider.cs | 11 ++- Moonlight/App/Services/OAuth2Service.cs | 20 +++++ .../Navigations/ProfileNavigation.razor | 11 ++- Moonlight/Shared/Views/Profile/Discord.razor | 77 +++++++++++++++++++ Moonlight/Shared/Views/Profile/Security.razor | 2 +- .../Shared/Views/Profile/Subscriptions.razor | 2 +- 9 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 Moonlight/Shared/Views/Profile/Discord.razor diff --git a/Moonlight/App/Http/Controllers/Api/Moonlight/OAuth2Controller.cs b/Moonlight/App/Http/Controllers/Api/Moonlight/OAuth2Controller.cs index 5fc50874..9cff03d0 100644 --- a/Moonlight/App/Http/Controllers/Api/Moonlight/OAuth2Controller.cs +++ b/Moonlight/App/Http/Controllers/Api/Moonlight/OAuth2Controller.cs @@ -1,6 +1,7 @@ using Logging.Net; using Microsoft.AspNetCore.Mvc; using Moonlight.App.Services; +using Moonlight.App.Services.Sessions; namespace Moonlight.App.Http.Controllers.Api.Moonlight; @@ -11,12 +12,41 @@ public class OAuth2Controller : Controller private readonly UserService UserService; private readonly OAuth2Service OAuth2Service; private readonly DateTimeService DateTimeService; + private readonly IdentityService IdentityService; - public OAuth2Controller(UserService userService, OAuth2Service oAuth2Service, DateTimeService dateTimeService) + public OAuth2Controller( + UserService userService, + OAuth2Service oAuth2Service, + DateTimeService dateTimeService, + IdentityService identityService) { UserService = userService; OAuth2Service = oAuth2Service; DateTimeService = dateTimeService; + IdentityService = identityService; + } + + [HttpGet("{id}/start")] + public async Task Start([FromRoute] string id) + { + try + { + if (OAuth2Service.Providers.ContainsKey(id)) + { + return Redirect(await OAuth2Service.GetUrl(id)); + } + + Logger.Warn($"Someone tried to start an oauth2 flow using the id '{id}' which is not registered"); + + return Redirect("/"); + } + catch (Exception e) + { + Logger.Warn($"Error starting oauth2 flow for id: {id}"); + Logger.Warn(e); + + return Redirect("/"); + } } [HttpGet("{id}")] @@ -24,6 +54,18 @@ public class OAuth2Controller : Controller { try { + var currentUser = await IdentityService.Get(); + + if (currentUser != null) + { + if (await OAuth2Service.CanBeLinked(id)) + { + await OAuth2Service.LinkToUser(id, currentUser, code); + + return Redirect("/profile"); + } + } + var user = await OAuth2Service.HandleCode(id, code); Response.Cookies.Append("token", await UserService.GenerateToken(user), new() diff --git a/Moonlight/App/OAuth2/OAuth2Provider.cs b/Moonlight/App/OAuth2/OAuth2Provider.cs index af582c13..bad02d7e 100644 --- a/Moonlight/App/OAuth2/OAuth2Provider.cs +++ b/Moonlight/App/OAuth2/OAuth2Provider.cs @@ -9,7 +9,9 @@ public abstract class OAuth2Provider public string Url { get; set; } public IServiceScopeFactory ServiceScopeFactory { get; set; } public string DisplayName { get; set; } + public bool CanBeLinked { get; set; } = false; public abstract Task GetUrl(); public abstract Task HandleCode(string code); + public abstract Task LinkToUser(User user, string code); } \ No newline at end of file diff --git a/Moonlight/App/OAuth2/Providers/DiscordOAuth2Provider.cs b/Moonlight/App/OAuth2/Providers/DiscordOAuth2Provider.cs index 5ed2efea..ffe58b29 100644 --- a/Moonlight/App/OAuth2/Providers/DiscordOAuth2Provider.cs +++ b/Moonlight/App/OAuth2/Providers/DiscordOAuth2Provider.cs @@ -12,6 +12,11 @@ namespace Moonlight.App.OAuth2.Providers; public class DiscordOAuth2Provider : OAuth2Provider { + public DiscordOAuth2Provider() + { + CanBeLinked = true; + } + public override Task GetUrl() { string url = $"https://discord.com/api/oauth2/authorize?client_id={Config.ClientId}" + @@ -119,4 +124,74 @@ public class DiscordOAuth2Provider : OAuth2Provider return user; } } + + public override async Task LinkToUser(User user, string code) + { + // Endpoints + + var endpoint = Url + "/api/moonlight/oauth2/discord"; + var discordUserDataEndpoint = "https://discordapp.com/api/users/@me"; + var discordEndpoint = "https://discordapp.com/api/oauth2/token"; + + // Generate access token + + using var client = new RestClient(); + var request = new RestRequest(discordEndpoint); + + request.AddParameter("client_id", Config.ClientId); + request.AddParameter("client_secret", Config.ClientSecret); + request.AddParameter("grant_type", "authorization_code"); + request.AddParameter("code", code); + request.AddParameter("redirect_uri", endpoint); + + var response = await client.ExecutePostAsync(request); + + if (!response.IsSuccessful) + { + Logger.Warn("Error verifying oauth2 code"); + Logger.Warn(response.ErrorMessage); + throw new DisplayException("An error occured while verifying oauth2 code"); + } + + // parse response + + var data = new ConfigurationBuilder().AddJsonStream( + new MemoryStream(Encoding.ASCII.GetBytes(response.Content!)) + ).Build(); + + var accessToken = data.GetValue("access_token"); + + // Now, we will call the discord api with our access token to get the data we need + + var getRequest = new RestRequest(discordUserDataEndpoint); + getRequest.AddHeader("Authorization", $"Bearer {accessToken}"); + + var getResponse = await client.ExecuteGetAsync(getRequest); + + if (!getResponse.IsSuccessful) + { + Logger.Warn("An unexpected error occured while fetching user data from remote api"); + Logger.Warn(getResponse.ErrorMessage); + + throw new DisplayException("An unexpected error occured while fetching user data from remote api"); + } + + // Parse response + + var getData = new ConfigurationBuilder().AddJsonStream( + new MemoryStream(Encoding.ASCII.GetBytes(getResponse.Content!)) + ).Build(); + + var id = getData.GetValue("id"); + + // Handle data + + using var scope = ServiceScopeFactory.CreateScope(); + + var userRepo = scope.ServiceProvider.GetRequiredService>(); + + user.DiscordId = id; + + userRepo.Update(user); + } } \ No newline at end of file diff --git a/Moonlight/App/OAuth2/Providers/GoogleOAuth2Provider.cs b/Moonlight/App/OAuth2/Providers/GoogleOAuth2Provider.cs index 72758542..c0e56d55 100644 --- a/Moonlight/App/OAuth2/Providers/GoogleOAuth2Provider.cs +++ b/Moonlight/App/OAuth2/Providers/GoogleOAuth2Provider.cs @@ -4,7 +4,6 @@ using Moonlight.App.ApiClients.Google.Requests; using Moonlight.App.Database.Entities; using Moonlight.App.Exceptions; using Moonlight.App.Helpers; -using Moonlight.App.Models.Misc; using Moonlight.App.Repositories; using Moonlight.App.Services; using RestSharp; @@ -13,6 +12,11 @@ namespace Moonlight.App.OAuth2.Providers; public class GoogleOAuth2Provider : OAuth2Provider { + public GoogleOAuth2Provider() + { + CanBeLinked = false; + } + public override Task GetUrl() { var endpoint = Url + "/api/moonlight/oauth2/google"; @@ -127,4 +131,9 @@ public class GoogleOAuth2Provider : OAuth2Provider return user; } } + + public override Task LinkToUser(User user, string code) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/Moonlight/App/Services/OAuth2Service.cs b/Moonlight/App/Services/OAuth2Service.cs index c96bd497..31f17ff4 100644 --- a/Moonlight/App/Services/OAuth2Service.cs +++ b/Moonlight/App/Services/OAuth2Service.cs @@ -80,6 +80,26 @@ public class OAuth2Service return await provider.HandleCode(code); } + public Task CanBeLinked(string id) + { + if (Providers.All(x => x.Key != id)) + throw new DisplayException("Invalid oauth2 id"); + + var provider = Providers[id]; + + return Task.FromResult(provider.CanBeLinked); + } + + public async Task LinkToUser(string id, User user, string code) + { + if (Providers.All(x => x.Key != id)) + throw new DisplayException("Invalid oauth2 id"); + + var provider = Providers[id]; + + await provider.LinkToUser(user, code); + } + private string GetAppUrl() { if (EnableOverrideUrl) diff --git a/Moonlight/Shared/Components/Navigations/ProfileNavigation.razor b/Moonlight/Shared/Components/Navigations/ProfileNavigation.razor index ab04ef6e..dbf3bea5 100644 --- a/Moonlight/Shared/Components/Navigations/ProfileNavigation.razor +++ b/Moonlight/Shared/Components/Navigations/ProfileNavigation.razor @@ -9,7 +9,7 @@
@(User.FirstName) @(User.LastName) - + @if (User.Status == UserStatus.Verified) { @@ -31,12 +31,17 @@ + diff --git a/Moonlight/Shared/Views/Profile/Discord.razor b/Moonlight/Shared/Views/Profile/Discord.razor new file mode 100644 index 00000000..c6236d00 --- /dev/null +++ b/Moonlight/Shared/Views/Profile/Discord.razor @@ -0,0 +1,77 @@ +@page "/profile/discord" + +@using Moonlight.Shared.Components.Navigations +@using Moonlight.App.Database.Entities +@using Moonlight.App.Repositories +@using Moonlight.App.Services + +@inject Repository UserRepository +@inject SmartTranslateService SmartTranslateService + + + +@if (User.DiscordId == 0) +{ +
+
+
+ + +
+

+ Your account is currently not linked to discord +

+ To use features like the discord bot, link your moonlight account with your discord account
+
+
+
+ + +
+} +else +{ +
+
+
+ + +
+

+ Your account is linked to a discord account +

+ You are able to use features like the discord bot of moonlight +
+
+
+
+
+ + +
+
+
+} + +@code +{ + [CascadingParameter] + public User User { get; set; } + + private async Task RemoveLink() + { + User.DiscordId = 0; + UserRepository.Update(User); + + await InvokeAsync(StateHasChanged); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Profile/Security.razor b/Moonlight/Shared/Views/Profile/Security.razor index 7cd7bff4..5aad28bc 100644 --- a/Moonlight/Shared/Views/Profile/Security.razor +++ b/Moonlight/Shared/Views/Profile/Security.razor @@ -19,7 +19,7 @@ @inject AlertService AlertService @inject ToastService ToastService - +
diff --git a/Moonlight/Shared/Views/Profile/Subscriptions.razor b/Moonlight/Shared/Views/Profile/Subscriptions.razor index c8382775..2cfc7d1a 100644 --- a/Moonlight/Shared/Views/Profile/Subscriptions.razor +++ b/Moonlight/Shared/Views/Profile/Subscriptions.razor @@ -11,7 +11,7 @@ @inject SubscriptionService SubscriptionService @inject SmartTranslateService SmartTranslateService - +
From b270b48ac16bbab62ed4eae9159955527bba820d Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Sun, 28 May 2023 03:39:36 +0200 Subject: [PATCH 04/15] Patched event system. Storage issues when using the support chat should be fixed --- Moonlight/App/Events/EventSystem.cs | 31 ++--------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/Moonlight/App/Events/EventSystem.cs b/Moonlight/App/Events/EventSystem.cs index 1d0f1d40..58b27ded 100644 --- a/Moonlight/App/Events/EventSystem.cs +++ b/Moonlight/App/Events/EventSystem.cs @@ -5,7 +5,6 @@ namespace Moonlight.App.Events; public class EventSystem { - private Dictionary Storage = new(); private List Subscribers = new(); private readonly bool Debug = false; @@ -33,16 +32,8 @@ public class EventSystem return Task.CompletedTask; } - public Task Emit(string id, object? d = null) + public Task Emit(string id, object? data = null) { - int hashCode = -1; - - if (d != null) - { - hashCode = d.GetHashCode(); - Storage.TryAdd(hashCode, d); - } - Subscriber[] subscribers; lock (Subscribers) @@ -58,23 +49,6 @@ public class EventSystem { tasks.Add(new Task(() => { - int storageId = hashCode + 0; // To create a copy of the hash code - - object? data = null; - - if (storageId != -1) - { - if (Storage.TryGetValue(storageId, out var value)) - { - data = value; - } - else - { - Logger.Warn($"Object with the hash '{storageId}' was not present in the storage"); - return; - } - } - var stopWatch = new Stopwatch(); stopWatch.Start(); @@ -115,8 +89,7 @@ public class EventSystem Task.Run(() => { Task.WaitAll(tasks.ToArray()); - Storage.Remove(hashCode); - + if(Debug) Logger.Debug($"Completed all event tasks for '{id}' and removed object from storage"); }); From feec9426b9ef706a73f7e802de08af66a0fb0130 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Sun, 28 May 2023 04:27:00 +0200 Subject: [PATCH 05/15] Added new console streaming --- .../App/Helpers/Files/WingsFileAccess.cs | 1 + Moonlight/App/Helpers/Formatter.cs | 9 +- .../App/Helpers/Wings/Data/ConsoleMessage.cs | 7 + .../App/Helpers/Wings/Data/ServerResource.cs | 36 ++ .../App/Helpers/Wings/Enums/ConsoleState.cs | 8 + .../App/Helpers/Wings/Enums/ServerState.cs | 10 + .../App/Helpers/Wings/Events/BaseEvent.cs | 7 + .../Helpers/Wings/Events/SendTokenEvent.cs | 7 + Moonlight/App/Helpers/Wings/WingsConsole.cs | 377 ++++++++++++++++++ .../Helpers/{ => Wings}/WingsConsoleHelper.cs | 25 +- .../App/Helpers/{ => Wings}/WingsJwtHelper.cs | 2 +- .../{ => Wings}/WingsServerConverter.cs | 2 +- .../Api/Remote/ServersController.cs | 1 + Moonlight/App/Services/ServerService.cs | 1 + Moonlight/Moonlight.csproj | 1 - Moonlight/Program.cs | 1 + .../ServerControl/ServerBackups.razor | 7 +- .../ServerControl/ServerConsole.razor | 48 ++- .../ServerControl/ServerFiles.razor | 1 + .../ServerControl/ServerNavigation.razor | 25 +- Moonlight/Shared/Views/Server/Index.razor | 29 +- 21 files changed, 540 insertions(+), 65 deletions(-) create mode 100644 Moonlight/App/Helpers/Wings/Data/ConsoleMessage.cs create mode 100644 Moonlight/App/Helpers/Wings/Data/ServerResource.cs create mode 100644 Moonlight/App/Helpers/Wings/Enums/ConsoleState.cs create mode 100644 Moonlight/App/Helpers/Wings/Enums/ServerState.cs create mode 100644 Moonlight/App/Helpers/Wings/Events/BaseEvent.cs create mode 100644 Moonlight/App/Helpers/Wings/Events/SendTokenEvent.cs create mode 100644 Moonlight/App/Helpers/Wings/WingsConsole.cs rename Moonlight/App/Helpers/{ => Wings}/WingsConsoleHelper.cs (83%) rename Moonlight/App/Helpers/{ => Wings}/WingsJwtHelper.cs (97%) rename Moonlight/App/Helpers/{ => Wings}/WingsServerConverter.cs (99%) diff --git a/Moonlight/App/Helpers/Files/WingsFileAccess.cs b/Moonlight/App/Helpers/Files/WingsFileAccess.cs index 47acdc9d..8e1b7093 100644 --- a/Moonlight/App/Helpers/Files/WingsFileAccess.cs +++ b/Moonlight/App/Helpers/Files/WingsFileAccess.cs @@ -3,6 +3,7 @@ using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings.Requests; using Moonlight.App.ApiClients.Wings.Resources; using Moonlight.App.Database.Entities; +using Moonlight.App.Helpers.Wings; using Moonlight.App.Services; using RestSharp; diff --git a/Moonlight/App/Helpers/Formatter.cs b/Moonlight/App/Helpers/Formatter.cs index d23d3b5e..99a8ab62 100644 --- a/Moonlight/App/Helpers/Formatter.cs +++ b/Moonlight/App/Helpers/Formatter.cs @@ -8,7 +8,14 @@ public static class Formatter { TimeSpan t = TimeSpan.FromMilliseconds(uptime); - return $"{t.Days}d {t.Hours}h {t.Minutes}m {t.Seconds}s"; + if (t.Days > 0) + { + return $"{t.Days}d {t.Hours}h {t.Minutes}m {t.Seconds}s"; + } + else + { + return $"{t.Hours}h {t.Minutes}m {t.Seconds}s"; + } } private static double Round(this double d, int decimals) diff --git a/Moonlight/App/Helpers/Wings/Data/ConsoleMessage.cs b/Moonlight/App/Helpers/Wings/Data/ConsoleMessage.cs new file mode 100644 index 00000000..87d86f6d --- /dev/null +++ b/Moonlight/App/Helpers/Wings/Data/ConsoleMessage.cs @@ -0,0 +1,7 @@ +namespace Moonlight.App.Helpers.Wings.Data; + +public class ConsoleMessage +{ + public string Content { get; set; } = ""; + public bool IsInternal { get; set; } = false; +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Wings/Data/ServerResource.cs b/Moonlight/App/Helpers/Wings/Data/ServerResource.cs new file mode 100644 index 00000000..2b7ec53f --- /dev/null +++ b/Moonlight/App/Helpers/Wings/Data/ServerResource.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Helpers.Wings.Data; + +public class ServerResource +{ + [JsonProperty("memory_bytes")] + public long MemoryBytes { get; set; } + + [JsonProperty("memory_limit_bytes")] + public long MemoryLimitBytes { get; set; } + + [JsonProperty("cpu_absolute")] + public float CpuAbsolute { get; set; } + + [JsonProperty("network")] + public NetworkData Network { get; set; } + + [JsonProperty("uptime")] + public double Uptime { get; set; } + + [JsonProperty("state")] + public string State { get; set; } + + [JsonProperty("disk_bytes")] + public long DiskBytes { get; set; } + + public class NetworkData + { + [JsonProperty("rx_bytes")] + public long RxBytes { get; set; } + + [JsonProperty("tx_bytes")] + public long TxBytes { get; set; } + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Wings/Enums/ConsoleState.cs b/Moonlight/App/Helpers/Wings/Enums/ConsoleState.cs new file mode 100644 index 00000000..b2fce78e --- /dev/null +++ b/Moonlight/App/Helpers/Wings/Enums/ConsoleState.cs @@ -0,0 +1,8 @@ +namespace Moonlight.App.Helpers.Wings.Enums; + +public enum ConsoleState +{ + Disconnected, + Connecting, + Connected +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Wings/Enums/ServerState.cs b/Moonlight/App/Helpers/Wings/Enums/ServerState.cs new file mode 100644 index 00000000..1ced0020 --- /dev/null +++ b/Moonlight/App/Helpers/Wings/Enums/ServerState.cs @@ -0,0 +1,10 @@ +namespace Moonlight.App.Helpers.Wings.Enums; + +public enum ServerState +{ + Starting, + Running, + Stopping, + Offline, + Installing +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Wings/Events/BaseEvent.cs b/Moonlight/App/Helpers/Wings/Events/BaseEvent.cs new file mode 100644 index 00000000..9c0455bd --- /dev/null +++ b/Moonlight/App/Helpers/Wings/Events/BaseEvent.cs @@ -0,0 +1,7 @@ +namespace Moonlight.App.Helpers.Wings.Events; + +public class BaseEvent +{ + public string Event { get; set; } = ""; + public string[] Args { get; set; } = Array.Empty(); +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Wings/Events/SendTokenEvent.cs b/Moonlight/App/Helpers/Wings/Events/SendTokenEvent.cs new file mode 100644 index 00000000..660ae448 --- /dev/null +++ b/Moonlight/App/Helpers/Wings/Events/SendTokenEvent.cs @@ -0,0 +1,7 @@ +namespace Moonlight.App.Helpers.Wings.Events; + +public class SendTokenEvent +{ + public string Event { get; set; } = "auth"; + public List Args = new(); +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Wings/WingsConsole.cs b/Moonlight/App/Helpers/Wings/WingsConsole.cs new file mode 100644 index 00000000..bb6edfbb --- /dev/null +++ b/Moonlight/App/Helpers/Wings/WingsConsole.cs @@ -0,0 +1,377 @@ +using System.Net.WebSockets; +using System.Text; +using Logging.Net; +using Moonlight.App.Helpers.Wings.Data; +using Moonlight.App.Helpers.Wings.Enums; +using Moonlight.App.Helpers.Wings.Events; +using Newtonsoft.Json; +using ConsoleMessage = Moonlight.App.Helpers.Wings.Data.ConsoleMessage; + +namespace Moonlight.App.Helpers.Wings; + +public class WingsConsole : IDisposable +{ + private ClientWebSocket WebSocket; + public List Messages; + private Task? ConsoleTask; + + private string Socket = ""; + private string Origin = ""; + private string Token = ""; + + private bool Disconnecting; + + public ConsoleState ConsoleState { get; private set; } + public ServerState ServerState { get; private set; } + public ServerResource Resource { get; private set; } + + public EventHandler OnConsoleStateUpdated { get; set; } + public EventHandler OnServerStateUpdated { get; set; } + public EventHandler OnResourceUpdated { get; set; } + public EventHandler OnMessage { get; set; } + public Func> OnRequestNewToken { get; set; } + + public WingsConsole() + { + ConsoleState = ConsoleState.Disconnected; + ServerState = ServerState.Offline; + Messages = new(); + + Resource = new() + { + Network = new() + { + RxBytes = 0, + TxBytes = 0 + }, + State = "offline", + Uptime = 0, + CpuAbsolute = 0, + DiskBytes = 0, + MemoryBytes = 0, + MemoryLimitBytes = 0 + }; + } + + public Task Connect(string origin, string socket, string token) + { + Disconnecting = false; + WebSocket = new(); + ConsoleState = ConsoleState.Disconnected; + ServerState = ServerState.Offline; + Messages = new(); + + Resource = new() + { + Network = new() + { + RxBytes = 0, + TxBytes = 0 + }, + State = "offline", + Uptime = 0, + CpuAbsolute = 0, + DiskBytes = 0, + MemoryBytes = 0, + MemoryLimitBytes = 0 + }; + + Socket = socket; + Origin = origin; + Token = token; + + WebSocket.Options.SetRequestHeader("Origin", Origin); + WebSocket.Options.SetRequestHeader("Authorization", "Bearer " + Token); + + ConsoleTask = Task.Run(async () => + { + try + { + await Work(); + } + catch (Exception e) + { + Logger.Warn("Error connecting to wings console"); + Logger.Warn(e); + } + }); + + return Task.CompletedTask; + } + + private async Task Work() + { + await UpdateConsoleState(ConsoleState.Connecting); + + await WebSocket.ConnectAsync( + new Uri(Socket), + CancellationToken.None + ); + + if (WebSocket.State != WebSocketState.Connecting && WebSocket.State != WebSocketState.Open) + { + await SaveMessage("Unable to connect to websocket", true); + await UpdateConsoleState(ConsoleState.Disconnected); + return; + } + + await UpdateConsoleState(ConsoleState.Connected); + + await Send(new SendTokenEvent() + { + Args = { Token } + }); + + while (WebSocket.State == WebSocketState.Open) + { + try + { + var raw = await ReceiveRaw(); + + if(string.IsNullOrEmpty(raw)) + continue; + + var eventData = JsonConvert.DeserializeObject(raw); + + if (eventData == null) + { + await SaveMessage("Unable to parse event", true); + continue; + } + + switch (eventData.Event) + { + case "jwt error": + await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, "Jwt error detected", + CancellationToken.None); + + await UpdateServerState(ServerState.Offline); + await UpdateConsoleState(ConsoleState.Disconnected); + + await SaveMessage("Received a jwt error", true); + break; + + case "token expired": + await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, "Jwt error detected", + CancellationToken.None); + + await UpdateServerState(ServerState.Offline); + await UpdateConsoleState(ConsoleState.Disconnected); + + await SaveMessage("Token expired", true); + + break; + + case "token expiring": + await SaveMessage("Token will expire soon. Generating a new one", true); + + Token = await OnRequestNewToken.Invoke(this); + + await Send(new SendTokenEvent() + { + Args = { Token } + }); + break; + + case "auth success": + // Send intents + await SendRaw("{\"event\":\"send logs\",\"args\":[null]}"); + await SendRaw("{\"event\":\"send stats\",\"args\":[null]}"); + break; + + case "stats": + var stats = JsonConvert.DeserializeObject(eventData.Args[0]); + + if (stats == null) + break; + + var serverState = ParseServerState(stats.State); + + if (ServerState != serverState) + await UpdateServerState(serverState); + + await UpdateResource(stats); + break; + + case "status": + var serverStateParsed = ParseServerState(eventData.Args[0]); + + if (ServerState != serverStateParsed) + await UpdateServerState(serverStateParsed); + break; + + case "console output": + foreach (var line in eventData.Args) + { + await SaveMessage(line); + } + + break; + + case "install output": + foreach (var line in eventData.Args) + { + await SaveMessage(line); + } + + break; + + case "daemon message": + foreach (var line in eventData.Args) + { + await SaveMessage(line); + } + + break; + + case "install started": + await UpdateServerState(ServerState.Installing); + break; + + case "install completed": + await UpdateServerState(ServerState.Offline); + break; + } + } + catch (Exception e) + { + if (!Disconnecting) + { + Logger.Warn("Error while performing websocket actions"); + Logger.Warn(e); + + await SaveMessage("A unknown error occured while processing websocket", true); + } + } + } + } + + private Task UpdateConsoleState(ConsoleState consoleState) + { + ConsoleState = consoleState; + OnConsoleStateUpdated?.Invoke(this, consoleState); + + return Task.CompletedTask; + } + private Task UpdateServerState(ServerState serverState) + { + ServerState = serverState; + OnServerStateUpdated?.Invoke(this, serverState); + + return Task.CompletedTask; + } + private Task UpdateResource(ServerResource resource) + { + Resource = resource; + OnResourceUpdated?.Invoke(this, Resource); + + return Task.CompletedTask; + } + + private Task SaveMessage(string content, bool internalMessage = false) + { + var msg = new ConsoleMessage() + { + Content = content, + IsInternal = internalMessage + }; + + lock (Messages) + { + Messages.Add(msg); + } + + OnMessage?.Invoke(this, msg); + + return Task.CompletedTask; + } + + private ServerState ParseServerState(string raw) + { + switch (raw) + { + case "offline": + return ServerState.Offline; + case "starting": + return ServerState.Starting; + case "running": + return ServerState.Running; + case "stopping": + return ServerState.Stopping; + case "installing": + return ServerState.Installing; + default: + return ServerState.Offline; + } + } + + public async Task EnterCommand(string content) + { + if (ConsoleState == ConsoleState.Connected) + { + await SendRaw("{\"event\":\"send command\",\"args\":[\"" + content + "\"]}"); + } + } + + public async Task SetPowerState(string state) + { + if (ConsoleState == ConsoleState.Connected) + { + await SendRaw("{\"event\":\"set state\",\"args\":[\"" + state + "\"]}"); + } + } + + private async Task Send(object data) + { + await SendRaw(JsonConvert.SerializeObject(data)); + } + + private async Task SendRaw(string data) + { + if (WebSocket.State == WebSocketState.Open) + { + byte[] byteContentBuffer = Encoding.UTF8.GetBytes(data); + await WebSocket.SendAsync(new ArraySegment(byteContentBuffer), WebSocketMessageType.Text, true, + CancellationToken.None); + } + } + + private async Task ReceiveRaw() + { + ArraySegment receivedBytes = new ArraySegment(new byte[1024]); + WebSocketReceiveResult result = await WebSocket.ReceiveAsync(receivedBytes, CancellationToken.None); + return Encoding.UTF8.GetString(receivedBytes.Array!, 0, result.Count); + } + + public async Task Disconnect() + { + Disconnecting = true; + + if (WebSocket != null) + { + if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open) + await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); + + WebSocket.Dispose(); + } + + if(ConsoleTask != null && ConsoleTask.IsCompleted) + ConsoleTask.Dispose(); + } + + public void Dispose() + { + Disconnecting = true; + + if (WebSocket != null) + { + if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open) + WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None).Wait(); + + WebSocket.Dispose(); + } + + if(ConsoleTask != null && ConsoleTask.IsCompleted) + ConsoleTask.Dispose(); + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/WingsConsoleHelper.cs b/Moonlight/App/Helpers/Wings/WingsConsoleHelper.cs similarity index 83% rename from Moonlight/App/Helpers/WingsConsoleHelper.cs rename to Moonlight/App/Helpers/Wings/WingsConsoleHelper.cs index 720a293b..af2fc7d5 100644 --- a/Moonlight/App/Helpers/WingsConsoleHelper.cs +++ b/Moonlight/App/Helpers/Wings/WingsConsoleHelper.cs @@ -7,37 +7,34 @@ using Moonlight.App.Database.Entities; using Moonlight.App.Repositories.Servers; using Moonlight.App.Services; -namespace Moonlight.App.Helpers; +namespace Moonlight.App.Helpers.Wings; public class WingsConsoleHelper { private readonly ServerRepository ServerRepository; - private readonly WingsJwtHelper WingsJwtHelper; private readonly string AppUrl; public WingsConsoleHelper( ServerRepository serverRepository, - ConfigService configService, - WingsJwtHelper wingsJwtHelper) + ConfigService configService) { ServerRepository = serverRepository; - WingsJwtHelper = wingsJwtHelper; AppUrl = configService.GetSection("Moonlight").GetValue("AppUrl"); } - public async Task ConnectWings(PteroConsole.NET.PteroConsole pteroConsole, Server server) + public async Task ConnectWings(WingsConsole console, Server server) { var serverData = ServerRepository .Get() .Include(x => x.Node) .First(x => x.Id == server.Id); - - var token = GenerateToken(serverData); + + var token = await GenerateToken(serverData); if (serverData.Node.Ssl) { - await pteroConsole.Connect( + await console.Connect( AppUrl, $"wss://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws", token @@ -45,7 +42,7 @@ public class WingsConsoleHelper } else { - await pteroConsole.Connect( + await console.Connect( AppUrl, $"ws://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws", token @@ -53,20 +50,20 @@ public class WingsConsoleHelper } } - public string GenerateToken(Server server) + public async Task GenerateToken(Server server) { var serverData = ServerRepository .Get() .Include(x => x.Node) .First(x => x.Id == server.Id); - + var userid = 1; var secret = serverData.Node.Token; using (MD5 md5 = MD5.Create()) { - var inputBytes = Encoding.ASCII.GetBytes(userid + serverData.Uuid.ToString()); + var inputBytes = Encoding.ASCII.GetBytes(userid + server.Uuid.ToString()); var outputBytes = md5.ComputeHash(inputBytes); var identifier = Convert.ToHexString(outputBytes).ToLower(); @@ -77,7 +74,7 @@ public class WingsConsoleHelper .WithAlgorithm(new HMACSHA256Algorithm()) .WithSecret(secret) .AddClaim("user_id", userid) - .AddClaim("server_uuid", serverData.Uuid.ToString()) + .AddClaim("server_uuid", server.Uuid.ToString()) .AddClaim("permissions", new[] { "*", diff --git a/Moonlight/App/Helpers/WingsJwtHelper.cs b/Moonlight/App/Helpers/Wings/WingsJwtHelper.cs similarity index 97% rename from Moonlight/App/Helpers/WingsJwtHelper.cs rename to Moonlight/App/Helpers/Wings/WingsJwtHelper.cs index 221abd1f..8e4af78a 100644 --- a/Moonlight/App/Helpers/WingsJwtHelper.cs +++ b/Moonlight/App/Helpers/Wings/WingsJwtHelper.cs @@ -4,7 +4,7 @@ using JWT.Algorithms; using JWT.Builder; using Moonlight.App.Services; -namespace Moonlight.App.Helpers; +namespace Moonlight.App.Helpers.Wings; public class WingsJwtHelper { diff --git a/Moonlight/App/Helpers/WingsServerConverter.cs b/Moonlight/App/Helpers/Wings/WingsServerConverter.cs similarity index 99% rename from Moonlight/App/Helpers/WingsServerConverter.cs rename to Moonlight/App/Helpers/Wings/WingsServerConverter.cs index c7375e3d..c0875ab1 100644 --- a/Moonlight/App/Helpers/WingsServerConverter.cs +++ b/Moonlight/App/Helpers/Wings/WingsServerConverter.cs @@ -5,7 +5,7 @@ using Moonlight.App.Http.Resources.Wings; using Moonlight.App.Repositories; using Moonlight.App.Repositories.Servers; -namespace Moonlight.App.Helpers; +namespace Moonlight.App.Helpers.Wings; public class WingsServerConverter { diff --git a/Moonlight/App/Http/Controllers/Api/Remote/ServersController.cs b/Moonlight/App/Http/Controllers/Api/Remote/ServersController.cs index 08f4ddc0..b7afee2e 100644 --- a/Moonlight/App/Http/Controllers/Api/Remote/ServersController.cs +++ b/Moonlight/App/Http/Controllers/Api/Remote/ServersController.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Moonlight.App.Events; using Moonlight.App.Helpers; +using Moonlight.App.Helpers.Wings; using Moonlight.App.Http.Resources.Wings; using Moonlight.App.Repositories; using Moonlight.App.Repositories.Servers; diff --git a/Moonlight/App/Services/ServerService.cs b/Moonlight/App/Services/ServerService.cs index fb9c4db0..ae0390b0 100644 --- a/Moonlight/App/Services/ServerService.cs +++ b/Moonlight/App/Services/ServerService.cs @@ -8,6 +8,7 @@ using Moonlight.App.Events; using Moonlight.App.Exceptions; using Moonlight.App.Helpers; using Moonlight.App.Helpers.Files; +using Moonlight.App.Helpers.Wings; using Moonlight.App.Models.Misc; using Moonlight.App.Repositories; using Moonlight.App.Repositories.Servers; diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index 64f9e048..d519c643 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -41,7 +41,6 @@ - diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index d9a46b8e..70466a08 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -9,6 +9,7 @@ using Moonlight.App.ApiClients.Wings; using Moonlight.App.Database; using Moonlight.App.Events; using Moonlight.App.Helpers; +using Moonlight.App.Helpers.Wings; using Moonlight.App.LogMigrator; using Moonlight.App.Repositories; using Moonlight.App.Repositories.Domains; diff --git a/Moonlight/Shared/Components/ServerControl/ServerBackups.razor b/Moonlight/Shared/Components/ServerControl/ServerBackups.razor index 7175a3c7..fc71c474 100644 --- a/Moonlight/Shared/Components/ServerControl/ServerBackups.razor +++ b/Moonlight/Shared/Components/ServerControl/ServerBackups.razor @@ -1,6 +1,4 @@ -@using PteroConsole.NET -@using Moonlight.App.Services -@using Task = System.Threading.Tasks.Task +@using Moonlight.App.Services @using Moonlight.App.Helpers @using Logging.Net @using BlazorContextMenu @@ -101,9 +99,6 @@ @code { - [CascadingParameter] - public PteroConsole Console { get; set; } - [CascadingParameter] public Server CurrentServer { get; set; } diff --git a/Moonlight/Shared/Components/ServerControl/ServerConsole.razor b/Moonlight/Shared/Components/ServerControl/ServerConsole.razor index 474efa29..454e6d87 100644 --- a/Moonlight/Shared/Components/ServerControl/ServerConsole.razor +++ b/Moonlight/Shared/Components/ServerControl/ServerConsole.razor @@ -1,11 +1,9 @@ -@using PteroConsole.NET -@using PteroConsole.NET.Enums -@using Task = System.Threading.Tasks.Task -@using Moonlight.App.Helpers +@using Moonlight.App.Helpers @using Moonlight.App.Repositories @using Moonlight.App.Services -@using Logging.Net @using Moonlight.App.Database.Entities +@using Moonlight.App.Helpers.Wings +@using Moonlight.App.Helpers.Wings.Data @using Moonlight.App.Services.Interop @using Moonlight.Shared.Components.Xterm @@ -37,7 +35,7 @@ @code { [CascadingParameter] - public PteroConsole Console { get; set; } + public WingsConsole Console { get; set; } [CascadingParameter] public Server CurrentServer { get; set; } @@ -51,21 +49,35 @@ Console.OnMessage += OnMessage; } - private async void OnMessage(object? sender, string e) + private async void OnMessage(object? sender, ConsoleMessage message) { if (Terminal != null) { - var s = e; + if (message.IsInternal) + { + await Terminal.WriteLine("\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m " + message.Content + "\x1b[0m"); + } + else + { + var s = message.Content; - s = s.Replace("Pterodactyl Daemon", "Moonlight Daemon"); - s = s.Replace("Checking server disk space usage, this could take a few seconds...", TranslationService.Translate("Checking disk space")); - s = s.Replace("Updating process configuration files...", TranslationService.Translate("Updating config files")); - s = s.Replace("Ensuring file permissions are set correctly, this could take a few seconds...", TranslationService.Translate("Checking file permissions")); - s = s.Replace("Pulling Docker container image, this could take a few minutes to complete...", TranslationService.Translate("Downloading server image")); - s = s.Replace("Finished pulling Docker container image", TranslationService.Translate("Downloaded server image")); - s = s.Replace("container@pterodactyl~", "server@moonlight >"); + if (s.Contains("Moonlight Daemon") || s.Contains("Pterodactyl Daemon")) + { + s = s.Replace("[39m", "\x1b[0m"); + s = s.Replace("[33m", "[38;5;16;48;5;135m\x1b[39m"); + } - await Terminal.WriteLine(s); + s = s.Replace("[Pterodactyl Daemon]:", " Moonlight "); + s = s.Replace("[Moonlight Daemon]:", " Moonlight "); + s = s.Replace("Checking server disk space usage, this could take a few seconds...", TranslationService.Translate("Checking disk space")); + s = s.Replace("Updating process configuration files...", TranslationService.Translate("Updating config files")); + s = s.Replace("Ensuring file permissions are set correctly, this could take a few seconds...", TranslationService.Translate("Checking file permissions")); + s = s.Replace("Pulling Docker container image, this could take a few minutes to complete...", TranslationService.Translate("Downloading server image")); + s = s.Replace("Finished pulling Docker container image", TranslationService.Translate("Downloaded server image")); + s = s.Replace("container@pterodactyl~", "server@moonlight >"); + + await Terminal.WriteLine(s); + } } } @@ -85,9 +97,9 @@ private void RunOnFirstRender() { - lock (Console.MessageCache) + lock (Console.Messages) { - foreach (var message in Console.MessageCache.TakeLast(30)) + foreach (var message in Console.Messages) { OnMessage(null, message); } diff --git a/Moonlight/Shared/Components/ServerControl/ServerFiles.razor b/Moonlight/Shared/Components/ServerControl/ServerFiles.razor index 6267a9b8..9ce54cd2 100644 --- a/Moonlight/Shared/Components/ServerControl/ServerFiles.razor +++ b/Moonlight/Shared/Components/ServerControl/ServerFiles.razor @@ -4,6 +4,7 @@ @using Moonlight.App.Helpers.Files @using Moonlight.App.Services @using Moonlight.App.ApiClients.Wings +@using Moonlight.App.Helpers.Wings @inject WingsApiHelper WingsApiHelper @inject WingsJwtHelper WingsJwtHelper diff --git a/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor b/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor index 4d10f7b2..912b3c3e 100644 --- a/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor +++ b/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor @@ -1,9 +1,8 @@ -@using PteroConsole.NET -@using PteroConsole.NET.Enums -@using Task = System.Threading.Tasks.Task -@using Moonlight.App.Services +@using Moonlight.App.Services @using Moonlight.App.Database.Entities @using Moonlight.App.Helpers +@using Moonlight.App.Helpers.Wings +@using Moonlight.App.Helpers.Wings.Enums @inject SmartTranslateService TranslationService @@ -77,32 +76,32 @@ case ServerState.Starting: Starting - (@(Formatter.FormatUptime(Console.ServerResource.Uptime))) + (@(Formatter.FormatUptime(Console.Resource.Uptime))) break; case ServerState.Stopping: Stopping - (@(Formatter.FormatUptime(Console.ServerResource.Uptime))) + (@(Formatter.FormatUptime(Console.Resource.Uptime))) break; case ServerState.Running: Online - (@(Formatter.FormatUptime(Console.ServerResource.Uptime))) + (@(Formatter.FormatUptime(Console.Resource.Uptime))) break; }
Cpu: - @(Math.Round(Console.ServerResource.CpuAbsolute, 2))% + @(Math.Round(Console.Resource.CpuAbsolute, 2))%
Memory: - @(Formatter.FormatSize(Console.ServerResource.MemoryBytes)) / @(Formatter.FormatSize(Console.ServerResource.MemoryLimitBytes)) + @(Formatter.FormatSize(Console.Resource.MemoryBytes)) / @(Formatter.FormatSize(Console.Resource.MemoryLimitBytes))
Disk: - @(Formatter.FormatSize(Console.ServerResource.DiskBytes)) / @(Math.Round(CurrentServer.Disk / 1024f, 2)) GB + @(Formatter.FormatSize(Console.Resource.DiskBytes)) / @(Math.Round(CurrentServer.Disk / 1024f, 2)) GB
@@ -178,7 +177,7 @@ public User User { get; set; } [CascadingParameter] - public PteroConsole Console { get; set; } + public WingsConsole Console { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } @@ -190,8 +189,8 @@ protected override void OnInitialized() { - Console.OnServerStateUpdated += async (sender, state) => { await InvokeAsync(StateHasChanged); }; - Console.OnServerResourceUpdated += async (sender, x) => { await InvokeAsync(StateHasChanged); }; + Console.OnServerStateUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); }; + Console.OnResourceUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); }; } #region Power Actions diff --git a/Moonlight/Shared/Views/Server/Index.razor b/Moonlight/Shared/Views/Server/Index.razor index 63754a15..bfa80cad 100644 --- a/Moonlight/Shared/Views/Server/Index.razor +++ b/Moonlight/Shared/Views/Server/Index.razor @@ -1,13 +1,13 @@ @page "/server/{ServerUuid}/{Route?}" -@using PteroConsole.NET @using Task = System.Threading.Tasks.Task @using Moonlight.App.Repositories.Servers -@using PteroConsole.NET.Enums @using Microsoft.EntityFrameworkCore @using Logging.Net @using Moonlight.App.Database.Entities @using Moonlight.App.Events @using Moonlight.App.Helpers +@using Moonlight.App.Helpers.Wings +@using Moonlight.App.Helpers.Wings.Enums @using Moonlight.App.Repositories @using Moonlight.App.Services @using Moonlight.Shared.Components.Xterm @@ -44,7 +44,7 @@ { if (NodeOnline) { - if (Console.ConnectionState == ConnectionState.Connected) + if (Console.ConsoleState == ConsoleState.Connected) { if (Console.ServerState == ServerState.Installing) { @@ -179,7 +179,7 @@ [Parameter] public string? Route { get; set; } - private PteroConsole? Console; + private WingsConsole? Console; private Server? CurrentServer; private Node Node; private bool NodeOnline = false; @@ -193,11 +193,11 @@ { Console = new(); - Console.OnConnectionStateUpdated += (_, _) => { InvokeAsync(StateHasChanged); }; - Console.OnServerResourceUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); }; - Console.OnServerStateUpdated += async (_, _) => { await InvokeAsync(StateHasChanged); }; + Console.OnConsoleStateUpdated += (_, _) => { InvokeAsync(StateHasChanged); }; + Console.OnResourceUpdated += (_, _) => { InvokeAsync(StateHasChanged); }; + Console.OnServerStateUpdated += (_, _) => { InvokeAsync(StateHasChanged); }; - Console.RequestToken += (_) => WingsConsoleHelper.GenerateToken(CurrentServer!); + Console.OnRequestNewToken += async _ => await WingsConsoleHelper.GenerateToken(CurrentServer!); Console.OnMessage += async (_, s) => { @@ -205,7 +205,10 @@ { if (InstallConsole != null) { - await InstallConsole.WriteLine(s); + if (s.IsInternal) + await InstallConsole.WriteLine("\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m " + s.Content + "\x1b[0m"); + else + await InstallConsole.WriteLine(s.Content); } } }; @@ -280,7 +283,7 @@ await lazyLoader.SetText("Connecting to console"); - await WingsConsoleHelper.ConnectWings(Console!, CurrentServer); + await ReconnectConsole(); await Event.On($"server.{CurrentServer.Uuid}.installComplete", this, server => { @@ -295,6 +298,12 @@ Logger.Debug("Server is null"); } } + + private async Task ReconnectConsole() + { + await Console!.Disconnect(); + await WingsConsoleHelper.ConnectWings(Console!, CurrentServer!); + } public async void Dispose() { From 3527bc1bd59e809b3c54f6d00f96dbd6949c5ff5 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Sun, 28 May 2023 04:33:11 +0200 Subject: [PATCH 06/15] Fixed decompress issue (hopefully) --- .../App/Helpers/Files/WingsFileAccess.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/Moonlight/App/Helpers/Files/WingsFileAccess.cs b/Moonlight/App/Helpers/Files/WingsFileAccess.cs index 8e1b7093..f28d65a2 100644 --- a/Moonlight/App/Helpers/Files/WingsFileAccess.cs +++ b/Moonlight/App/Helpers/Files/WingsFileAccess.cs @@ -212,13 +212,27 @@ public class WingsFileAccess : FileAccess public override async Task Decompress(FileData fileData) { - var req = new DecompressFile() + try { - Root = CurrentPath, - File = fileData.Name - }; + var req = new DecompressFile() + { + Root = CurrentPath, + File = fileData.Name + }; - await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/decompress", req); + await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/decompress", req); + } + catch (Exception e) + { + if (e.Message.ToLower().Contains("canceled")) + { + // ignore, maybe do smth better here, like showing a waiting thing or so + } + else + { + throw; + } + } } public override Task GetLaunchUrl() From 7128a7f8a77cf3d567bbd9dec651ae485514fbfd Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Thu, 1 Jun 2023 00:44:14 +0200 Subject: [PATCH 07/15] Add console streaming dispose --- Moonlight/Shared/Views/Server/Index.razor | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Moonlight/Shared/Views/Server/Index.razor b/Moonlight/Shared/Views/Server/Index.razor index bfa80cad..137f6241 100644 --- a/Moonlight/Shared/Views/Server/Index.razor +++ b/Moonlight/Shared/Views/Server/Index.razor @@ -311,5 +311,10 @@ { await Event.Off($"server.{CurrentServer.Uuid}.installComplete", this); } + + if (Console != null) + { + Console.Dispose(); + } } } \ No newline at end of file From d7fb3382f7c7d4eff7880b35c1dbcaab90d70cde Mon Sep 17 00:00:00 2001 From: Dannyx Date: Sat, 3 Jun 2023 09:06:22 +0200 Subject: [PATCH 08/15] =?UTF-8?q?Ein=20paar=20kleine=20=C3=84nderungen=20;?= =?UTF-8?q?-)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../defaultstorage/resources/lang/de_de.lang | Bin 45980 -> 23204 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Moonlight/defaultstorage/resources/lang/de_de.lang b/Moonlight/defaultstorage/resources/lang/de_de.lang index 4f127265e71718fb4753f2f4e933a429cab840b3..08ed8741ae7fba0dde17d5c63bcb8edf94fe66a9 100644 GIT binary patch literal 23204 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 XArhI0(7?OGMPLGz(0=uqquc)iryIEM literal 45980 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 From 343e527fb6e7f4aff201c4436dbb13d4cbf6cca9 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Sun, 4 Jun 2023 20:56:47 +0200 Subject: [PATCH 09/15] Added message cache clear for console streaming --- Moonlight/App/Helpers/Wings/WingsConsole.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Moonlight/App/Helpers/Wings/WingsConsole.cs b/Moonlight/App/Helpers/Wings/WingsConsole.cs index bb6edfbb..a15bbfcc 100644 --- a/Moonlight/App/Helpers/Wings/WingsConsole.cs +++ b/Moonlight/App/Helpers/Wings/WingsConsole.cs @@ -346,6 +346,7 @@ public class WingsConsole : IDisposable public async Task Disconnect() { Disconnecting = true; + Messages.Clear(); if (WebSocket != null) { @@ -362,6 +363,7 @@ public class WingsConsole : IDisposable public void Dispose() { Disconnecting = true; + Messages.Clear(); if (WebSocket != null) { From 233c304b3c6560951a0e5635143c1627aa162718 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Sun, 4 Jun 2023 21:41:15 +0200 Subject: [PATCH 10/15] Fixed error when closing a failed websocket connection --- Moonlight/App/Helpers/Wings/WingsConsole.cs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Moonlight/App/Helpers/Wings/WingsConsole.cs b/Moonlight/App/Helpers/Wings/WingsConsole.cs index a15bbfcc..c51910a8 100644 --- a/Moonlight/App/Helpers/Wings/WingsConsole.cs +++ b/Moonlight/App/Helpers/Wings/WingsConsole.cs @@ -142,18 +142,28 @@ public class WingsConsole : IDisposable switch (eventData.Event) { case "jwt error": - await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, "Jwt error detected", - CancellationToken.None); + if (WebSocket != null) + { + if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open) + await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); + + WebSocket.Dispose(); + } await UpdateServerState(ServerState.Offline); await UpdateConsoleState(ConsoleState.Disconnected); - await SaveMessage("Received a jwt error", true); + await SaveMessage("Received a jwt error. Disconnected", true); break; case "token expired": - await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, "Jwt error detected", - CancellationToken.None); + if (WebSocket != null) + { + if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open) + await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None); + + WebSocket.Dispose(); + } await UpdateServerState(ServerState.Offline); await UpdateConsoleState(ConsoleState.Disconnected); From 4fb4a2415b8745f6dc9782784b50a02dba68adce Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Mon, 5 Jun 2023 21:34:09 +0200 Subject: [PATCH 11/15] Removed bundle service --- .../Api/Moonlight/ResourcesController.cs | 34 +---- .../App/Services/Sessions/BundleService.cs | 129 ------------------ Moonlight/Pages/_Layout.cshtml | 83 ++++++----- Moonlight/Program.cs | 1 - 4 files changed, 42 insertions(+), 205 deletions(-) delete mode 100644 Moonlight/App/Services/Sessions/BundleService.cs diff --git a/Moonlight/App/Http/Controllers/Api/Moonlight/ResourcesController.cs b/Moonlight/App/Http/Controllers/Api/Moonlight/ResourcesController.cs index 6632386c..d701a2c2 100644 --- a/Moonlight/App/Http/Controllers/Api/Moonlight/ResourcesController.cs +++ b/Moonlight/App/Http/Controllers/Api/Moonlight/ResourcesController.cs @@ -16,14 +16,12 @@ public class ResourcesController : Controller { private readonly SecurityLogService SecurityLogService; private readonly BucketService BucketService; - private readonly BundleService BundleService; public ResourcesController(SecurityLogService securityLogService, - BucketService bucketService, BundleService bundleService) + BucketService bucketService) { SecurityLogService = securityLogService; BucketService = bucketService; - BundleService = bundleService; } [HttpGet("images/{name}")] @@ -77,34 +75,4 @@ public class ResourcesController : Controller return Problem(); } } - - [HttpGet("bundle/js")] - public Task GetJs() - { - if (BundleService.BundledFinished) - { - return Task.FromResult( - File(Encoding.ASCII.GetBytes(BundleService.BundledJs), "text/javascript") - ); - } - - return Task.FromResult( - NotFound() - ); - } - - [HttpGet("bundle/css")] - public Task GetCss() - { - if (BundleService.BundledFinished) - { - return Task.FromResult( - File(Encoding.ASCII.GetBytes(BundleService.BundledCss), "text/css") - ); - } - - return Task.FromResult( - NotFound() - ); - } } \ No newline at end of file diff --git a/Moonlight/App/Services/Sessions/BundleService.cs b/Moonlight/App/Services/Sessions/BundleService.cs deleted file mode 100644 index 2b0cfbfc..00000000 --- a/Moonlight/App/Services/Sessions/BundleService.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Logging.Net; - -namespace Moonlight.App.Services.Sessions; - -public class BundleService -{ - public BundleService(ConfigService configService) - { - var url = configService - .GetSection("Moonlight") - .GetValue("AppUrl"); - - #region JS - - JsFiles = new(); - - JsFiles.AddRange(new[] - { - url + "/_framework/blazor.server.js", - url + "/assets/plugins/global/plugins.bundle.js", - url + "/_content/XtermBlazor/XtermBlazor.min.js", - url + "/_content/BlazorTable/BlazorTable.min.js", - url + "/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js", - url + "/_content/Blazor.ContextMenu/blazorContextMenu.min.js", - "https://www.google.com/recaptcha/api.js", - "https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.min.js", - "https://cdn.jsdelivr.net/npm/xterm-addon-search@0.8.2/lib/xterm-addon-search.min.js", - "https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.5.0/lib/xterm-addon-web-links.min.js", - url + "/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js", - "require.config({ paths: { 'vs': '/_content/BlazorMonaco/lib/monaco-editor/min/vs' } });", - url + "/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js", - url + "/_content/BlazorMonaco/jsInterop.js", - url + "/assets/js/scripts.bundle.js", - url + "/assets/js/moonlight.js", - "moonlight.loading.registerXterm();", - url + "/_content/Blazor-ApexCharts/js/apex-charts.min.js", - url + "/_content/Blazor-ApexCharts/js/blazor-apex-charts.js" - }); - - #endregion - - #region CSS - - CssFiles = new(); - - CssFiles.AddRange(new[] - { - "https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700", - url + "/assets/css/style.bundle.css", - url + "/assets/css/flashbang.css", - url + "/assets/css/snow.css", - url + "/assets/css/utils.css", - url + "/assets/css/blazor.css", - url + "/_content/XtermBlazor/XtermBlazor.css", - url + "/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.css", - url + "/_content/Blazor.ContextMenu/blazorContextMenu.min.css", - url + "/assets/plugins/global/plugins.bundle.css" - }); - - #endregion - - CacheId = Guid.NewGuid().ToString(); - - Task.Run(Bundle); - } - - // Javascript - public string BundledJs { get; private set; } - public readonly List JsFiles; - - // CSS - public string BundledCss { get; private set; } - public readonly List CssFiles; - - // States - public string CacheId { get; private set; } - public bool BundledFinished { get; set; } = false; - private bool IsBundling { get; set; } = false; - - private async Task Bundle() - { - if (!IsBundling) - IsBundling = true; - - Logger.Info("Bundling js and css files"); - - BundledJs = ""; - BundledCss = ""; - - BundledJs = await BundleFiles( - JsFiles - ); - - BundledCss = await BundleFiles( - CssFiles - ); - - Logger.Info("Successfully bundled"); - BundledFinished = true; - } - - private async Task BundleFiles(IEnumerable items) - { - var bundled = ""; - - using HttpClient client = new HttpClient(); - foreach (string item in items) - { - // Item is a url, fetch it - if (item.StartsWith("http")) - { - try - { - var jsCode = await client.GetStringAsync(item); - bundled += jsCode + "\n"; - } - catch (Exception e) - { - Logger.Warn($"Error fetching '{item}' while bundling"); - Logger.Warn(e); - } - } - else // If not, it is probably a manual addition, so add it - bundled += item + "\n"; - } - - return bundled; - } -} \ No newline at end of file diff --git a/Moonlight/Pages/_Layout.cshtml b/Moonlight/Pages/_Layout.cshtml index 975533a9..73ba593c 100644 --- a/Moonlight/Pages/_Layout.cshtml +++ b/Moonlight/Pages/_Layout.cshtml @@ -7,7 +7,6 @@ @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @inject ConfigService ConfigService -@inject BundleService BundleService @inject LoadingMessageRepository LoadingMessageRepository @{ @@ -38,29 +37,20 @@ - @*This import is not in the bundle because the files it references are linked relative to the current lath*@ + + + + + + - - @if (BundleService.BundledFinished) - { - - } - else - { - foreach (var cssFile in BundleService.CssFiles) - { - if (cssFile.StartsWith("http")) - { - - } - else - { - - } - } - } + + + + + + + @@ -106,24 +96,33 @@
-@if (BundleService.BundledFinished) -{ - -} -else -{ - foreach (var jsFile in BundleService.JsFiles) - { - if (jsFile.StartsWith("http")) - { - - } - else - { - @Html.Raw("") - } - } -} + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 70466a08..a3795997 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -128,7 +128,6 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); From bfa1a09aab38b27229536529409c736f5cdc614d Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Mon, 5 Jun 2023 21:47:33 +0200 Subject: [PATCH 12/15] Improved cpu usage calculation --- .../Shared/Components/ServerControl/ServerNavigation.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor b/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor index 912b3c3e..aff316a3 100644 --- a/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor +++ b/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor @@ -93,7 +93,7 @@
Cpu: - @(Math.Round(Console.Resource.CpuAbsolute, 2))% + @(Math.Round(Console.Resource.CpuAbsolute / (CurrentServer.Cpu / 100f), 2))%
Memory: From 15d8f49ce9fa354ae5d8f0752e3e6624d52cf33b Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Mon, 5 Jun 2023 22:01:21 +0200 Subject: [PATCH 13/15] Added error handler for folder download --- .../Shared/Components/FileManagerPartials/FileManager.razor | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor b/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor index cedca894..5925c9d2 100644 --- a/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor +++ b/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor @@ -185,6 +185,8 @@ else await ToastService.Info(SmartTranslateService.Translate("Starting download")); } } + else + await ToastService.Error(SmartTranslateService.Translate("You are not able to download folders using the moonlight file manager")); } }); From 23644eb93f3841e5e13e6149fb10e81ae818eebf Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Wed, 7 Jun 2023 02:23:30 +0200 Subject: [PATCH 14/15] Added smart deploy node override option --- Moonlight/App/Services/SmartDeployService.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Moonlight/App/Services/SmartDeployService.cs b/Moonlight/App/Services/SmartDeployService.cs index 38d013a0..88f6ff84 100644 --- a/Moonlight/App/Services/SmartDeployService.cs +++ b/Moonlight/App/Services/SmartDeployService.cs @@ -9,21 +9,36 @@ public class SmartDeployService private readonly Repository CloudPanelRepository; private readonly WebSpaceService WebSpaceService; private readonly NodeService NodeService; + private readonly ConfigService ConfigService; public SmartDeployService( NodeRepository nodeRepository, NodeService nodeService, WebSpaceService webSpaceService, - Repository cloudPanelRepository) + Repository cloudPanelRepository, + ConfigService configService) { NodeRepository = nodeRepository; NodeService = nodeService; WebSpaceService = webSpaceService; CloudPanelRepository = cloudPanelRepository; + ConfigService = configService; } public async Task GetNode() { + var config = ConfigService + .GetSection("Moonlight") + .GetSection("SmartDeploy") + .GetSection("Server"); + + if (config.GetValue("EnableOverride")) + { + var nodeId = config.GetValue("OverrideNode"); + + return NodeRepository.Get().FirstOrDefault(x => x.Id == nodeId); + } + var data = new Dictionary(); foreach (var node in NodeRepository.Get().ToArray()) From 4241debc3b9d9146e4b6f0b0e625a529486bd1ee Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Wed, 7 Jun 2023 02:38:21 +0200 Subject: [PATCH 15/15] Improved user experience for enabling and disabling join2start --- .../Settings/Join2StartSetting.razor | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Moonlight/Shared/Components/ServerControl/Settings/Join2StartSetting.razor b/Moonlight/Shared/Components/ServerControl/Settings/Join2StartSetting.razor index 54254ed4..21c0df49 100644 --- a/Moonlight/Shared/Components/ServerControl/Settings/Join2StartSetting.razor +++ b/Moonlight/Shared/Components/ServerControl/Settings/Join2StartSetting.razor @@ -5,9 +5,11 @@ @using Moonlight.App.Repositories @using Moonlight.App.Repositories.Servers @using Logging.Net +@using Moonlight.App.ApiClients.Wings @using Moonlight.App.Database.Entities @inject ServerRepository ServerRepository +@inject ServerService ServerService @inject SmartTranslateService TranslationService
@@ -28,7 +30,8 @@ OnClick="Save" Text="@(TranslationService.Translate("Change"))" WorkingText="@(TranslationService.Translate("Changing"))" - CssClasses="btn-primary"> + CssClasses="btn-primary"> + @@ -55,9 +58,23 @@ private async Task Save() { CurrentServer.Variables.First(x => x.Key == "J2S").Value = Value ? "1" : "0"; - + ServerRepository.Update(CurrentServer); - + + var details = await ServerService.GetDetails(CurrentServer); + + // For better user experience, we start the j2s server right away when the user enables j2s + if (details.State == "offline") + { + await ServerService.SetPowerState(CurrentServer, PowerSignal.Start); + } + + // For better user experience, we kill the j2s server right away when the user disables j2s and the server is starting + if (details.State == "starting") + { + await ServerService.SetPowerState(CurrentServer, PowerSignal.Kill); + } + await Loader.Reload(); } } \ No newline at end of file