diff --git a/src/Shared/Contracts/IBaseProjectManager.cs b/src/Shared/Contracts/IBaseProjectManager.cs index d2745264..d2eaf23f 100644 --- a/src/Shared/Contracts/IBaseProjectManager.cs +++ b/src/Shared/Contracts/IBaseProjectManager.cs @@ -20,6 +20,8 @@ public interface IBaseProjectManager /// public string ManagerType { get; } + public TimeSpan? Timeout { get; } + /// /// Per-object option container. /// diff --git a/src/Shared/Contracts/IProjectManagerFactory.cs b/src/Shared/Contracts/IProjectManagerFactory.cs index 2fff38e5..67f1b3a7 100644 --- a/src/Shared/Contracts/IProjectManagerFactory.cs +++ b/src/Shared/Contracts/IProjectManagerFactory.cs @@ -4,6 +4,7 @@ namespace Microsoft.CST.OpenSource.Contracts; using PackageManagers; using PackageUrl; +using System; public interface IProjectManagerFactory { @@ -12,6 +13,7 @@ public interface IProjectManagerFactory /// /// The for the package to create the project manager for. /// The new destination directory, if provided. + /// The to wait before the request times out. /// The implementation of for this 's type. - IBaseProjectManager? CreateProjectManager(PackageURL purl, string destinationDirectory = "."); + IBaseProjectManager? CreateProjectManager(PackageURL purl, string destinationDirectory = ".", TimeSpan? timeout = null); } \ No newline at end of file diff --git a/src/Shared/PackageManagers/BaseProjectManager.cs b/src/Shared/PackageManagers/BaseProjectManager.cs index 280aa375..666fd175 100644 --- a/src/Shared/PackageManagers/BaseProjectManager.cs +++ b/src/Shared/PackageManagers/BaseProjectManager.cs @@ -25,18 +25,22 @@ public abstract class BaseProjectManager : IBaseProjectManager /// public abstract string ManagerType { get; } + /// + public TimeSpan? Timeout { get; } = null; + /// /// Initializes a new instance of the class. /// - public BaseProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory = ".") + public BaseProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory = ".", TimeSpan? timeout = null) { EnvironmentHelper.OverrideEnvironmentVariables(this); Options = new Dictionary(); TopLevelExtractionDirectory = destinationDirectory; HttpClientFactory = httpClientFactory; + Timeout = timeout; } - public BaseProjectManager(string destinationDirectory = ".") : this(new DefaultHttpClientFactory(), destinationDirectory) + public BaseProjectManager(string destinationDirectory = ".", TimeSpan? timeout = null) : this(new DefaultHttpClientFactory(), destinationDirectory, timeout) { } @@ -524,7 +528,12 @@ protected virtual Task> SearchRepoUrlsInPackageMe protected HttpClient CreateHttpClient() { - return HttpClientFactory.CreateClient(GetType().Name); + HttpClient client = HttpClientFactory.CreateClient(GetType().Name); + if (Timeout.HasValue) + { + client.Timeout = Timeout.Value; + } + return client; } } } diff --git a/src/Shared/PackageManagers/CPANProjectManager.cs b/src/Shared/PackageManagers/CPANProjectManager.cs index 4513e049..db243eec 100644 --- a/src/Shared/PackageManagers/CPANProjectManager.cs +++ b/src/Shared/PackageManagers/CPANProjectManager.cs @@ -34,11 +34,11 @@ internal class CPANProjectManager : BaseProjectManager [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Modified through reflection.")] public string ENV_CPAN_API_ENDPOINT { get; set; } = "https://fastapi.metacpan.org"; - public CPANProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory) : base(httpClientFactory, destinationDirectory) + public CPANProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory, TimeSpan? timeout = null) : base(httpClientFactory, destinationDirectory, timeout) { } - public CPANProjectManager(string destinationDirectory) : base(destinationDirectory) + public CPANProjectManager(string destinationDirectory, TimeSpan? timeout = null) : base(destinationDirectory, timeout) { } diff --git a/src/Shared/PackageManagers/CRANProjectManager.cs b/src/Shared/PackageManagers/CRANProjectManager.cs index a0c59c67..98d24b5b 100644 --- a/src/Shared/PackageManagers/CRANProjectManager.cs +++ b/src/Shared/PackageManagers/CRANProjectManager.cs @@ -26,11 +26,11 @@ internal class CRANProjectManager : BaseProjectManager [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Modified through reflection.")] public string ENV_CRAN_ENDPOINT { get; set; } = "https://cran.r-project.org"; - public CRANProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory) : base(httpClientFactory, destinationDirectory) + public CRANProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory, TimeSpan? timeout = null) : base(httpClientFactory, destinationDirectory, timeout) { } - public CRANProjectManager(string destinationDirectory) : base(destinationDirectory) + public CRANProjectManager(string destinationDirectory, TimeSpan? timeout = null) : base(destinationDirectory, timeout) { } diff --git a/src/Shared/PackageManagers/CargoProjectManager.cs b/src/Shared/PackageManagers/CargoProjectManager.cs index 513de0a3..1361db3d 100644 --- a/src/Shared/PackageManagers/CargoProjectManager.cs +++ b/src/Shared/PackageManagers/CargoProjectManager.cs @@ -42,8 +42,9 @@ public class CargoProjectManager : TypedManager? actions = null, - IHttpClientFactory? httpClientFactory = null) - : base(actions ?? new NoOpPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory) + IHttpClientFactory? httpClientFactory = null, + TimeSpan? timeout = null) + : base(actions ?? new NoOpPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory, timeout) { } diff --git a/src/Shared/PackageManagers/CocoapodsProjectManager.cs b/src/Shared/PackageManagers/CocoapodsProjectManager.cs index b12e82bf..29093360 100644 --- a/src/Shared/PackageManagers/CocoapodsProjectManager.cs +++ b/src/Shared/PackageManagers/CocoapodsProjectManager.cs @@ -17,6 +17,7 @@ namespace Microsoft.CST.OpenSource.PackageManagers using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; + using System.Threading; using System.Threading.Tasks; internal class CocoapodsProjectManager : BaseProjectManager @@ -38,11 +39,11 @@ internal class CocoapodsProjectManager : BaseProjectManager [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Modified through reflection.")] public string ENV_COCOAPODS_METADATA_ENDPOINT { get; set; } = "https://cocoapods.org"; - public CocoapodsProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory) : base(httpClientFactory, destinationDirectory) + public CocoapodsProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory, TimeSpan? timeout = null) : base(httpClientFactory, destinationDirectory, timeout) { } - public CocoapodsProjectManager(string destinationDirectory) : base(destinationDirectory) + public CocoapodsProjectManager(string destinationDirectory, TimeSpan? timeout = null) : base(destinationDirectory, timeout) { } diff --git a/src/Shared/PackageManagers/ComposerProjectManager.cs b/src/Shared/PackageManagers/ComposerProjectManager.cs index e4e200c1..5d13b157 100644 --- a/src/Shared/PackageManagers/ComposerProjectManager.cs +++ b/src/Shared/PackageManagers/ComposerProjectManager.cs @@ -26,11 +26,11 @@ internal class ComposerProjectManager : BaseProjectManager [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Modified through reflection.")] public string ENV_COMPOSER_ENDPOINT { get; set; } = "https://repo.packagist.org"; - public ComposerProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory) : base(httpClientFactory, destinationDirectory) + public ComposerProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory, TimeSpan? timeout = null) : base(httpClientFactory, destinationDirectory, timeout) { } - public ComposerProjectManager(string destinationDirectory) : base(destinationDirectory) + public ComposerProjectManager(string destinationDirectory, TimeSpan? timeout = null) : base(destinationDirectory, timeout) { } diff --git a/src/Shared/PackageManagers/CondaProjectManager.cs b/src/Shared/PackageManagers/CondaProjectManager.cs index a299e3e4..95abe991 100644 --- a/src/Shared/PackageManagers/CondaProjectManager.cs +++ b/src/Shared/PackageManagers/CondaProjectManager.cs @@ -33,8 +33,9 @@ public class CondaProjectManager : TypedManager? actions = null, - IHttpClientFactory? httpClientFactory = null) - : base(actions ?? new NoOpPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory) + IHttpClientFactory? httpClientFactory = null, + TimeSpan? timeout = null) + : base(actions ?? new NoOpPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory, timeout) { } diff --git a/src/Shared/PackageManagers/GemProjectManager.cs b/src/Shared/PackageManagers/GemProjectManager.cs index b457b408..85a0ba0a 100644 --- a/src/Shared/PackageManagers/GemProjectManager.cs +++ b/src/Shared/PackageManagers/GemProjectManager.cs @@ -27,11 +27,11 @@ internal class GemProjectManager : BaseProjectManager public string ENV_RUBYGEMS_ENDPOINT { get; set; } = "https://rubygems.org"; public string ENV_RUBYGEMS_ENDPOINT_API { get; set; } = "https://api.rubygems.org"; - public GemProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory) : base(httpClientFactory, destinationDirectory) + public GemProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory, TimeSpan? timeout = null) : base(httpClientFactory, destinationDirectory, timeout) { } - public GemProjectManager(string destinationDirectory) : base(destinationDirectory) + public GemProjectManager(string destinationDirectory, TimeSpan? timeout = null) : base(destinationDirectory, timeout) { } diff --git a/src/Shared/PackageManagers/GitHubProjectManager.cs b/src/Shared/PackageManagers/GitHubProjectManager.cs index 95c5de73..5e968460 100644 --- a/src/Shared/PackageManagers/GitHubProjectManager.cs +++ b/src/Shared/PackageManagers/GitHubProjectManager.cs @@ -28,11 +28,11 @@ internal class GitHubProjectManager : BaseProjectManager private const string DEFAULT_GITHUB_ENDPOINT = "https://github.com"; public string ENV_GITHUB_ENDPOINT { get; set; } = DEFAULT_GITHUB_ENDPOINT; - public GitHubProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory) : base(httpClientFactory, destinationDirectory) + public GitHubProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory, TimeSpan? timeout = null) : base(httpClientFactory, destinationDirectory, timeout) { } - public GitHubProjectManager(string destinationDirectory) : base(destinationDirectory) + public GitHubProjectManager(string destinationDirectory, TimeSpan? timeout = null) : base(destinationDirectory, timeout) { } diff --git a/src/Shared/PackageManagers/GolangProjectManager.cs b/src/Shared/PackageManagers/GolangProjectManager.cs index 380aa4ef..7067adb9 100644 --- a/src/Shared/PackageManagers/GolangProjectManager.cs +++ b/src/Shared/PackageManagers/GolangProjectManager.cs @@ -37,8 +37,9 @@ public class GolangProjectManager : TypedManager? actions = null, - IHttpClientFactory? httpClientFactory = null) - : base(actions ?? new NoOpPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory) + IHttpClientFactory? httpClientFactory = null, + TimeSpan? timeout = null) + : base(actions ?? new NoOpPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory, timeout) { } diff --git a/src/Shared/PackageManagers/HackageProjectManager.cs b/src/Shared/PackageManagers/HackageProjectManager.cs index e339014f..9eda543e 100644 --- a/src/Shared/PackageManagers/HackageProjectManager.cs +++ b/src/Shared/PackageManagers/HackageProjectManager.cs @@ -26,11 +26,11 @@ internal class HackageProjectManager : BaseProjectManager [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Modified through reflection.")] public string ENV_HACKAGE_ENDPOINT { get; set; } = "https://hackage.haskell.org"; - public HackageProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory) : base(httpClientFactory, destinationDirectory) + public HackageProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory, TimeSpan? timeout = null) : base(httpClientFactory, destinationDirectory, timeout) { } - public HackageProjectManager(string destinationDirectory) : base(destinationDirectory) + public HackageProjectManager(string destinationDirectory, TimeSpan? timeout = null) : base(destinationDirectory, timeout) { } diff --git a/src/Shared/PackageManagers/MavenProjectManager.cs b/src/Shared/PackageManagers/MavenProjectManager.cs index 75bb6844..fca738b7 100644 --- a/src/Shared/PackageManagers/MavenProjectManager.cs +++ b/src/Shared/PackageManagers/MavenProjectManager.cs @@ -33,8 +33,9 @@ public class MavenProjectManager : TypedManager? actions = null, - IHttpClientFactory? httpClientFactory = null) - : base(actions ?? new NoOpPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory) + IHttpClientFactory? httpClientFactory = null, + TimeSpan? timeout = null) + : base(actions ?? new NoOpPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory, timeout) { } diff --git a/src/Shared/PackageManagers/NPMProjectManager.cs b/src/Shared/PackageManagers/NPMProjectManager.cs index 4c2541e8..c8fb0369 100644 --- a/src/Shared/PackageManagers/NPMProjectManager.cs +++ b/src/Shared/PackageManagers/NPMProjectManager.cs @@ -43,8 +43,9 @@ public class NPMProjectManager : TypedManager? actions = null, - IHttpClientFactory? httpClientFactory = null) - : base(actions ?? new NoOpPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory) + IHttpClientFactory? httpClientFactory = null, + TimeSpan? timeout = null) + : base(actions ?? new NoOpPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory, timeout) { } @@ -129,6 +130,11 @@ public override async Task> DownloadVersionAsync(PackageURL downloadedPaths.Add(extractionPath); } } + catch (TaskCanceledException ex) + { + Logger.Debug(ex, "Downloading NPM package timed out: {0}", ex.Message); + throw; + } catch (Exception ex) { Logger.Debug(ex, "Error downloading NPM package: {0}", ex.Message); diff --git a/src/Shared/PackageManagers/NuGetProjectManager.cs b/src/Shared/PackageManagers/NuGetProjectManager.cs index 9518e8ec..077413ea 100644 --- a/src/Shared/PackageManagers/NuGetProjectManager.cs +++ b/src/Shared/PackageManagers/NuGetProjectManager.cs @@ -46,8 +46,9 @@ public class NuGetProjectManager : TypedManager? actions = null, - IHttpClientFactory? httpClientFactory = null) - : base(actions ?? new NuGetPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory) + IHttpClientFactory? httpClientFactory = null, + TimeSpan? timeout = null) + : base(actions ?? new NuGetPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory, timeout) { GetRegistrationEndpointAsync().Wait(); } diff --git a/src/Shared/PackageManagers/ProjectManagerFactory.cs b/src/Shared/PackageManagers/ProjectManagerFactory.cs index ee766fe5..d7acffa1 100644 --- a/src/Shared/PackageManagers/ProjectManagerFactory.cs +++ b/src/Shared/PackageManagers/ProjectManagerFactory.cs @@ -17,13 +17,14 @@ public class ProjectManagerFactory : IProjectManagerFactory /// /// The only runtime parameter we need is the destination directory. Everything else can be defined in the constructor call itself. /// The destination that any files should be saved to when downloading from this ProjectManager, defaults to the runtime directory. + /// The to wait before the request times out. /// An implementation of the class, or null if unable to construct. /// /// destinationDirectory => /// new NPMProjectManager(httpClientFactory, destinationDirectory) /// /// Example implementations in GetDefaultManagers(IHttpClientFactory?) - public delegate BaseProjectManager? ConstructProjectManager(string destinationDirectory = "."); + public delegate BaseProjectManager? ConstructProjectManager(string destinationDirectory = ".", TimeSpan? timeout = null); /// /// The dictionary of project managers. @@ -61,82 +62,82 @@ public static Dictionary GetDefaultManagers(IHt return new Dictionary(StringComparer.InvariantCultureIgnoreCase) { { - CargoProjectManager.Type, destinationDirectory => - new CargoProjectManager(destinationDirectory, new NoOpPackageActions(), httpClientFactory) + CargoProjectManager.Type, (destinationDirectory, timeout) => + new CargoProjectManager(destinationDirectory, new NoOpPackageActions(), httpClientFactory, timeout) }, { - CocoapodsProjectManager.Type, destinationDirectory => - new CocoapodsProjectManager(httpClientFactory, destinationDirectory) + CocoapodsProjectManager.Type, (destinationDirectory, timeout) => + new CocoapodsProjectManager(httpClientFactory, destinationDirectory, timeout) }, { - ComposerProjectManager.Type, destinationDirectory => - new ComposerProjectManager(httpClientFactory, destinationDirectory) + ComposerProjectManager.Type, (destinationDirectory, timeout) => + new ComposerProjectManager(httpClientFactory, destinationDirectory, timeout) }, { - CondaProjectManager.Type, destinationDirectory => - new CondaProjectManager(destinationDirectory, new NoOpPackageActions(), httpClientFactory) + CondaProjectManager.Type, (destinationDirectory, timeout) => + new CondaProjectManager(destinationDirectory, new NoOpPackageActions(), httpClientFactory, timeout) }, { - CPANProjectManager.Type, destinationDirectory => - new CPANProjectManager(httpClientFactory, destinationDirectory) + CPANProjectManager.Type, (destinationDirectory, timeout) => + new CPANProjectManager(httpClientFactory, destinationDirectory, timeout) }, { - CRANProjectManager.Type, destinationDirectory => - new CRANProjectManager(httpClientFactory, destinationDirectory) + CRANProjectManager.Type, (destinationDirectory, timeout) => + new CRANProjectManager(httpClientFactory, destinationDirectory, timeout) }, { - GemProjectManager.Type, destinationDirectory => - new GemProjectManager(httpClientFactory, destinationDirectory) + GemProjectManager.Type, (destinationDirectory, timeout) => + new GemProjectManager(httpClientFactory, destinationDirectory, timeout) }, { - GitHubProjectManager.Type, destinationDirectory => - new GitHubProjectManager(httpClientFactory, destinationDirectory) + GitHubProjectManager.Type, (destinationDirectory, timeout) => + new GitHubProjectManager(httpClientFactory, destinationDirectory, timeout) }, { - GolangProjectManager.Type, destinationDirectory => - new GolangProjectManager(destinationDirectory, new NoOpPackageActions(), httpClientFactory) + GolangProjectManager.Type, (destinationDirectory, timeout) => + new GolangProjectManager(destinationDirectory, new NoOpPackageActions(), httpClientFactory, timeout) }, { - HackageProjectManager.Type, destinationDirectory => - new HackageProjectManager(httpClientFactory, destinationDirectory) + HackageProjectManager.Type, (destinationDirectory, timeout) => + new HackageProjectManager(httpClientFactory, destinationDirectory, timeout) }, { - MavenProjectManager.Type, destinationDirectory => - new MavenProjectManager(destinationDirectory, new NoOpPackageActions(), httpClientFactory) + MavenProjectManager.Type, (destinationDirectory, timeout) => + new MavenProjectManager(destinationDirectory, new NoOpPackageActions(), httpClientFactory, timeout) }, { - NPMProjectManager.Type, destinationDirectory => - new NPMProjectManager(destinationDirectory, new NoOpPackageActions(), httpClientFactory) + NPMProjectManager.Type, (destinationDirectory, timeout) => + new NPMProjectManager(destinationDirectory, new NoOpPackageActions(), httpClientFactory, timeout) }, { - NuGetProjectManager.Type, destinationDirectory => - new NuGetProjectManager(destinationDirectory, new NuGetPackageActions(), httpClientFactory) // Add the NuGetPackageActions to the NuGetProjectManager. + NuGetProjectManager.Type, (destinationDirectory, timeout) => + new NuGetProjectManager(destinationDirectory, new NuGetPackageActions(), httpClientFactory, timeout) // Add the NuGetPackageActions to the NuGetProjectManager. }, { - PyPIProjectManager.Type, destinationDirectory => - new PyPIProjectManager(destinationDirectory, new NoOpPackageActions(), httpClientFactory) + PyPIProjectManager.Type, (destinationDirectory, timeout) => + new PyPIProjectManager(destinationDirectory, new NoOpPackageActions(), httpClientFactory, timeout) }, { - UbuntuProjectManager.Type, destinationDirectory => - new UbuntuProjectManager(httpClientFactory, destinationDirectory) + UbuntuProjectManager.Type, (destinationDirectory, timeout) => + new UbuntuProjectManager(httpClientFactory, destinationDirectory, timeout) }, { - URLProjectManager.Type, destinationDirectory => - new URLProjectManager(httpClientFactory, destinationDirectory) + URLProjectManager.Type, (destinationDirectory, timeout) => + new URLProjectManager(httpClientFactory, destinationDirectory, timeout) }, { - VSMProjectManager.Type, destinationDirectory => - new VSMProjectManager(httpClientFactory, destinationDirectory) + VSMProjectManager.Type, (destinationDirectory, timeout) => + new VSMProjectManager(httpClientFactory, destinationDirectory, timeout) }, }; } /// - public IBaseProjectManager? CreateProjectManager(PackageURL purl, string destinationDirectory = ".") + public IBaseProjectManager? CreateProjectManager(PackageURL purl, string destinationDirectory = ".", TimeSpan? timeout = null) { ConstructProjectManager? projectManager = _projectManagers.GetValueOrDefault(purl.Type); - return projectManager?.Invoke(destinationDirectory); + return projectManager?.Invoke(destinationDirectory, timeout); } /// diff --git a/src/Shared/PackageManagers/PyPIProjectManager.cs b/src/Shared/PackageManagers/PyPIProjectManager.cs index 37656938..4178b447 100644 --- a/src/Shared/PackageManagers/PyPIProjectManager.cs +++ b/src/Shared/PackageManagers/PyPIProjectManager.cs @@ -37,8 +37,9 @@ public class PyPIProjectManager : TypedManager? actions = null, - IHttpClientFactory? httpClientFactory = null) - : base(actions ?? new NoOpPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory) + IHttpClientFactory? httpClientFactory = null, + TimeSpan? timeout = null) + : base(actions ?? new NoOpPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory, timeout) { } diff --git a/src/Shared/PackageManagers/TypedManager.cs b/src/Shared/PackageManagers/TypedManager.cs index 6f7db4ec..bed2be4a 100644 --- a/src/Shared/PackageManagers/TypedManager.cs +++ b/src/Shared/PackageManagers/TypedManager.cs @@ -35,7 +35,7 @@ public abstract class TypedManager : BaseProjectManager, IT /// protected readonly IManagerPackageActions Actions; - protected TypedManager(IManagerPackageActions actions, IHttpClientFactory httpClientFactory, string directory) : base(httpClientFactory, directory) + protected TypedManager(IManagerPackageActions actions, IHttpClientFactory httpClientFactory, string directory, TimeSpan? timeout = null) : base(httpClientFactory, directory, timeout) { Actions = actions; } diff --git a/src/Shared/PackageManagers/URLProjectManager.cs b/src/Shared/PackageManagers/URLProjectManager.cs index 1bf6af1a..32a1b196 100644 --- a/src/Shared/PackageManagers/URLProjectManager.cs +++ b/src/Shared/PackageManagers/URLProjectManager.cs @@ -20,11 +20,11 @@ internal class URLProjectManager : BaseProjectManager public override string ManagerType => Type; - public URLProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory) : base(httpClientFactory, destinationDirectory) + public URLProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory, TimeSpan? timeout = null) : base(httpClientFactory, destinationDirectory, timeout) { } - public URLProjectManager(string destinationDirectory) : base(destinationDirectory) + public URLProjectManager(string destinationDirectory, TimeSpan? timeout = null) : base(destinationDirectory, timeout) { } diff --git a/src/Shared/PackageManagers/UbuntuProjectManager.cs b/src/Shared/PackageManagers/UbuntuProjectManager.cs index 2a0bbd20..ed013d42 100644 --- a/src/Shared/PackageManagers/UbuntuProjectManager.cs +++ b/src/Shared/PackageManagers/UbuntuProjectManager.cs @@ -34,11 +34,11 @@ internal class UbuntuProjectManager : BaseProjectManager [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Modified through reflection.")] public string ENV_UBUNTU_POOL_NAMES { get; set; } = "main,universe,multiverse,restricted"; - public UbuntuProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory) : base(httpClientFactory, destinationDirectory) + public UbuntuProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory, TimeSpan? timeout = null) : base(httpClientFactory, destinationDirectory, timeout) { } - public UbuntuProjectManager(string destinationDirectory) : base(destinationDirectory) + public UbuntuProjectManager(string destinationDirectory, TimeSpan? timeout = null) : base(destinationDirectory, timeout) { } diff --git a/src/Shared/PackageManagers/VSMProjectManager.cs b/src/Shared/PackageManagers/VSMProjectManager.cs index b30cd5e0..96638244 100644 --- a/src/Shared/PackageManagers/VSMProjectManager.cs +++ b/src/Shared/PackageManagers/VSMProjectManager.cs @@ -28,11 +28,11 @@ internal class VSMProjectManager : BaseProjectManager [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Modified through reflection.")] public string ENV_VS_MARKETPLACE_ENDPOINT { get; set; } = "https://marketplace.visualstudio.com"; - public VSMProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory) : base(httpClientFactory, destinationDirectory) + public VSMProjectManager(IHttpClientFactory httpClientFactory, string destinationDirectory, TimeSpan? timeout = null) : base(httpClientFactory, destinationDirectory, timeout) { } - public VSMProjectManager(string destinationDirectory) : base(destinationDirectory) + public VSMProjectManager(string destinationDirectory, TimeSpan? timeout = null) : base(destinationDirectory, timeout) { } diff --git a/src/oss-tests/FindSquatsTests.cs b/src/oss-tests/FindSquatsTests.cs index f0204376..ca845dfe 100644 --- a/src/oss-tests/FindSquatsTests.cs +++ b/src/oss-tests/FindSquatsTests.cs @@ -50,7 +50,7 @@ public async Task DetectSquats(string packageUrl, bool generateSquats, bool expe // Override the NuGet constructor to add the mocked NuGetPackageActions. managerOverrides[NuGetProjectManager.Type] = - _ => new NuGetProjectManager(".", nugetPackageActions, httpClientFactory); + (destinationDirectory, timeout) => new NuGetProjectManager(".", nugetPackageActions, httpClientFactory); ProjectManagerFactory projectManagerFactory = new(managerOverrides); FindSquatsTool fst = new(projectManagerFactory); @@ -554,8 +554,8 @@ public async Task NewtonsoftMutations_Succeeds_Async() IManagerPackageActions packageActions = PackageActionsHelper.SetupPackageActions(newtonsoft, validSquats: squattingPackages) ?? throw new InvalidOperationException(); Dictionary overrideDict = ProjectManagerFactory.GetDefaultManagers(httpClientFactory); - overrideDict[NuGetProjectManager.Type] = directory => - new NuGetProjectManager(directory, packageActions, httpClientFactory); + overrideDict[NuGetProjectManager.Type] = (destinationDirectory, timeout) => + new NuGetProjectManager(destinationDirectory, packageActions, httpClientFactory); FindPackageSquats findPackageSquats = new(new ProjectManagerFactory(overrideDict), newtonsoft); @@ -589,8 +589,8 @@ public async Task RequestsMutations_Succeeds_Async() IManagerPackageActions packageActions = PackageActionsHelper.SetupPackageActions(requests, validSquats: squattingPackages) ?? throw new InvalidOperationException(); Dictionary overrideDict = ProjectManagerFactory.GetDefaultManagers(httpClientFactory); - overrideDict[NuGetProjectManager.Type] = directory => - new NuGetProjectManager(directory, packageActions, httpClientFactory); + overrideDict[NuGetProjectManager.Type] = (destinationDirectory, timeout) => + new NuGetProjectManager(destinationDirectory, packageActions, httpClientFactory); FindPackageSquats findPackageSquats = new(new ProjectManagerFactory(overrideDict), requests); diff --git a/src/oss-tests/ProjectManagerTests/CondaProjectManagerTests.cs b/src/oss-tests/ProjectManagerTests/CondaProjectManagerTests.cs index bdf7141b..1dd0ee26 100644 --- a/src/oss-tests/ProjectManagerTests/CondaProjectManagerTests.cs +++ b/src/oss-tests/ProjectManagerTests/CondaProjectManagerTests.cs @@ -25,7 +25,7 @@ public CondaProjectManagerTests() Mock mockFactory = new(); _httpFactory = mockFactory.Object; - _projectManager = new Mock(".", new NoOpPackageActions(), _httpFactory) { CallBase = true }; + _projectManager = new Mock(".", new NoOpPackageActions(), _httpFactory, null) { CallBase = true }; } [DataTestMethod] diff --git a/src/oss-tests/ProjectManagerTests/GolangProjectManagerTests.cs b/src/oss-tests/ProjectManagerTests/GolangProjectManagerTests.cs index 0786846d..2d828edb 100644 --- a/src/oss-tests/ProjectManagerTests/GolangProjectManagerTests.cs +++ b/src/oss-tests/ProjectManagerTests/GolangProjectManagerTests.cs @@ -44,7 +44,7 @@ public GolangProjectManagerTests() mockFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(mockHttp.ToHttpClient()); _httpFactory = mockFactory.Object; - _projectManager = new Mock(".", new NoOpPackageActions(), _httpFactory) { CallBase = true }; + _projectManager = new Mock(".", new NoOpPackageActions(), _httpFactory, null) { CallBase = true }; } [DataTestMethod] diff --git a/src/oss-tests/ProjectManagerTests/MavenProjectManagerTests.cs b/src/oss-tests/ProjectManagerTests/MavenProjectManagerTests.cs index 33b937ba..64ac2b1b 100644 --- a/src/oss-tests/ProjectManagerTests/MavenProjectManagerTests.cs +++ b/src/oss-tests/ProjectManagerTests/MavenProjectManagerTests.cs @@ -51,7 +51,7 @@ public MavenProjectManagerTests() mockFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(mockHttp.ToHttpClient()); _httpFactory = mockFactory.Object; - _projectManager = new Mock(".", new NoOpPackageActions(), _httpFactory) { CallBase = true }; + _projectManager = new Mock(".", new NoOpPackageActions(), _httpFactory, null) { CallBase = true }; } [DataTestMethod] diff --git a/src/oss-tests/ProjectManagerTests/NPMProjectManagerTests.cs b/src/oss-tests/ProjectManagerTests/NPMProjectManagerTests.cs index 04f5a91e..65b9a5d7 100644 --- a/src/oss-tests/ProjectManagerTests/NPMProjectManagerTests.cs +++ b/src/oss-tests/ProjectManagerTests/NPMProjectManagerTests.cs @@ -127,7 +127,7 @@ public NPMProjectManagerTests() mockFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(mockHttp.ToHttpClient()); _httpFactory = mockFactory.Object; - _projectManager = new Mock(".", new NoOpPackageActions(), _httpFactory) { CallBase = true }; + _projectManager = new Mock(".", new NoOpPackageActions(), _httpFactory, null) { CallBase = true }; } [DataTestMethod] @@ -290,6 +290,8 @@ await _projectManager.Object.DetailedPackageExistsAsync(new PackageURL("pkg:npm/ public async Task PackageVersionPulledAsync(string purlString, bool expectedPulled = true) { PackageURL purl = new(purlString); + + var project = _projectManager; string? content = await _projectManager.Object.GetMetadataAsync(purl); diff --git a/src/oss-tests/ProjectManagerTests/ProjectManagerFactoryTests.cs b/src/oss-tests/ProjectManagerTests/ProjectManagerFactoryTests.cs index a67b7275..96a8f320 100644 --- a/src/oss-tests/ProjectManagerTests/ProjectManagerFactoryTests.cs +++ b/src/oss-tests/ProjectManagerTests/ProjectManagerFactoryTests.cs @@ -3,13 +3,17 @@ namespace Microsoft.CST.OpenSource.Tests.ProjectManagerTests; using Contracts; +using Moq; using PackageActions; using PackageManagers; using PackageUrl; +using RichardSzalay.MockHttp; using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Net.Http; +using System.Threading.Tasks; using VisualStudio.TestTools.UnitTesting; [TestClass] @@ -53,20 +57,20 @@ public void DefaultSucceeds() AssertFactoryCreatesCorrect(projectManagerFactory); } - + /// /// Test adding a new manager to the dictionary, not overriding an existing one. /// [TestMethod] public void AddTestManagerSucceeds() { - _managerOverrides["test"] = directory => new NuGetProjectManager(directory, null, _httpClientFactory); // Create a test type with the NuGetProjectManager. + _managerOverrides["test"] = (destinationDirectory, timeout) => new NuGetProjectManager(destinationDirectory, null, _httpClientFactory); // Create a test type with the NuGetProjectManager. ProjectManagerFactory projectManagerFactory = new(_managerOverrides); AssertFactoryCreatesCorrect(projectManagerFactory); } - + /// /// Test overriding the nuget and npm entries as well as their destination directories in the dictionary. /// @@ -74,9 +78,9 @@ public void AddTestManagerSucceeds() public void OverrideManagerSucceeds() { _managerOverrides[NuGetProjectManager.Type] = - _ => new NuGetProjectManager("nugetTestDirectory", null, _httpClientFactory); // Override the default entry for nuget, and override the destinationDirectory. + (destinationDirectory, timeout) => new NuGetProjectManager("nugetTestDirectory", null, _httpClientFactory); // Override the default entry for nuget, and override the destinationDirectory. _managerOverrides[NPMProjectManager.Type] = - _ => new NPMProjectManager("npmTestDirectory", null, _httpClientFactory); // Override the default entry for npm, and override the destinationDirectory. + (destinationDirectory, timeout) => new NPMProjectManager("npmTestDirectory", null, _httpClientFactory); // Override the default entry for npm, and override the destinationDirectory. ProjectManagerFactory projectManagerFactory = new(_managerOverrides); @@ -85,24 +89,24 @@ public void OverrideManagerSucceeds() // Assert that the overrides worked by checking the TopLevelExtractionDirectory was changed. IBaseProjectManager? nuGetProjectManager = projectManagerFactory.CreateProjectManager(new PackageURL("pkg:nuget/foo")); Assert.AreEqual("nugetTestDirectory", nuGetProjectManager?.TopLevelExtractionDirectory); - + IBaseProjectManager? npmProjectManager = projectManagerFactory.CreateProjectManager(new PackageURL("pkg:npm/foo")); Assert.AreEqual("npmTestDirectory", npmProjectManager?.TopLevelExtractionDirectory); } - + /// /// Test changing an entry in the dictionary of constructors to construct a manager of a different type. /// [TestMethod] public void ChangeProjectManagerSucceeds() { - _managerOverrides[NuGetProjectManager.Type] = directory => new NPMProjectManager(directory, null, _httpClientFactory); // Override the default entry for nuget and set it as another NPMProjectManager. - + _managerOverrides[NuGetProjectManager.Type] = (destinationDirectory, timeout) => new NPMProjectManager(destinationDirectory, null, _httpClientFactory); // Override the default entry for nuget and set it as another NPMProjectManager. + ProjectManagerFactory projectManagerFactory = new(_managerOverrides); AssertFactoryCreatesCorrect(projectManagerFactory); } - + /// /// Test removing an entry from the default dictionary of project manager constructors. /// @@ -112,11 +116,11 @@ public void RemoveProjectManagerSucceeds() Assert.IsTrue(_managerOverrides.Remove(NuGetProjectManager.Type)); ProjectManagerFactory projectManagerFactory = new(_managerOverrides); - + PackageURL packageUrl = new("pkg:nuget/foo"); Assert.IsNull(projectManagerFactory.CreateProjectManager(packageUrl)); } - + /// /// Test removing all project managers from the dictionary of project manager constructors. /// @@ -127,9 +131,9 @@ public void RemoveAllProjectManagersSucceeds() _managerOverrides.Clear(); ProjectManagerFactory projectManagerFactory = new(_managerOverrides); - + AssertFactoryCreatesCorrect(projectManagerFactory); - + foreach (PackageURL packageUrl in ProjectManagerFactory.GetDefaultManagers(_httpClientFactory).Keys .Select(purlType => new PackageURL($"pkg:{purlType}/foo"))) { @@ -137,6 +141,61 @@ public void RemoveAllProjectManagersSucceeds() } } + /// + /// Test that timeout is set if a value is passed. + /// + [TestMethod] + public void CreateProjectManagerSetsTimeOutCorrectly() + { + // Arrange + ProjectManagerFactory projectManagerFactory = new(); + TimeSpan testTimeout = TimeSpan.FromMilliseconds(100); + PackageURL testPackageUrl = new("pkg:npm/foo"); + + // Act + IBaseProjectManager? testProjectManager = projectManagerFactory.CreateProjectManager(testPackageUrl, ".", testTimeout); + IBaseProjectManager? testProjectManagerWithoutTimeout = projectManagerFactory.CreateProjectManager(testPackageUrl, "."); + + // Assert + Assert.AreEqual(testTimeout, testProjectManager?.Timeout); + Assert.IsNull(testProjectManagerWithoutTimeout?.Timeout); + } + + /// + /// Test that requests time out after the timespan if specified. + /// + [TestMethod] + public async Task PackageManagerRequestsTimeOutCorrectly() + { + // Arrange + Mock mockFactory = new(); + MockHttpMessageHandler mockHttp = new(); + mockHttp + .When(HttpMethod.Get, "*") + .Respond(async () => + { + await Task.Delay(120); // simulate a delay slightly longer than the timeout set + return new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent("This is a delayed response") + }; + }); + HttpClient testClient = mockHttp.ToHttpClient(); + testClient.Timeout = TimeSpan.FromMilliseconds(100); + mockFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(testClient); + IHttpClientFactory httpFactory = mockFactory.Object; + PackageURL testPackageUrl = new("pkg:npm/foo@0.1"); + + Mock testProjectManager = new Mock(".", new NoOpPackageActions(), httpFactory, null) { CallBase = true }; + + //Act + Exception exception = await Assert.ThrowsExceptionAsync(() => testProjectManager.Object.DownloadVersionAsync(testPackageUrl, false, false)); + + //Assert + Assert.IsNotNull(exception.InnerException); + Assert.IsInstanceOfType(exception.InnerException, typeof(TimeoutException)); + } + /// /// Helper method to assert that the creates the expected implementation of . ///