Cleaned up diagnose system. Fixed smaller inconsistencies

This commit is contained in:
2025-05-17 19:38:36 +02:00
parent f87e4a0800
commit 255bfba9e3
7 changed files with 153 additions and 114 deletions

View File

@@ -10,6 +10,7 @@ namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys;
[ApiController] [ApiController]
[Route("api/admin/system/diagnose")] [Route("api/admin/system/diagnose")]
[RequirePermission("admin.system.diagnose")]
public class DiagnoseController : Controller public class DiagnoseController : Controller
{ {
private readonly DiagnoseService DiagnoseService; private readonly DiagnoseService DiagnoseService;
@@ -20,20 +21,21 @@ public class DiagnoseController : Controller
} }
[HttpPost] [HttpPost]
[RequirePermission("admin.system.diagnose")] public async Task Diagnose([FromBody] GenerateDiagnoseRequest request)
public async Task<IActionResult> Diagnose([FromBody] string[]? requestedDiagnoseProviders = null)
{ {
var stream = new MemoryStream(); var stream = await DiagnoseService.GenerateDiagnose(request.Providers);
await DiagnoseService.GenerateDiagnose(stream, requestedDiagnoseProviders); await Results.Stream(
stream,
return File(stream, "application/zip", "diagnose.zip"); contentType: "application/zip",
fileDownloadName: "diagnose.zip"
)
.ExecuteAsync(HttpContext);
} }
[HttpGet("available")] [HttpGet("providers")]
[RequirePermission("admin.system.diagnose")] public async Task<DiagnoseProvideResponse[]> GetProviders()
public async Task<DiagnoseProvideResponse[]> GetAvailable()
{ {
return await DiagnoseService.GetAvailable(); return await DiagnoseService.GetProviders();
} }
} }

View File

@@ -24,28 +24,34 @@ public class CoreConfigDiagnoseProvider : IDiagnoseProvider
public async Task ModifyZipArchive(ZipArchive archive) public async Task ModifyZipArchive(ZipArchive archive)
{ {
var json = JsonSerializer.Serialize(Config); var json = JsonSerializer.Serialize(Config);
var config = JsonSerializer.Deserialize<AppConfiguration>(json); var config = JsonSerializer.Deserialize<AppConfiguration>(json);
if (config == null) if (config == null)
{ {
await archive.AddText("core/config.txt","Could not fetch config."); await archive.AddText("core/config.txt", "Could not fetch config.");
return; return;
} }
config.Database.Password = CheckForNullOrEmpty(config.Database.Password); config.Database.Password = CheckForNullOrEmpty(config.Database.Password);
config.Authentication.OAuth2.ClientSecret = CheckForNullOrEmpty(config.Authentication.OAuth2.ClientSecret); config.Authentication.OAuth2.ClientSecret = CheckForNullOrEmpty(config.Authentication.OAuth2.ClientSecret);
config.Authentication.OAuth2.Secret = CheckForNullOrEmpty(config.Authentication.OAuth2.Secret); config.Authentication.OAuth2.Secret = CheckForNullOrEmpty(config.Authentication.OAuth2.Secret);
config.Authentication.Secret = CheckForNullOrEmpty(config.Authentication.Secret); config.Authentication.Secret = CheckForNullOrEmpty(config.Authentication.Secret);
config.Authentication.OAuth2.ClientId = CheckForNullOrEmpty(config.Authentication.OAuth2.ClientId); config.Authentication.OAuth2.ClientId = CheckForNullOrEmpty(config.Authentication.OAuth2.ClientId);
await archive.AddText("core/config.txt", await archive.AddText(
JsonSerializer.Serialize(config, new JsonSerializerOptions() { WriteIndented = true })); "core/config.txt",
JsonSerializer.Serialize(
config,
new JsonSerializerOptions()
{
WriteIndented = true
}
)
);
} }
} }

View File

