Simplified plugin service and loading

This commit is contained in:
2025-02-26 17:06:25 +01:00
parent cdc4744f28
commit caa8d47af2
14 changed files with 188 additions and 401 deletions

View File

@@ -1,37 +0,0 @@
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.FileProviders.Physical;
using Microsoft.Extensions.Primitives;
using Moonlight.ApiServer.Services;
namespace Moonlight.ApiServer.Helpers;
public class PluginAssetFileProvider : IFileProvider
{
private readonly PluginService PluginService;
public PluginAssetFileProvider(PluginService pluginService)
{
PluginService = pluginService;
}
public IDirectoryContents GetDirectoryContents(string subpath)
{
return NotFoundDirectoryContents.Singleton;
}
public IFileInfo GetFileInfo(string subpath)
{
if (!PluginService.AssetMap.TryGetValue(subpath, out var physicalPath))
return new NotFoundFileInfo(subpath);
if (!File.Exists(physicalPath))
return new NotFoundFileInfo(subpath);
return new PhysicalFileInfo(new FileInfo(physicalPath));
}
public IChangeToken Watch(string filter)
{
return NullChangeToken.Singleton;
}
}

View File

@@ -13,17 +13,14 @@ public class FrontendController : Controller
{ {
private readonly AppConfiguration Configuration; private readonly AppConfiguration Configuration;
private readonly PluginService PluginService; private readonly PluginService PluginService;
private readonly AssetService AssetService;
public FrontendController( public FrontendController(
AppConfiguration configuration, AppConfiguration configuration,
PluginService pluginService, PluginService pluginService
AssetService assetService
) )
{ {
Configuration = configuration; Configuration = configuration;
PluginService = pluginService; PluginService = pluginService;
AssetService = assetService;
} }
[HttpGet("frontend.json")] [HttpGet("frontend.json")]
@@ -36,27 +33,54 @@ public class FrontendController : Controller
HostEnvironment = "ApiServer" HostEnvironment = "ApiServer"
}; };
// Load theme if it exists #region Load theme.json if it exists
var themePath = PathBuilder.File("storage", "theme.json"); var themePath = PathBuilder.File("storage", "theme.json");
if (System.IO.File.Exists(themePath)) if (System.IO.File.Exists(themePath))
{ {
var variablesJson = await System.IO.File.ReadAllTextAsync(themePath); var variablesJson = await System.IO.File.ReadAllTextAsync(themePath);
configuration.Theme.Variables = JsonSerializer.Deserialize<Dictionary<string, string>>(variablesJson) ?? new(); configuration.Theme.Variables =
JsonSerializer.Deserialize<Dictionary<string, string>>(variablesJson) ?? new();
} }
configuration.Plugins.Entrypoints = PluginService.HostedPluginsManifest.Entrypoints; #endregion
configuration.Plugins.Assemblies = PluginService.HostedPluginsManifest.Assemblies;
configuration.Scripts = AssetService.GetJavascriptAssets(); // Collect assemblies for the 'client' section
configuration.Assemblies = PluginService
.GetAssemblies("client")
.Keys
.ToArray();
// Collect scripts to execute
configuration.Scripts = PluginService
.LoadedPlugins
.Keys
.SelectMany(x => x.Scripts)
.ToArray();
// Collect styles
var styles = new List<string>();
styles.AddRange(
PluginService
.LoadedPlugins
.Keys
.SelectMany(x => x.Styles)
);
// Add bundle css
styles.Add("css/bundle.min.css");
configuration.Styles = styles.ToArray();
return configuration; return configuration;
} }
[HttpGet("plugins/{assemblyName}")] // TODO: Test this [HttpGet("plugins/{assemblyName}")]
public async Task GetPluginAssembly(string assemblyName) public async Task GetPluginAssembly(string assemblyName)
{ {
var assembliesMap = PluginService.ClientAssemblyMap; var assembliesMap = PluginService.GetAssemblies("client");
if (assembliesMap.ContainsKey(assemblyName)) if (assembliesMap.ContainsKey(assemblyName))
throw new HttpApiException("The requested assembly could not be found", 404); throw new HttpApiException("The requested assembly could not be found", 404);

View File

@@ -7,5 +7,9 @@ public class PluginManifest
public string Author { get; set; } public string Author { get; set; }
public string[] Dependencies { get; set; } = []; public string[] Dependencies { get; set; } = [];
public Dictionary<string, string[]> Entrypoints { get; set; } = new(); public string[] Scripts { get; set; } = [];
public string[] Styles { get; set; } = [];
public string[] BundledStyles { get; set; } = [];
public Dictionary<string, string[]> Assemblies { get; set; } = new();
} }

