Implemented basic plugin store and improved plugin system

This commit is contained in:
Marcel Baumgartner
2023-07-23 21:30:57 +02:00
parent 21bea974a9
commit 0658e55a78
14 changed files with 288 additions and 15 deletions

View File

@@ -0,0 +1,6 @@
namespace Moonlight.App.Models.Misc;
public class OfficialMoonlightPlugin
{
public string Name { get; set; }
}

View File

@@ -15,6 +15,13 @@ public static class Permissions
Name = "Admin Statistics", Name = "Admin Statistics",
Description = "View statistical information about the moonlight instance" Description = "View statistical information about the moonlight instance"
}; };
public static Permission AdminSysPlugins = new()
{
Index = 2,
Name = "Admin system plugins",
Description = "View and install plugins"
};
public static Permission AdminDomains = new() public static Permission AdminDomains = new()
{ {

View File

@@ -1,5 +1,4 @@
using Moonlight.App.Plugin.UI; using Moonlight.App.Plugin.UI.Servers;
using Moonlight.App.Plugin.UI.Servers;
using Moonlight.App.Plugin.UI.Webspaces; using Moonlight.App.Plugin.UI.Webspaces;
namespace Moonlight.App.Plugin; namespace Moonlight.App.Plugin;
@@ -12,4 +11,5 @@ public abstract class MoonlightPlugin
public Func<ServerPageContext, Task>? OnBuildServerPage { get; set; } public Func<ServerPageContext, Task>? OnBuildServerPage { get; set; }
public Func<WebspacePageContext, Task>? OnBuildWebspacePage { get; set; } public Func<WebspacePageContext, Task>? OnBuildWebspacePage { get; set; }
public Func<IServiceCollection, Task>? OnBuildServices { get; set; }
} }

View File

@@ -8,4 +8,5 @@ public class ServerPageContext
public List<ServerSetting> Settings { get; set; } = new(); public List<ServerSetting> Settings { get; set; } = new();
public Server Server { get; set; } public Server Server { get; set; }
public User User { get; set; } public User User { get; set; }
public string[] ImageTags { get; set; }
} }

View File