@@ -1,6 +1,4 @@
using System.IO.Compression; using System.IO.Compression;
using System.Text;
using MoonCore.Helpers;
using Moonlight.ApiServer.Extensions; using Moonlight.ApiServer.Extensions;
using Moonlight.ApiServer.Interfaces; using Moonlight.ApiServer.Interfaces;
@@ -10,15 +8,15 @@ public class LogsDiagnoseProvider : IDiagnoseProvider
{ {
public async Task ModifyZipArchive(ZipArchive archive) public async Task ModifyZipArchive(ZipArchive archive)
{ {
var path = Path.Combine("storage", "logs", "latest.log");
var logs = await File.ReadAllTextAsync(PathBuilder.File("storage", "logs", "latest.log")); if (!File.Exists(path))
{
if (string.IsNullOrEmpty(logs)) await archive.AddText("logs.txt", "Logs file latest.log has not been found");
{ return;
await archive.AddText("logs.txt", "Could not read the logs"); }
return;
} var logsContent = await File.ReadAllTextAsync(path);
await archive.AddText("logs.txt", logsContent);
await archive.AddText("logs.txt", logs);
} }
} }

View File

@@ -1,6 +1,7 @@
using Moonlight.ApiServer.Interfaces; using Moonlight.ApiServer.Interfaces;
using System.IO.Compression; using System.IO.Compression;
using MoonCore.Attributes; using MoonCore.Attributes;
using MoonCore.Exceptions;
using Moonlight.Shared.Http.Responses.Admin.Sys; using Moonlight.Shared.Http.Responses.Admin.Sys;
namespace Moonlight.ApiServer.Services; namespace Moonlight.ApiServer.Services;
@@ -9,13 +10,18 @@ namespace Moonlight.ApiServer.Services;
public class DiagnoseService public class DiagnoseService
{ {
private readonly IEnumerable<IDiagnoseProvider> DiagnoseProviders; private readonly IEnumerable<IDiagnoseProvider> DiagnoseProviders;
private readonly ILogger<DiagnoseService> Logger;
public DiagnoseService(IEnumerable<IDiagnoseProvider> diagnoseProviders) public DiagnoseService(
IEnumerable<IDiagnoseProvider> diagnoseProviders,
ILogger<DiagnoseService> logger
)
{ {
DiagnoseProviders = diagnoseProviders; DiagnoseProviders = diagnoseProviders;
Logger = logger;
} }
public async Task<DiagnoseProvideResponse[]> GetAvailable() public Task<DiagnoseProvideResponse[]> GetProviders()
{ {
var availableProviders = new List<DiagnoseProvideResponse>(); var availableProviders = new List<DiagnoseProvideResponse>();
@@ -26,55 +32,64 @@ public class DiagnoseService
var type = diagnoseProvider.GetType().FullName; var type = diagnoseProvider.GetType().FullName;
// The type name is null if the type is a generic type, unlikely, but still could happen // The type name is null if the type is a generic type, unlikely, but still could happen
if (type != null) if (type == null)
availableProviders.Add(new DiagnoseProvideResponse() continue;
{
Name = name, availableProviders.Add(new DiagnoseProvideResponse()
Type = type {
}); Name = name,
Type = type
});
} }
return availableProviders.ToArray(); return Task.FromResult(
availableProviders.ToArray()
);
} }
public async Task GenerateDiagnose(Stream outputStream, string[]? requestedProviders) public async Task<MemoryStream> GenerateDiagnose(string[] requestedProviders)
{ {
IDiagnoseProvider[] providers; IDiagnoseProvider[] providers;
if (requestedProviders != null && requestedProviders.Length > 0) if (requestedProviders.Length == 0)
providers = DiagnoseProviders.ToArray();
else
{ {
var requesteDiagnoseProviders = new List<IDiagnoseProvider>(); var foundProviders = new List<IDiagnoseProvider>();
foreach (var requestedProvider in requestedProviders) foreach (var requestedProvider in requestedProviders)
{ {
var provider = DiagnoseProviders.FirstOrDefault(x => x.GetType().FullName == requestedProvider); var provider = DiagnoseProviders.FirstOrDefault(x => x.GetType().FullName == requestedProvider);
if (provider != null) if (provider == null)
requesteDiagnoseProviders.Add(provider); continue;
foundProviders.Add(provider);
} }
providers = requesteDiagnoseProviders.ToArray(); providers = foundProviders.ToArray();
}
else
{
providers = DiagnoseProviders.ToArray();
} }
try try
{ {
using (var zip = new ZipArchive(outputStream, ZipArchiveMode.Create, leaveOpen: true)) var outputStream = new MemoryStream();
{ var zipArchive = new ZipArchive(outputStream, ZipArchiveMode.Create, leaveOpen: true);
foreach (var provider in providers)
{
await provider.ModifyZipArchive(zip);
}
}
outputStream.Seek(0, SeekOrigin.Begin); foreach (var provider in providers)
{
await provider.ModifyZipArchive(zipArchive);
}
zipArchive.Dispose();
outputStream.Position = 0;
return outputStream;
} }
catch (Exception ex) catch (Exception e)
{ {
throw new Exception($"An unknown error occured while building the Diagnose: {ex.Message}"); Logger.LogError("An unhandled error occured while generated the diagnose file: {e}", e);
throw new HttpApiException("An unhandled error occured while generating the diagnose file", 500);
} }
} }
} }

