Can load external plugins during dev to debug more easily (#4518)

* Can load external plugins during dev to debug more easily

* Add again load plugin by project reference

* Make sure we don't load same plugin twice
This commit is contained in:
Nicolas Dorier 2023-01-16 10:37:17 +09:00 committed by GitHub
parent e4237c9511
commit 2e31816979
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 115 additions and 104 deletions

1
.gitignore vendored
View file

@ -298,3 +298,4 @@ Packed Plugins
Plugins/packed Plugins/packed
BTCPayServer/wwwroot/swagger/v1/openapi.json BTCPayServer/wwwroot/swagger/v1/openapi.json
BTCPayServer/appsettings.dev.json

View file

@ -5,7 +5,7 @@
<ItemGroup> <ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="4.2.3" /> <PackageReference Include="NBXplorer.Client" Version="4.2.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" /> <PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'"> <ItemGroup Condition="'$(Altcoins)' != 'true'">
<Compile Remove="Altcoins\**\*.cs"></Compile> <Compile Remove="Altcoins\**\*.cs"></Compile>

View file

@ -60,8 +60,8 @@
<PackageReference Include="System.IO.Pipelines" Version="6.0.3" /> <PackageReference Include="System.IO.Pipelines" Version="6.0.3" />
<PackageReference Include="NBitpayClient" Version="1.0.0.39" /> <PackageReference Include="NBitpayClient" Version="1.0.0.39" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" /> <PackageReference Include="NicolasDorier.CommandLine" Version="2.0.0" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" /> <PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="2.0.0" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.2.3" /> <PackageReference Include="NicolasDorier.RateLimits" Version="1.2.3" />
<PackageReference Include="Serilog" Version="2.9.0" /> <PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
@ -22,10 +23,7 @@ namespace BTCPayServer.Plugins
{ {
public const string BTCPayPluginSuffix = ".btcpay"; public const string BTCPayPluginSuffix = ".btcpay";
private static readonly List<Assembly> _pluginAssemblies = new List<Assembly>(); private static readonly List<Assembly> _pluginAssemblies = new List<Assembly>();
private static readonly List<PluginLoader> _plugins = new List<PluginLoader>();
private static ILogger _logger;
private static List<(PluginLoader, Assembly, IFileProvider)> loadedPlugins;
public static bool IsExceptionByPlugin(Exception exception) public static bool IsExceptionByPlugin(Exception exception)
{ {
return _pluginAssemblies.Any(assembly => assembly?.FullName?.Contains(exception.Source!, StringComparison.OrdinalIgnoreCase) is true); 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, public static IMvcBuilder AddPlugins(this IMvcBuilder mvcBuilder, IServiceCollection serviceCollection,
IConfiguration config, ILoggerFactory loggerFactory) IConfiguration config, ILoggerFactory loggerFactory)
{ {
_logger = loggerFactory.CreateLogger(typeof(PluginManager)); var logger = loggerFactory.CreateLogger(typeof(PluginManager));
var pluginsFolder = new DataDirectories().Configure(config).PluginDir; var pluginsFolder = new DataDirectories().Configure(config).PluginDir;
var plugins = new List<IBTCPayServerPlugin>(); var plugins = new List<IBTCPayServerPlugin>();
var loadedPluginIdentifiers = new HashSet<string>();
serviceCollection.Configure<KestrelServerOptions>(options => serviceCollection.Configure<KestrelServerOptions>(options =>
{ {
options.Limits.MaxRequestBodySize = int.MaxValue; // if don't set default value is: 30 MB 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); Directory.CreateDirectory(pluginsFolder);
ExecuteCommands(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<string>();
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); var disabledPlugins = GetDisabledPlugins(pluginsFolder);
var systemAssembly = typeof(Program).Assembly;
foreach (var dir in orderedDirs) // Load the referenced assembly plugins
{ // All referenced plugins should have at least one plugin with exact same plugin identifier
var pluginName = Path.GetFileName(dir); // as the assembly. Except for the system assembly (btcpayserver assembly) which are fake plugins
var pluginFilePath = Path.Combine(dir, pluginName + ".dll"); foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
if (disabledPlugins.Contains(pluginName))
{ {
var pluginIdentifier = assembly.GetName().Name;
bool isSystemPlugin = assembly == systemAssembly;
if (!isSystemPlugin && disabledPlugins.Contains(pluginIdentifier))
continue; 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 "<PLUGIN_IDENTIFIER>::<PathToDll>" or "<PathToDll>"
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)) if (!File.Exists(pluginFilePath))
{
_logger.LogError(
$"Error when loading plugin {pluginName} - {pluginFilePath} does not exist");
continue; 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 try
{ {
var plugin = PluginLoader.CreateFromAssemblyFile( var plugin = PluginLoader.CreateFromAssemblyFile(
pluginFilePath, // create a plugin from for the .dll file toLoad.PluginFilePath, // create a plugin from for the .dll file
config => config =>
{ {
@ -116,25 +113,25 @@ namespace BTCPayServer.Plugins
config.PreferSharedTypes = true; config.PreferSharedTypes = true;
config.IsUnloadable = true; config.IsUnloadable = true;
}); });
mvcBuilder.AddPluginLoader(plugin);
var pluginAssembly = plugin.LoadDefaultAssembly(); var pluginAssembly = plugin.LoadDefaultAssembly();
_pluginAssemblies.Add(pluginAssembly);
_plugins.Add(plugin); var p = GetPluginInstanceFromAssembly(toLoad.PluginIdentifier, pluginAssembly);
var fileProvider = CreateEmbeddedFileProviderForAssembly(pluginAssembly); if (p == null)
loadedPlugins.Add((plugin, pluginAssembly, fileProvider));
foreach (var p in GetAllPluginTypesFromAssembly(pluginAssembly)
.Select(GetPluginInstanceFromType))
{ {
logger.LogError($"The plugin assembly doesn't contain the plugin {toLoad.PluginIdentifier}");
}
else
{
mvcBuilder.AddPluginLoader(plugin);
_pluginAssemblies.Add(pluginAssembly);
p.SystemPlugin = false; p.SystemPlugin = false;
plugins.Add(p); plugins.Add(p);
} }
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, logger.LogError(e,
$"Error when loading plugin {pluginName}"); $"Error when loading plugin {toLoad.PluginIdentifier}");
} }
} }
@ -142,14 +139,14 @@ namespace BTCPayServer.Plugins
{ {
try try
{ {
_logger.LogInformation( logger.LogInformation(
$"Adding and executing plugin {plugin.Identifier} - {plugin.Version}"); $"Adding and executing plugin {plugin.Identifier} - {plugin.Version}");
plugin.Execute(serviceCollection); plugin.Execute(serviceCollection);
serviceCollection.AddSingleton(plugin); serviceCollection.AddSingleton(plugin);
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError( logger.LogError(
$"Error when loading plugin {plugin.Identifier} - {plugin.Version}{Environment.NewLine}{e.Message}"); $"Error when loading plugin {plugin.Identifier} - {plugin.Version}{Environment.NewLine}{e.Message}");
} }
} }
@ -157,42 +154,56 @@ namespace BTCPayServer.Plugins
return mvcBuilder; return mvcBuilder;
} }
private static void ReorderPlugins(string pluginsFolder, List<(string PluginIdentifier, string PluginFilePath)> pluginsToLoad)
{
Dictionary<string, int> ordersByPlugin = new Dictionary<string, int>();
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) public static void UsePlugins(this IApplicationBuilder applicationBuilder)
{ {
HashSet<Assembly> assemblies = new HashSet<Assembly>();
foreach (var extension in applicationBuilder.ApplicationServices foreach (var extension in applicationBuilder.ApplicationServices
.GetServices<IBTCPayServerPlugin>()) .GetServices<IBTCPayServerPlugin>())
{ {
extension.Execute(applicationBuilder, extension.Execute(applicationBuilder,
applicationBuilder.ApplicationServices); applicationBuilder.ApplicationServices);
assemblies.Add(extension.GetType().Assembly);
} }
var webHostEnvironment = applicationBuilder.ApplicationServices.GetService<IWebHostEnvironment>(); var webHostEnvironment = applicationBuilder.ApplicationServices.GetService<IWebHostEnvironment>();
List<IFileProvider> providers = new List<IFileProvider>() { webHostEnvironment.WebRootFileProvider }; List<IFileProvider> providers = new List<IFileProvider>() { webHostEnvironment.WebRootFileProvider };
providers.AddRange(loadedPlugins.Select(tuple => tuple.Item3)); providers.AddRange(assemblies.Select(a => new EmbeddedFileProvider(a)));
webHostEnvironment.WebRootFileProvider = new CompositeFileProvider(providers); webHostEnvironment.WebRootFileProvider = new CompositeFileProvider(providers);
} }
private static Assembly[] GetDefaultLoadedPluginAssemblies() private static IEnumerable<IBTCPayServerPlugin> GetPluginInstancesFromAssembly(Assembly assembly)
{
return AppDomain.CurrentDomain.GetAssemblies()
.ToArray();
}
private static Type[] GetAllPluginTypesFromAssembly(Assembly assembly)
{ {
return assembly.GetTypes().Where(type => return assembly.GetTypes().Where(type =>
typeof(IBTCPayServerPlugin).IsAssignableFrom(type) && type != typeof(PluginService.AvailablePlugin) && typeof(IBTCPayServerPlugin).IsAssignableFrom(type) && type != typeof(PluginService.AvailablePlugin) &&
!type.IsAbstract).ToArray(); !type.IsAbstract).
Select(type => (IBTCPayServerPlugin)Activator.CreateInstance(type, Array.Empty<object>()));
} }
private static IBTCPayServerPlugin GetPluginInstanceFromAssembly(string pluginIdentifier, Assembly assembly)
private static IBTCPayServerPlugin GetPluginInstanceFromType(Type type)
{ {
return (IBTCPayServerPlugin)Activator.CreateInstance(type, Array.Empty<object>()); return GetPluginInstancesFromAssembly(assembly)
} .Where(plugin => plugin.Identifier == pluginIdentifier)
.FirstOrDefault();
private static IFileProvider CreateEmbeddedFileProviderForAssembly(Assembly assembly)
{
return new EmbeddedFileProvider(assembly);
} }
private static void ExecuteCommands(string pluginsFolder) private static void ExecuteCommands(string pluginsFolder)
@ -318,20 +329,15 @@ namespace BTCPayServer.Plugins
QueueCommands(pluginDir, ("disable", plugin)); QueueCommands(pluginDir, ("disable", plugin));
} }
public static void Unload() public static HashSet<string> GetDisabledPlugins(string pluginsFolder)
{
_plugins.ForEach(loader => loader.Dispose());
}
public static string[] GetDisabledPlugins(string pluginsFolder)
{ {
var disabledFilePath = Path.Combine(pluginsFolder, "disabled"); var disabledFilePath = Path.Combine(pluginsFolder, "disabled");
if (File.Exists(disabledFilePath)) if (File.Exists(disabledFilePath))
{ {
return File.ReadLines(disabledFilePath).ToArray(); return File.ReadLines(disabledFilePath).ToHashSet();
} }
return Array.Empty<string>(); return new HashSet<string>();
} }
} }
} }

View file

@ -142,7 +142,7 @@ namespace BTCPayServer.Plugins
public string[] GetDisabledPlugins() public string[] GetDisabledPlugins()
{ {
return PluginManager.GetDisabledPlugins(_dataDirectories.Value.PluginDir); return PluginManager.GetDisabledPlugins(_dataDirectories.Value.PluginDir).ToArray();
} }
} }
} }

View file

@ -35,7 +35,11 @@ namespace BTCPayServer
IConfiguration conf = null; IConfiguration conf = null;
try 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) if (conf == null)
return; return;