@@ -16,6 +16,7 @@ public class StorageService
Directory.CreateDirectory(PathBuilder.Dir("storage", "resources")); Directory.CreateDirectory(PathBuilder.Dir("storage", "resources"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "backups")); Directory.CreateDirectory(PathBuilder.Dir("storage", "backups"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "logs")); Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins"));
if(IsEmpty(PathBuilder.Dir("storage", "resources"))) if(IsEmpty(PathBuilder.Dir("storage", "resources")))
{ {

View File

@@ -46,7 +46,7 @@ public class MoonlightService
try try
{ {
var client = new GitHubClient(new ProductHeaderValue("Moonlight")); var client = new GitHubClient(new ProductHeaderValue("Moonlight-Panel"));
var pullRequests = await client.PullRequest.GetAllForRepository("Moonlight-Panel", "Moonlight", new PullRequestRequest var pullRequests = await client.PullRequest.GetAllForRepository("Moonlight-Panel", "Moonlight", new PullRequestRequest
{ {

View File

@@ -1,25 +1,57 @@
using System.Reflection; using System.Reflection;
using Moonlight.App.Database.Entities; using System.Runtime.Loader;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Plugin; using Moonlight.App.Plugin;
using Moonlight.App.Plugin.UI;
using Moonlight.App.Plugin.UI.Servers; using Moonlight.App.Plugin.UI.Servers;
using Moonlight.App.Plugin.UI.Webspaces; using Moonlight.App.Plugin.UI.Webspaces;
namespace Moonlight.App.Services; namespace Moonlight.App.Services.Plugins;
public class PluginService public class PluginService
{ {
public List<MoonlightPlugin> Plugins { get; set; } public List<MoonlightPlugin> Plugins { get; private set; }
public Dictionary<MoonlightPlugin, string> PluginFiles { get; private set; }
private AssemblyLoadContext LoadContext;
public PluginService() public PluginService()
{ {
LoadPlugins(); LoadContext = new(null, true);
ReloadPlugins().Wait();
} }
private void LoadPlugins() private Task UnloadPlugins()
{ {
Plugins = new(); Plugins = new();
PluginFiles = new();
if(LoadContext.Assemblies.Any())
LoadContext.Unload();
return Task.CompletedTask;
}
public async Task ReloadPlugins()
{
await UnloadPlugins();
// Try to update all plugins ending with .dll.cache
foreach (var pluginFile in Directory.EnumerateFiles(
PathBuilder.Dir(Directory.GetCurrentDirectory(), "storage", "plugins"))
.Where(x => x.EndsWith(".dll.cache")))
{
try
{
var realPath = pluginFile.Replace(".cache", "");
File.Copy(pluginFile, realPath, true);
File.Delete(pluginFile);
Logger.Info($"Updated plugin {realPath} on startup");
}
catch (Exception)
{
// ignored
}
}
var pluginType = typeof(MoonlightPlugin); var pluginType = typeof(MoonlightPlugin);
@@ -27,7 +59,7 @@ public class PluginService
PathBuilder.Dir(Directory.GetCurrentDirectory(), "storage", "plugins")) PathBuilder.Dir(Directory.GetCurrentDirectory(), "storage", "plugins"))
.Where(x => x.EndsWith(".dll"))) .Where(x => x.EndsWith(".dll")))
{ {
var assembly = Assembly.LoadFile(pluginFile); var assembly = LoadContext.LoadFromAssemblyPath(pluginFile);
foreach (var type in assembly.GetTypes()) foreach (var type in assembly.GetTypes())
{ {
@@ -38,6 +70,7 @@ public class PluginService
Logger.Info($"Loaded plugin '{plugin.Name}' ({plugin.Version}) by {plugin.Author}"); Logger.Info($"Loaded plugin '{plugin.Name}' ({plugin.Version}) by {plugin.Author}");
Plugins.Add(plugin); Plugins.Add(plugin);
PluginFiles.Add(plugin, pluginFile);
} }
} }
} }
@@ -66,4 +99,13 @@ public class PluginService
return context; return context;
} }
public async Task BuildServices(IServiceCollection serviceCollection)
{
foreach (var plugin in Plugins)
{
if (plugin.OnBuildServices != null)
await plugin.OnBuildServices.Invoke(serviceCollection);
}
}
} }

View File

@@ -0,0 +1,63 @@
using System.Text;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Octokit;
namespace Moonlight.App.Services.Plugins;
public class PluginStoreService
{
private readonly GitHubClient Client;
private readonly PluginService PluginService;
public PluginStoreService(PluginService pluginService)
{
PluginService = pluginService;
Client = new(new ProductHeaderValue("Moonlight-Panel"));
}
public async Task<OfficialMoonlightPlugin[]> GetPlugins()
{
var items = await Client.Repository.Content.GetAllContents("Moonlight-Panel", "OfficialPlugins");
if (items == null)
{
Logger.Fatal("Unable to read plugin repo contents");
return Array.Empty<OfficialMoonlightPlugin>();
}
return items
.Where(x => x.Type == ContentType.Dir)
.Select(x => new OfficialMoonlightPlugin()
{
Name = x.Name
})
.ToArray();
}
public async Task<string> GetPluginReadme(OfficialMoonlightPlugin plugin)
{
var rawReadme = await Client.Repository.Content
.GetRawContent("Moonlight-Panel", "OfficialPlugins", $"{plugin.Name}/README.md");
if (rawReadme == null)
return "Error";
return Encoding.UTF8.GetString(rawReadme);
}
public async Task InstallPlugin(OfficialMoonlightPlugin plugin, bool updating = false)
{
var rawPlugin = await Client.Repository.Content
.GetRawContent("Moonlight-Panel", "OfficialPlugins", $"{plugin.Name}/{plugin.Name}.dll");
if (updating)
{
await File.WriteAllBytesAsync(PathBuilder.File("storage", "plugins", $"{plugin.Name}.dll.cache"), rawPlugin);
return;
}
await File.WriteAllBytesAsync(PathBuilder.File("storage", "plugins", $"{plugin.Name}.dll"), rawPlugin);
await PluginService.ReloadPlugins();
}
}

View File

@@ -25,6 +25,7 @@ using Moonlight.App.Services.Interop;
using Moonlight.App.Services.Mail; using Moonlight.App.Services.Mail;
using Moonlight.App.Services.Minecraft; using Moonlight.App.Services.Minecraft;
using Moonlight.App.Services.Notifications; using Moonlight.App.Services.Notifications;
using Moonlight.App.Services.Plugins;
using Moonlight.App.Services.Sessions; using Moonlight.App.Services.Sessions;
using Moonlight.App.Services.Statistics; using Moonlight.App.Services.Statistics;
using Moonlight.App.Services.SupportChat; using Moonlight.App.Services.SupportChat;
@@ -110,6 +111,9 @@ namespace Moonlight
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
var pluginService = new PluginService();
await pluginService.BuildServices(builder.Services);
// Switch to logging.net injection // Switch to logging.net injection
// TODO: Enable in production // TODO: Enable in production
builder.Logging.ClearProviders(); builder.Logging.ClearProviders();
@@ -208,6 +212,7 @@ namespace Moonlight
builder.Services.AddScoped<PopupService>(); builder.Services.AddScoped<PopupService>();
builder.Services.AddScoped<SubscriptionService>(); builder.Services.AddScoped<SubscriptionService>();
builder.Services.AddScoped<BillingService>(); builder.Services.AddScoped<BillingService>();
builder.Services.AddSingleton<PluginStoreService>();
builder.Services.AddScoped<SessionClientService>(); builder.Services.AddScoped<SessionClientService>();
builder.Services.AddSingleton<SessionServerService>(); builder.Services.AddSingleton<SessionServerService>();
@@ -239,7 +244,8 @@ namespace Moonlight
builder.Services.AddSingleton<MalwareScanService>(); builder.Services.AddSingleton<MalwareScanService>();
builder.Services.AddSingleton<TelemetryService>(); builder.Services.AddSingleton<TelemetryService>();
builder.Services.AddSingleton<TempMailService>(); builder.Services.AddSingleton<TempMailService>();
builder.Services.AddSingleton<PluginService>();
builder.Services.AddSingleton(pluginService);
// Other // Other
builder.Services.AddSingleton<MoonlightService>(); builder.Services.AddSingleton<MoonlightService>();
@@ -290,8 +296,7 @@ namespace Moonlight
_ = app.Services.GetRequiredService<MalwareScanService>(); _ = app.Services.GetRequiredService<MalwareScanService>();
_ = app.Services.GetRequiredService<TelemetryService>(); _ = app.Services.GetRequiredService<TelemetryService>();
_ = app.Services.GetRequiredService<TempMailService>(); _ = app.Services.GetRequiredService<TempMailService>();
_ = app.Services.GetRequiredService<PluginService>();
_ = app.Services.GetRequiredService<MoonlightService>(); _ = app.Services.GetRequiredService<MoonlightService>();
// Discord bot service // Discord bot service

View File

@@ -34,6 +34,11 @@
<TL>Mail</TL> <TL>Mail</TL>
</a> </a>
</li> </li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 10 ? "active" : "")" href="/admin/system/plugins">
<TL>Plugins</TL>
</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -40,6 +40,8 @@
{ {
SecurityLogs = SecurityLogRepository SecurityLogs = SecurityLogRepository
.Get() .Get()
.ToArray()
.OrderByDescending(x => x.CreatedAt)
.ToArray(); .ToArray();
return Task.CompletedTask; return Task.CompletedTask;

View File

@@ -0,0 +1,138 @@
@page "/admin/system/plugins"
@using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Services.Plugins
@using BlazorTable
@using Moonlight.App.Models.Misc
@using Moonlight.App.Plugin
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@inject PluginStoreService PluginStoreService
@inject SmartTranslateService SmartTranslateService
@inject PluginService PluginService
@inject ToastService ToastService
@inject ModalService ModalService
@attribute [PermissionRequired(nameof(Permissions.AdminSysPlugins))]
<AdminSystemNavigation Index="10"/>
<div class="card mb-5">
<div class="card-header">
<span class="card-title">
<TL>Installed plugins</TL>
</span>
</div>
<div class="card-body">
<div class="table-responsive">
<Table TableItem="MoonlightPlugin" Items="PluginService.Plugins" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="MoonlightPlugin" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Filterable="true" Sortable="false"/>
<Column TableItem="MoonlightPlugin" Title="@(SmartTranslateService.Translate("Author"))" Field="@(x => x.Author)" Filterable="true" Sortable="false"/>
<Column TableItem="MoonlightPlugin" Title="@(SmartTranslateService.Translate("Version"))" Field="@(x => x.Version)" Filterable="true" Sortable="false"/>
<Column TableItem="MoonlightPlugin" Title="@(SmartTranslateService.Translate("Path"))" Field="@(x => x.Name)" Filterable="false" Sortable="false">
<Template>
@{
var path = PluginService.PluginFiles[context];
}
<span>@(path)</span>
</Template>
</Column>
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
</Table>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">
<TL>Official plugins</TL>
</span>
</div>
<div class="card-body">
<LazyLoader @ref="PluginsLazyLoader" Load="LoadOfficialPlugins">
<div class="table-responsive">
<Table TableItem="OfficialMoonlightPlugin" Items="PluginList" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="OfficialMoonlightPlugin" Width="80%" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Filterable="true" Sortable="false"/>
<Column TableItem="OfficialMoonlightPlugin" Width="10%" Title="" Field="@(x => x.Name)" Filterable="false" Sortable="false">
<Template>
<WButton Text="@(SmartTranslateService.Translate("Show readme"))"
CssClasses="btn-secondary"
OnClick="() => ShowOfficialPluginReadme(context)"/>
</Template>
</Column>
<Column TableItem="OfficialMoonlightPlugin" Width="10%" Title="" Field="@(x => x.Name)" Filterable="false" Sortable="false">
<Template>
@if (PluginService.PluginFiles.Values.Any(x =>
Path.GetFileName(x).Replace(".dll", "") == context.Name))
{
<WButton Text="@(SmartTranslateService.Translate("Update"))"
WorkingText="@(SmartTranslateService.Translate("Updating"))"
CssClasses="btn-primary"
OnClick="() => UpdateOfficialPlugin(context)"/>
}
else
{
<WButton Text="@(SmartTranslateService.Translate("Install"))"
WorkingText="@(SmartTranslateService.Translate("Installing"))"
CssClasses="btn-primary"
OnClick="() => InstallOfficialPlugin(context)"/>
}
</Template>
</Column>
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
</Table>
</div>
</LazyLoader>
</div>
</div>
<div id="pluginReadme" class="modal" style="display: none">
<div class="modal-dialog modal-dialog-centered modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<TL>Plugin readme</TL>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
@((MarkupString)Markdig.Markdown.ToHtml(PluginReadme))
</div>
</div>
</div>
</div>
@code
{
private LazyLoader PluginsLazyLoader;
private OfficialMoonlightPlugin[] PluginList;
private string PluginReadme = "";
private async Task LoadOfficialPlugins(LazyLoader lazyLoader)
{
PluginList = await PluginStoreService.GetPlugins();
}
private async Task ShowOfficialPluginReadme(OfficialMoonlightPlugin plugin)
{
PluginReadme = await PluginStoreService.GetPluginReadme(plugin);
await InvokeAsync(StateHasChanged);
await ModalService.Show("pluginReadme");
}
private async Task InstallOfficialPlugin(OfficialMoonlightPlugin plugin)
{
await PluginStoreService.InstallPlugin(plugin);
await ToastService.Success(SmartTranslateService.Translate("Successfully installed plugin"));
await InvokeAsync(StateHasChanged);
}
private async Task UpdateOfficialPlugin(OfficialMoonlightPlugin plugin)
{
await PluginStoreService.InstallPlugin(plugin, true);
await ToastService.Success(SmartTranslateService.Translate("Successfully installed plugin. You need to reboot to apply changes"));
}
}

View File

@@ -11,6 +11,7 @@
@using Moonlight.App.Plugin.UI.Servers @using Moonlight.App.Plugin.UI.Servers
@using Moonlight.App.Repositories @using Moonlight.App.Repositories
@using Moonlight.App.Services @using Moonlight.App.Services
@using Moonlight.App.Services.Plugins
@using Moonlight.App.Services.Sessions @using Moonlight.App.Services.Sessions
@using Moonlight.Shared.Components.Xterm @using Moonlight.Shared.Components.Xterm
@using Moonlight.Shared.Views.Server.Settings @using Moonlight.Shared.Views.Server.Settings
@@ -240,7 +241,8 @@
Context = new ServerPageContext() Context = new ServerPageContext()
{ {
Server = CurrentServer, Server = CurrentServer,
User = IdentityService.User User = IdentityService.User,
ImageTags = Tags
}; };
Context.Tabs.Add(new() Context.Tabs.Add(new()

View File

@@ -6,6 +6,7 @@
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using Moonlight.App.Helpers @using Moonlight.App.Helpers
@using Moonlight.App.Plugin.UI.Webspaces @using Moonlight.App.Plugin.UI.Webspaces
@using Moonlight.App.Services.Plugins
@using Moonlight.App.Services.Sessions @using Moonlight.App.Services.Sessions
@inject Repository<WebSpace> WebSpaceRepository @inject Repository<WebSpace> WebSpaceRepository