View File

@@ -2,8 +2,8 @@
@using MoonCore.Attributes @using MoonCore.Attributes
@using MoonCore.Helpers @using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Sys
@using Moonlight.Shared.Http.Responses.Admin.Sys @using Moonlight.Shared.Http.Responses.Admin.Sys
@using Moonlight.Shared.Misc
@attribute [RequirePermission("admin.system.diagnose")] @attribute [RequirePermission("admin.system.diagnose")]
@@ -11,7 +11,7 @@
@inject DownloadService DownloadService @inject DownloadService DownloadService
<div class="mb-5"> <div class="mb-5">
<NavTabs Index="5" Names="UiConstants.AdminNavNames" Links="UiConstants.AdminNavLinks" /> <NavTabs Index="5" Names="UiConstants.AdminNavNames" Links="UiConstants.AdminNavLinks"/>
</div> </div>
<div class="grid grid-cols-2"> <div class="grid grid-cols-2">
@@ -22,27 +22,43 @@
<div class="card-body"> <div class="card-body">
<p> <p>
If you're experiencing issues or need help via our Discord, you're in the right place here! If you're experiencing issues or need help via our Discord, you're in the right place here!
By pressing the button below, Moonlight will run all available diagnostic checks and package the results into a By pressing the button below, Moonlight will run all available diagnostic checks and package the results
into a
downloadable zip file. downloadable zip file.
The report includes useful information about your system, plugins, and environment, making it easier to identify problems or share with support. The report includes useful information about your system, plugins, and environment, making it easier to
identify problems or share with support.
</p> </p>
<WButton OnClick="GenerateDiagnose" CssClasses="btn btn-primary my-5">Generate diagnose</WButton> <WButton OnClick="GenerateDiagnose" CssClasses="btn btn-primary my-5">Generate diagnose</WButton>
<div> <div class="text-sm">
<a class="text-primary cursor-pointer" @onclick:preventDefault @onclick="ToggleDropDown">Advanced <i class="icon-chevron-@(DropdownOpen ? "up" : "down")"></i></a> <a class="text-primary cursor-pointer flex items-center" @onclick:preventDefault
@onclick="ToggleDropDown">
<span class="me-1.5">Advanced</span>
@if (DropdownOpen)
{
<i class="icon-chevron-up"></i>
}
else
{
<i class="icon-chevron-down"></i>
}
</a>
<div class="@(DropdownOpen ? "" : "hidden")"> <div class="@(DropdownOpen ? "" : "hidden")">
<LazyLoader Load="Load"> <LazyLoader Load="Load">
<div class="mb-2 pb-2 pt-4 border-b border-white/5 flex items-center gap-3"> <div class="mb-2 py-2 border-b border-gray-700 flex items-center gap-3">
<input class="rounded" @bind="SelectAll" type="checkbox" id="selectall_checkbox"/> <input id="selectall_checkbox" @bind="SelectAll" type="checkbox" class="form-checkbox">
<label for="selectall_checkbox">Select all</label> <label for="selectall_checkbox">Select all</label>
</div> </div>
@foreach (var item in AvailableProviders) @foreach (var item in AvailableProviders)
{ {
<div class="mt-1 flex gap-3 items-center"> <div class="mt-1 flex gap-3 items-center">
<input class="rounded" type="checkbox" id="@(item.Key.Type + "_checkbox")" @bind="@AvailableProviders[item.Key]" /> <input class="form-checkbox" type="checkbox" id="@(item.Key.Type + "_checkbox")"
<label for="@(item.Key.Type + "_checkbox")">@Formatter.ConvertCamelCaseToSpaces(item.Key.Name)</label> @bind="@AvailableProviders[item.Key]"/>
<label
for="@(item.Key.Type + "_checkbox")">@Formatter.ConvertCamelCaseToSpaces(item.Key.Name)</label>
</div> </div>
} }
</LazyLoader> </LazyLoader>
@@ -54,54 +70,51 @@
@code @code
{ {
private async Task GenerateDiagnose(WButton _)
{
string[] payload = [];
if (!SelectAll)
{
// filter the providers which have been selected if not all providers have been selected
payload = AvailableProviders
.Where(x => x.Value)
.Select(x => x.Key)
.Select(x => x.Type)
.ToArray();
}
var stream = await ApiClient.PostStream("api/admin/system/diagnose", payload);
await DownloadService.DownloadStream("diagnose.zip", stream);
}
private bool DropdownOpen = false; private bool DropdownOpen = false;
private bool AllSelected = true;
private Dictionary<DiagnoseProvideResponse, bool> AvailableProviders; private Dictionary<DiagnoseProvideResponse, bool> AvailableProviders;
private async Task ToggleDropDown()
{
DropdownOpen = !DropdownOpen;
await InvokeAsync(StateHasChanged);
}
private async Task Load(LazyLoader arg)
{
AvailableProviders = (await ApiClient.GetJson<DiagnoseProvideResponse[]>("api/admin/system/diagnose/available"))
.ToDictionary(x => x, _ => true);
}
private bool SelectAll private bool SelectAll
{ {
get => AvailableProviders.Values.All(v => v); get => AvailableProviders.Values.All(v => v);
set set
{ {
// flip every entry to the new value foreach (var k in AvailableProviders.Keys)
var keys = AvailableProviders.Keys.ToList();
foreach (var k in keys)
{
AvailableProviders[k] = value; AvailableProviders[k] = value;
}
} }
} }
private async Task Load(LazyLoader arg)
{
var providers = await ApiClient.GetJson<DiagnoseProvideResponse[]>(
"api/admin/system/diagnose/providers"
);
AvailableProviders = providers
.ToDictionary(x => x, _ => true);
}
private async Task GenerateDiagnose(WButton _)
{
var request = new GenerateDiagnoseRequest();
if (!SelectAll)
{
// filter the providers which have been selected if not all providers have been selected
request.Providers = AvailableProviders
.Where(x => x.Value)
.Select(x => x.Key.Type)
.ToArray();
}
var stream = await ApiClient.PostStream("api/admin/system/diagnose", request);
await DownloadService.DownloadStream("diagnose.zip", stream);
}
private async Task ToggleDropDown()
{
DropdownOpen = !DropdownOpen;
await InvokeAsync(StateHasChanged);
}
} }

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Shared.Http.Requests.Admin.Sys;
public class GenerateDiagnoseRequest
{
public string[] Providers { get; set; } = [];
}

View File

@@ -3,6 +3,5 @@ namespace Moonlight.Shared.Http.Responses.Admin.Sys;
public class DiagnoseProvideResponse public class DiagnoseProvideResponse
{ {
public string Name { get; set; } public string Name { get; set; }
public string Type { get; set; } public string Type { get; set; }
} }