View File

@@ -1,7 +0,0 @@
namespace Moonlight.ApiServer.Models;
public class PluginMeta
{
public PluginManifest Manifest { get; set; }
public string Path { get; set; }
}

View File

@@ -1,45 +0,0 @@
using MoonCore.Attributes;
namespace Moonlight.ApiServer.Services;
[Singleton]
public class AssetService
{
public string[] JavascriptFiles { get; private set; }
private bool HasBeenCollected = false;
private readonly List<string> AdditionalCssAssets = new();
private readonly List<string> AdditionalJavascriptAssets = new();
private readonly PluginService PluginService;
public AssetService(PluginService pluginService)
{
PluginService = pluginService;
}
public void CollectAssets()
{
// Javascript
var jsFiles = new List<string>();
jsFiles.AddRange(AdditionalJavascriptAssets);
jsFiles.AddRange(PluginService.AssetMap.Keys.Where(x => x.EndsWith(".js")));
JavascriptFiles = jsFiles.ToArray();
}
public void AddJavascriptAsset(string asset)
=> AdditionalJavascriptAssets.Add(asset);
public string[] GetJavascriptAssets()
{
if (HasBeenCollected)
return JavascriptFiles;
CollectAssets();
return JavascriptFiles;
}
}

View File

@@ -7,5 +7,8 @@ public class BundleService
public void BundleCss(string path) public void BundleCss(string path)
=> CssFiles.Add(path); => CssFiles.Add(path);
public void BundleCssRange(string[] paths)
=> CssFiles.AddRange(paths);
public IEnumerable<string> GetCssFiles() => CssFiles; public IEnumerable<string> GetCssFiles() => CssFiles;
} }

View File

