refactor: add parallel checks and DRY mapper

This commit is contained in:
thgO.O
2025-08-20 23:08:36 -03:00
parent 9e8f99c1d3
commit 5035b47e7a

View File

@@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Configuration; using BTCPayServer.Configuration;
@@ -9,6 +11,7 @@ using BTCPayServer.Services;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@@ -20,6 +23,7 @@ namespace BTCPayServer.Plugins
private readonly IOptions<DataDirectories> _dataDirectories; private readonly IOptions<DataDirectories> _dataDirectories;
private readonly PoliciesSettings _policiesSettings; private readonly PoliciesSettings _policiesSettings;
private readonly PluginBuilderClient _pluginBuilderClient; private readonly PluginBuilderClient _pluginBuilderClient;
private readonly ILogger<PluginService> _logger;
private static readonly HashSet<string> _builtInPluginIdentifiers = new(StringComparer.OrdinalIgnoreCase) private static readonly HashSet<string> _builtInPluginIdentifiers = new(StringComparer.OrdinalIgnoreCase)
{ {
"BTCPayServer", "BTCPayServer",
@@ -37,7 +41,9 @@ namespace BTCPayServer.Plugins
PluginBuilderClient pluginBuilderClient, PluginBuilderClient pluginBuilderClient,
IOptions<DataDirectories> dataDirectories, IOptions<DataDirectories> dataDirectories,
PoliciesSettings policiesSettings, PoliciesSettings policiesSettings,
BTCPayServerEnvironment env) BTCPayServerEnvironment env,
ILogger<PluginService> logger
)
{ {
LoadedPlugins = btcPayServerPlugins; LoadedPlugins = btcPayServerPlugins;
Installed = btcPayServerPlugins.ToDictionary(p => p.Identifier, p => p.Version, StringComparer.OrdinalIgnoreCase); Installed = btcPayServerPlugins.ToDictionary(p => p.Identifier, p => p.Version, StringComparer.OrdinalIgnoreCase);
@@ -45,6 +51,7 @@ namespace BTCPayServer.Plugins
_dataDirectories = dataDirectories; _dataDirectories = dataDirectories;
_policiesSettings = policiesSettings; _policiesSettings = policiesSettings;
Env = env; Env = env;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
public Dictionary<string, Version> Installed { get; set; } public Dictionary<string, Version> Installed { get; set; }
@@ -67,20 +74,10 @@ namespace BTCPayServer.Plugins
var versions = await _pluginBuilderClient.GetPublishedVersions( var versions = await _pluginBuilderClient.GetPublishedVersions(
btcpayVersion, _policiesSettings.PluginPreReleases, searchPluginName); btcpayVersion, _policiesSettings.PluginPreReleases, searchPluginName);
var plugins = versions.Select(v => var plugins = versions
{ .Select(MapToAvailablePlugin)
var p = v.ManifestInfo.ToObject<AvailablePlugin>(); .Where(p => p is not null)
p.Documentation = v.Documentation; .ToList()!;
var github = v.BuildInfo.GetGithubRepository();
if (github != null)
{
p.Source = github.GetSourceUrl(v.BuildInfo.gitCommit, v.BuildInfo.pluginDir);
p.Author = github.Owner;
p.AuthorLink = $"https://github.com/{github.Owner}";
}
p.SystemPlugin = false;
return p;
}).ToList();
var unlistedUpdates = await GetUpdatesForUnlistedInstalledAsync(plugins, btcpayVersion); var unlistedUpdates = await GetUpdatesForUnlistedInstalledAsync(plugins, btcpayVersion);
plugins.AddRange(unlistedUpdates); plugins.AddRange(unlistedUpdates);
@@ -89,22 +86,33 @@ namespace BTCPayServer.Plugins
} }
private async Task<List<AvailablePlugin>> GetUpdatesForUnlistedInstalledAsync( private async Task<List<AvailablePlugin>> GetUpdatesForUnlistedInstalledAsync(
List<AvailablePlugin> publishedPlugins, List<AvailablePlugin> listedPlugins,
string btcpayVersion) string btcpayVersion,
CancellationToken ct = default)
{ {
var updatables = new List<AvailablePlugin>(); var listedIdentifiers = new HashSet<string>(
var presentIdentifiers = new HashSet<string>( listedPlugins.Select(p => p.Identifier),
publishedPlugins.Select(p => p.Identifier),
StringComparer.OrdinalIgnoreCase); StringComparer.OrdinalIgnoreCase);
foreach (var (installedIdentifier, installedVersion) in Installed) var installedToCheck = Installed
.Where(installedPlugin => !_builtInPluginIdentifiers.Contains(installedPlugin.Key) &&
!listedIdentifiers.Contains(installedPlugin.Key))
.ToList();
var results = new ConcurrentBag<AvailablePlugin>();
var parallelOpts = new ParallelOptions
{ {
if (_builtInPluginIdentifiers.Contains(installedIdentifier)) MaxDegreeOfParallelism = Math.Min(6, Environment.ProcessorCount),
continue; CancellationToken = ct
};
if (presentIdentifiers.Contains(installedIdentifier)) await Parallel.ForEachAsync(installedToCheck, parallelOpts, async (installedPlugin, ct2) =>
continue; {
var (installedIdentifier, installedVersion) = installedPlugin;
try
{
var publishedVersions = await _pluginBuilderClient.GetPluginVersionsForDownload( var publishedVersions = await _pluginBuilderClient.GetPluginVersionsForDownload(
installedIdentifier, installedIdentifier,
btcpayVersion, btcpayVersion,
@@ -112,49 +120,67 @@ namespace BTCPayServer.Plugins
includeAllVersions: true); includeAllVersions: true);
if (publishedVersions == null || !publishedVersions.Any()) if (publishedVersions == null || !publishedVersions.Any())
continue; return;
var candidates = publishedVersions var latestCandidate = publishedVersions
.Select(versionInfo => new .Select(publishedVersion => (VersionInfo: publishedVersion, Manifest: publishedVersion.ManifestInfo))
.Where(versionTuple => versionTuple.Manifest != null)
.Select(versionTuple =>
{ {
VersionInfo = versionInfo, var identifier = versionTuple.Manifest!["Identifier"]?.ToString();
Available = versionInfo.ManifestInfo?.ToObject<AvailablePlugin>() var parsedVersion = Version.TryParse(versionTuple.Manifest!["Version"]?.ToString(), out var ver) ? ver : null;
return new { versionTuple.VersionInfo, Identifier = identifier, Version = parsedVersion };
}) })
.Where(candidate => candidate.Available != null && .Where(candidate => candidate.Identifier != null &&
candidate.Available.Identifier.Equals(installedIdentifier, StringComparison.OrdinalIgnoreCase)) candidate.Identifier.Equals(installedIdentifier, StringComparison.OrdinalIgnoreCase) &&
.ToList(); candidate.Version is not null)
.OrderByDescending(candidate => candidate.Version)
.FirstOrDefault();
if (candidates.Count == 0) if (latestCandidate == null)
continue; return;
var latestCandidate = candidates var latestAvailable = MapToAvailablePlugin(latestCandidate.VersionInfo);
.OrderByDescending(candidate => candidate.Available!.Version)
.First();
var latestAvailable = latestCandidate.Available!;
if (latestAvailable is null)
return;
if (latestAvailable.Version <= installedVersion) if (latestAvailable.Version <= installedVersion)
continue; return;
latestAvailable.Documentation = latestCandidate.VersionInfo.Documentation; results.Add(latestAvailable);
}
var githubRepository = latestCandidate.VersionInfo.BuildInfo.GetGithubRepository(); catch (Exception ex)
if (githubRepository != null)
{ {
latestAvailable.Source = githubRepository.GetSourceUrl( _logger.LogWarning(ex,
latestCandidate.VersionInfo.BuildInfo.gitCommit, "Error while checking for updates for installed plugin {InstalledPluginIdentifier}",
latestCandidate.VersionInfo.BuildInfo.pluginDir); installedIdentifier);
latestAvailable.Author = githubRepository.Owner; }
latestAvailable.AuthorLink = $"https://github.com/{githubRepository.Owner}"; });
return results.ToList();
} }
latestAvailable.SystemPlugin = false; private AvailablePlugin MapToAvailablePlugin(PublishedVersion publishedVersion)
{
updatables.Add(latestAvailable); var availablePlugin = publishedVersion.ManifestInfo.ToObject<AvailablePlugin>();
if (availablePlugin is null)
{
_logger.LogWarning("ManifestInfo missing for published version {Version}", publishedVersion.ToString());
return null;
} }
return updatables; availablePlugin.Documentation = publishedVersion.Documentation;
var github = publishedVersion.BuildInfo?.GetGithubRepository();
if (github != null)
{
availablePlugin.Source = github.GetSourceUrl(publishedVersion.BuildInfo.gitCommit, publishedVersion.BuildInfo.pluginDir);
availablePlugin.Author = github.Owner;
availablePlugin.AuthorLink = $"https://github.com/{github.Owner}";
}
availablePlugin.SystemPlugin = false;
return availablePlugin;
} }
public async Task<AvailablePlugin> DownloadRemotePlugin(string pluginIdentifier, string version, VersionCondition condition = null) public async Task<AvailablePlugin> DownloadRemotePlugin(string pluginIdentifier, string version, VersionCondition condition = null)