Skip to content

Commit c550c51

Browse files
authored
Merge pull request #22468 from michaelnebel/csharp/refactorfeedmanager
C#: Re-factor FeedManager to allow better unit testing.
2 parents 158d358 + b76f793 commit c550c51

19 files changed

Lines changed: 462 additions & 193 deletions

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
namespace Semmle.Extraction.CSharp.DependencyFetching
1111
{
12-
public class DependabotProxy : IDisposable
12+
public class DependabotProxy : IDependabotProxy
1313
{
1414
/// <summary>
1515
/// Represents configurations for package registries.
@@ -21,24 +21,15 @@ public record class RegistryConfig(string Type, string URL);
2121
private readonly string host;
2222
private readonly string port;
2323

24-
/// <summary>
25-
/// The full address of the Dependabot proxy, if available.
26-
/// </summary>
27-
internal string Address { get; }
28-
/// <summary>
29-
/// The URLs of package registries that are configured for the proxy.
30-
/// </summary>
31-
internal HashSet<string> RegistryURLs { get; }
32-
/// <summary>
33-
/// The path to the temporary file where the certificate is stored.
34-
/// </summary>
35-
internal string? CertificatePath { get; private set; }
36-
/// <summary>
37-
/// The certificate used for the Dependabot proxy.
38-
/// </summary>
39-
internal X509Certificate2? Certificate { get; private set; }
24+
public string Address { get; }
25+
26+
public HashSet<string> RegistryURLs { get; }
27+
28+
public string? CertificatePath { get; private set; }
29+
30+
public X509Certificate2? Certificate { get; private set; }
4031

41-
internal static DependabotProxy? GetDependabotProxy(
32+
internal static IDependabotProxy? GetDependabotProxy(
4233
ILogger logger, IDiagnosticsWriter diagnosticsWriter, TemporaryDirectory tempWorkingDirectory)
4334
{
4435
// Setting HTTP(S)_PROXY and SSL_CERT_FILE have no effect on Windows or macOS,

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependencyManager.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ public sealed partial class DependencyManager : IDisposable, ICompilationInfoCon
2727
private readonly ILogger logger;
2828
private readonly IDiagnosticsWriter diagnosticsWriter;
2929
private readonly NugetPackageRestorer nugetPackageRestorer;
30-
private readonly DependabotProxy? dependabotProxy;
30+
private readonly IDependabotProxy? dependabotProxy;
3131
private readonly IDotNet dotnet;
3232
private readonly FileContent fileContent;
33-
private readonly FileProvider fileProvider;
33+
private readonly IFileProvider fileProvider;
3434

3535
// Only used as a set, but ConcurrentDictionary is the only concurrent set in .NET.
3636
private readonly IDictionary<string, bool> usedReferences = new ConcurrentDictionary<string, bool>();

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ private DotNet(IDotNetCliInvoker dotnetCliInvoker, ILogger logger, bool runDotne
3131
}
3232
}
3333

34-
private DotNet(ILogger logger, string? dotNetPath, TemporaryDirectory tempWorkingDirectory, DependabotProxy? dependabotProxy) : this(new DotNetCliInvoker(logger, Path.Join(dotNetPath ?? string.Empty, "dotnet"), dependabotProxy), logger, dotNetPath is null, tempWorkingDirectory) { }
34+
private DotNet(ILogger logger, string? dotNetPath, TemporaryDirectory tempWorkingDirectory, IDependabotProxy? dependabotProxy) : this(new DotNetCliInvoker(logger, Path.Join(dotNetPath ?? string.Empty, "dotnet"), dependabotProxy), logger, dotNetPath is null, tempWorkingDirectory) { }
3535

3636
internal static IDotNet Make(IDotNetCliInvoker dotnetCliInvoker, ILogger logger, bool runDotnetInfo) => new DotNet(dotnetCliInvoker, logger, runDotnetInfo);
3737

38-
public static IDotNet Make(ILogger logger, string? dotNetPath, TemporaryDirectory tempWorkingDirectory, DependabotProxy? dependabotProxy) => new DotNet(logger, dotNetPath, tempWorkingDirectory, dependabotProxy);
38+
public static IDotNet Make(ILogger logger, string? dotNetPath, TemporaryDirectory tempWorkingDirectory, IDependabotProxy? dependabotProxy) => new DotNet(logger, dotNetPath, tempWorkingDirectory, dependabotProxy);
3939

4040
private static void HandleRetryExitCode143(string dotnet, int attempt, ILogger logger)
4141
{
@@ -90,7 +90,8 @@ private List<string> GetRestoreArgs(RestoreSettings restoreSettings)
9090
args.Add("/p:EnableWindowsTargeting=true");
9191
}
9292

93-
args.AddRange(restoreSettings.NugetSources);
93+
var nugetSources = restoreSettings.NugetSources.SelectMany<string, string>(source => ["-s", source]).ToList();
94+
args.AddRange(nugetSources);
9495

9596
return args;
9697
}

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNetCliInvoker.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ namespace Semmle.Extraction.CSharp.DependencyFetching
1212
internal sealed class DotNetCliInvoker : IDotNetCliInvoker
1313
{
1414
private readonly ILogger logger;
15-
private readonly DependabotProxy? proxy;
15+
private readonly IDependabotProxy? proxy;
1616

1717
public string Exec { get; }
1818

19-
public DotNetCliInvoker(ILogger logger, string exec, DependabotProxy? dependabotProxy)
19+
public DotNetCliInvoker(ILogger logger, string exec, IDependabotProxy? dependabotProxy)
2020
{
2121
this.logger = logger;
2222
this.proxy = dependabotProxy;

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs

Lines changed: 15 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Collections.Immutable;
4-
using System.IO;
54
using System.Linq;
6-
using System.Net;
7-
using System.Net.Http;
8-
using System.Security.Cryptography.X509Certificates;
9-
using System.Text;
105
using System.Text.RegularExpressions;
11-
using System.Threading;
12-
using System.Threading.Tasks;
136
using Semmle.Util;
147
using Semmle.Util.Logging;
158

@@ -21,10 +14,10 @@ internal sealed partial class FeedManager : IDisposable
2114

2215
private readonly ILogger logger;
2316
private readonly IDotNet dotnet;
24-
private readonly FileProvider fileProvider;
25-
private readonly DependabotProxy? dependabotProxy;
17+
private readonly IFileProvider fileProvider;
2618
private readonly DependencyDirectory emptyPackageDirectory;
2719
private readonly ImmutableHashSet<string> privateRegistryFeeds;
20+
private readonly IFeedManagerIO feedManagerIo;
2821

2922
/// <summary>
3023
/// Gets whether there are private package registries configured for C#.
@@ -79,12 +72,12 @@ internal sealed partial class FeedManager : IDisposable
7972
/// </summary>
8073
public ImmutableHashSet<string> ReachableFallbackFeeds => lazyReachableFallbackFeeds.Value;
8174

82-
public FeedManager(ILogger logger, IDotNet dotnet, DependabotProxy? dependabotProxy, FileProvider fileProvider)
75+
public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotProxy, IFileProvider fileProvider, IFeedManagerIO feedManagerIo)
8376
{
8477
this.logger = logger;
8578
this.dotnet = dotnet;
86-
this.dependabotProxy = dependabotProxy;
8779
this.fileProvider = fileProvider;
80+
this.feedManagerIo = feedManagerIo;
8881
privateRegistryFeeds = dependabotProxy?.RegistryURLs.ToImmutableHashSet() ?? [];
8982
HasPrivateRegistryFeeds = privateRegistryFeeds.Count > 0;
9083
emptyPackageDirectory = new DependencyDirectory("empty", "empty package", logger);
@@ -105,17 +98,9 @@ public FeedManager(ILogger logger, IDotNet dotnet, DependabotProxy? dependabotPr
10598
});
10699
}
107100

108-
private string? GetDirectoryName(string path)
101+
public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotProxy, IFileProvider fileProvider)
102+
: this(logger, dotnet, dependabotProxy, fileProvider, new FeedManagerIO(logger, dependabotProxy))
109103
{
110-
try
111-
{
112-
return new FileInfo(path).Directory?.FullName;
113-
}
114-
catch (Exception exc)
115-
{
116-
logger.LogWarning($"Failed to get directory of '{path}': {exc}");
117-
}
118-
return null;
119104
}
120105

121106
private IEnumerable<string> GetFeeds(Func<IList<string>> getNugetFeeds)
@@ -157,19 +142,16 @@ private IEnumerable<string> GetFeedsFromNugetConfig(string nugetConfigPath) =>
157142
/// If there are no feeds, a dummy source argument is added to override any default feeds that `restore` would use.
158143
/// </summary>
159144
/// <param name="feeds">The list of feeds to use for the restore command.</param>
160-
/// <param name="sourceArgumentPrefix">The prefix to use for each source argument (e.g., "-s").</param>
161145
/// <returns>The list of NuGet sources arguments for the restore command.</returns>
162-
public List<string> FeedsToRestoreArgument(IEnumerable<string> feeds, string sourceArgumentPrefix)
146+
public List<string> RestoreFeeds(IEnumerable<string> feeds)
163147
{
164148
// If there are no feeds, we want to override any default feeds that `restore` would use by passing a dummy source argument.
165149
if (!feeds.Any())
166150
{
167-
return [sourceArgumentPrefix, emptyPackageDirectory.DirInfo.FullName];
151+
return [emptyPackageDirectory.DirInfo.FullName];
168152
}
169153

170-
// Add package sources. If any are present, they override all sources specified in
171-
// the configuration file(s).
172-
return feeds.SelectMany<string, string>(feed => [sourceArgumentPrefix, feed]).ToList();
154+
return feeds.ToList();
173155
}
174156

175157
private IEnumerable<string> FeedsToUseAux(HashSet<string> feedsToConsider)
@@ -196,30 +178,20 @@ private IEnumerable<string> FeedsToUseAux(HashSet<string> feedsToConsider)
196178
public IEnumerable<string> FeedsToUse(string path)
197179
{
198180
// Find the path specific feeds.
199-
var folder = GetDirectoryName(path);
181+
var folder = feedManagerIo.GetDirectoryName(path);
200182
var feedsToConsider = folder is not null ? GetFeedsFromFolder(folder).ToHashSet() : new HashSet<string>();
201183

202184
return FeedsToUseAux(feedsToConsider);
203185
}
204186

205-
/// <summary>
206-
/// Constructs the NuGet sources argument for the `dotnet restore` command based on the given feeds.
207-
/// </summary>
208-
/// <param name="feeds">The list of NuGet feeds to use for the restore command.</param>
209-
/// <returns>A list representing the NuGet sources arguments for the `dotnet restore` command.</returns>
210-
public List<string> FeedsToDotnetRestoreArgument(IEnumerable<string> feeds)
211-
{
212-
return FeedsToRestoreArgument(feeds, "-s");
213-
}
214-
215187
/// <summary>
216188
/// Constructs the list of NuGet sources to use for dotnet restore.
217189
/// (1) Use the feeds we get from `dotnet nuget list source`
218190
/// (2) Use private registries, if they are configured
219191
/// </summary>
220192
/// <param name="path">Path to project/solution</param>
221193
/// <returns>A list representing the NuGet sources arguments for the `dotnet restore` command.</returns>
222-
public List<string> MakeDotnetRestoreSourcesArguments(string path)
194+
public List<string> MakeRestoreFeeds(string path)
223195
{
224196
// Do not construct a set of explicit NuGet sources to use for restore.
225197
if (!CheckNugetFeedResponsiveness && !HasPrivateRegistryFeeds)
@@ -229,7 +201,7 @@ public List<string> MakeDotnetRestoreSourcesArguments(string path)
229201

230202
var feedsToUse = FeedsToUse(path);
231203

232-
return FeedsToDotnetRestoreArgument(feedsToUse);
204+
return RestoreFeeds(feedsToUse);
233205
}
234206

235207
private (int initialTimeout, int tryCount) GetFeedRequestSettings(bool isFallback)
@@ -251,76 +223,6 @@ public List<string> MakeDotnetRestoreSourcesArguments(string path)
251223
return (timeoutMilliSeconds, tryCount);
252224
}
253225

