Plugin FailSafe (#2351)

This introduces the concept of plugins being disabled in the case of an unrecoverable runtime error caused by a plugin.
This commit is contained in:
Andrew Camilleri 2021-04-01 05:27:22 +02:00 committed by GitHub
parent 64db865e1e
commit 6ead5c3800
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 139 additions and 6 deletions

View file

@ -39,6 +39,7 @@ namespace BTCPayServer.Controllers
Installed = pluginService.LoadedPlugins,
Available = availablePlugins,
Commands = pluginService.GetPendingCommands(),
Disabled = pluginService.GetDisabledPlugins(),
CanShowRestart = btcPayServerOptions.DockerDeployment
};
return View(res);
@ -50,6 +51,7 @@ namespace BTCPayServer.Controllers
public IEnumerable<PluginService.AvailablePlugin> Available { get; set; }
public (string command, string plugin)[] Commands { get; set; }
public bool CanShowRestart { get; set; }
public string[] Disabled { get; set; }
}
[HttpPost("server/plugins/uninstall")]

View file

@ -23,8 +23,13 @@ namespace BTCPayServer.Plugins
{
public const string BTCPayPluginSuffix = ".btcpay";
private static readonly List<Assembly> _pluginAssemblies = new List<Assembly>();
private static readonly List<PluginLoader> _plugins = new List<PluginLoader>();
private static ILogger _logger;
public static bool IsExceptionByPlugin(Exception exception)
{
return _pluginAssemblies.Any(assembly => assembly.FullName.Contains(exception.Source));
}
public static IMvcBuilder AddPlugins(this IMvcBuilder mvcBuilder, IServiceCollection serviceCollection,
IConfiguration config, ILoggerFactory loggerFactory)
{
@ -50,6 +55,7 @@ namespace BTCPayServer.Plugins
}
var orderFilePath = Path.Combine(pluginsFolder, "order");
var availableDirs = Directory.GetDirectories(pluginsFolder);
var orderedDirs = new List<string>();
if (File.Exists(orderFilePath))
@ -70,19 +76,32 @@ namespace BTCPayServer.Plugins
orderedDirs = availableDirs.ToList();
}
var disabledPlugins = GetDisabledPlugins(pluginsFolder);
foreach (var dir in orderedDirs)
{
var pluginName = Path.GetFileName(dir);
if (disabledPlugins.Contains(pluginName))
{
continue;
}
var plugin = PluginLoader.CreateFromAssemblyFile(
Path.Combine(dir, pluginName + ".dll"), // create a plugin from for the .dll file
config =>
{
// this ensures that the version of MVC is shared between this app and the plugin
config.PreferSharedTypes = true);
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));
plugins.AddRange(GetAllPluginTypesFromAssembly(pluginAssembly)
@ -166,23 +185,27 @@ namespace BTCPayServer.Plugins
switch (command.command)
{
case "update":
ExecuteCommand(("enable", command.extension), pluginsFolder, true);
ExecuteCommand(("delete", command.extension), pluginsFolder, true);
ExecuteCommand(("install", command.extension), pluginsFolder, true);
break;
case "delete":
ExecuteCommand(("enable", command.extension), pluginsFolder, true);
if (Directory.Exists(dirName))
{
Directory.Delete(dirName, true);
if (!ignoreOrder && File.Exists(Path.Combine(pluginsFolder, "order")))
{
var orders = File.ReadAllLines(Path.Combine(pluginsFolder, "order"));
File.AppendAllLines(Path.Combine(pluginsFolder, "order"),
File.WriteAllLines(Path.Combine(pluginsFolder, "order"),
orders.Where(s => s != command.extension));
}
}
break;
case "install":
ExecuteCommand(("enable", command.extension), pluginsFolder, true);
var fileName = dirName + BTCPayPluginSuffix;
if (File.Exists(fileName))
{
@ -195,6 +218,40 @@ namespace BTCPayServer.Plugins
File.Delete(fileName);
}
break;
case "disable":
if (Directory.Exists(dirName))
{
if (File.Exists(Path.Combine(pluginsFolder, "disabled")))
{
var disabled = File.ReadAllLines(Path.Combine(pluginsFolder, "disabled"));
if (!disabled.Contains(command.extension))
{
File.AppendAllLines(Path.Combine(pluginsFolder, "disabled"), new []{ command.extension});
}
}
else
{
File.AppendAllLines(Path.Combine(pluginsFolder, "disabled"), new []{ command.extension});
}
}
break;
case "enable":
if (Directory.Exists(dirName))
{
if (File.Exists(Path.Combine(pluginsFolder, "disabled")))
{
var disabled = File.ReadAllLines(Path.Combine(pluginsFolder, "disabled"));
if (!disabled.Contains(command.extension))
{
File.WriteAllLines(Path.Combine(pluginsFolder, "disabled"), disabled.Where(s=> s!= command.extension));
}
}
}
break;
}
}
@ -225,5 +282,27 @@ namespace BTCPayServer.Plugins
File.Delete(Path.Combine(pluginDir, "commands"));
QueueCommands(pluginDir, cmds);
}
public static void DisablePlugin(string pluginDir, string plugin)
{
QueueCommands(pluginDir, ("disable",plugin));
}
public static void Unload()
{
_plugins.ForEach(loader => loader.Dispose());
}
public static string[] GetDisabledPlugins(string pluginsFolder)
{
var disabledFilePath = Path.Combine(pluginsFolder, "disabled");
if (File.Exists(disabledFilePath))
{
return File.ReadLines(disabledFilePath).ToArray();
}
return Array.Empty<string>();
}
}
}

