From 5035b47e7aa06de98c4399866a8c6f0970b5e101 Mon Sep 17 00:00:00 2001 From: "thgO.O" Date: Wed, 20 Aug 2025 23:08:36 -0300 Subject: [PATCH] refactor: add parallel checks and DRY mapper --- BTCPayServer/Plugins/PluginService.cs | 164 +++++++++++++++----------- 1 file changed, 95 insertions(+), 69 deletions(-) diff --git a/BTCPayServer/Plugins/PluginService.cs b/BTCPayServer/Plugins/PluginService.cs index 7036e6513..809d5f721 100644 --- a/BTCPayServer/Plugins/PluginService.cs +++ b/BTCPayServer/Plugins/PluginService.cs @@ -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; private readonly PoliciesSettings _policiesSettings; private readonly PluginBuilderClient _pluginBuilderClient; + private readonly ILogger _logger; private static readonly HashSet _builtInPluginIdentifiers = new(StringComparer.OrdinalIgnoreCase) { "BTCPayServer", @@ -37,7 +41,9 @@ namespace BTCPayServer.Plugins PluginBuilderClient pluginBuilderClient, IOptions dataDirectories, PoliciesSettings policiesSettings, - BTCPayServerEnvironment env) + BTCPayServerEnvironment env, + ILogger 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 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(); - 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> GetUpdatesForUnlistedInstalledAsync( - List publishedPlugins, - string btcpayVersion) + List listedPlugins, + string btcpayVersion, + CancellationToken ct = default) { - var updatables = new List(); - var presentIdentifiers = new HashSet( - publishedPlugins.Select(p => p.Identifier), + var listedIdentifiers = new HashSet( + 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(); + + 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() - }) - .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(); + 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 DownloadRemotePlugin(string pluginIdentifier, string version, VersionCondition condition = null)