mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-03 17:36:59 +01:00
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:
parent
64db865e1e
commit
6ead5c3800
5 changed files with 139 additions and 6 deletions
|
@ -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")]
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -134,5 +134,10 @@ namespace BTCPayServer.Plugins
|
|||
{
|
||||
PluginManager.CancelCommands(_dataDirectories.Value.PluginDir, plugin);
|
||||
}
|
||||
|
||||
public string[] GetDisabledPlugins()
|
||||
{
|
||||
return PluginManager.GetDisabledPlugins(_dataDirectories.Value.PluginDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue