Allow scheduling installs/updates of future plugins (#5537)

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
Andrew Camilleri 2023-12-13 12:36:23 +01:00 committed by GitHub
parent 26374ef476
commit 7a06423bc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 215 additions and 150 deletions

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -10,7 +9,6 @@ using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Configuration; using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using McMaster.NETCore.Plugins; using McMaster.NETCore.Plugins;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@ -19,14 +17,14 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBXplorer; using Newtonsoft.Json.Linq;
namespace BTCPayServer.Plugins namespace BTCPayServer.Plugins
{ {
public static class PluginManager public static class PluginManager
{ {
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 ();
public static bool IsExceptionByPlugin(Exception exception, [MaybeNullWhen(false)] out string pluginName) public static bool IsExceptionByPlugin(Exception exception, [MaybeNullWhen(false)] out string pluginName)
{ {
@ -65,6 +63,31 @@ 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, ServiceProvider bootstrapServiceProvider) IConfiguration config, ILoggerFactory loggerFactory, ServiceProvider bootstrapServiceProvider)
{ {
void LoadPluginsFromAssemblies(Assembly systemAssembly1, HashSet<string> hashSet, HashSet<string> loadedPluginIdentifiers1,
List<IBTCPayServerPlugin> btcPayServerPlugins)
{
// 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 assemblyName = assembly.GetName().Name;
bool isSystemPlugin = assembly == systemAssembly1;
if (!isSystemPlugin && hashSet.Contains(assemblyName))
continue;
foreach (var plugin in GetPluginInstancesFromAssembly(assembly))
{
if (!isSystemPlugin && plugin.Identifier != assemblyName)
continue;
if (!loadedPluginIdentifiers1.Add(plugin.Identifier))
continue;
btcPayServerPlugins.Add(plugin);
plugin.SystemPlugin = isSystemPlugin;
}
}
}
var 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>();
@ -80,25 +103,12 @@ namespace BTCPayServer.Plugins
var disabledPlugins = GetDisabledPlugins(pluginsFolder); var disabledPlugins = GetDisabledPlugins(pluginsFolder);
var systemAssembly = typeof(Program).Assembly; var systemAssembly = typeof(Program).Assembly;
// Load the referenced assembly plugins LoadPluginsFromAssemblies(systemAssembly, disabledPlugins, loadedPluginIdentifiers, 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 assemblyName = assembly.GetName().Name;
bool isSystemPlugin = assembly == systemAssembly;
if (!isSystemPlugin && disabledPlugins.Contains(assemblyName))
continue;
foreach (var plugin in GetPluginInstancesFromAssembly(assembly)) if (ExecuteCommands(pluginsFolder, plugins.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version)))
{ {
if (!isSystemPlugin && plugin.Identifier != assemblyName) plugins.Clear();
continue; LoadPluginsFromAssemblies(systemAssembly, disabledPlugins, loadedPluginIdentifiers, plugins);
if (!loadedPluginIdentifiers.Add(plugin.Identifier))
continue;
plugins.Add(plugin);
plugin.SystemPlugin = isSystemPlugin;
}
} }
var pluginsToLoad = new List<(string PluginIdentifier, string PluginFilePath)>(); var pluginsToLoad = new List<(string PluginIdentifier, string PluginFilePath)>();
@ -244,37 +254,56 @@ namespace BTCPayServer.Plugins
!type.IsAbstract). !type.IsAbstract).
Select(type => (IBTCPayServerPlugin)Activator.CreateInstance(type, Array.Empty<object>())); Select(type => (IBTCPayServerPlugin)Activator.CreateInstance(type, Array.Empty<object>()));
} }
private static IBTCPayServerPlugin GetPluginInstanceFromAssembly(string pluginIdentifier, Assembly assembly) private static IBTCPayServerPlugin GetPluginInstanceFromAssembly(string pluginIdentifier, Assembly assembly)
{ {
return GetPluginInstancesFromAssembly(assembly) return GetPluginInstancesFromAssembly(assembly).FirstOrDefault(plugin => plugin.Identifier == pluginIdentifier);
.Where(plugin => plugin.Identifier == pluginIdentifier)
.FirstOrDefault();
} }
private static void ExecuteCommands(string pluginsFolder) private static bool ExecuteCommands(string pluginsFolder, Dictionary<string, Version> installed = null)
{ {
var pendingCommands = GetPendingCommands(pluginsFolder); var pendingCommands = GetPendingCommands(pluginsFolder);
foreach (var command in pendingCommands) if (!pendingCommands.Any())
{ {
ExecuteCommand(command, pluginsFolder); return false;
} }
var remainingCommands = (from command in pendingCommands where !ExecuteCommand(command, pluginsFolder, false, installed) select $"{command.command}:{command.plugin}").ToList();
if (remainingCommands.Any())
{
File.WriteAllLines(Path.Combine(pluginsFolder, "commands"), remainingCommands);
}
else
{
File.Delete(Path.Combine(pluginsFolder, "commands")); File.Delete(Path.Combine(pluginsFolder, "commands"));
} }
private static void ExecuteCommand((string command, string extension) command, string pluginsFolder, return remainingCommands.Count != pendingCommands.Length;
bool ignoreOrder = false) }
private static bool DependenciesMet(string pluginsFolder, string plugin, Dictionary<string, Version> installed)
{
var dirName = Path.Combine(pluginsFolder, plugin);
var manifestFileName = dirName + ".json";
if (!File.Exists(manifestFileName)) return true;
var pluginManifest = JObject.Parse(File.ReadAllText(manifestFileName)).ToObject<PluginService.AvailablePlugin>();
return DependenciesMet(pluginManifest.Dependencies, installed);
}
private static bool ExecuteCommand((string command, string extension) command, string pluginsFolder,
bool ignoreOrder = false, Dictionary<string, Version> installed = null)
{ {
var dirName = Path.Combine(pluginsFolder, command.extension); var dirName = Path.Combine(pluginsFolder, command.extension);
switch (command.command) switch (command.command)
{ {
case "update": case "update":
ExecuteCommand(("enable", command.extension), pluginsFolder, true); if (!DependenciesMet(pluginsFolder, command.extension, installed))
return false;
ExecuteCommand(("delete", command.extension), pluginsFolder, true); ExecuteCommand(("delete", command.extension), pluginsFolder, true);
ExecuteCommand(("install", command.extension), pluginsFolder, true); ExecuteCommand(("install", command.extension), pluginsFolder, true);
break; break;
case "delete":
case "delete":
ExecuteCommand(("enable", command.extension), pluginsFolder, true); ExecuteCommand(("enable", command.extension), pluginsFolder, true);
if (File.Exists(dirName)) if (File.Exists(dirName))
{ {
@ -290,11 +319,15 @@ namespace BTCPayServer.Plugins
orders.Where(s => s != command.extension)); orders.Where(s => s != command.extension));
} }
} }
break; break;
case "install": case "install":
ExecuteCommand(("enable", command.extension), pluginsFolder, true);
var fileName = dirName + BTCPayPluginSuffix; var fileName = dirName + BTCPayPluginSuffix;
var manifestFileName = dirName + ".json";
if (!DependenciesMet(pluginsFolder, command.extension, installed))
return false;
ExecuteCommand(("enable", command.extension), pluginsFolder, true);
if (File.Exists(fileName)) if (File.Exists(fileName))
{ {
ZipFile.ExtractToDirectory(fileName, dirName, true); ZipFile.ExtractToDirectory(fileName, dirName, true);
@ -302,10 +335,12 @@ namespace BTCPayServer.Plugins
{ {
File.AppendAllLines(Path.Combine(pluginsFolder, "order"), new[] { command.extension }); File.AppendAllLines(Path.Combine(pluginsFolder, "order"), new[] { command.extension });
} }
File.Delete(fileName); File.Delete(fileName);
if (File.Exists(manifestFileName))
{
File.Move(manifestFileName, Path.Combine(dirName, Path.GetFileName(manifestFileName)));
}
} }
break; break;
case "disable": case "disable":
@ -339,6 +374,8 @@ namespace BTCPayServer.Plugins
break; break;
} }
return true;
} }
public static (string command, string plugin)[] GetPendingCommands(string pluginsFolder) public static (string command, string plugin)[] GetPendingCommands(string pluginsFolder)
@ -364,25 +401,84 @@ namespace BTCPayServer.Plugins
var cmds = GetPendingCommands(pluginDir).Where(tuple => var cmds = GetPendingCommands(pluginDir).Where(tuple =>
!tuple.plugin.Equals(plugin, StringComparison.InvariantCultureIgnoreCase)).ToArray(); !tuple.plugin.Equals(plugin, StringComparison.InvariantCultureIgnoreCase)).ToArray();
if (File.Exists(Path.Combine(pluginDir, plugin, BTCPayPluginSuffix)))
{
File.Delete(Path.Combine(pluginDir, plugin, BTCPayPluginSuffix));
}
if (File.Exists(Path.Combine(pluginDir, plugin, ".json")))
{
File.Delete(Path.Combine(pluginDir, plugin, ".json"));
}
File.Delete(Path.Combine(pluginDir, "commands")); File.Delete(Path.Combine(pluginDir, "commands"));
QueueCommands(pluginDir, cmds); QueueCommands(pluginDir, cmds);
} }
public static void DisablePlugin(string pluginDir, string plugin) public static void DisablePlugin(string pluginDir, string plugin)
{ {
QueueCommands(pluginDir, ("disable", plugin)); QueueCommands(pluginDir, ("disable", plugin));
} }
public static HashSet<string> GetDisabledPlugins(string pluginsFolder) public static HashSet<string> GetDisabledPlugins(string pluginsFolder)
{ {
var disabledFilePath = Path.Combine(pluginsFolder, "disabled"); var disabledFilePath = Path.Combine(pluginsFolder, "disabled");
if (File.Exists(disabledFilePath)) return File.Exists(disabledFilePath)
{ ? File.ReadLines(disabledFilePath).ToHashSet()
return File.ReadLines(disabledFilePath).ToHashSet(); : [];
} }
return new HashSet<string>(); public static bool DependencyMet(IBTCPayServerPlugin.PluginDependency dependency,
Dictionary<string, Version> installed = null)
{
var plugin = dependency.Identifier.ToLowerInvariant();
var versionReq = dependency.Condition;
// ensure installed is not null and has lowercased keys for comparison
installed = installed == null
? new Dictionary<string, Version>()
: installed.ToDictionary(x => x.Key.ToLowerInvariant(), x => x.Value);
if (!installed.ContainsKey(plugin) && !versionReq.Equals("!"))
{
return false;
}
var versionConditions = versionReq.Split("||", StringSplitOptions.RemoveEmptyEntries);
return versionConditions.Any(s =>
{
s = s.Trim();
var v = s.Substring(1);
if (s[1] == '=')
{
v = s.Substring(2);
}
var parsedV = Version.Parse(v);
switch (s)
{
case { } xx when xx.StartsWith(">="):
return installed[plugin] >= parsedV;
case { } xx when xx.StartsWith("<="):
return installed[plugin] <= parsedV;
case { } xx when xx.StartsWith(">"):
return installed[plugin] > parsedV;
case { } xx when xx.StartsWith("<"):
return installed[plugin] >= parsedV;
case { } xx when xx.StartsWith("^"):
return installed[plugin] >= parsedV && installed[plugin].Major == parsedV.Major;
case { } xx when xx.StartsWith("~"):
return installed[plugin] >= parsedV && installed[plugin].Major == parsedV.Major &&
installed[plugin].Minor == parsedV.Minor;
case { } xx when xx.StartsWith("!="):
return installed[plugin] != parsedV;
case { } xx when xx.StartsWith("=="):
default:
return installed[plugin] == parsedV;
}
});
}
public static bool DependenciesMet(IEnumerable<IBTCPayServerPlugin.PluginDependency> dependencies,
Dictionary<string, Version> installed = null)
{
return dependencies.All(dependency => DependencyMet(dependency, installed));
} }
} }
} }

View file

@ -2,22 +2,14 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Configuration; using BTCPayServer.Configuration;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Services; using BTCPayServer.Services;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileSystemGlobbing;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NBitcoin.DataEncoders;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -27,10 +19,8 @@ namespace BTCPayServer.Plugins
{ {
private readonly IOptions<DataDirectories> _dataDirectories; private readonly IOptions<DataDirectories> _dataDirectories;
private readonly PoliciesSettings _policiesSettings; private readonly PoliciesSettings _policiesSettings;
private readonly ISettingsRepository _settingsRepository;
private readonly PluginBuilderClient _pluginBuilderClient; private readonly PluginBuilderClient _pluginBuilderClient;
public PluginService( public PluginService(
ISettingsRepository settingsRepository,
IEnumerable<IBTCPayServerPlugin> btcPayServerPlugins, IEnumerable<IBTCPayServerPlugin> btcPayServerPlugins,
PluginBuilderClient pluginBuilderClient, PluginBuilderClient pluginBuilderClient,
IOptions<DataDirectories> dataDirectories, IOptions<DataDirectories> dataDirectories,
@ -39,7 +29,6 @@ namespace BTCPayServer.Plugins
{ {
LoadedPlugins = btcPayServerPlugins; LoadedPlugins = btcPayServerPlugins;
_pluginBuilderClient = pluginBuilderClient; _pluginBuilderClient = pluginBuilderClient;
_settingsRepository = settingsRepository;
_dataDirectories = dataDirectories; _dataDirectories = dataDirectories;
_policiesSettings = policiesSettings; _policiesSettings = policiesSettings;
Env = env; Env = env;
@ -48,6 +37,15 @@ namespace BTCPayServer.Plugins
public IEnumerable<IBTCPayServerPlugin> LoadedPlugins { get; } public IEnumerable<IBTCPayServerPlugin> LoadedPlugins { get; }
public BTCPayServerEnvironment Env { get; } public BTCPayServerEnvironment Env { get; }
public Version? GetVersionOfPendingInstall(string plugin)
{
var dirName = Path.Combine(_dataDirectories.Value.PluginDir, plugin);
var manifestFileName = dirName + ".json";
if (!File.Exists(manifestFileName)) return null;
var pluginManifest = JObject.Parse(File.ReadAllText(manifestFileName)).ToObject<AvailablePlugin>();
return pluginManifest.Version;
}
public async Task<AvailablePlugin[]> GetRemotePlugins() public async Task<AvailablePlugin[]> GetRemotePlugins()
{ {
var versions = await _pluginBuilderClient.GetPublishedVersions(null, _policiesSettings.PluginPreReleases); var versions = await _pluginBuilderClient.GetPublishedVersions(null, _policiesSettings.PluginPreReleases);
@ -66,14 +64,18 @@ namespace BTCPayServer.Plugins
return p; return p;
}).ToArray(); }).ToArray();
} }
public async Task DownloadRemotePlugin(string pluginIdentifier, string version) public async Task DownloadRemotePlugin(string pluginIdentifier, string version)
{ {
var dest = _dataDirectories.Value.PluginDir; var dest = _dataDirectories.Value.PluginDir;
var filedest = Path.Join(dest, pluginIdentifier + ".btcpay"); var filedest = Path.Join(dest, pluginIdentifier + ".btcpay");
var filemanifestdest = Path.Join(dest, pluginIdentifier + ".json");
Directory.CreateDirectory(Path.GetDirectoryName(filedest)); Directory.CreateDirectory(Path.GetDirectoryName(filedest));
var url = $"api/v1/plugins/[{Uri.EscapeDataString(pluginIdentifier)}]/versions/{Uri.EscapeDataString(version)}/download"; var url = $"api/v1/plugins/[{Uri.EscapeDataString(pluginIdentifier)}]/versions/{Uri.EscapeDataString(version)}/download";
var manifest = (await _pluginBuilderClient.GetPublishedVersions(null, true)).Select(v => v.ManifestInfo.ToObject<AvailablePlugin>()).FirstOrDefault(p => p.Identifier == pluginIdentifier);
await File.WriteAllTextAsync(filemanifestdest, JsonConvert.SerializeObject(manifest, Formatting.Indented));
using var resp2 = await _pluginBuilderClient.HttpClient.GetAsync(url); using var resp2 = await _pluginBuilderClient.HttpClient.GetAsync(url);
using var fs = new FileStream(filedest, FileMode.Create, FileAccess.ReadWrite); await using var fs = new FileStream(filedest, FileMode.Create, FileAccess.ReadWrite);
await resp2.Content.CopyToAsync(fs); await resp2.Content.CopyToAsync(fs);
await fs.FlushAsync(); await fs.FlushAsync();
} }
@ -84,6 +86,7 @@ namespace BTCPayServer.Plugins
UninstallPlugin(plugin); UninstallPlugin(plugin);
PluginManager.QueueCommands(dest, ("install", plugin)); PluginManager.QueueCommands(dest, ("install", plugin));
} }
public void UpdatePlugin(string plugin) public void UpdatePlugin(string plugin)
{ {
var dest = _dataDirectories.Value.PluginDir; var dest = _dataDirectories.Value.PluginDir;
@ -122,8 +125,7 @@ namespace BTCPayServer.Plugins
public string Author { get; set; } public string Author { get; set; }
public string AuthorLink { get; set; } public string AuthorLink { get; set; }
public void Execute(IApplicationBuilder applicationBuilder, public void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices)
IServiceProvider applicationBuilderApplicationServices)
{ {
} }

View file

@ -1,15 +1,15 @@
@using BTCPayServer.Configuration @using BTCPayServer.Configuration
@using BTCPayServer.Plugins @using BTCPayServer.Plugins
@using BTCPayServer.Abstractions.Contracts
@model BTCPayServer.Controllers.UIServerController.ListPluginsViewModel @model BTCPayServer.Controllers.UIServerController.ListPluginsViewModel
@inject BTCPayServerOptions BTCPayServerOptions @inject BTCPayServerOptions BTCPayServerOptions
@inject PluginService PluginService
@{ @{
Layout = "_Layout"; Layout = "_Layout";
ViewData.SetActivePage(ServerNavPages.Plugins); ViewData.SetActivePage(ServerNavPages.Plugins);
var installed = Model.Installed.ToDictionary(plugin => plugin.Identifier.ToLowerInvariant(), plugin => plugin.Version); var installed = Model.Installed.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version);
var availableAndNotInstalledx = Model.Available var availableAndNotInstalledx = Model.Available
.Where(plugin => !installed.ContainsKey(plugin.Identifier.ToLowerInvariant())) .Where(plugin => !installed.ContainsKey(plugin.Identifier))
.GroupBy(plugin => plugin.Identifier) .GroupBy(plugin => plugin.Identifier)
.ToList(); .ToList();
@ -17,7 +17,7 @@
foreach (var availableAndNotInstalledItem in availableAndNotInstalledx) foreach (var availableAndNotInstalledItem in availableAndNotInstalledx)
{ {
var ordered = availableAndNotInstalledItem.OrderByDescending(plugin => plugin.Version).ToArray(); var ordered = availableAndNotInstalledItem.OrderByDescending(plugin => plugin.Version).ToArray();
availableAndNotInstalled.Add(ordered.FirstOrDefault(availablePlugin => DependenciesMet(availablePlugin.Dependencies)) ?? ordered.FirstOrDefault()); availableAndNotInstalled.Add(ordered.FirstOrDefault(availablePlugin => PluginManager.DependenciesMet(availablePlugin.Dependencies, installed)) ?? ordered.FirstOrDefault());
} }
bool DependentOn(string plugin) bool DependentOn(string plugin)
@ -41,74 +41,11 @@
} }
return false; return false;
} }
bool DependencyMet(IBTCPayServerPlugin.PluginDependency dependency)
{
var plugin = dependency.Identifier.ToLowerInvariant();
var versionReq = dependency.Condition;
if (!installed.ContainsKey(plugin) && !versionReq.Equals("!"))
{
return false;
}
if (installed.ContainsKey(plugin) && versionReq.Equals("!"))
{
return false;
}
var versionConditions = versionReq.Split("||", StringSplitOptions.RemoveEmptyEntries);
return versionConditions.Any(s =>
{
s = s.Trim();
var v = s.Substring(1);
if (s[1] == '=')
{
v = s.Substring(2);
}
var parsedV = Version.Parse(v);
switch (s)
{
case { } xx when xx.StartsWith(">="):
return installed[plugin] >= parsedV;
case { } xx when xx.StartsWith("<="):
return installed[plugin] <= parsedV;
case { } xx when xx.StartsWith(">"):
return installed[plugin] > parsedV;
case { } xx when xx.StartsWith("<"):
return installed[plugin] >= parsedV;
case { } xx when xx.StartsWith("^"):
return installed[plugin] >= parsedV && installed[plugin].Major == parsedV.Major;
case { } xx when xx.StartsWith("~"):
return installed[plugin] >= parsedV && installed[plugin].Major == parsedV.Major && installed[plugin].Minor == parsedV.Minor;
case { } xx when xx.StartsWith("!="):
return installed[plugin] != parsedV;
case { } xx when xx.StartsWith("=="):
default:
return installed[plugin] == parsedV;
}
});
}
bool DependenciesMet(IBTCPayServerPlugin.PluginDependency[] dependencies)
{
foreach (var dependency in dependencies)
{
if (!DependencyMet(dependency))
{
return false;
}
}
return true;
}
} }
<style> <style>
.version-switch .nav-link { .version-switch .nav-link { display: inline; }
display: inline; .version-switch .nav-link.active { display: none; }
}
.version-switch .nav-link.active {
display: none;
}
</style> </style>
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
@ -157,7 +94,7 @@
{ {
Model.DownloadedPluginsByIdentifier.TryGetValue(plugin.Identifier, out var downloadInfo); Model.DownloadedPluginsByIdentifier.TryGetValue(plugin.Identifier, out var downloadInfo);
var matchedAvailable = Model.Available.Where(availablePlugin => availablePlugin.Identifier == plugin.Identifier && availablePlugin.Version > plugin.Version).OrderByDescending(availablePlugin => availablePlugin.Version).ToArray(); var matchedAvailable = Model.Available.Where(availablePlugin => availablePlugin.Identifier == plugin.Identifier && availablePlugin.Version > plugin.Version).OrderByDescending(availablePlugin => availablePlugin.Version).ToArray();
var x = matchedAvailable.FirstOrDefault(availablePlugin => DependenciesMet(availablePlugin.Dependencies)) ?? matchedAvailable.FirstOrDefault(); var x = matchedAvailable.FirstOrDefault(availablePlugin => PluginManager.DependenciesMet(availablePlugin.Dependencies, installed)) ?? matchedAvailable.FirstOrDefault();
var updateAvailable = matchedAvailable.Any(); var updateAvailable = matchedAvailable.Any();
var tabId = plugin.Identifier.ToLowerInvariant().Replace(".", "_"); var tabId = plugin.Identifier.ToLowerInvariant().Replace(".", "_");
<div class="col col-12 col-md-6 col-lg-12 col-xl-6 col-xxl-4 mb-4"> <div class="col col-12 col-md-6 col-lg-12 col-xl-6 col-xxl-4 mb-4">
@ -205,7 +142,7 @@
{ {
<li class="list-group-item p-2 d-inline-flex align-items-center gap-2"> <li class="list-group-item p-2 d-inline-flex align-items-center gap-2">
@dependency @dependency
@if (!DependencyMet(dependency)) @if (!PluginManager.DependencyMet(dependency, installed))
{ {
<span title="Dependency not met." data-bs-toggle="tooltip" class="text-danger"> <span title="Dependency not met." data-bs-toggle="tooltip" class="text-danger">
<vc:icon symbol="warning" /> <vc:icon symbol="warning" />
@ -229,7 +166,7 @@
{ {
<li class="list-group-item p-2 d-inline-flex align-items-center gap-2"> <li class="list-group-item p-2 d-inline-flex align-items-center gap-2">
@dependency @dependency
@if (!DependencyMet(dependency)) @if (!PluginManager.DependencyMet(dependency, installed))
{ {
<span title="Dependency not met." data-bs-toggle="tooltip" class="text-danger"> <span title="Dependency not met." data-bs-toggle="tooltip" class="text-danger">
<vc:icon symbol="warning" /> <vc:icon symbol="warning" />
@ -280,23 +217,42 @@
@{ @{
var pendingAction = Model.Commands.Any(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase)); var pendingAction = Model.Commands.Any(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase));
} }
@if (pendingAction || (updateAvailable && x != null && DependenciesMet(x.Dependencies)) || !DependentOn(plugin.Identifier)) @if (pendingAction || (updateAvailable && x != null && !DependentOn(plugin.Identifier)))
{ {
<div class="card-footer border-0 pb-3 d-flex"> var exclusivePendingAction = true;
<div class="card-footer border-0 pb-3 d-flex gap-2">
@if (pendingAction && updateAvailable)
{
var isUpdateAction = Model.Commands.Last(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase)).command == "update";
if (isUpdateAction)
{
var version = PluginService.GetVersionOfPendingInstall(plugin.Identifier);
exclusivePendingAction = version == x.Version;
}
}
@if (pendingAction) @if (pendingAction)
{ {
<form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier"> <form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier">
<button type="submit" class="btn btn-outline-secondary">Cancel pending action</button> <button type="submit" class="btn btn-outline-secondary">Cancel pending action</button>
</form> </form>
} }
else @if(!pendingAction || !exclusivePendingAction)
{ {
@if (updateAvailable && x != null && DependenciesMet(x.Dependencies)) @if (updateAvailable && x != null)
{
if (PluginManager.DependenciesMet(x.Dependencies, installed))
{ {
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@x.Version" asp-route-update="true" class="me-3"> <form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@x.Version" asp-route-update="true" class="me-3">
<button type="submit" class="btn btn-secondary">Update</button> <button type="submit" class="btn btn-secondary">Update</button>
</form> </form>
} }
else
{
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@x.Version" asp-route-update="true" class="me-3">
<button title="Schedule upgrade for when the dependencies have been met to ensure a smooth update" data-bs-toggle="tooltip" type="submit" class="btn btn-secondary">Schedule update</button>
</form>
}
}
@if (DependentOn(plugin.Identifier)) @if (DependentOn(plugin.Identifier))
{ {
<button type="button" class="btn btn-outline-danger" data-bs-toggle="tooltip" title="This plugin cannot be uninstalled as it is depended on by other plugins.">Uninstall <span class="fa fa-exclamation"></span></button> <button type="button" class="btn btn-outline-danger" data-bs-toggle="tooltip" title="This plugin cannot be uninstalled as it is depended on by other plugins.">Uninstall <span class="fa fa-exclamation"></span></button>
@ -322,7 +278,7 @@
<div class="row mb-4"> <div class="row mb-4">
@foreach (var plugin in availableAndNotInstalled) @foreach (var plugin in availableAndNotInstalled)
{ {
var recommended = BTCPayServerOptions.RecommendedPlugins.Contains(plugin.Identifier.ToLowerInvariant()); var recommended = BTCPayServerOptions.RecommendedPlugins.Any(id => string.Equals(id, plugin.Identifier, StringComparison.InvariantCultureIgnoreCase));
var disabled = Model.Disabled?.Contains(plugin.Identifier) ?? false; var disabled = Model.Disabled?.Contains(plugin.Identifier) ?? false;
<div class="col col-12 col-md-6 col-lg-12 col-xl-6 col-xxl-4 mb-4"> <div class="col col-12 col-md-6 col-lg-12 col-xl-6 col-xxl-4 mb-4">
@ -360,7 +316,7 @@
{ {
<li class="list-group-item p-2 d-inline-flex align-items-center gap-2"> <li class="list-group-item p-2 d-inline-flex align-items-center gap-2">
@dependency @dependency
@if (!DependencyMet(dependency)) @if (!PluginManager.DependencyMet(dependency, installed))
{ {
<span title="Dependency not met." data-bs-toggle="tooltip" class="text-danger"> <span title="Dependency not met." data-bs-toggle="tooltip" class="text-danger">
<vc:icon symbol="warning" /> <vc:icon symbol="warning" />
@ -369,6 +325,12 @@
</li> </li>
} }
</ul> </ul>
if(!PluginManager.DependenciesMet(plugin.Dependencies, installed))
{
<div class="text-warning py-2 ">
Dependencies not met.
</div>
}
} }
@if (plugin != null) @if (plugin != null)
@ -405,17 +367,21 @@
</ul> </ul>
} }
</div> </div>
<div class="card-footer border-0 pb-3"> <div class="card-footer border-0 pb-3 d-flex gap-2">
@{ @{
var pending = Model.Commands.LastOrDefault(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase)); var pendingAction = Model.Commands.LastOrDefault(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase));
var version = PluginService.GetVersionOfPendingInstall(plugin.Identifier);
var exclusivePendingAction = version == plugin.Version;
} }
@if (!pending.Equals(default)) @if (!pendingAction.Equals(default))
{ {
<form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier"> <form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier">
<button type="submit" class="btn btn-outline-secondary">Cancel pending @pending.command</button> <button type="submit" class="btn btn-outline-secondary">Cancel pending @pendingAction.command</button>
</form> </form>
} }
else if (DependenciesMet(plugin.Dependencies)) @if (pendingAction.Equals(default) || !exclusivePendingAction)
{
if (PluginManager.DependenciesMet(plugin.Dependencies, installed))
{ {
@* Don't show the "Install" button if plugin has been disabled *@ @* Don't show the "Install" button if plugin has been disabled *@
@if (!disabled) @if (!disabled)
@ -427,9 +393,10 @@
} }
else else
{ {
<div class="text-danger"> <form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@plugin.Version">
Cannot install until dependencies are met. <button title="Schedule install for when the dependencies have been met to ensure a smooth update" data-bs-toggle="tooltip" type="submit" class="btn btn-primary">Schedule install</button>
</div> </form>
}
} }
</div> </div>
</div> </div>