22using System . IO ;
33using System . Linq ;
44using System . Net . Http ;
5+ using System . Security . Cryptography . X509Certificates ;
56using System . Threading ;
67using System . Threading . Tasks ;
78
@@ -15,6 +16,13 @@ namespace Xamarin.Android.BuildTools.PrepTasks {
1516
1617 public class DownloadUri : MTask , ICancelableTask
1718 {
19+ const int DefaultMaxRetries = 3 ;
20+ static readonly TimeSpan [ ] RetryDelays = {
21+ TimeSpan . FromSeconds ( 5 ) ,
22+ TimeSpan . FromSeconds ( 15 ) ,
23+ TimeSpan . FromSeconds ( 30 ) ,
24+ } ;
25+
1826 public DownloadUri ( )
1927 {
2028 }
@@ -27,6 +35,8 @@ public DownloadUri ()
2735
2836 public string HashHeader { get ; set ; }
2937
38+ public int MaxRetries { get ; set ; } = DefaultMaxRetries ;
39+
3040 CancellationTokenSource cancellationTokenSource ;
3141
3242 public void Cancel ( )
@@ -44,13 +54,18 @@ public override bool Execute ()
4454 var source = cancellationTokenSource = new CancellationTokenSource ( ) ;
4555 var tasks = new Task < ITaskItem > [ SourceUris . Length ] ;
4656
47- // LGTM recommendation
48- //
49- // Using HttpClient without providing a platform specific handler (WinHttpHandler or CurlHandler) where the CheckCertificateRevocationList property is set
50- // to true, will allow revoked certificates to be accepted by the HttpClient as valid.
51- //
52- var handler = new HttpClientHandler {
53- CheckCertificateRevocationList = true ,
57+ // Configure cert revocation checking in a fail-open state to avoid intermittent
58+ // failures on macOS when the CRL/OCSP endpoint is unreachable.
59+ // Matches the approach in dotnet/arcade's DownloadFile task:
60+ // https://github.com/dotnet/arcade/blob/a07b621/src/Microsoft.DotNet.Arcade.Sdk/src/DownloadFile.cs#L122-L145
61+ var handler = new SocketsHttpHandler ( ) ;
62+ handler . SslOptions . CertificateChainPolicy = new X509ChainPolicy {
63+ RevocationMode = X509RevocationMode . Online ,
64+ RevocationFlag = X509RevocationFlag . ExcludeRoot ,
65+ VerificationFlags =
66+ X509VerificationFlags . IgnoreCertificateAuthorityRevocationUnknown |
67+ X509VerificationFlags . IgnoreEndRevocationUnknown ,
68+ VerificationTimeIgnored = true ,
5469 } ;
5570
5671 using ( var client = new HttpClient ( handler ) ) {
@@ -89,21 +104,39 @@ async Task<ITaskItem> DownloadFile (HttpClient client, CancellationTokenSource s
89104 var tempPath = Path . Combine ( dp , "." + dn + ".download" ) ;
90105 Directory . CreateDirectory ( dp ) ;
91106
92- Log . LogMessage ( MessageImportance . Normal , $ "Downloading `{ uri } ` to `{ tempPath } `.") ;
93- try {
94- using ( var r = await client . GetAsync ( uri , HttpCompletionOption . ResponseHeadersRead , source . Token ) ) {
95- r . EnsureSuccessStatusCode ( ) ;
96- using ( var s = await r . Content . ReadAsStreamAsync ( ) )
97- using ( var o = File . OpenWrite ( tempPath ) ) {
98- await s . CopyToAsync ( o , 4096 , source . Token ) ;
107+ int retries = Math . Max ( 0 , MaxRetries ) ;
108+ for ( int attempt = 0 ; attempt <= retries ; attempt ++ ) {
109+ Log . LogMessage ( MessageImportance . Normal , $ "Downloading `{ uri } ` to `{ tempPath } ` (attempt { attempt + 1 } of { retries + 1 } ).") ;
110+ try {
111+ using ( var r = await client . GetAsync ( uri , HttpCompletionOption . ResponseHeadersRead , source . Token ) ) {
112+ r . EnsureSuccessStatusCode ( ) ;
113+ using ( var s = await r . Content . ReadAsStreamAsync ( ) )
114+ using ( var o = File . Create ( tempPath ) ) {
115+ await s . CopyToAsync ( o , 4096 , source . Token ) ;
116+ }
99117 }
118+ Log . LogMessage ( MessageImportance . Low , $ "mv '{ tempPath } ' '{ destinationFile } '.") ;
119+ File . Move ( tempPath , destinationFile . ItemSpec ) ;
120+ return destinationFile ;
121+ }
122+ catch ( Exception e ) when ( attempt < retries && ! source . IsCancellationRequested ) {
123+ var delay = attempt < RetryDelays . Length ? RetryDelays [ attempt ] : RetryDelays [ RetryDelays . Length - 1 ] ;
124+ Log . LogWarning ( "Failed to download URL `{0}` (attempt {1} of {2}): {3}. Retrying in {4} seconds." ,
125+ uri , attempt + 1 , retries + 1 , e . Message , ( int ) delay . TotalSeconds ) ;
126+ try {
127+ File . Delete ( tempPath ) ;
128+ } catch {
129+ }
130+ try {
131+ await TTask . Delay ( delay , source . Token ) ;
132+ } catch ( OperationCanceledException ) {
133+ break ;
134+ }
135+ }
136+ catch ( Exception e ) {
137+ Log . LogError ( "Unable to download URL `{0}` to `{1}`: {2}" , uri , destinationFile , e . Message ) ;
138+ Log . LogErrorFromException ( e ) ;
100139 }
101- Log . LogMessage ( MessageImportance . Low , $ "mv '{ tempPath } ' '{ destinationFile } '.") ;
102- File . Move ( tempPath , destinationFile . ItemSpec ) ;
103- }
104- catch ( Exception e ) {
105- Log . LogError ( "Unable to download URL `{0}` to `{1}`: {2}" , uri , destinationFile , e . Message ) ;
106- Log . LogErrorFromException ( e ) ;
107140 }
108141 return destinationFile ;
109142 }
0 commit comments