mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-10 09:19:24 +01:00
Allow scheduling installs/updates of future plugins (#5537)
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
parent
26374ef476
commit
7a06423bc7
3 changed files with 215 additions and 150 deletions
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Reference in a new issue