diff --git a/.gitignore b/.gitignore index ed60e2bba..72025a549 100644 --- a/.gitignore +++ b/.gitignore @@ -298,3 +298,4 @@ Packed Plugins Plugins/packed BTCPayServer/wwwroot/swagger/v1/openapi.json +BTCPayServer/appsettings.dev.json \ No newline at end of file diff --git a/BTCPayServer.Common/BTCPayServer.Common.csproj b/BTCPayServer.Common/BTCPayServer.Common.csproj index a92b201f7..9535980ce 100644 --- a/BTCPayServer.Common/BTCPayServer.Common.csproj +++ b/BTCPayServer.Common/BTCPayServer.Common.csproj @@ -5,7 +5,7 @@ - + diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 656186350..a25291574 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -60,8 +60,8 @@ - - + + diff --git a/BTCPayServer/Plugins/PluginManager.cs b/BTCPayServer/Plugins/PluginManager.cs index 3bc7c6a5f..8d69dcd2c 100644 --- a/BTCPayServer/Plugins/PluginManager.cs +++ b/BTCPayServer/Plugins/PluginManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Globalization; using System.IO; using System.IO.Compression; @@ -22,10 +23,7 @@ namespace BTCPayServer.Plugins { public const string BTCPayPluginSuffix = ".btcpay"; private static readonly List _pluginAssemblies = new List(); - private static readonly List _plugins = new List(); - private static ILogger _logger; - private static List<(PluginLoader, Assembly, IFileProvider)> loadedPlugins; public static bool IsExceptionByPlugin(Exception exception) { return _pluginAssemblies.Any(assembly => assembly?.FullName?.Contains(exception.Source!, StringComparison.OrdinalIgnoreCase) is true); @@ -33,82 +31,81 @@ namespace BTCPayServer.Plugins public static IMvcBuilder AddPlugins(this IMvcBuilder mvcBuilder, IServiceCollection serviceCollection, IConfiguration config, ILoggerFactory loggerFactory) { - _logger = loggerFactory.CreateLogger(typeof(PluginManager)); + var logger = loggerFactory.CreateLogger(typeof(PluginManager)); var pluginsFolder = new DataDirectories().Configure(config).PluginDir; var plugins = new List(); + var loadedPluginIdentifiers = new HashSet(); serviceCollection.Configure(options => { options.Limits.MaxRequestBodySize = int.MaxValue; // if don't set default value is: 30 MB }); - _logger.LogInformation($"Loading plugins from {pluginsFolder}"); + logger.LogInformation($"Loading plugins from {pluginsFolder}"); Directory.CreateDirectory(pluginsFolder); ExecuteCommands(pluginsFolder); - loadedPlugins = new List<(PluginLoader, Assembly, IFileProvider)>(); - var systemPlugins = GetDefaultLoadedPluginAssemblies(); - - foreach (Assembly systemExtension in systemPlugins) - { - var detectedPlugins = GetAllPluginTypesFromAssembly(systemExtension).Select(GetPluginInstanceFromType); - if (!detectedPlugins.Any()) - { - continue; - - } - - detectedPlugins = detectedPlugins.Select(plugin => - { - plugin.SystemPlugin = true; - return plugin; - }); - - loadedPlugins.Add((null, systemExtension, CreateEmbeddedFileProviderForAssembly(systemExtension))); - plugins.AddRange(detectedPlugins); - } - var orderFilePath = Path.Combine(pluginsFolder, "order"); - - var availableDirs = Directory.GetDirectories(pluginsFolder); - var orderedDirs = new List(); - if (File.Exists(orderFilePath)) - { - var order = File.ReadLines(orderFilePath); - foreach (var s in order) - { - if (availableDirs.Contains(s)) - { - orderedDirs.Add(s); - } - } - - orderedDirs.AddRange(availableDirs.Where(s => !orderedDirs.Contains(s))); - } - else - { - orderedDirs = availableDirs.ToList(); - } var disabledPlugins = GetDisabledPlugins(pluginsFolder); - - foreach (var dir in orderedDirs) + var systemAssembly = typeof(Program).Assembly; + // Load the referenced assembly plugins + // All referenced plugins should have at least one plugin with exact same plugin identifier + // as the assembly. Except for the system assembly (btcpayserver assembly) which are fake plugins + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { - var pluginName = Path.GetFileName(dir); - var pluginFilePath = Path.Combine(dir, pluginName + ".dll"); - if (disabledPlugins.Contains(pluginName)) - { + var pluginIdentifier = assembly.GetName().Name; + bool isSystemPlugin = assembly == systemAssembly; + if (!isSystemPlugin && disabledPlugins.Contains(pluginIdentifier)) continue; - } - if (!File.Exists(pluginFilePath)) - { - _logger.LogError( - $"Error when loading plugin {pluginName} - {pluginFilePath} does not exist"); - continue; - } + foreach (var plugin in GetPluginInstancesFromAssembly(assembly)) + { + if (!isSystemPlugin && plugin.Identifier != pluginIdentifier) + continue; + if (!loadedPluginIdentifiers.Add(plugin.Identifier)) + continue; + plugins.Add(plugin); + plugin.SystemPlugin = isSystemPlugin; + } + } + + var pluginsToLoad = new List<(string PluginIdentifier, string PluginFilePath)>(); + +#if DEBUG + // Load from DEBUG_PLUGINS, in an optional appsettings.dev.json + var debugPlugins = config["DEBUG_PLUGINS"] ?? ""; + foreach (var plugin in debugPlugins.Split(';', StringSplitOptions.RemoveEmptyEntries)) + { + // Formatted either as "::" or "" + var idx = plugin.IndexOf("::"); + if (idx != -1) + pluginsToLoad.Add((plugin[0..idx], plugin[(idx+1)..])); + else + pluginsToLoad.Add((Path.GetFileNameWithoutExtension(plugin), plugin)); + } +#endif + + // Load from the plugins folder + foreach (var directory in Directory.GetDirectories(pluginsFolder)) + { + var pluginIdentifier = Path.GetDirectoryName(directory); + var pluginFilePath = Path.Combine(directory, pluginIdentifier + ".dll"); + if (!File.Exists(pluginFilePath)) + continue; + if (disabledPlugins.Contains(pluginIdentifier)) + continue; + pluginsToLoad.Add((pluginIdentifier, pluginFilePath)); + } + + ReorderPlugins(pluginsFolder, pluginsToLoad); + + foreach (var toLoad in pluginsToLoad) + { + if (!loadedPluginIdentifiers.Add(toLoad.PluginIdentifier)) + continue; try { var plugin = PluginLoader.CreateFromAssemblyFile( - pluginFilePath, // create a plugin from for the .dll file + toLoad.PluginFilePath, // create a plugin from for the .dll file config => { @@ -116,25 +113,25 @@ namespace BTCPayServer.Plugins config.PreferSharedTypes = true; config.IsUnloadable = true; }); - - mvcBuilder.AddPluginLoader(plugin); var pluginAssembly = plugin.LoadDefaultAssembly(); - _pluginAssemblies.Add(pluginAssembly); - _plugins.Add(plugin); - var fileProvider = CreateEmbeddedFileProviderForAssembly(pluginAssembly); - loadedPlugins.Add((plugin, pluginAssembly, fileProvider)); - foreach (var p in GetAllPluginTypesFromAssembly(pluginAssembly) - .Select(GetPluginInstanceFromType)) + + var p = GetPluginInstanceFromAssembly(toLoad.PluginIdentifier, pluginAssembly); + if (p == null) { + logger.LogError($"The plugin assembly doesn't contain the plugin {toLoad.PluginIdentifier}"); + } + else + { + mvcBuilder.AddPluginLoader(plugin); + _pluginAssemblies.Add(pluginAssembly); p.SystemPlugin = false; plugins.Add(p); } - } catch (Exception e) { - _logger.LogError(e, - $"Error when loading plugin {pluginName}"); + logger.LogError(e, + $"Error when loading plugin {toLoad.PluginIdentifier}"); } } @@ -142,14 +139,14 @@ namespace BTCPayServer.Plugins { try { - _logger.LogInformation( + logger.LogInformation( $"Adding and executing plugin {plugin.Identifier} - {plugin.Version}"); plugin.Execute(serviceCollection); serviceCollection.AddSingleton(plugin); } catch (Exception e) { - _logger.LogError( + logger.LogError( $"Error when loading plugin {plugin.Identifier} - {plugin.Version}{Environment.NewLine}{e.Message}"); } } @@ -157,42 +154,56 @@ namespace BTCPayServer.Plugins return mvcBuilder; } + private static void ReorderPlugins(string pluginsFolder, List<(string PluginIdentifier, string PluginFilePath)> pluginsToLoad) + { + Dictionary ordersByPlugin = new Dictionary(); + var orderFilePath = Path.Combine(pluginsFolder, "order"); + int order = 0; + if (File.Exists(orderFilePath)) + { + foreach (var o in File.ReadLines(orderFilePath)) + { + if (ordersByPlugin.TryAdd(o, order)) + order++; + } + } + foreach (var p in pluginsToLoad) + { + if (ordersByPlugin.TryAdd(p.PluginIdentifier, order)) + order++; + } + pluginsToLoad.Sort((a,b) => ordersByPlugin[a.PluginIdentifier] - ordersByPlugin[b.PluginIdentifier]); + } + public static void UsePlugins(this IApplicationBuilder applicationBuilder) { + HashSet assemblies = new HashSet(); foreach (var extension in applicationBuilder.ApplicationServices .GetServices()) { extension.Execute(applicationBuilder, applicationBuilder.ApplicationServices); + assemblies.Add(extension.GetType().Assembly); } var webHostEnvironment = applicationBuilder.ApplicationServices.GetService(); List providers = new List() { webHostEnvironment.WebRootFileProvider }; - providers.AddRange(loadedPlugins.Select(tuple => tuple.Item3)); + providers.AddRange(assemblies.Select(a => new EmbeddedFileProvider(a))); webHostEnvironment.WebRootFileProvider = new CompositeFileProvider(providers); } - private static Assembly[] GetDefaultLoadedPluginAssemblies() - { - return AppDomain.CurrentDomain.GetAssemblies() - .ToArray(); - } - - private static Type[] GetAllPluginTypesFromAssembly(Assembly assembly) + private static IEnumerable GetPluginInstancesFromAssembly(Assembly assembly) { return assembly.GetTypes().Where(type => typeof(IBTCPayServerPlugin).IsAssignableFrom(type) && type != typeof(PluginService.AvailablePlugin) && - !type.IsAbstract).ToArray(); + !type.IsAbstract). + Select(type => (IBTCPayServerPlugin)Activator.CreateInstance(type, Array.Empty())); } - - private static IBTCPayServerPlugin GetPluginInstanceFromType(Type type) + private static IBTCPayServerPlugin GetPluginInstanceFromAssembly(string pluginIdentifier, Assembly assembly) { - return (IBTCPayServerPlugin)Activator.CreateInstance(type, Array.Empty()); - } - - private static IFileProvider CreateEmbeddedFileProviderForAssembly(Assembly assembly) - { - return new EmbeddedFileProvider(assembly); + return GetPluginInstancesFromAssembly(assembly) + .Where(plugin => plugin.Identifier == pluginIdentifier) + .FirstOrDefault(); } private static void ExecuteCommands(string pluginsFolder) @@ -318,20 +329,15 @@ namespace BTCPayServer.Plugins QueueCommands(pluginDir, ("disable", plugin)); } - public static void Unload() - { - _plugins.ForEach(loader => loader.Dispose()); - } - - public static string[] GetDisabledPlugins(string pluginsFolder) + public static HashSet GetDisabledPlugins(string pluginsFolder) { var disabledFilePath = Path.Combine(pluginsFolder, "disabled"); if (File.Exists(disabledFilePath)) { - return File.ReadLines(disabledFilePath).ToArray(); + return File.ReadLines(disabledFilePath).ToHashSet(); } - return Array.Empty(); + return new HashSet(); } } } diff --git a/BTCPayServer/Plugins/PluginService.cs b/BTCPayServer/Plugins/PluginService.cs index 3f6a66abe..bfa46d3e0 100644 --- a/BTCPayServer/Plugins/PluginService.cs +++ b/BTCPayServer/Plugins/PluginService.cs @@ -142,7 +142,7 @@ namespace BTCPayServer.Plugins public string[] GetDisabledPlugins() { - return PluginManager.GetDisabledPlugins(_dataDirectories.Value.PluginDir); + return PluginManager.GetDisabledPlugins(_dataDirectories.Value.PluginDir).ToArray(); } } } diff --git a/BTCPayServer/Program.cs b/BTCPayServer/Program.cs index ce0bd08b3..0aff901ed 100644 --- a/BTCPayServer/Program.cs +++ b/BTCPayServer/Program.cs @@ -35,7 +35,11 @@ namespace BTCPayServer IConfiguration conf = null; try { - conf = new DefaultConfiguration() { Logger = logger }.CreateConfiguration(args); + var confBuilder = new DefaultConfiguration() { Logger = logger }.CreateConfigurationBuilder(args); +#if DEBUG + confBuilder.AddJsonFile("appsettings.dev.json", true, false); +#endif + conf = confBuilder.Build(); if (conf == null) return;