2
2
using System . Collections . Generic ;
3
3
using System . IO ;
4
4
using System . Linq ;
5
+ using System . Net . Http ;
5
6
using System . Text . RegularExpressions ;
7
+ using System . Threading ;
6
8
using System . Threading . Tasks ;
7
9
using Semmle . Util ;
8
10
@@ -14,6 +16,13 @@ private void RestoreNugetPackages(List<FileInfo> allNonBinaryFiles, IEnumerable<
14
16
{
15
17
try
16
18
{
19
+ var checkNugetFeedResponsiveness = EnvironmentVariables . GetBoolean ( EnvironmentVariableNames . CheckNugetFeedResponsiveness ) ;
20
+ if ( checkNugetFeedResponsiveness && ! CheckFeeds ( allNonBinaryFiles ) )
21
+ {
22
+ DownloadMissingPackages ( allNonBinaryFiles , dllPaths , withNugetConfig : false ) ;
23
+ return ;
24
+ }
25
+
17
26
using ( var nuget = new NugetPackages ( sourceDir . FullName , legacyPackageDirectory , logger ) )
18
27
{
19
28
var count = nuget . InstallPackages ( ) ;
@@ -139,7 +148,7 @@ private void RestoreProjects(IEnumerable<string> projects, out IEnumerable<strin
139
148
CompilationInfos . Add ( ( "Failed project restore with package source error" , nugetSourceFailures . ToString ( ) ) ) ;
140
149
}
141
150
142
- private void DownloadMissingPackages ( List < FileInfo > allFiles , ISet < string > dllPaths )
151
+ private void DownloadMissingPackages ( List < FileInfo > allFiles , ISet < string > dllPaths , bool withNugetConfig = true )
143
152
{
144
153
var alreadyDownloadedPackages = GetRestoredPackageDirectoryNames ( packageDirectory . DirInfo ) ;
145
154
var alreadyDownloadedLegacyPackages = GetRestoredLegacyPackageNames ( ) ;
@@ -172,30 +181,9 @@ private void DownloadMissingPackages(List<FileInfo> allFiles, ISet<string> dllPa
172
181
}
173
182
174
183
logger . LogInfo ( $ "Found { notYetDownloadedPackages . Count } packages that are not yet restored") ;
175
-
176
- var nugetConfigs = allFiles . SelectFileNamesByName ( "nuget.config" ) . ToArray ( ) ;
177
- string ? nugetConfig = null ;
178
- if ( nugetConfigs . Length > 1 )
179
- {
180
- logger . LogInfo ( $ "Found multiple nuget.config files: { string . Join ( ", " , nugetConfigs ) } .") ;
181
- nugetConfig = allFiles
182
- . SelectRootFiles ( sourceDir )
183
- . SelectFileNamesByName ( "nuget.config" )
184
- . FirstOrDefault ( ) ;
185
- if ( nugetConfig == null )
186
- {
187
- logger . LogInfo ( "Could not find a top-level nuget.config file." ) ;
188
- }
189
- }
190
- else
191
- {
192
- nugetConfig = nugetConfigs . FirstOrDefault ( ) ;
193
- }
194
-
195
- if ( nugetConfig != null )
196
- {
197
- logger . LogInfo ( $ "Using nuget.config file { nugetConfig } .") ;
198
- }
184
+ var nugetConfig = withNugetConfig
185
+ ? GetNugetConfig ( allFiles )
186
+ : null ;
199
187
200
188
CompilationInfos . Add ( ( "Fallback nuget restore" , notYetDownloadedPackages . Count . ToString ( ) ) ) ;
201
189
@@ -221,6 +209,37 @@ private void DownloadMissingPackages(List<FileInfo> allFiles, ISet<string> dllPa
221
209
dllPaths . Add ( missingPackageDirectory . DirInfo . FullName ) ;
222
210
}
223
211
212
+ private string [ ] GetAllNugetConfigs ( List < FileInfo > allFiles ) => allFiles . SelectFileNamesByName ( "nuget.config" ) . ToArray ( ) ;
213
+
214
+ private string ? GetNugetConfig ( List < FileInfo > allFiles )
215
+ {
216
+ var nugetConfigs = GetAllNugetConfigs ( allFiles ) ;
217
+ string ? nugetConfig ;
218
+ if ( nugetConfigs . Length > 1 )
219
+ {
220
+ logger . LogInfo ( $ "Found multiple nuget.config files: { string . Join ( ", " , nugetConfigs ) } .") ;
221
+ nugetConfig = allFiles
222
+ . SelectRootFiles ( sourceDir )
223
+ . SelectFileNamesByName ( "nuget.config" )
224
+ . FirstOrDefault ( ) ;
225
+ if ( nugetConfig == null )
226
+ {
227
+ logger . LogInfo ( "Could not find a top-level nuget.config file." ) ;
228
+ }
229
+ }
230
+ else
231
+ {
232
+ nugetConfig = nugetConfigs . FirstOrDefault ( ) ;
233
+ }
234
+
235
+ if ( nugetConfig != null )
236
+ {
237
+ logger . LogInfo ( $ "Using nuget.config file { nugetConfig } .") ;
238
+ }
239
+
240
+ return nugetConfig ;
241
+ }
242
+
224
243
private void LogAllUnusedPackages ( DependencyContainer dependencies )
225
244
{
226
245
var allPackageDirectories = GetAllPackageDirectories ( ) ;
@@ -279,9 +298,6 @@ private static IEnumerable<string> GetRestoredPackageDirectoryNames(DirectoryInf
279
298
. Select ( d => Path . GetFileName ( d ) . ToLowerInvariant ( ) ) ;
280
299
}
281
300
282
- [ GeneratedRegex ( @"<TargetFramework>.*</TargetFramework>" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
283
- private static partial Regex TargetFramework ( ) ;
284
-
285
301
private bool TryRestorePackageManually ( string package , string ? nugetConfig , PackageReferenceSource packageReferenceSource = PackageReferenceSource . SdkCsProj )
286
302
{
287
303
logger . LogInfo ( $ "Restoring package { package } ...") ;
@@ -358,7 +374,126 @@ private void TryChangeTargetFrameworkMoniker(DirectoryInfo tempDir)
358
374
}
359
375
}
360
376
377
+ private static async Task ExecuteGetRequest ( string address , HttpClient httpClient , CancellationToken cancellationToken )
378
+ {
379
+ using var stream = await httpClient . GetStreamAsync ( address , cancellationToken ) ;
380
+ var buffer = new byte [ 1024 ] ;
381
+ int bytesRead ;
382
+ while ( ( bytesRead = stream . Read ( buffer , 0 , buffer . Length ) ) > 0 )
383
+ {
384
+ // do nothing
385
+ }
386
+ }
387
+
388
+ private bool IsFeedReachable ( string feed )
389
+ {
390
+ using HttpClient client = new ( ) ;
391
+ var timeoutSeconds = 1 ;
392
+ var tryCount = 4 ;
393
+
394
+ for ( var i = 0 ; i < tryCount ; i ++ )
395
+ {
396
+ using var cts = new CancellationTokenSource ( ) ;
397
+ cts . CancelAfter ( timeoutSeconds * 1000 ) ;
398
+ try
399
+ {
400
+ ExecuteGetRequest ( feed , client , cts . Token ) . GetAwaiter ( ) . GetResult ( ) ;
401
+ return true ;
402
+ }
403
+ catch ( Exception exc )
404
+ {
405
+ if ( exc is TaskCanceledException tce &&
406
+ tce . CancellationToken == cts . Token &&
407
+ cts . Token . IsCancellationRequested )
408
+ {
409
+ logger . LogWarning ( $ "Didn't receive answer from Nuget feed '{ feed } ' in { timeoutSeconds } seconds.") ;
410
+ timeoutSeconds *= 2 ;
411
+ continue ;
412
+ }
413
+
414
+ // We're only interested in timeouts.
415
+ logger . LogWarning ( $ "Querying Nuget feed '{ feed } ' failed: { exc } ") ;
416
+ return true ;
417
+ }
418
+ }
419
+
420
+ logger . LogError ( $ "Didn't receive answer from Nuget feed '{ feed } '. Tried it { tryCount } times.") ;
421
+ return false ;
422
+ }
423
+
424
+ private bool CheckFeeds ( List < FileInfo > allFiles )
425
+ {
426
+ logger . LogInfo ( "Checking Nuget feeds..." ) ;
427
+ var feeds = GetAllFeeds ( allFiles ) ;
428
+
429
+ var excludedFeeds = Environment . GetEnvironmentVariable ( EnvironmentVariableNames . ExcludedNugetFeedsFromResponsivenessCheck )
430
+ ? . Split ( Path . PathSeparator , StringSplitOptions . RemoveEmptyEntries )
431
+ . ToHashSet ( ) ?? [ ] ;
432
+
433
+ if ( excludedFeeds . Count > 0 )
434
+ {
435
+ logger . LogInfo ( $ "Excluded feeds from responsiveness check: { string . Join ( ", " , excludedFeeds ) } ") ;
436
+ }
437
+
438
+ var allFeedsReachable = feeds . All ( feed => excludedFeeds . Contains ( feed ) || IsFeedReachable ( feed ) ) ;
439
+ if ( ! allFeedsReachable )
440
+ {
441
+ diagnosticsWriter . AddEntry ( new DiagnosticMessage (
442
+ Language . CSharp ,
443
+ "buildless/unreachable-feed" ,
444
+ "Found unreachable Nuget feed in C# analysis with build-mode 'none'" ,
445
+ visibility : new DiagnosticMessage . TspVisibility ( statusPage : true , cliSummaryTable : true , telemetry : true ) ,
446
+ markdownMessage : "Found unreachable Nuget feed in C# analysis with build-mode 'none'. This may cause missing dependencies in the analysis." ,
447
+ severity : DiagnosticMessage . TspSeverity . Warning
448
+ ) ) ;
449
+ }
450
+ CompilationInfos . Add ( ( "All Nuget feeds reachable" , allFeedsReachable ? "1" : "0" ) ) ;
451
+ return allFeedsReachable ;
452
+ }
453
+
454
+ private IEnumerable < string > GetFeeds ( string nugetConfig )
455
+ {
456
+ logger . LogInfo ( $ "Getting Nuget feeds from '{ nugetConfig } '...") ;
457
+ var results = dotnet . GetNugetFeeds ( nugetConfig ) ;
458
+ var regex = EnabledNugetFeed ( ) ;
459
+ foreach ( var result in results )
460
+ {
461
+ var match = regex . Match ( result ) ;
462
+ if ( ! match . Success )
463
+ {
464
+ logger . LogError ( $ "Failed to parse feed from '{ result } '") ;
465
+ continue ;
466
+ }
467
+
468
+ var url = match . Groups [ 1 ] . Value ;
469
+ if ( ! url . StartsWith ( "https://" , StringComparison . InvariantCultureIgnoreCase ) &&
470
+ ! url . StartsWith ( "http://" , StringComparison . InvariantCultureIgnoreCase ) )
471
+ {
472
+ logger . LogInfo ( $ "Skipping feed '{ url } ' as it is not a valid URL.") ;
473
+ continue ;
474
+ }
475
+
476
+ yield return url ;
477
+ }
478
+ }
479
+
480
+ private HashSet < string > GetAllFeeds ( List < FileInfo > allFiles )
481
+ {
482
+ var nugetConfigs = GetAllNugetConfigs ( allFiles ) ;
483
+ var feeds = nugetConfigs
484
+ . SelectMany ( nf => GetFeeds ( nf ) )
485
+ . Where ( str => ! string . IsNullOrWhiteSpace ( str ) )
486
+ . ToHashSet ( ) ;
487
+ return feeds ;
488
+ }
489
+
490
+ [ GeneratedRegex ( @"<TargetFramework>.*</TargetFramework>" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
491
+ private static partial Regex TargetFramework ( ) ;
492
+
361
493
[ GeneratedRegex ( @"^(.+)\.(\d+\.\d+\.\d+(-(.+))?)$" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
362
494
private static partial Regex LegacyNugetPackage ( ) ;
495
+
496
+ [ GeneratedRegex ( @"^E (.*)$" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
497
+ private static partial Regex EnabledNugetFeed ( ) ;
363
498
}
364
499
}
0 commit comments