254-
private static async Task<HttpResponseMessage> ExecuteGetRequest(string address, HttpClient httpClient, CancellationToken cancellationToken)
255-
{
256-
return await httpClient.GetAsync(address, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
257-
}
258-
259-
private bool IsFeedReachable(string feed, int timeoutMilliSeconds, int tryCount)
260-
{
261-
logger.LogInfo($"Checking if NuGet feed '{feed}' is reachable...");
262-
263-
// Configure the HttpClient to be aware of the Dependabot Proxy, if used.
264-
HttpClientHandler httpClientHandler = new();
265-
if (dependabotProxy != null)
266-
{
267-
httpClientHandler.Proxy = new WebProxy(dependabotProxy.Address);
268-
269-
if (dependabotProxy.Certificate != null)
270-
{
271-
httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, _) =>
272-
{
273-
if (chain is null || cert is null)
274-
{
275-
var msg = cert is null && chain is null
276-
? "certificate and chain"
277-
: chain is null
278-
? "chain"
279-
: "certificate";
280-
logger.LogWarning($"Dependabot proxy certificate validation failed due to missing {msg}");
281-
return false;
282-
}
283-
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
284-
chain.ChainPolicy.CustomTrustStore.Add(dependabotProxy.Certificate);
285-
return chain.Build(cert);
286-
};
287-
}
288-
}
289-
290-
using HttpClient client = new(httpClientHandler);
291-
292-
for (var i = 0; i < tryCount; i++)
293-
{
294-
using var cts = new CancellationTokenSource();
295-
cts.CancelAfter(timeoutMilliSeconds);
296-
try
297-
{
298-
logger.LogInfo($"Attempt {i + 1}/{tryCount} to reach NuGet feed '{feed}'.");
299-
using var response = ExecuteGetRequest(feed, client, cts.Token).GetAwaiter().GetResult();
300-
response.EnsureSuccessStatusCode();
301-
logger.LogInfo($"Querying NuGet feed '{feed}' succeeded.");
302-
return true;
303-
}
304-
catch (Exception exc)
305-
{
306-
if (exc is TaskCanceledException tce &&
307-
tce.CancellationToken == cts.Token &&
308-
cts.Token.IsCancellationRequested)
309-
{
310-
logger.LogInfo($"Didn't receive answer from NuGet feed '{feed}' in {timeoutMilliSeconds}ms.");
311-
timeoutMilliSeconds *= 2;
312-
continue;
313-
}
314-
315-
logger.LogInfo($"Querying NuGet feed '{feed}' failed. The reason for the failure: {exc.Message}");
316-
return false;
317-
}
318-
}
319-
320-
logger.LogWarning($"Didn't receive answer from NuGet feed '{feed}'. Tried it {tryCount} times.");
321-
return false;
322-
}
323-
324226
/// <summary>
325227
/// Retrieves a list of excluded NuGet feeds from the corresponding environment variable.
326228
/// </summary>
@@ -374,7 +276,7 @@ public bool IsDefaultFeedReachable()
374276
if (CheckNugetFeedResponsiveness)
375277
{
376278
var (initialTimeout, tryCount) = GetFeedRequestSettings(isFallback: false);
377-
return IsFeedReachable(PublicNugetOrgFeed, initialTimeout, tryCount);
279+
return feedManagerIo.IsFeedReachable(PublicNugetOrgFeed, initialTimeout, tryCount);
378280
}
379281

380282
return true;
@@ -393,7 +295,7 @@ private List<string> GetReachableNuGetFeeds(HashSet<string> feedsToCheck, bool i
393295

394296
var (initialTimeout, tryCount) = GetFeedRequestSettings(isFallback);
395297
var reachableFeeds = feedsToCheck
396-
.Where(feed => IsFeedReachable(feed, initialTimeout, tryCount))
298+
.Where(feed => feedManagerIo.IsFeedReachable(feed, initialTimeout, tryCount))
397299
.ToList();
398300

399301
if (reachableFeeds.Count == 0)
@@ -477,7 +379,7 @@ private ImmutableHashSet<string> GetAllFeeds()
477379
if (nugetConfigs.Count > 0)
478380
{
479381
var nugetConfigFeeds = nugetConfigs
480-
.Select(GetDirectoryName)
382+
.Select(feedManagerIo.GetDirectoryName)
481383
.Where(folder => folder != null)
482384
.SelectMany(folder => GetFeedsFromFolder(folder!))
483385
.ToHashSet();

0 commit comments

Comments
 (0)