diff --git a/Moonlight.ApiServer/Helpers/PluginAssetFileProvider.cs b/Moonlight.ApiServer/Helpers/PluginAssetFileProvider.cs deleted file mode 100644 index 9eff0231..00000000 --- a/Moonlight.ApiServer/Helpers/PluginAssetFileProvider.cs +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Controllers/FrontendController.cs b/Moonlight.ApiServer/Http/Controllers/FrontendController.cs index e997862f..58ee891e 100644 --- a/Moonlight.ApiServer/Http/Controllers/FrontendController.cs +++ b/Moonlight.ApiServer/Http/Controllers/FrontendController.cs @@ -13,17 +13,14 @@ public class FrontendController : Controller { private readonly AppConfiguration Configuration; private readonly PluginService PluginService; - private readonly AssetService AssetService; public FrontendController( AppConfiguration configuration, - PluginService pluginService, - AssetService assetService + PluginService pluginService ) { Configuration = configuration; PluginService = pluginService; - AssetService = assetService; } [HttpGet("frontend.json")] @@ -35,28 +32,55 @@ public class FrontendController : Controller ApiUrl = Configuration.PublicUrl, HostEnvironment = "ApiServer" }; - - // Load theme if it exists + + #region Load theme.json if it exists + var themePath = PathBuilder.File("storage", "theme.json"); if (System.IO.File.Exists(themePath)) { var variablesJson = await System.IO.File.ReadAllTextAsync(themePath); - configuration.Theme.Variables = JsonSerializer.Deserialize>(variablesJson) ?? new(); + configuration.Theme.Variables = + JsonSerializer.Deserialize>(variablesJson) ?? new(); } - configuration.Plugins.Entrypoints = PluginService.HostedPluginsManifest.Entrypoints; - configuration.Plugins.Assemblies = PluginService.HostedPluginsManifest.Assemblies; + #endregion - 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(); + + styles.AddRange( + PluginService + .LoadedPlugins + .Keys + .SelectMany(x => x.Styles) + ); + + // Add bundle css + styles.Add("css/bundle.min.css"); + + configuration.Styles = styles.ToArray(); return configuration; } - [HttpGet("plugins/{assemblyName}")] // TODO: Test this + [HttpGet("plugins/{assemblyName}")] public async Task GetPluginAssembly(string assemblyName) { - var assembliesMap = PluginService.ClientAssemblyMap; + var assembliesMap = PluginService.GetAssemblies("client"); if (assembliesMap.ContainsKey(assemblyName)) throw new HttpApiException("The requested assembly could not be found", 404); diff --git a/Moonlight.ApiServer/Models/PluginManifest.cs b/Moonlight.ApiServer/Models/PluginManifest.cs index a4393b46..bcd7bf18 100644 --- a/Moonlight.ApiServer/Models/PluginManifest.cs +++ b/Moonlight.ApiServer/Models/PluginManifest.cs @@ -7,5 +7,9 @@ public class PluginManifest public string Author { get; set; } public string[] Dependencies { get; set; } = []; - public Dictionary Entrypoints { get; set; } = new(); + public string[] Scripts { get; set; } = []; + public string[] Styles { get; set; } = []; + + public string[] BundledStyles { get; set; } = []; + public Dictionary Assemblies { get; set; } = new(); } \ No newline at end of file diff --git a/Moonlight.ApiServer/Models/PluginMeta.cs b/Moonlight.ApiServer/Models/PluginMeta.cs deleted file mode 100644 index b60c5dfb..00000000 --- a/Moonlight.ApiServer/Models/PluginMeta.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Moonlight.ApiServer.Models; - -public class PluginMeta -{ - public PluginManifest Manifest { get; set; } - public string Path { get; set; } -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Services/AssetService.cs b/Moonlight.ApiServer/Services/AssetService.cs deleted file mode 100644 index cd88475a..00000000 --- a/Moonlight.ApiServer/Services/AssetService.cs +++ /dev/null @@ -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 AdditionalCssAssets = new(); - private readonly List AdditionalJavascriptAssets = new(); - - private readonly PluginService PluginService; - - public AssetService(PluginService pluginService) - { - PluginService = pluginService; - } - - public void CollectAssets() - { - // Javascript - var jsFiles = new List(); - - 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; - } -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Services/BundleService.cs b/Moonlight.ApiServer/Services/BundleService.cs index 28aa6507..65dff75a 100644 --- a/Moonlight.ApiServer/Services/BundleService.cs +++ b/Moonlight.ApiServer/Services/BundleService.cs @@ -6,6 +6,9 @@ public class BundleService public void BundleCss(string path) => CssFiles.Add(path); + + public void BundleCssRange(string[] paths) + => CssFiles.AddRange(paths); public IEnumerable GetCssFiles() => CssFiles; } \ No newline at end of file diff --git a/Moonlight.ApiServer/Services/PluginService.cs b/Moonlight.ApiServer/Services/PluginService.cs index 1a7c5b8f..2892d95d 100644 --- a/Moonlight.ApiServer/Services/PluginService.cs +++ b/Moonlight.ApiServer/Services/PluginService.cs @@ -1,181 +1,136 @@ using System.Text.Json; +using Microsoft.Extensions.FileProviders; using MoonCore.Helpers; -using MoonCore.Models; using Moonlight.ApiServer.Models; -using Moonlight.Shared.Http.Responses.PluginsStream; namespace Moonlight.ApiServer.Services; public class PluginService { - public List Plugins { get; private set; } = new(); - public Dictionary AssetMap { get; private set; } = new(); - public HostedPluginsManifest HostedPluginsManifest { get; private set; } - public Dictionary ClientAssemblyMap { get; private set; } - - private static string PluginsFolder = PathBuilder.Dir("storage", "plugins"); private readonly ILogger Logger; + private readonly string PluginRoot; - private readonly JsonSerializerOptions SerializerOptions = new() - { - PropertyNameCaseInsensitive = true - }; + public readonly Dictionary LoadedPlugins = new(); + public IFileProvider WwwRootFileProvider; public PluginService(ILogger logger) { Logger = logger; + + PluginRoot = PathBuilder.Dir("storage", "plugins"); } public async Task Load() { - // Load all manifest files - foreach (var pluginFolder in Directory.EnumerateDirectories(PluginsFolder)) + var jsonOptions = new JsonSerializerOptions() { - var manifestPath = PathBuilder.File(pluginFolder, "plugin.json"); + PropertyNameCaseInsensitive = true + }; + + var pluginDirs = Directory.GetDirectories(PluginRoot); + var pluginMap = new Dictionary(); - if (!File.Exists(manifestPath)) + #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; } - PluginManifest manifest; + var json = await File.ReadAllTextAsync(metaPath); try { - var manifestText = await File.ReadAllTextAsync(manifestPath); - manifest = JsonSerializer.Deserialize(manifestText, SerializerOptions)!; + var meta = JsonSerializer.Deserialize(json, jsonOptions); + + 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}", - 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(); - - 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; + Logger.LogError("Unable to load plugin.json at '{path}': {e}", metaPath, e); } } - // Remove unloadable plugins from cache - Plugins.RemoveAll(x => pluginsToNotLoad.Contains(x.Manifest.Id)); - - // Generate assembly map for client - ClientAssemblyMap = GetAssemblies("client"); - - // Generate plugin stream manifest for client - HostedPluginsManifest = new() + #endregion + + #region Depdenency check + + foreach (var plugin in pluginMap.Keys) { - Assemblies = ClientAssemblyMap.Keys.ToArray(), - Entrypoints = GetEntrypoints("client") - }; - - // Generate asset map - GenerateAssetMap(); + var hasMissingDep = false; + + foreach (var dependency in plugin.Dependencies) + { + 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 GetAssemblies(string section) { - var pathMappings = new Dictionary(); + var assemblyMap = new Dictionary(); - foreach (var plugin in Plugins) + foreach (var loadedPlugin in LoadedPlugins.Keys) { - var binaryPath = PathBuilder.Dir(plugin.Path, "bin", section); - - if(!Directory.Exists(binaryPath)) + // Skip all plugins which haven't defined any assemblies in that section + if (!loadedPlugin.Assemblies.ContainsKey(section)) continue; - - foreach (var file in Directory.EnumerateFiles(binaryPath)) - { - if (!file.EndsWith(".dll")) - continue; - var fileName = Path.GetFileName(file); - pathMappings[fileName] = file; + var pluginPath = LoadedPlugins[loadedPlugin]; + + foreach (var assembly in loadedPlugin.Assemblies[section]) + { + 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 - .Where(x => x.Manifest.Entrypoints.ContainsKey(section)) - .SelectMany(x => x.Manifest.Entrypoints[section]) - .ToArray(); - } + List wwwRootProviders = new(); - private void GenerateAssetMap() - { - AssetMap.Clear(); - - foreach (var plugin in Plugins) + foreach (var pluginFolder in LoadedPlugins.Values) { - var assetPath = PathBuilder.Dir(plugin.Path, "wwwroot"); + var wwwRootPath = Path.GetFullPath( + PathBuilder.Dir(pluginFolder, "wwwroot") + ); - if (!Directory.Exists(assetPath)) - continue; - - var files = new List(); - 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; - } + wwwRootProviders.Add( + new PhysicalFileProvider(wwwRootPath) + ); } - } - private void GetFilesInDirectory(string directory, List files) - { - files.AddRange(Directory.EnumerateFiles(directory)); - - foreach (var dir in Directory.EnumerateDirectories(directory)) - GetFilesInDirectory(dir, files); + return new CompositeFileProvider(wwwRootProviders); } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Startup.cs b/Moonlight.ApiServer/Startup.cs index 6cf4ef89..b5d3858a 100644 --- a/Moonlight.ApiServer/Startup.cs +++ b/Moonlight.ApiServer/Startup.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.Loader; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -19,7 +20,9 @@ using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Helpers; using Moonlight.ApiServer.Interfaces.OAuth2; using Moonlight.ApiServer.Interfaces.Startup; +using Moonlight.ApiServer.Models; using Moonlight.ApiServer.Services; +using Moonlight.Client.Services; namespace Moonlight.ApiServer; @@ -30,6 +33,7 @@ public class Startup { private string[] Args; private Assembly[] AdditionalAssemblies; + private PluginManifest[] AdditionalPluginManifests; // Logging private ILoggerProvider[] LoggerProviders; @@ -47,17 +51,18 @@ public class Startup // Plugin Loading private PluginService PluginService; - private PluginLoaderService PluginLoaderService; + private AssemblyLoadContext PluginLoadContext; // Asset bundling - private BundleService BundleService; + private BundleService BundleService = new(); 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; AdditionalAssemblies = additionalAssemblies ?? []; + AdditionalPluginManifests = additionalManifests ?? []; await PrintVersion(); @@ -76,18 +81,16 @@ public class Startup await RegisterAuth(); await RegisterCaching(); await HookPluginBuild(); - await HandleConfigureArguments(); await RegisterPluginAssets(); await BuildWebApplication(); - await HandleServiceArguments(); await PrepareDatabase(); + await UsePluginAssets(); // We need to move the plugin assets to the top to allow plugins to override content await UseBase(); await UseAuth(); await HookPluginConfigure(); - await UsePluginAssets(); await MapBase(); await HookPluginEndpoints(); @@ -123,73 +126,6 @@ public class Startup 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(); - - 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 private Task RegisterBase() @@ -207,7 +143,7 @@ public class Startup var mvcBuilder = WebApplicationBuilder.Services.AddControllers(); // Add plugin and additional assemblies as application parts - foreach (var pluginAssembly in PluginLoaderService.PluginAssemblies) + foreach (var pluginAssembly in PluginLoadContext.Assemblies) mvcBuilder.AddApplicationPart(pluginAssembly); foreach (var additionalAssembly in AdditionalAssemblies) @@ -238,9 +174,7 @@ public class Startup WebApplication.MapControllers(); if (Configuration.Client.Enable) - { WebApplication.MapFallbackToFile("index.html"); - } return Task.CompletedTask; } @@ -255,26 +189,33 @@ public class Startup PluginService = new PluginService( LoggerFactory.CreateLogger() ); + + // 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(); - // Initialize api server plugin loader - PluginLoaderService = new PluginLoaderService( - LoggerFactory.CreateLogger() - ); - - // Search up entrypoints and assemblies for the apiServer + // Search up assemblies for the apiServer var assemblyFiles = PluginService.GetAssemblies("apiServer") .Values .ToArray(); - var entrypoints = PluginService.GetEntrypoints("apiServer"); + // Create the load context and add assemblies + PluginLoadContext = new AssemblyLoadContext(null); - // Build source from the retrieved data - PluginLoaderService.AddFilesSource(assemblyFiles, entrypoints); - - // Perform assembly loading - await PluginLoaderService.Load(); + foreach (var assemblyFile in assemblyFiles) + { + try + { + PluginLoadContext.LoadFromAssemblyPath(assemblyFile); + } + catch (Exception e) + { + Logger.LogError("Unable to load plugin assembly '{assemblyFile}': {e}", assemblyFile, e); + } + } } private Task InitializePlugins() @@ -284,9 +225,13 @@ public class Startup // Configure base services for initialisation startupSc.AddSingleton(Configuration); - - BundleService = new BundleService(); + + // Add bundle service so plugins can do additional bundling if required 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 => { @@ -304,7 +249,7 @@ public class Startup var assembliesToScan = new List(); assembliesToScan.Add(typeof(Startup).Assembly); - assembliesToScan.AddRange(PluginLoaderService.PluginAssemblies); + assembliesToScan.AddRange(PluginLoadContext.Assemblies); assembliesToScan.AddRange(AdditionalAssemblies); foreach (var pluginAssembly in assembliesToScan) @@ -348,7 +293,7 @@ public class Startup WebApplication.UseStaticFiles(new StaticFileOptions() { - FileProvider = new PluginAssetFileProvider(PluginService) + FileProvider = PluginService.WwwRootFileProvider }); return Task.CompletedTask; diff --git a/Moonlight.Client/Implementations/RemotePluginSource.cs b/Moonlight.Client/Implementations/RemotePluginSource.cs deleted file mode 100644 index eba47421..00000000 --- a/Moonlight.Client/Implementations/RemotePluginSource.cs +++ /dev/null @@ -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 Logger; - private readonly HttpClient HttpClient; - - public RemotePluginSource( - FrontendConfiguration configuration, - ILogger logger, - HttpClient httpClient - ) - { - Configuration = configuration; - Logger = logger; - HttpClient = httpClient; - } - - public async Task Load(AssemblyLoadContext loadContext, List 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); - } - } - } -} \ No newline at end of file diff --git a/Moonlight.Client/Services/ApplicationAssemblyService.cs b/Moonlight.Client/Services/ApplicationAssemblyService.cs index f7e4eb1e..2c0ec274 100644 --- a/Moonlight.Client/Services/ApplicationAssemblyService.cs +++ b/Moonlight.Client/Services/ApplicationAssemblyService.cs @@ -4,7 +4,5 @@ namespace Moonlight.Client.Services; public class ApplicationAssemblyService { - public Assembly[] AdditionalAssemblies { get; set; } - public Assembly[] PluginAssemblies { get; set; } - public Assembly[] NavigationAssemblies => PluginAssemblies.Concat(AdditionalAssemblies).ToArray(); + public List Assemblies { get; set; } = new(); } \ No newline at end of file diff --git a/Moonlight.Client/Startup.cs b/Moonlight.Client/Startup.cs index 047503d8..b83d19c1 100644 --- a/Moonlight.Client/Startup.cs +++ b/Moonlight.Client/Startup.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.Loader; using System.Text.Json; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; @@ -34,20 +35,15 @@ public class Startup private WebAssemblyHost WebAssemblyHost; // Plugin Loading - private PluginLoaderService PluginLoaderService; - private ApplicationAssemblyService ApplicationAssemblyService; + private AssemblyLoadContext PluginLoadContext; + private Assembly[] AdditionalAssemblies; private IPluginStartup[] PluginStartups; - public async Task Run(string[] args, Assembly[]? assemblies = null) + public async Task Run(string[] args, Assembly[]? additionalAssemblies = null) { Args = args; - - // Setup assembly storage - ApplicationAssemblyService = new() - { - AdditionalAssemblies = assemblies ?? [] - }; + AdditionalAssemblies = additionalAssemblies ?? []; await PrintVersion(); await SetupLogging(); @@ -174,11 +170,6 @@ public class Startup private async Task LoadPlugins() { - // Initialize api server plugin loader - PluginLoaderService = new PluginLoaderService( - LoggerFactory.CreateLogger() - ); - // Create everything required to stream plugins using var clientForStreaming = new HttpClient(); @@ -187,19 +178,21 @@ public class Startup : WebAssemblyHostBuilder.HostEnvironment.BaseAddress ); - PluginLoaderService.AddSource(new RemotePluginSource( - Configuration, - LoggerFactory.CreateLogger(), - clientForStreaming - )); + PluginLoadContext = new AssemblyLoadContext(null); - // Perform assembly loading - await PluginLoaderService.Load(); + foreach (var assembly in Configuration.Assemblies) + { + var assemblyStream = await clientForStreaming.GetStreamAsync($"plugins/{assembly}"); + PluginLoadContext.LoadFromStream(assemblyStream); + } + + // Add application assembly service + var appAssemblyService = new ApplicationAssemblyService(); + + appAssemblyService.Assemblies.AddRange(AdditionalAssemblies); + appAssemblyService.Assemblies.AddRange(PluginLoadContext.Assemblies); - // Add plugin loader service to di for the Router/App.razor - ApplicationAssemblyService.PluginAssemblies = PluginLoaderService.PluginAssemblies; - - WebAssemblyHostBuilder.Services.AddSingleton(ApplicationAssemblyService); + WebAssemblyHostBuilder.Services.AddSingleton(appAssemblyService); } private Task InitializePlugins() @@ -224,8 +217,8 @@ public class Startup var assembliesToScan = new List(); assembliesToScan.Add(typeof(Startup).Assembly); - assembliesToScan.AddRange(PluginLoaderService.PluginAssemblies); - assembliesToScan.AddRange(ApplicationAssemblyService.AdditionalAssemblies); + assembliesToScan.AddRange(AdditionalAssemblies); + assembliesToScan.AddRange(PluginLoadContext.Assemblies); foreach (var pluginAssembly in assembliesToScan) { diff --git a/Moonlight.Client/UI/App.razor b/Moonlight.Client/UI/App.razor index 0c6fc580..056f1edc 100644 --- a/Moonlight.Client/UI/App.razor +++ b/Moonlight.Client/UI/App.razor @@ -5,4 +5,4 @@ \ No newline at end of file + AdditionalAssemblies="ApplicationAssemblyService.Assemblies" /> \ No newline at end of file diff --git a/Moonlight.Client/UI/Partials/AppSidebar.razor b/Moonlight.Client/UI/Partials/AppSidebar.razor index 4e1012a3..455306e0 100644 --- a/Moonlight.Client/UI/Partials/AppSidebar.razor +++ b/Moonlight.Client/UI/Partials/AppSidebar.razor @@ -71,7 +71,7 @@