View file

@ -134,5 +134,10 @@ namespace BTCPayServer.Plugins
{
PluginManager.CancelCommands(_dataDirectories.Value.PluginDir, plugin);
}
public string[] GetDisabledPlugins()
{
return PluginManager.GetDisabledPlugins(_dataDirectories.Value.PluginDir);
}
}
}

View file

@ -1,11 +1,14 @@
using System;
using System.IO;
using System.Net;
using System.Runtime.CompilerServices;
using BTCPayServer.Configuration;
using BTCPayServer.Hosting;
using BTCPayServer.Logging;
using BTCPayServer.Plugins;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
[assembly: InternalsVisibleTo("BTCPayServer.Tests")]
@ -22,10 +25,11 @@ namespace BTCPayServer
using var loggerFactory = new LoggerFactory();
loggerFactory.AddProvider(loggerProvider);
var logger = loggerFactory.CreateLogger("Configuration");
IConfiguration conf = null;
try
{
// This is the only way that LoadArgs can print to console. Because LoadArgs is called by the HostBuilder before Logs.Configure is called
var conf = new DefaultConfiguration() { Logger = logger }.CreateConfiguration(args);
conf = new DefaultConfiguration() { Logger = logger }.CreateConfiguration(args);
if (conf == null)
return;
Logs.Configure(loggerFactory);
@ -60,6 +64,11 @@ namespace BTCPayServer
if (!string.IsNullOrEmpty(ex.Message))
Logs.Configuration.LogError(ex.Message);
}
catch(Exception e) when( PluginManager.IsExceptionByPlugin(e))
{
var pluginDir = new DataDirectories().Configure(conf).PluginDir;
PluginManager.DisablePlugin(pluginDir, e.Source);
}
finally
{
processor.Dispose();

View file

@ -94,7 +94,15 @@
.version-switch .nav-link { display: inline; }
.version-switch .nav-link.active { display: none; }
</style>
@if (Model.Disabled.Any())
{
<div class="alert alert-danger mb-5">
Some plugins were disabled due to fatal errors. They may be incompatible with this version of BTCPay Server.
<button class="btn btn-danger" data-toggle="collapse" data-target="#disabled-plugins">
View disabled plugins
</button>
</div>
}
@if (Model.Commands.Any())
{
<div class="alert alert-info mb-5">
@ -350,6 +358,36 @@
</div>
</div>
}
@if (Model.Disabled.Any())
{
<div class="mb-4">
<button class="btn btn-link text-secondary mb-2" type="button" data-toggle="collapse" data-target="#disabled-plugins">
Disabled plugins
</button>
<div class="row collapse" id="disabled-plugins">
<div class="col col-12 col-lg-6 mb-4">
<div class="card">
<div class="card-body">
<h4 class="card-title">Disabled plugins</h4>
<ul class="list-group list-group-flush">
@foreach (var d in Model.Disabled)
{
<li class="list-group-item px-0">
<div class="d-flex flex-wrap align-items-center justify-content-between">
<span class="my-2 mr-3">@d</span>
<form asp-action="UnInstallPlugin" asp-route-plugin="@d">
<button type="submit" class="btn btn-outline-secondary">Uninstall</button>
</form>
</div>
</li>
}
</ul>
</div>
</div>
</div>
</div>
</div>
}
@section Scripts {
<script>