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