diff options
Diffstat (limited to 'MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs')
| -rw-r--r-- | MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs | 268 |
1 files changed, 199 insertions, 69 deletions
diff --git a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs index 5702af6d1..1f82c5eb0 100644 --- a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs +++ b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs @@ -1,4 +1,6 @@ -using MediaBrowser.Common.Configuration; +using System.Net.Sockets; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Model.Logging; @@ -12,7 +14,6 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Cache; -using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -40,7 +41,6 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; - private readonly IConfigurationManager _config; /// <summary> /// Initializes a new instance of the <see cref="HttpClientManager" /> class. @@ -51,7 +51,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager /// <exception cref="System.ArgumentNullException">appPaths /// or /// logger</exception> - public HttpClientManager(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem, IConfigurationManager config) + public HttpClientManager(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem) { if (appPaths == null) { @@ -64,7 +64,6 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager _logger = logger; _fileSystem = fileSystem; - _config = config; _appPaths = appPaths; // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c @@ -122,7 +121,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager } request.Method = method; - request.Timeout = 20000; + request.Timeout = options.TimeoutMs; if (!string.IsNullOrEmpty(options.Host)) { @@ -134,9 +133,22 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager request.Referer = options.Referer; } + //request.ServicePoint.BindIPEndPointDelegate = BindIPEndPointCallback; + return request; } + private static IPEndPoint BindIPEndPointCallback(ServicePoint servicePoint, IPEndPoint remoteEndPoint, int retryCount) + { + // Prefer local ipv4 + if (remoteEndPoint.AddressFamily == AddressFamily.InterNetworkV6) + { + return new IPEndPoint(IPAddress.IPv6Any, 0); + } + + return new IPEndPoint(IPAddress.Any, 0); + } + private void AddRequestHeaders(HttpWebRequest request, HttpRequestOptions options) { foreach (var header in options.RequestHeaders.ToList()) @@ -157,6 +169,20 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager } /// <summary> + /// The _semaphoreLocks + /// </summary> + private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = new ConcurrentDictionary<string, SemaphoreSlim>(StringComparer.OrdinalIgnoreCase); + /// <summary> + /// Gets the lock. + /// </summary> + /// <param name="url">The filename.</param> + /// <returns>System.Object.</returns> + private SemaphoreSlim GetLock(string url) + { + return _semaphoreLocks.GetOrAdd(url, key => new SemaphoreSlim(1, 1)); + } + + /// <summary> /// Gets the response internal. /// </summary> /// <param name="options">The options.</param> @@ -216,6 +242,110 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager /// </exception> public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod) { + HttpResponseInfo response; + + if (options.CacheMode == CacheMode.None) + { + response = await SendAsyncInternal(options, httpMethod).ConfigureAwait(false); + return response; + } + + var url = options.Url; + var urlHash = url.ToLower().GetMD5().ToString("N"); + var semaphore = GetLock(url); + + var responseCachePath = Path.Combine(_appPaths.CachePath, "httpclient", urlHash); + + response = await GetCachedResponse(responseCachePath, options.CacheLength, url).ConfigureAwait(false); + if (response != null) + { + return response; + } + + await semaphore.WaitAsync(options.CancellationToken).ConfigureAwait(false); + + try + { + response = await GetCachedResponse(responseCachePath, options.CacheLength, url).ConfigureAwait(false); + if (response != null) + { + return response; + } + + response = await SendAsyncInternal(options, httpMethod).ConfigureAwait(false); + + if (response.StatusCode == HttpStatusCode.OK) + { + await CacheResponse(response, responseCachePath).ConfigureAwait(false); + } + + return response; + } + finally + { + semaphore.Release(); + } + } + + private async Task<HttpResponseInfo> GetCachedResponse(string responseCachePath, TimeSpan cacheLength, string url) + { + try + { + if (_fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow) + { + using (var stream = _fileSystem.GetFileStream(responseCachePath, FileMode.Open, FileAccess.Read, FileShare.Read, true)) + { + var memoryStream = new MemoryStream(); + + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; + + return new HttpResponseInfo + { + ResponseUrl = url, + Content = memoryStream, + StatusCode = HttpStatusCode.OK, + Headers = new NameValueCollection(), + ContentLength = memoryStream.Length + }; + } + } + } + catch (FileNotFoundException) + { + + } + catch (DirectoryNotFoundException) + { + + } + + return null; + } + + private async Task CacheResponse(HttpResponseInfo response, string responseCachePath) + { + Directory.CreateDirectory(Path.GetDirectoryName(responseCachePath)); + + using (var responseStream = response.Content) + { + using (var fileStream = _fileSystem.GetFileStream(responseCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, true)) + { + var memoryStream = new MemoryStream(); + + await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false); + + memoryStream.Position = 0; + await memoryStream.CopyToAsync(fileStream).ConfigureAwait(false); + + memoryStream.Position = 0; + response.Content = memoryStream; + } + } + } + + private async Task<HttpResponseInfo> SendAsyncInternal(HttpRequestOptions options, string httpMethod) + { ValidateParams(options); options.CancellationToken.ThrowIfCancellationRequested(); @@ -236,11 +366,11 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager !string.IsNullOrEmpty(options.RequestContent) || string.Equals(httpMethod, "post", StringComparison.OrdinalIgnoreCase)) { - var bytes = options.RequestContentBytes ?? + var bytes = options.RequestContentBytes ?? Encoding.UTF8.GetBytes(options.RequestContent ?? string.Empty); httpWebRequest.ContentType = options.RequestContentType ?? "application/x-www-form-urlencoded"; - + httpWebRequest.ContentLength = bytes.Length; httpWebRequest.GetRequestStream().Write(bytes, 0, bytes.Length); } @@ -271,7 +401,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager if (!options.BufferContent) { - var response = await httpWebRequest.GetResponseAsync().ConfigureAwait(false); + var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false); var httpResponse = (HttpWebResponse)response; @@ -279,10 +409,10 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager options.CancellationToken.ThrowIfCancellationRequested(); - return GetResponseInfo(httpResponse, httpResponse.GetResponseStream(), GetContentLength(httpResponse)); + return GetResponseInfo(httpResponse, httpResponse.GetResponseStream(), GetContentLength(httpResponse), httpResponse); } - using (var response = await httpWebRequest.GetResponseAsync().ConfigureAwait(false)) + using (var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false)) { var httpResponse = (HttpWebResponse)response; @@ -298,7 +428,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager memoryStream.Position = 0; - return GetResponseInfo(httpResponse, memoryStream, memoryStream.Length); + return GetResponseInfo(httpResponse, memoryStream, memoryStream.Length, null); } } } @@ -315,21 +445,9 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager throw exception; } - catch (HttpRequestException ex) - { - _logger.ErrorException("Error getting response from " + options.Url, ex); - - throw new HttpException(ex.Message, ex); - } - catch (WebException ex) - { - throw GetException(ex, options); - } catch (Exception ex) { - _logger.ErrorException("Error getting response from " + options.Url, ex); - - throw; + throw GetException(ex, options); } finally { @@ -361,9 +479,9 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager return exception; } - private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, Stream content, long? contentLength) + private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, Stream content, long? contentLength, IDisposable disposable) { - return new HttpResponseInfo + return new HttpResponseInfo(disposable) { Content = content, @@ -517,21 +635,10 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager return GetResponseInfo(httpResponse, tempFile, contentLength); } } - catch (OperationCanceledException ex) - { - throw GetTempFileException(ex, options, tempFile); - } - catch (HttpRequestException ex) - { - throw GetTempFileException(ex, options, tempFile); - } - catch (WebException ex) - { - throw GetTempFileException(ex, options, tempFile); - } catch (Exception ex) { - throw GetTempFileException(ex, options, tempFile); + DeleteTempFile(tempFile); + throw GetException(ex, options); } finally { @@ -556,44 +663,25 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager protected static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - /// <summary> - /// Handles the temp file exception. - /// </summary> - /// <param name="ex">The ex.</param> - /// <param name="options">The options.</param> - /// <param name="tempFile">The temp file.</param> - /// <returns>Task.</returns> - /// <exception cref="HttpException"></exception> - private Exception GetTempFileException(Exception ex, HttpRequestOptions options, string tempFile) + private Exception GetException(Exception ex, HttpRequestOptions options) { - var operationCanceledException = ex as OperationCanceledException; + var webException = ex as WebException + ?? ex.InnerException as WebException; - if (operationCanceledException != null) + if (webException != null) { - // Cleanup - DeleteTempFile(tempFile); - - return GetCancellationException(options.Url, options.CancellationToken, operationCanceledException); + return GetException(webException, options); } - _logger.ErrorException("Error getting response from " + options.Url, ex); - - // Cleanup - DeleteTempFile(tempFile); + var operationCanceledException = ex as OperationCanceledException + ?? ex.InnerException as OperationCanceledException; - var httpRequestException = ex as HttpRequestException; - - if (httpRequestException != null) + if (operationCanceledException != null) { - return new HttpException(ex.Message, ex); + return GetCancellationException(options.Url, options.CancellationToken, operationCanceledException); } - var webException = ex as WebException; - - if (webException != null) - { - throw GetException(webException, options); - } + _logger.ErrorException("Error getting response from " + options.Url, ex); return ex; } @@ -724,5 +812,47 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager { return Post(url, postData, null, cancellationToken); } + + private Task<WebResponse> GetResponseAsync(WebRequest request, TimeSpan timeout) + { + var taskCompletion = new TaskCompletionSource<WebResponse>(); + + Task<WebResponse> asyncTask = Task.Factory.FromAsync<WebResponse>(request.BeginGetResponse, request.EndGetResponse, null); + + ThreadPool.RegisterWaitForSingleObject((asyncTask as IAsyncResult).AsyncWaitHandle, TimeoutCallback, request, timeout, true); + asyncTask.ContinueWith(task => + { + taskCompletion.TrySetResult(task.Result); + + }, TaskContinuationOptions.NotOnFaulted); + + // Handle errors + asyncTask.ContinueWith(task => + { + if (task.Exception != null) + { + taskCompletion.TrySetException(task.Exception); + } + else + { + taskCompletion.TrySetException(new List<Exception>()); + } + + }, TaskContinuationOptions.OnlyOnFaulted); + + return taskCompletion.Task; + } + + private static void TimeoutCallback(object state, bool timedOut) + { + if (timedOut) + { + WebRequest request = (WebRequest)state; + if (state != null) + { + request.Abort(); + } + } + } } } |
