Merge pull request #440 from Moonlight-Panel/v2_ChangeArchitecture_AddDiagnose
Added diagnose system
This commit is contained in:
33
Moonlight.ApiServer/Extensions/ZipArchiveExtensions.cs
Normal file
33
Moonlight.ApiServer/Extensions/ZipArchiveExtensions.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Extensions;
|
||||||
|
|
||||||
|
public static class ZipArchiveExtensions
|
||||||
|
{
|
||||||
|
public static async Task AddBinary(this ZipArchive archive, string name, byte[] bytes)
|
||||||
|
{
|
||||||
|
var entry = archive.CreateEntry(name);
|
||||||
|
await using var dataStream = entry.Open();
|
||||||
|
|
||||||
|
await dataStream.WriteAsync(bytes);
|
||||||
|
await dataStream.FlushAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task AddText(this ZipArchive archive, string name, string content)
|
||||||
|
{
|
||||||
|
var data = Encoding.UTF8.GetBytes(content);
|
||||||
|
await archive.AddBinary(name, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task AddFile(this ZipArchive archive, string name, string path)
|
||||||
|
{
|
||||||
|
var fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
|
||||||
|
var entry = archive.CreateEntry(name);
|
||||||
|
await using var dataStream = entry.Open();
|
||||||
|
|
||||||
|
await fs.CopyToAsync(dataStream);
|
||||||
|
await dataStream.FlushAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MoonCore.Attributes;
|
||||||
|
using Moonlight.ApiServer.Services;
|
||||||
|
using Moonlight.Shared.Http.Requests.Admin.Sys;
|
||||||
|
using Moonlight.Shared.Http.Responses.Admin.Sys;
|
||||||
|
using Moonlight.Shared.Misc;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/system/diagnose")]
|
||||||
|
[RequirePermission("admin.system.diagnose")]
|
||||||
|
public class DiagnoseController : Controller
|
||||||
|
{
|
||||||
|
private readonly DiagnoseService DiagnoseService;
|
||||||
|
|
||||||
|
public DiagnoseController(DiagnoseService diagnoseService)
|
||||||
|
{
|
||||||
|
DiagnoseService = diagnoseService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task Diagnose([FromBody] GenerateDiagnoseRequest request)
|
||||||
|
{
|
||||||
|
var stream = await DiagnoseService.GenerateDiagnose(request.Providers);
|
||||||
|
|
||||||
|
await Results.Stream(
|
||||||
|
stream,
|
||||||
|
contentType: "application/zip",
|
||||||
|
fileDownloadName: "diagnose.zip"
|
||||||
|
)
|
||||||
|
.ExecuteAsync(HttpContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("providers")]
|
||||||
|
public async Task<DiagnoseProvideResponse[]> GetProviders()
|
||||||
|
{
|
||||||
|
return await DiagnoseService.GetProviders();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using MoonCore.Attributes;
|
using MoonCore.Attributes;
|
||||||
|
using Moonlight.ApiServer.Interfaces;
|
||||||
using Moonlight.ApiServer.Services;
|
using Moonlight.ApiServer.Services;
|
||||||
using Moonlight.Shared.Http.Responses.Admin.Sys;
|
using Moonlight.Shared.Http.Responses.Admin.Sys;
|
||||||
|
|
||||||
@@ -10,10 +11,13 @@ namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys;
|
|||||||
public class SystemController : Controller
|
public class SystemController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationService ApplicationService;
|
private readonly ApplicationService ApplicationService;
|
||||||
|
private readonly IEnumerable<IDiagnoseProvider> DiagnoseProviders;
|
||||||
|
|
||||||
public SystemController(ApplicationService applicationService)
|
|
||||||
|
public SystemController(ApplicationService applicationService, IEnumerable<IDiagnoseProvider> diagnoseProviders)
|
||||||
{
|
{
|
||||||
ApplicationService = applicationService;
|
ApplicationService = applicationService;
|
||||||
|
DiagnoseProviders = diagnoseProviders;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Moonlight.ApiServer.Configuration;
|
||||||
|
using Moonlight.ApiServer.Extensions;
|
||||||
|
using Moonlight.ApiServer.Interfaces;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Implementations.Diagnose;
|
||||||
|
|
||||||
|
public class CoreConfigDiagnoseProvider : IDiagnoseProvider
|
||||||
|
{
|
||||||
|
private readonly AppConfiguration Config;
|
||||||
|
|
||||||
|
public CoreConfigDiagnoseProvider(AppConfiguration config)
|
||||||
|
{
|
||||||
|
Config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CheckForNullOrEmpty(string? content)
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(content)
|
||||||
|
? "ISEMPTY"
|
||||||
|
: "ISNOTEMPTY";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ModifyZipArchive(ZipArchive archive)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(Config);
|
||||||
|
var config = JsonSerializer.Deserialize<AppConfiguration>(json);
|
||||||
|
|
||||||
|
if (config == null)
|
||||||
|
{
|
||||||
|
await archive.AddText("core/config.txt", "Could not fetch config.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Database.Password = CheckForNullOrEmpty(config.Database.Password);
|
||||||
|
|
||||||
|
config.Authentication.OAuth2.ClientSecret = CheckForNullOrEmpty(config.Authentication.OAuth2.ClientSecret);
|
||||||
|
|
||||||
|
config.Authentication.OAuth2.Secret = CheckForNullOrEmpty(config.Authentication.OAuth2.Secret);
|
||||||
|
|
||||||
|
config.Authentication.Secret = CheckForNullOrEmpty(config.Authentication.Secret);
|
||||||
|
|
||||||
|
config.Authentication.OAuth2.ClientId = CheckForNullOrEmpty(config.Authentication.OAuth2.ClientId);
|
||||||
|
|
||||||
|
await archive.AddText(
|
||||||
|
"core/config.txt",
|
||||||
|
JsonSerializer.Serialize(
|
||||||
|
config,
|
||||||
|
new JsonSerializerOptions()
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
using Moonlight.ApiServer.Extensions;
|
||||||
|
using Moonlight.ApiServer.Interfaces;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Implementations.Diagnose;
|
||||||
|
|
||||||
|
public class LogsDiagnoseProvider : IDiagnoseProvider
|
||||||
|
{
|
||||||
|
public async Task ModifyZipArchive(ZipArchive archive)
|
||||||
|
{
|
||||||
|
var path = Path.Combine("storage", "logs", "latest.log");
|
||||||
|
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
await archive.AddText("logs.txt", "Logs file latest.log has not been found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var logsContent = await File.ReadAllTextAsync(path);
|
||||||
|
await archive.AddText("logs.txt", logsContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Moonlight.ApiServer.Configuration;
|
using Moonlight.ApiServer.Configuration;
|
||||||
using Moonlight.ApiServer.Database;
|
using Moonlight.ApiServer.Database;
|
||||||
|
using Moonlight.ApiServer.Implementations.Diagnose;
|
||||||
|
using Moonlight.ApiServer.Interfaces;
|
||||||
using Moonlight.ApiServer.Plugins;
|
using Moonlight.ApiServer.Plugins;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Implementations.Startup;
|
namespace Moonlight.ApiServer.Implementations.Startup;
|
||||||
@@ -46,6 +48,13 @@ public class CoreStartup : IPluginStartup
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Diagnose
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IDiagnoseProvider, CoreConfigDiagnoseProvider>();
|
||||||
|
builder.Services.AddSingleton<IDiagnoseProvider, LogsDiagnoseProvider>();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
Moonlight.ApiServer/Interfaces/IDiagnoseProvider.cs
Normal file
8
Moonlight.ApiServer/Interfaces/IDiagnoseProvider.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Interfaces;
|
||||||
|
|
||||||
|
public interface IDiagnoseProvider
|
||||||
|
{
|
||||||
|
public Task ModifyZipArchive(ZipArchive archive);
|
||||||
|
}
|
||||||
95
Moonlight.ApiServer/Services/DiagnoseService.cs
Normal file
95
Moonlight.ApiServer/Services/DiagnoseService.cs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
using Moonlight.ApiServer.Interfaces;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using MoonCore.Attributes;
|
||||||
|
using MoonCore.Exceptions;
|
||||||
|
using Moonlight.Shared.Http.Responses.Admin.Sys;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Services;
|
||||||
|
|
||||||
|
[Scoped]
|
||||||
|
public class DiagnoseService
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<IDiagnoseProvider> DiagnoseProviders;
|
||||||
|
private readonly ILogger<DiagnoseService> Logger;
|
||||||
|
|
||||||
|
public DiagnoseService(
|
||||||
|
IEnumerable<IDiagnoseProvider> diagnoseProviders,
|
||||||
|
ILogger<DiagnoseService> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
DiagnoseProviders = diagnoseProviders;
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<DiagnoseProvideResponse[]> GetProviders()
|
||||||
|
{
|
||||||
|
var availableProviders = new List<DiagnoseProvideResponse>();
|
||||||
|
|
||||||
|
foreach (var diagnoseProvider in DiagnoseProviders)
|
||||||
|
{
|
||||||
|
var name = diagnoseProvider.GetType().Name;
|
||||||
|
|
||||||
|
var type = diagnoseProvider.GetType().FullName;
|
||||||
|
|
||||||
|
// The type name is null if the type is a generic type, unlikely, but still could happen
|
||||||
|
if (type == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
availableProviders.Add(new DiagnoseProvideResponse()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Type = type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(
|
||||||
|
availableProviders.ToArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MemoryStream> GenerateDiagnose(string[] requestedProviders)
|
||||||
|
{
|
||||||
|
IDiagnoseProvider[] providers;
|
||||||
|
|
||||||
|
if (requestedProviders.Length == 0)
|
||||||
|
providers = DiagnoseProviders.ToArray();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var foundProviders = new List<IDiagnoseProvider>();
|
||||||
|
|
||||||
|
foreach (var requestedProvider in requestedProviders)
|
||||||
|
{
|
||||||
|
var provider = DiagnoseProviders.FirstOrDefault(x => x.GetType().FullName == requestedProvider);
|
||||||
|
|
||||||
|
if (provider == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foundProviders.Add(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
providers = foundProviders.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var outputStream = new MemoryStream();
|
||||||
|
var zipArchive = new ZipArchive(outputStream, ZipArchiveMode.Create, leaveOpen: true);
|
||||||
|
|
||||||
|
foreach (var provider in providers)
|
||||||
|
{
|
||||||
|
await provider.ModifyZipArchive(zipArchive);
|
||||||
|
}
|
||||||
|
|
||||||
|
zipArchive.Dispose();
|
||||||
|
|
||||||
|
outputStream.Position = 0;
|
||||||
|
return outputStream;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,6 +110,7 @@ public class Startup
|
|||||||
private Task CreateStorage()
|
private Task CreateStorage()
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory("storage");
|
Directory.CreateDirectory("storage");
|
||||||
|
Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
|
||||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins"));
|
Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins"));
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -338,7 +339,10 @@ public class Startup
|
|||||||
{
|
{
|
||||||
configuration.Console.Enable = true;
|
configuration.Console.Enable = true;
|
||||||
configuration.Console.EnableAnsiMode = true;
|
configuration.Console.EnableAnsiMode = true;
|
||||||
configuration.FileLogging.Enable = false;
|
configuration.FileLogging.Enable = true;
|
||||||
|
configuration.FileLogging.Path = PathBuilder.File("storage", "logs", "latest.log");
|
||||||
|
configuration.FileLogging.EnableLogRotation = true;
|
||||||
|
configuration.FileLogging.RotateLogNameTemplate = PathBuilder.File("storage", "logs", "apiserver.{0}.log");
|
||||||
});
|
});
|
||||||
|
|
||||||
LoggerFactory = new LoggerFactory();
|
LoggerFactory = new LoggerFactory();
|
||||||
|
|||||||
120
Moonlight.Client/UI/Views/Admin/Sys/Diagnose.razor
Normal file
120
Moonlight.Client/UI/Views/Admin/Sys/Diagnose.razor
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
@page "/admin/system/diagnose"
|
||||||
|
|
||||||
|
@using MoonCore.Attributes
|
||||||
|
@using MoonCore.Helpers
|
||||||
|
@using Moonlight.Shared.Http.Requests.Admin.Sys
|
||||||
|
@using Moonlight.Shared.Http.Responses.Admin.Sys
|
||||||
|
|
||||||
|
@attribute [RequirePermission("admin.system.diagnose")]
|
||||||
|
|
||||||
|
@inject HttpApiClient ApiClient
|
||||||
|
@inject DownloadService DownloadService
|
||||||
|
|
||||||
|
<div class="mb-5">
|
||||||
|
<NavTabs Index="5" Names="UiConstants.AdminNavNames" Links="UiConstants.AdminNavLinks"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2">
|
||||||
|
<div class="col-span-2 md:col-span-1 card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">Diagnose</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>
|
||||||
|
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
|
||||||
|
downloadable zip file.
|
||||||
|
The report includes useful information about your system, plugins, and environment, making it easier to
|
||||||
|
identify problems or share with support.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<WButton OnClick="GenerateDiagnose" CssClasses="btn btn-primary my-5">Generate diagnose</WButton>
|
||||||
|
|
||||||
|
<div class="text-sm">
|
||||||
|
<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")">
|
||||||
|
<LazyLoader Load="Load">
|
||||||
|
<div class="mb-2 py-2 border-b border-gray-700 flex items-center gap-3">
|
||||||
|
<input id="selectall_checkbox" @bind="SelectAll" type="checkbox" class="form-checkbox">
|
||||||
|
<label for="selectall_checkbox">Select all</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (var item in AvailableProviders)
|
||||||
|
{
|
||||||
|
<div class="mt-1 flex gap-3 items-center">
|
||||||
|
<input class="form-checkbox" type="checkbox" id="@(item.Key.Type + "_checkbox")"
|
||||||
|
@bind="@AvailableProviders[item.Key]"/>
|
||||||
|
<label
|
||||||
|
for="@(item.Key.Type + "_checkbox")">@Formatter.ConvertCamelCaseToSpaces(item.Key.Name)</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private bool DropdownOpen = false;
|
||||||
|
private Dictionary<DiagnoseProvideResponse, bool> AvailableProviders;
|
||||||
|
|
||||||
|
private bool SelectAll
|
||||||
|
{
|
||||||
|
get => AvailableProviders.Values.All(v => v);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
foreach (var k in AvailableProviders.Keys)
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,12 @@ public static class UiConstants
|
|||||||
{
|
{
|
||||||
public static readonly string[] AdminNavNames =
|
public static readonly string[] AdminNavNames =
|
||||||
[
|
[
|
||||||
"Overview", "Theme", "Files", "Hangfire", "Advanced"
|
"Overview", "Theme", "Files", "Hangfire", "Advanced", "Diagnose"
|
||||||
];
|
];
|
||||||
|
|
||||||
public static readonly string[] AdminNavLinks =
|
public static readonly string[] AdminNavLinks =
|
||||||
[
|
[
|
||||||
"/admin/system", "/admin/system/theme", "/admin/system/files", "/admin/system/hangfire",
|
"/admin/system", "/admin/system/theme", "/admin/system/files", "/admin/system/hangfire",
|
||||||
"/admin/system/advanced"
|
"/admin/system/advanced", "/admin/system/diagnose"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Moonlight.Shared.Http.Requests.Admin.Sys;
|
||||||
|
|
||||||
|
public class GenerateDiagnoseRequest
|
||||||
|
{
|
||||||
|
public string[] Providers { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.Shared.Http.Responses.Admin.Sys;
|
||||||
|
|
||||||
|
public class DiagnoseProvideResponse
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user