22using System . Collections . Generic ;
33using System . IO ;
44using System . Linq ;
5+ using System . Net . Http ;
56using System . Text . RegularExpressions ;
7+ using System . Threading ;
68using System . Threading . Tasks ;
79using Semmle . Util ;
810
@@ -14,6 +16,13 @@ private void RestoreNugetPackages(List<FileInfo> allNonBinaryFiles, IEnumerable<
1416 {
1517 try
1618 {
19+ var checkNugetFeedResponsiveness = EnvironmentVariables . GetBoolean ( EnvironmentVariableNames . CheckNugetFeedResponsiveness ) ;
20+ if ( checkNugetFeedResponsiveness && ! CheckFeeds ( allNonBinaryFiles ) )
21+ {
22+ DownloadMissingPackages ( allNonBinaryFiles , dllPaths , withNugetConfig : false ) ;
23+ return ;
24+ }
25+
1726 using ( var nuget = new NugetPackages ( sourceDir . FullName , legacyPackageDirectory , logger ) )
1827 {
1928 var count = nuget . InstallPackages ( ) ;
@@ -139,7 +148,7 @@ private void RestoreProjects(IEnumerable<string> projects, out IEnumerable<strin
139148 CompilationInfos . Add ( ( "Failed project restore with package source error" , nugetSourceFailures . ToString ( ) ) ) ;
140149 }
141150
142- private void DownloadMissingPackages ( List < FileInfo > allFiles , ISet < string > dllPaths )
151+ private void DownloadMissingPackages ( List < FileInfo > allFiles , ISet < string > dllPaths , bool withNugetConfig = true )
143152 {
144153 var alreadyDownloadedPackages = GetRestoredPackageDirectoryNames ( packageDirectory . DirInfo ) ;
145154 var alreadyDownloadedLegacyPackages = GetRestoredLegacyPackageNames ( ) ;
@@ -172,30 +181,9 @@ private void DownloadMissingPackages(List<FileInfo> allFiles, ISet<string> dllPa
172181 }
173182
174183 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 ;
199187
200188 CompilationInfos . Add ( ( "Fallback nuget restore" , notYetDownloadedPackages . Count . ToString ( ) ) ) ;
201189
@@ -221,6 +209,37 @@ private void DownloadMissingPackages(List<FileInfo> allFiles, ISet<string> dllPa
221209 dllPaths . Add ( missingPackageDirectory . DirInfo . FullName ) ;
222210 }
223211
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+
224243 private void LogAllUnusedPackages ( DependencyContainer dependencies )
225244 {
226245 var allPackageDirectories = GetAllPackageDirectories ( ) ;
@@ -279,9 +298,6 @@ private static IEnumerable<string> GetRestoredPackageDirectoryNames(DirectoryInf
279298 . Select ( d => Path . GetFileName ( d ) . ToLowerInvariant ( ) ) ;
280299 }
281300
282- [ GeneratedRegex ( @"<TargetFramework>.*</TargetFramework>" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
283- private static partial Regex TargetFramework ( ) ;
284-
285301 private bool TryRestorePackageManually ( string package , string ? nugetConfig , PackageReferenceSource packageReferenceSource = PackageReferenceSource . SdkCsProj )
286302 {
287303 logger . LogInfo ( $ "Restoring package { package } ...") ;
@@ -358,7 +374,126 @@ private void TryChangeTargetFrameworkMoniker(DirectoryInfo tempDir)
358374 }
359375 }
360376
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+
361493 [ GeneratedRegex ( @"^(.+)\.(\d+\.\d+\.\d+(-(.+))?)$" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
362494 private static partial Regex LegacyNugetPackage ( ) ;
495+
496+ [ GeneratedRegex ( @"^E (.*)$" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
497+ private static partial Regex EnabledNugetFeed ( ) ;
363498 }
364499}
0 commit comments