7
7
using System . IO . Compression ;
8
8
using System . Linq ;
9
9
using System . Net . Http ;
10
+ using System . Threading ;
10
11
using System . Threading . Tasks ;
11
12
using Microsoft . Azure . WebJobs . Script . Config ;
12
13
using Microsoft . Azure . WebJobs . Script . Configuration ;
@@ -22,22 +23,25 @@ namespace Microsoft.Azure.WebJobs.Script.ExtensionBundle
22
23
{
23
24
public class ExtensionBundleManager : IExtensionBundleManager
24
25
{
26
+ private const string ExtensionBundleClientName = nameof ( ExtensionBundleManager ) ;
25
27
private readonly IEnvironment _environment ;
26
28
private readonly ExtensionBundleOptions _options ;
27
29
private readonly FunctionsHostingConfigOptions _configOption ;
28
30
private readonly ILogger _logger ;
29
31
private readonly string _cdnUri ;
30
32
private readonly string _platformReleaseChannel ;
33
+ private readonly IHttpClientFactory _httpClientFactory ;
31
34
private string _extensionBundleVersion ;
32
35
33
- public ExtensionBundleManager ( ExtensionBundleOptions options , IEnvironment environment , ILoggerFactory loggerFactory , FunctionsHostingConfigOptions configOption )
36
+ public ExtensionBundleManager ( ExtensionBundleOptions options , IEnvironment environment , ILoggerFactory loggerFactory , FunctionsHostingConfigOptions configOption , IHttpClientFactory httpClientFactory )
34
37
{
35
38
_environment = environment ?? throw new ArgumentNullException ( nameof ( environment ) ) ;
36
39
_logger = loggerFactory . CreateLogger < ExtensionBundleManager > ( ) ?? throw new ArgumentNullException ( nameof ( loggerFactory ) ) ;
37
40
_options = options ?? throw new ArgumentNullException ( nameof ( options ) ) ;
38
41
_configOption = configOption ?? throw new ArgumentNullException ( nameof ( configOption ) ) ;
39
42
_cdnUri = _environment . GetEnvironmentVariable ( EnvironmentSettingNames . ExtensionBundleSourceUri ) ?? ScriptConstants . ExtensionBundleDefaultSourceUri ;
40
43
_platformReleaseChannel = _environment . GetEnvironmentVariable ( EnvironmentSettingNames . AntaresPlatformReleaseChannel ) ?? ScriptConstants . LatestPlatformChannelNameUpper ;
44
+ _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException ( nameof ( httpClientFactory ) ) ;
41
45
}
42
46
43
47
public async Task < ExtensionBundleDetails > GetExtensionBundleDetails ( )
@@ -79,10 +83,8 @@ public bool IsLegacyExtensionBundle()
79
83
/// <returns>Path of the extension bundle.</returns>
80
84
public async Task < string > GetExtensionBundlePath ( )
81
85
{
82
- using ( var httpClient = new HttpClient ( ) )
83
- {
84
- return await GetBundle ( httpClient ) ;
85
- }
86
+ var client = _httpClientFactory . CreateClient ( ExtensionBundleClientName ) ;
87
+ return await GetBundle ( client ) ;
86
88
}
87
89
88
90
/// <summary>
@@ -199,34 +201,103 @@ private string GetBundleFlavorForCurrentEnvironment()
199
201
return ScriptConstants . ExtensionBundleForNonAppServiceEnvironment ;
200
202
}
201
203
202
- private async Task < bool > TryDownloadZipFileAsync ( Uri zipUri , string filePath , HttpClient httpClient )
204
+ private async Task < bool > TryDownloadZipFileAsync ( Uri zipUri , string filePath , HttpClient httpClient , CancellationToken cancellationToken = default )
203
205
{
204
- _logger . DownloadingZip ( zipUri , filePath ) ;
205
- var response = await httpClient . GetAsync ( zipUri ) ;
206
- if ( ! response . IsSuccessStatusCode )
206
+ string azureRef = string . Empty ;
207
+ try
208
+ {
209
+ _logger . DownloadingZip ( zipUri , filePath ) ;
210
+
211
+ using var response = await httpClient . GetAsync ( zipUri , HttpCompletionOption . ResponseHeadersRead , cancellationToken ) ;
212
+
213
+ // Log AzureRef header if present (debug level to avoid noise in normal operations)
214
+ response . TryGetAzureRef ( out azureRef ) ;
215
+
216
+ response . EnsureSuccessStatusCode ( ) ;
217
+
218
+ using var fileStream = new FileStream ( filePath , FileMode . Create , FileAccess . Write , FileShare . None , bufferSize : 4096 , useAsync : true ) ;
219
+ await response . Content . CopyToAsync ( fileStream , cancellationToken ) ;
220
+ await fileStream . FlushAsync ( cancellationToken ) ;
221
+
222
+ _logger . DownloadComplete ( zipUri , filePath ) ;
223
+
224
+ return true ;
225
+ }
226
+ catch ( HttpRequestException ex )
207
227
{
208
- _logger . ErrorDownloadingZip ( zipUri , response ) ;
228
+ var statusCode = ex . StatusCode ;
229
+ _logger . ErrorDownloadingExtensionBundleZipHttpRequest (
230
+ ex ,
231
+ zipUri ,
232
+ statusCode ,
233
+ ex . HttpRequestError ,
234
+ filePath ,
235
+ GetDiskUsageSafe ( filePath ) ,
236
+ azureRef ) ;
209
237
return false ;
210
238
}
211
-
212
- using ( var content = await response . Content . ReadAsStreamAsync ( ) )
213
- using ( var stream = new FileStream ( filePath , FileMode . Create , FileAccess . Write , FileShare . None , bufferSize : 4096 , useAsync : true ) )
239
+ catch ( IOException ex )
214
240
{
215
- await content . CopyToAsync ( stream ) ;
241
+ _logger . ErrorDownloadingExtensionBundleZipIO (
242
+ ex ,
243
+ zipUri ,
244
+ filePath ,
245
+ GetDiskUsageSafe ( filePath ) ,
246
+ azureRef ) ;
247
+ return false ;
216
248
}
249
+ catch ( Exception ex )
250
+ {
251
+ // Non-HttpRequestException path: log as unexpected without HTTP-specific fields.
252
+ _logger . ErrorDownloadingExtensionBundleZipUnexpected (
253
+ ex ,
254
+ zipUri ,
255
+ filePath ,
256
+ GetDiskUsageSafe ( filePath ) ,
257
+ azureRef ) ;
217
258
218
- _logger . DownloadComplete ( zipUri , filePath ) ;
219
- return true ;
259
+ return false ;
260
+ }
220
261
}
221
262
222
- private async Task < string > GetLatestMatchingBundleVersionAsync ( )
263
+ private string GetDiskUsageSafe ( string path )
223
264
{
224
- using ( var httpClient = new HttpClient ( ) )
265
+ try
266
+ {
267
+ var root = Path . GetPathRoot ( path ) ;
268
+ if ( string . IsNullOrEmpty ( root ) )
269
+ {
270
+ return "error=RootPathNotFound" ;
271
+ }
272
+
273
+ var di = new DriveInfo ( root ) ;
274
+ const double BytesPerMB = 1024d * 1024d ;
275
+ double freeMb = di . AvailableFreeSpace / BytesPerMB ;
276
+ double totalMb = di . TotalSize / BytesPerMB ;
277
+ return $ "free={ freeMb : F2} MB total={ totalMb : F2} MB";
278
+ }
279
+ catch ( Exception ex )
225
280
{
226
- return await GetLatestMatchingBundleVersionAsync ( httpClient ) ;
281
+ return FormatDiskError ( ex ) ;
227
282
}
228
283
}
229
284
285
+ private static string FormatDiskError ( Exception ex )
286
+ {
287
+ var msg = ex . Message ? . Replace ( Environment . NewLine , " " ) . Trim ( ) ;
288
+ if ( ! string . IsNullOrEmpty ( msg ) && msg . Length > 200 )
289
+ {
290
+ msg = msg . Substring ( 0 , 200 ) + "..." ;
291
+ }
292
+ return $ "error={ ex . GetType ( ) . Name } : { msg } ";
293
+ }
294
+
295
+ private async Task < string > GetLatestMatchingBundleVersionAsync ( )
296
+ {
297
+ var client = _httpClientFactory . CreateClient ( ExtensionBundleClientName ) ;
298
+ return await GetLatestMatchingBundleVersionAsync ( client ) ;
299
+ }
300
+
230
301
private async Task < string > GetLatestMatchingBundleVersionAsync ( HttpClient httpClient )
231
302
{
232
303
var uri = new Uri ( $ "{ _cdnUri } /{ ScriptConstants . ExtensionBundleDirectory } /{ _options . Id } /{ ScriptConstants . ExtensionBundleVersionIndexFile } ") ;
0 commit comments