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