@@ -1,181 +1,136 @@
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.FileProviders;
using MoonCore.Helpers; using MoonCore.Helpers;
using MoonCore.Models;
using Moonlight.ApiServer.Models; using Moonlight.ApiServer.Models;
using Moonlight.Shared.Http.Responses.PluginsStream;
namespace Moonlight.ApiServer.Services; namespace Moonlight.ApiServer.Services;
public class PluginService public class PluginService
{ {
public List<PluginMeta> Plugins { get; private set; } = new();
public Dictionary<string, string> AssetMap { get; private set; } = new();
public HostedPluginsManifest HostedPluginsManifest { get; private set; }
public Dictionary<string, string> ClientAssemblyMap { get; private set; }
private static string PluginsFolder = PathBuilder.Dir("storage", "plugins");
private readonly ILogger<PluginService> Logger; private readonly ILogger<PluginService> Logger;
private readonly string PluginRoot;
private readonly JsonSerializerOptions SerializerOptions = new() public readonly Dictionary<PluginManifest, string> LoadedPlugins = new();
{ public IFileProvider WwwRootFileProvider;
PropertyNameCaseInsensitive = true
};
public PluginService(ILogger<PluginService> logger) public PluginService(ILogger<PluginService> logger)
{ {
Logger = logger; Logger = logger;
PluginRoot = PathBuilder.Dir("storage", "plugins");
} }
public async Task Load() public async Task Load()
{ {
// Load all manifest files var jsonOptions = new JsonSerializerOptions()
foreach (var pluginFolder in Directory.EnumerateDirectories(PluginsFolder))
{ {
var manifestPath = PathBuilder.File(pluginFolder, "plugin.json"); PropertyNameCaseInsensitive = true
};
if (!File.Exists(manifestPath)) var pluginDirs = Directory.GetDirectories(PluginRoot);
var pluginMap = new Dictionary<PluginManifest, string>();
#region Scan plugins/ directory for plugin.json files
foreach (var dir in pluginDirs)
{
var metaPath = PathBuilder.File(dir, "plugin.json");
if (!File.Exists(metaPath))
{ {
Logger.LogWarning("Ignoring '{folder}' because no manifest has been found", pluginFolder); Logger.LogWarning("Skipped '{dir}' as it is missing a plugin.json", dir);
continue; continue;
} }
PluginManifest manifest; var json = await File.ReadAllTextAsync(metaPath);
try try
{ {
var manifestText = await File.ReadAllTextAsync(manifestPath); var meta = JsonSerializer.Deserialize<PluginManifest>(json, jsonOptions);
manifest = JsonSerializer.Deserialize<PluginManifest>(manifestText, SerializerOptions)!;
if (meta == null)
throw new JsonException("Unable to parse. Return value was null");
pluginMap.Add(meta, dir);
} }
catch (Exception e) catch (JsonException e)
{ {
Logger.LogError("An unhandled error occured while loading plugin manifest in '{folder}': {e}", Logger.LogError("Unable to load plugin.json at '{path}': {e}", metaPath, e);
pluginFolder, e);
break;
}
Logger.LogTrace("Loaded plugin manifest. Id: {id}", manifest.Id);
Plugins.Add(new()
{
Manifest = manifest,
Path = pluginFolder
});
}
// Check for missing dependencies
var pluginsToNotLoad = new List<string>();
foreach (var plugin in Plugins)
{
foreach (var dependency in plugin.Manifest.Dependencies)
{
// Check if dependency is found
if (Plugins.Any(x => x.Manifest.Id == dependency))
continue;
Logger.LogError(
"Unable to load plugin '{id}' ({path}) because the dependency '{dependency}' is missing",
plugin.Manifest.Id,
plugin.Path,
dependency
);
pluginsToNotLoad.Add(plugin.Manifest.Id);
break;
} }
} }
// Remove unloadable plugins from cache #endregion
Plugins.RemoveAll(x => pluginsToNotLoad.Contains(x.Manifest.Id));
// Generate assembly map for client #region Depdenency check
ClientAssemblyMap = GetAssemblies("client");
// Generate plugin stream manifest for client foreach (var plugin in pluginMap.Keys)
HostedPluginsManifest = new()
{ {
Assemblies = ClientAssemblyMap.Keys.ToArray(), var hasMissingDep = false;
Entrypoints = GetEntrypoints("client")
};
// Generate asset map foreach (var dependency in plugin.Dependencies)
GenerateAssetMap(); {
if (pluginMap.Keys.All(x => x.Id != dependency))
{
hasMissingDep = true;
Logger.LogWarning("Plugin '{name}' has missing dependency: {dep}", plugin.Name, dependency);
}
}
if (hasMissingDep)
Logger.LogWarning("Unable to load '{name}' due to missing dependencies", plugin.Name);
else
LoadedPlugins.Add(plugin, pluginMap[plugin]);
}
#endregion
#region Create wwwroot file provider
Logger.LogInformation("Creating wwwroot file provider");
WwwRootFileProvider = CreateWwwRootProvider();
#endregion
Logger.LogInformation("Loaded {count} plugins", LoadedPlugins.Count);
} }
public Dictionary<string, string> GetAssemblies(string section) public Dictionary<string, string> GetAssemblies(string section)
{ {
var pathMappings = new Dictionary<string, string>(); var assemblyMap = new Dictionary<string, string>();
foreach (var plugin in Plugins) foreach (var loadedPlugin in LoadedPlugins.Keys)
{ {
var binaryPath = PathBuilder.Dir(plugin.Path, "bin", section); // Skip all plugins which haven't defined any assemblies in that section
if (!loadedPlugin.Assemblies.ContainsKey(section))
if(!Directory.Exists(binaryPath))
continue; continue;
foreach (var file in Directory.EnumerateFiles(binaryPath)) var pluginPath = LoadedPlugins[loadedPlugin];
{
if (!file.EndsWith(".dll"))
continue;
var fileName = Path.GetFileName(file); foreach (var assembly in loadedPlugin.Assemblies[section])
pathMappings[fileName] = file; {
var assemblyFile = Path.GetFileName(assembly);
assemblyMap[assemblyFile] = PathBuilder.File(pluginPath, assembly);
} }
} }
return pathMappings; return assemblyMap;
} }
public string[] GetEntrypoints(string section) private IFileProvider CreateWwwRootProvider()
{ {
return Plugins List<IFileProvider> wwwRootProviders = new();
.Where(x => x.Manifest.Entrypoints.ContainsKey(section))
.SelectMany(x => x.Manifest.Entrypoints[section])
.ToArray();
}
private void GenerateAssetMap() foreach (var pluginFolder in LoadedPlugins.Values)
{
AssetMap.Clear();
foreach (var plugin in Plugins)
{ {
var assetPath = PathBuilder.Dir(plugin.Path, "wwwroot"); var wwwRootPath = Path.GetFullPath(
PathBuilder.Dir(pluginFolder, "wwwroot")
);
if (!Directory.Exists(assetPath)) wwwRootProviders.Add(
continue; new PhysicalFileProvider(wwwRootPath)
);
var files = new List<string>();
GetFilesInDirectory(assetPath, files);
foreach (var file in files)
{
var mapPath = Formatter.ReplaceStart(file, assetPath, "");
mapPath = mapPath.Replace("\\", "/"); // To handle fucking windows
mapPath = mapPath.StartsWith("/") ? mapPath : "/" + mapPath; // Ensure starting /
if (AssetMap.ContainsKey(mapPath))
{
Logger.LogWarning(
"The plugin '{name}' tries to map an asset to the path '{path}' which is already used by another plugin. Ignoring asset mapping",
plugin.Manifest.Id,
mapPath
);
continue;
}
AssetMap[mapPath] = file;
}
} }
}
private void GetFilesInDirectory(string directory, List<string> files) return new CompositeFileProvider(wwwRootProviders);
{
files.AddRange(Directory.EnumerateFiles(directory));
foreach (var dir in Directory.EnumerateDirectories(directory))
GetFilesInDirectory(dir, files);
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Reflection; using System.Reflection;
using System.Runtime.Loader;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -19,7 +20,9 @@ using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Helpers; using Moonlight.ApiServer.Helpers;
using Moonlight.ApiServer.Interfaces.OAuth2; using Moonlight.ApiServer.Interfaces.OAuth2;
using Moonlight.ApiServer.Interfaces.Startup; using Moonlight.ApiServer.Interfaces.Startup;
using Moonlight.ApiServer.Models;
using Moonlight.ApiServer.Services; using Moonlight.ApiServer.Services;
using Moonlight.Client.Services;
namespace Moonlight.ApiServer; namespace Moonlight.ApiServer;
@@ -30,6 +33,7 @@ public class Startup
{ {
private string[] Args; private string[] Args;
private Assembly[] AdditionalAssemblies; private Assembly[] AdditionalAssemblies;
private PluginManifest[] AdditionalPluginManifests;
// Logging // Logging
private ILoggerProvider[] LoggerProviders; private ILoggerProvider[] LoggerProviders;
@@ -47,17 +51,18 @@ public class Startup
// Plugin Loading // Plugin Loading
private PluginService PluginService; private PluginService PluginService;
private PluginLoaderService PluginLoaderService; private AssemblyLoadContext PluginLoadContext;
// Asset bundling // Asset bundling
private BundleService BundleService; private BundleService BundleService = new();
private IPluginStartup[] PluginStartups; private IPluginStartup[] PluginStartups;
public async Task Run(string[] args, Assembly[]? additionalAssemblies = null) public async Task Run(string[] args, Assembly[]? additionalAssemblies = null, PluginManifest[]? additionalManifests = null)
{ {
Args = args; Args = args;
AdditionalAssemblies = additionalAssemblies ?? []; AdditionalAssemblies = additionalAssemblies ?? [];
AdditionalPluginManifests = additionalManifests ?? [];
await PrintVersion(); await PrintVersion();
@@ -76,18 +81,16 @@ public class Startup
await RegisterAuth(); await RegisterAuth();
await RegisterCaching(); await RegisterCaching();
await HookPluginBuild(); await HookPluginBuild();
await HandleConfigureArguments();
await RegisterPluginAssets(); await RegisterPluginAssets();
await BuildWebApplication(); await BuildWebApplication();
await HandleServiceArguments();
await PrepareDatabase(); await PrepareDatabase();
await UsePluginAssets(); // We need to move the plugin assets to the top to allow plugins to override content
await UseBase(); await UseBase();
await UseAuth(); await UseAuth();
await HookPluginConfigure(); await HookPluginConfigure();
await UsePluginAssets();
await MapBase(); await MapBase();
await HookPluginEndpoints(); await HookPluginEndpoints();
@@ -123,73 +126,6 @@ public class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
#region Command line arguments
private Task HandleConfigureArguments()
{
return Task.CompletedTask;
}
private Task HandleServiceArguments()
{
// Handle manual asset loading arguments
if (Args.Any(x => x.StartsWith("--frontend-asset")))
{
if (!Configuration.Client.Enable)
{
Logger.LogWarning("The hosting of the moonlight frontend is disabled. Ignoring all --frontend-asset options");
return Task.CompletedTask; // TODO: Change this when adding more service argument handling functions
}
if (!WebApplicationBuilder.Environment.IsDevelopment())
Logger.LogWarning("Using the --frontend-asset option is not meant to be used in production. Plugin assets will be loaded automaticly");
var assetService = WebApplication.Services.GetRequiredService<AssetService>();
for (var i = 0; i < Args.Length; i++)
{
var currentArg = Args[i];
// Ignore all args without relation to our frontend assets
if(!currentArg.Equals("--frontend-asset", StringComparison.InvariantCultureIgnoreCase))
continue;
if (i + 1 >= Args.Length)
{
Logger.LogWarning("You need to specify an asset path after the --frontend-asset option");
continue;
}
var nextArg = Args[i + 1];
if (nextArg.StartsWith("--"))
{
Logger.LogWarning("You need to specify an asset path after the --frontend-asset option");
continue;
}
var extension = Path.GetExtension(nextArg);
switch (extension)
{
case ".css":
BundleService.BundleCss(nextArg);
break;
case ".js":
assetService.AddJavascriptAsset(nextArg);
break;
default:
Logger.LogWarning("Unknown asset extension {extension}. Ignoring it", extension);
break;
}
}
}
return Task.CompletedTask;
}
#endregion
#region Base #region Base
private Task RegisterBase() private Task RegisterBase()
@@ -207,7 +143,7 @@ public class Startup
var mvcBuilder = WebApplicationBuilder.Services.AddControllers(); var mvcBuilder = WebApplicationBuilder.Services.AddControllers();
// Add plugin and additional assemblies as application parts // Add plugin and additional assemblies as application parts
foreach (var pluginAssembly in PluginLoaderService.PluginAssemblies) foreach (var pluginAssembly in PluginLoadContext.Assemblies)
mvcBuilder.AddApplicationPart(pluginAssembly); mvcBuilder.AddApplicationPart(pluginAssembly);
foreach (var additionalAssembly in AdditionalAssemblies) foreach (var additionalAssembly in AdditionalAssemblies)
@@ -238,9 +174,7 @@ public class Startup
WebApplication.MapControllers(); WebApplication.MapControllers();
if (Configuration.Client.Enable) if (Configuration.Client.Enable)
{
WebApplication.MapFallbackToFile("index.html"); WebApplication.MapFallbackToFile("index.html");
}
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -256,25 +190,32 @@ public class Startup
LoggerFactory.CreateLogger<PluginService>() LoggerFactory.CreateLogger<PluginService>()
); );
// Add plugins manually if specified in the startup
foreach (var manifest in AdditionalPluginManifests)
PluginService.LoadedPlugins.Add(manifest, Directory.GetCurrentDirectory());
// Search and load all plugins
await PluginService.Load(); await PluginService.Load();
// Initialize api server plugin loader // Search up assemblies for the apiServer
PluginLoaderService = new PluginLoaderService(
LoggerFactory.CreateLogger<PluginLoaderService>()
);
// Search up entrypoints and assemblies for the apiServer
var assemblyFiles = PluginService.GetAssemblies("apiServer") var assemblyFiles = PluginService.GetAssemblies("apiServer")
.Values .Values
.ToArray(); .ToArray();
var entrypoints = PluginService.GetEntrypoints("apiServer"); // Create the load context and add assemblies
PluginLoadContext = new AssemblyLoadContext(null);
// Build source from the retrieved data foreach (var assemblyFile in assemblyFiles)
PluginLoaderService.AddFilesSource(assemblyFiles, entrypoints); {
try
// Perform assembly loading {
await PluginLoaderService.Load(); PluginLoadContext.LoadFromAssemblyPath(assemblyFile);
}
catch (Exception e)
{
Logger.LogError("Unable to load plugin assembly '{assemblyFile}': {e}", assemblyFile, e);
}
}
} }
private Task InitializePlugins() private Task InitializePlugins()
@@ -285,9 +226,13 @@ public class Startup
// Configure base services for initialisation // Configure base services for initialisation
startupSc.AddSingleton(Configuration); startupSc.AddSingleton(Configuration);
BundleService = new BundleService(); // Add bundle service so plugins can do additional bundling if required
startupSc.AddSingleton(BundleService); startupSc.AddSingleton(BundleService);
// Auto add all files specified in the bundledStyles section to the bundle job
foreach (var plugin in PluginService.LoadedPlugins.Keys)
BundleService.BundleCssRange(plugin.BundledStyles);
startupSc.AddLogging(builder => startupSc.AddLogging(builder =>
{ {
builder.ClearProviders(); builder.ClearProviders();
@@ -304,7 +249,7 @@ public class Startup
var assembliesToScan = new List<Assembly>(); var assembliesToScan = new List<Assembly>();
assembliesToScan.Add(typeof(Startup).Assembly); assembliesToScan.Add(typeof(Startup).Assembly);
assembliesToScan.AddRange(PluginLoaderService.PluginAssemblies); assembliesToScan.AddRange(PluginLoadContext.Assemblies);
assembliesToScan.AddRange(AdditionalAssemblies); assembliesToScan.AddRange(AdditionalAssemblies);
foreach (var pluginAssembly in assembliesToScan) foreach (var pluginAssembly in assembliesToScan)
@@ -348,7 +293,7 @@ public class Startup
WebApplication.UseStaticFiles(new StaticFileOptions() WebApplication.UseStaticFiles(new StaticFileOptions()
{ {
FileProvider = new PluginAssetFileProvider(PluginService) FileProvider = PluginService.WwwRootFileProvider
}); });
return Task.CompletedTask; return Task.CompletedTask;

View File

@@ -1,41 +0,0 @@
using System.Runtime.Loader;
using MoonCore.Plugins;
using Moonlight.Shared.Misc;
namespace Moonlight.Client.Implementations;
public class RemotePluginSource : IPluginSource
{
private readonly FrontendConfiguration Configuration;
private readonly ILogger<RemotePluginSource> Logger;
private readonly HttpClient HttpClient;
public RemotePluginSource(
FrontendConfiguration configuration,
ILogger<RemotePluginSource> logger,
HttpClient httpClient
)
{
Configuration = configuration;
Logger = logger;
HttpClient = httpClient;
}
public async Task Load(AssemblyLoadContext loadContext, List<string> entrypoints)
{
entrypoints.AddRange(Configuration.Plugins.Entrypoints);
foreach (var assembly in Configuration.Plugins.Assemblies)
{
try
{
var fileStream = await HttpClient.GetStreamAsync($"plugins/{assembly}");
loadContext.LoadFromStream(fileStream);
}
catch (Exception e)
{
Logger.LogCritical("Unable to load plugin assembly '{assembly}': {e}", assembly, e);
}
}
}
}

View File

@@ -4,7 +4,5 @@ namespace Moonlight.Client.Services;
public class ApplicationAssemblyService public class ApplicationAssemblyService
{ {
public Assembly[] AdditionalAssemblies { get; set; } public List<Assembly> Assemblies { get; set; } = new();
public Assembly[] PluginAssemblies { get; set; }
public Assembly[] NavigationAssemblies => PluginAssemblies.Concat(AdditionalAssemblies).ToArray();
} }

View File

@@ -1,4 +1,5 @@
using System.Reflection; using System.Reflection;
using System.Runtime.Loader;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
@@ -34,20 +35,15 @@ public class Startup
private WebAssemblyHost WebAssemblyHost; private WebAssemblyHost WebAssemblyHost;
// Plugin Loading // Plugin Loading
private PluginLoaderService PluginLoaderService; private AssemblyLoadContext PluginLoadContext;
private ApplicationAssemblyService ApplicationAssemblyService; private Assembly[] AdditionalAssemblies;
private IPluginStartup[] PluginStartups; private IPluginStartup[] PluginStartups;
public async Task Run(string[] args, Assembly[]? assemblies = null) public async Task Run(string[] args, Assembly[]? additionalAssemblies = null)
{ {
Args = args; Args = args;
AdditionalAssemblies = additionalAssemblies ?? [];
// Setup assembly storage
ApplicationAssemblyService = new()
{
AdditionalAssemblies = assemblies ?? []
};
await PrintVersion(); await PrintVersion();
await SetupLogging(); await SetupLogging();
@@ -174,11 +170,6 @@ public class Startup
private async Task LoadPlugins() private async Task LoadPlugins()
{ {
// Initialize api server plugin loader
PluginLoaderService = new PluginLoaderService(
LoggerFactory.CreateLogger<PluginLoaderService>()
);
// Create everything required to stream plugins // Create everything required to stream plugins
using var clientForStreaming = new HttpClient(); using var clientForStreaming = new HttpClient();
@@ -187,19 +178,21 @@ public class Startup
: WebAssemblyHostBuilder.HostEnvironment.BaseAddress : WebAssemblyHostBuilder.HostEnvironment.BaseAddress
); );
PluginLoaderService.AddSource(new RemotePluginSource( PluginLoadContext = new AssemblyLoadContext(null);
Configuration,
LoggerFactory.CreateLogger<RemotePluginSource>(),
clientForStreaming
));
// Perform assembly loading foreach (var assembly in Configuration.Assemblies)
await PluginLoaderService.Load(); {
var assemblyStream = await clientForStreaming.GetStreamAsync($"plugins/{assembly}");
PluginLoadContext.LoadFromStream(assemblyStream);
}
// Add plugin loader service to di for the Router/App.razor // Add application assembly service
ApplicationAssemblyService.PluginAssemblies = PluginLoaderService.PluginAssemblies; var appAssemblyService = new ApplicationAssemblyService();
WebAssemblyHostBuilder.Services.AddSingleton(ApplicationAssemblyService); appAssemblyService.Assemblies.AddRange(AdditionalAssemblies);
appAssemblyService.Assemblies.AddRange(PluginLoadContext.Assemblies);
WebAssemblyHostBuilder.Services.AddSingleton(appAssemblyService);
} }
private Task InitializePlugins() private Task InitializePlugins()
@@ -224,8 +217,8 @@ public class Startup
var assembliesToScan = new List<Assembly>(); var assembliesToScan = new List<Assembly>();
assembliesToScan.Add(typeof(Startup).Assembly); assembliesToScan.Add(typeof(Startup).Assembly);
assembliesToScan.AddRange(PluginLoaderService.PluginAssemblies); assembliesToScan.AddRange(AdditionalAssemblies);
assembliesToScan.AddRange(ApplicationAssemblyService.AdditionalAssemblies); assembliesToScan.AddRange(PluginLoadContext.Assemblies);
foreach (var pluginAssembly in assembliesToScan) foreach (var pluginAssembly in assembliesToScan)
{ {

View File

@@ -5,4 +5,4 @@
<ApplicationRouter DefaultLayout="@typeof(MainLayout)" <ApplicationRouter DefaultLayout="@typeof(MainLayout)"
AppAssembly="@typeof(App).Assembly" AppAssembly="@typeof(App).Assembly"
AdditionalAssemblies="ApplicationAssemblyService.NavigationAssemblies" /> AdditionalAssemblies="ApplicationAssemblyService.Assemblies" />

View File

@@ -71,7 +71,7 @@
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col"> <div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-800/60 px-6 pb-4"> <div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-800/60 px-6 pb-4">
<div class="flex h-16 shrink-0 items-center"> <div class="flex h-16 shrink-0 items-center">
<img class="h-8 w-auto" src="https://gamecp.masuowo.xyz/api/core/asset/Core/svg/logo.svg" <img class="h-8 w-auto" src="/svg/logo.svg"
alt="Your Company"> alt="Your Company">
</div> </div>
<nav class="flex flex-1 flex-col"> <nav class="flex flex-1 flex-col">

View File

@@ -7,16 +7,11 @@ public class FrontendConfiguration
public string HostEnvironment { get; set; } public string HostEnvironment { get; set; }
public ThemeData Theme { get; set; } = new(); public ThemeData Theme { get; set; } = new();
public string[] Scripts { get; set; } public string[] Scripts { get; set; }
public PluginData Plugins { get; set; } = new(); public string[] Styles { get; set; }
public string[] Assemblies { get; set; }
public class ThemeData public class ThemeData
{ {
public Dictionary<string, string> Variables { get; set; } = new(); public Dictionary<string, string> Variables { get; set; } = new();
} }
public class PluginData
{
public string[] Assemblies { get; set; }
public string[] Entrypoints { get; set; }
}
} }