From 0658e55a78e75524de29eff0065e49579d8ff85e Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Sun, 23 Jul 2023 21:30:57 +0200 Subject: [PATCH] Implemented basic plugin store and improved plugin system --- .../Models/Misc/OfficialMoonlightPlugin.cs | 6 + Moonlight/App/Perms/Permissions.cs | 7 + Moonlight/App/Plugin/MoonlightPlugin.cs | 4 +- .../Plugin/UI/Servers/ServerPageContext.cs | 1 + .../App/Services/Files/StorageService.cs | 1 + Moonlight/App/Services/MoonlightService.cs | 2 +- .../Services/{ => Plugins}/PluginService.cs | 58 +++++++- .../Services/Plugins/PluginStoreService.cs | 63 ++++++++ Moonlight/Program.cs | 11 +- .../Navigations/AdminSystemNavigation.razor | 5 + .../Shared/Views/Admin/Security/Logs.razor | 2 + .../Shared/Views/Admin/Sys/Plugins.razor | 138 ++++++++++++++++++ Moonlight/Shared/Views/Server/Index.razor | 4 +- Moonlight/Shared/Views/Webspace/Index.razor | 1 + 14 files changed, 288 insertions(+), 15 deletions(-) create mode 100644 Moonlight/App/Models/Misc/OfficialMoonlightPlugin.cs rename Moonlight/App/Services/{ => Plugins}/PluginService.cs (50%) create mode 100644 Moonlight/App/Services/Plugins/PluginStoreService.cs create mode 100644 Moonlight/Shared/Views/Admin/Sys/Plugins.razor diff --git a/Moonlight/App/Models/Misc/OfficialMoonlightPlugin.cs b/Moonlight/App/Models/Misc/OfficialMoonlightPlugin.cs new file mode 100644 index 00000000..2b3d090d --- /dev/null +++ b/Moonlight/App/Models/Misc/OfficialMoonlightPlugin.cs @@ -0,0 +1,6 @@ +namespace Moonlight.App.Models.Misc; + +public class OfficialMoonlightPlugin +{ + public string Name { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Perms/Permissions.cs b/Moonlight/App/Perms/Permissions.cs index 9a3fbd78..87b0ec58 100644 --- a/Moonlight/App/Perms/Permissions.cs +++ b/Moonlight/App/Perms/Permissions.cs @@ -15,6 +15,13 @@ public static class Permissions Name = "Admin Statistics", 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() { diff --git a/Moonlight/App/Plugin/MoonlightPlugin.cs b/Moonlight/App/Plugin/MoonlightPlugin.cs index 702dddc7..875c33ee 100644 --- a/Moonlight/App/Plugin/MoonlightPlugin.cs +++ b/Moonlight/App/Plugin/MoonlightPlugin.cs @@ -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; namespace Moonlight.App.Plugin; @@ -12,4 +11,5 @@ public abstract class MoonlightPlugin public Func? OnBuildServerPage { get; set; } public Func? OnBuildWebspacePage { get; set; } + public Func? OnBuildServices { get; set; } } \ No newline at end of file diff --git a/Moonlight/App/Plugin/UI/Servers/ServerPageContext.cs b/Moonlight/App/Plugin/UI/Servers/ServerPageContext.cs index 0905be1d..8179035a 100644 --- a/Moonlight/App/Plugin/UI/Servers/ServerPageContext.cs +++ b/Moonlight/App/Plugin/UI/Servers/ServerPageContext.cs @@ -8,4 +8,5 @@ public class ServerPageContext public List Settings { get; set; } = new(); public Server Server { get; set; } public User User { get; set; } + public string[] ImageTags { get; set; } } \ No newline at end of file diff --git a/Moonlight/App/Services/Files/StorageService.cs b/Moonlight/App/Services/Files/StorageService.cs index 9d05c692..9980e23a 100644 --- a/Moonlight/App/Services/Files/StorageService.cs +++ b/Moonlight/App/Services/Files/StorageService.cs @@ -16,6 +16,7 @@ public class StorageService Directory.CreateDirectory(PathBuilder.Dir("storage", "resources")); Directory.CreateDirectory(PathBuilder.Dir("storage", "backups")); Directory.CreateDirectory(PathBuilder.Dir("storage", "logs")); + Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins")); if(IsEmpty(PathBuilder.Dir("storage", "resources"))) { diff --git a/Moonlight/App/Services/MoonlightService.cs b/Moonlight/App/Services/MoonlightService.cs index 650a2b4f..f8699f4a 100644 --- a/Moonlight/App/Services/MoonlightService.cs +++ b/Moonlight/App/Services/MoonlightService.cs @@ -46,7 +46,7 @@ public class MoonlightService 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 { diff --git a/Moonlight/App/Services/PluginService.cs b/Moonlight/App/Services/Plugins/PluginService.cs similarity index 50% rename from Moonlight/App/Services/PluginService.cs rename to Moonlight/App/Services/Plugins/PluginService.cs index fb4f04eb..91e1cfbd 100644 --- a/Moonlight/App/Services/PluginService.cs +++ b/Moonlight/App/Services/Plugins/PluginService.cs @@ -1,25 +1,57 @@ using System.Reflection; -using Moonlight.App.Database.Entities; +using System.Runtime.Loader; using Moonlight.App.Helpers; using Moonlight.App.Plugin; -using Moonlight.App.Plugin.UI; using Moonlight.App.Plugin.UI.Servers; using Moonlight.App.Plugin.UI.Webspaces; -namespace Moonlight.App.Services; +namespace Moonlight.App.Services.Plugins; public class PluginService { - public List Plugins { get; set; } + public List Plugins { get; private set; } + public Dictionary PluginFiles { get; private set; } + + private AssemblyLoadContext LoadContext; public PluginService() { - LoadPlugins(); + LoadContext = new(null, true); + ReloadPlugins().Wait(); } - - private void LoadPlugins() + + private Task UnloadPlugins() { 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); @@ -27,7 +59,7 @@ public class PluginService PathBuilder.Dir(Directory.GetCurrentDirectory(), "storage", "plugins")) .Where(x => x.EndsWith(".dll"))) { - var assembly = Assembly.LoadFile(pluginFile); + var assembly = LoadContext.LoadFromAssemblyPath(pluginFile); foreach (var type in assembly.GetTypes()) { @@ -38,6 +70,7 @@ public class PluginService Logger.Info($"Loaded plugin '{plugin.Name}' ({plugin.Version}) by {plugin.Author}"); Plugins.Add(plugin); + PluginFiles.Add(plugin, pluginFile); } } } @@ -66,4 +99,13 @@ public class PluginService return context; } + + public async Task BuildServices(IServiceCollection serviceCollection) + { + foreach (var plugin in Plugins) + { + if (plugin.OnBuildServices != null) + await plugin.OnBuildServices.Invoke(serviceCollection); + } + } } \ No newline at end of file diff --git a/Moonlight/App/Services/Plugins/PluginStoreService.cs b/Moonlight/App/Services/Plugins/PluginStoreService.cs new file mode 100644 index 00000000..4d885d03 --- /dev/null +++ b/Moonlight/App/Services/Plugins/PluginStoreService.cs @@ -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 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(); + } + + return items + .Where(x => x.Type == ContentType.Dir) + .Select(x => new OfficialMoonlightPlugin() + { + Name = x.Name + }) + .ToArray(); + } + + public async Task 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(); + } +} \ No newline at end of file diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 4f504771..3038850b 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -25,6 +25,7 @@ using Moonlight.App.Services.Interop; using Moonlight.App.Services.Mail; using Moonlight.App.Services.Minecraft; using Moonlight.App.Services.Notifications; +using Moonlight.App.Services.Plugins; using Moonlight.App.Services.Sessions; using Moonlight.App.Services.Statistics; using Moonlight.App.Services.SupportChat; @@ -110,6 +111,9 @@ namespace Moonlight var builder = WebApplication.CreateBuilder(args); + var pluginService = new PluginService(); + await pluginService.BuildServices(builder.Services); + // Switch to logging.net injection // TODO: Enable in production builder.Logging.ClearProviders(); @@ -208,6 +212,7 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -239,7 +244,8 @@ namespace Moonlight builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + + builder.Services.AddSingleton(pluginService); // Other builder.Services.AddSingleton(); @@ -290,8 +296,7 @@ namespace Moonlight _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); - _ = app.Services.GetRequiredService(); - + _ = app.Services.GetRequiredService(); // Discord bot service diff --git a/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor b/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor index 9733aed1..5dffb252 100644 --- a/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor +++ b/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor @@ -34,6 +34,11 @@ Mail + diff --git a/Moonlight/Shared/Views/Admin/Security/Logs.razor b/Moonlight/Shared/Views/Admin/Security/Logs.razor index 2a7d1958..7dcabcb0 100644 --- a/Moonlight/Shared/Views/Admin/Security/Logs.razor +++ b/Moonlight/Shared/Views/Admin/Security/Logs.razor @@ -40,6 +40,8 @@ { SecurityLogs = SecurityLogRepository .Get() + .ToArray() + .OrderByDescending(x => x.CreatedAt) .ToArray(); return Task.CompletedTask; diff --git a/Moonlight/Shared/Views/Admin/Sys/Plugins.razor b/Moonlight/Shared/Views/Admin/Sys/Plugins.razor new file mode 100644 index 00000000..bafda85d --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Sys/Plugins.razor @@ -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))] + + + +
+
+ + Installed plugins + +
+
+
+ + + + + + + + +
+
+
+
+ +
+
+ + Official plugins + +
+
+ +
+ + + + + + + + + +
+
+
+
+
+ + + +@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")); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Server/Index.razor b/Moonlight/Shared/Views/Server/Index.razor index 3e0b4689..ce14d12b 100644 --- a/Moonlight/Shared/Views/Server/Index.razor +++ b/Moonlight/Shared/Views/Server/Index.razor @@ -11,6 +11,7 @@ @using Moonlight.App.Plugin.UI.Servers @using Moonlight.App.Repositories @using Moonlight.App.Services +@using Moonlight.App.Services.Plugins @using Moonlight.App.Services.Sessions @using Moonlight.Shared.Components.Xterm @using Moonlight.Shared.Views.Server.Settings @@ -240,7 +241,8 @@ Context = new ServerPageContext() { Server = CurrentServer, - User = IdentityService.User + User = IdentityService.User, + ImageTags = Tags }; Context.Tabs.Add(new() diff --git a/Moonlight/Shared/Views/Webspace/Index.razor b/Moonlight/Shared/Views/Webspace/Index.razor index d3e17f36..d15a8e51 100644 --- a/Moonlight/Shared/Views/Webspace/Index.razor +++ b/Moonlight/Shared/Views/Webspace/Index.razor @@ -6,6 +6,7 @@ @using Microsoft.EntityFrameworkCore @using Moonlight.App.Helpers @using Moonlight.App.Plugin.UI.Webspaces +@using Moonlight.App.Services.Plugins @using Moonlight.App.Services.Sessions @inject Repository WebSpaceRepository