diff --git a/Mono.Addins.Setup/Mono.Addins.Setup.csproj b/Mono.Addins.Setup/Mono.Addins.Setup.csproj index 26c276d2..ac0e039c 100644 --- a/Mono.Addins.Setup/Mono.Addins.Setup.csproj +++ b/Mono.Addins.Setup/Mono.Addins.Setup.csproj @@ -46,6 +46,7 @@ + {91DD5A2D-9FE3-4C3C-9253-876141874DAD} @@ -89,6 +90,8 @@ + + diff --git a/Mono.Addins.Setup/Mono.Addins.Setup/AddinStore.cs b/Mono.Addins.Setup/Mono.Addins.Setup/AddinStore.cs index eef58f29..cc8a1dae 100644 --- a/Mono.Addins.Setup/Mono.Addins.Setup/AddinStore.cs +++ b/Mono.Addins.Setup/Mono.Addins.Setup/AddinStore.cs @@ -46,6 +46,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; namespace Mono.Addins.Setup { @@ -641,28 +642,30 @@ internal string DownloadFile (IProgressMonitor monitor, string url) try { monitor.BeginTask ("Requesting " + url, 2); - var resp = WebRequestHelper.GetResponse ( - () => (HttpWebRequest)WebRequest.Create (url), - r => r.Headers ["Pragma"] = "no-cache" - ); - monitor.Step (1); - monitor.BeginTask ("Downloading " + url, (int) resp.ContentLength); + var task = DownloadFileRequest.DownloadFile (url, noCache: true); + if (!WaitForTask (task, monitor)) + throw new InstallException ("Installation cancelled."); - file = Path.GetTempFileName (); - fs = new FileStream (file, FileMode.Create, FileAccess.Write); - s = resp.GetResponseStream (); - byte[] buffer = new byte [4096]; - - int n; - while ((n = s.Read (buffer, 0, buffer.Length)) != 0) { - monitor.Step (n); - fs.Write (buffer, 0, n); - if (monitor.IsCancelRequested) - throw new InstallException ("Installation cancelled."); + using (var request = task.Result) { + monitor.Step (1); + monitor.BeginTask ("Downloading " + url, (int) request.ContentLength); + + file = Path.GetTempFileName (); + fs = new FileStream (file, FileMode.Create, FileAccess.Write); + s = request.Stream; + byte[] buffer = new byte [4096]; + + int n; + while ((n = s.Read (buffer, 0, buffer.Length)) != 0) { + monitor.Step (n); + fs.Write (buffer, 0, n); + if (monitor.IsCancelRequested) + throw new InstallException ("Installation cancelled."); + } + fs.Close (); + s.Close (); + return file; } - fs.Close (); - s.Close (); - return file; } catch { if (fs != null) fs.Close (); @@ -673,10 +676,22 @@ internal string DownloadFile (IProgressMonitor monitor, string url) throw; } finally { monitor.EndTask (); - monitor.EndTask (); } } - + + static bool WaitForTask (Task task, IProgressMonitor monitor) + { + bool result = SpinWait.SpinUntil (() => { + return monitor.IsCancelRequested || task.IsCompleted || task.IsFaulted; + }, 100000); // Use same default timeout as HttpClient. + + if (monitor.IsCancelRequested) + return false; + if (!result) + throw new InstallException ("Timed out."); + return result; + } + internal bool HasWriteAccess (string file) { FileInfo f = new FileInfo (file); diff --git a/Mono.Addins.Setup/Mono.Addins.Setup/DownloadFileRequest.cs b/Mono.Addins.Setup/Mono.Addins.Setup/DownloadFileRequest.cs new file mode 100644 index 00000000..565c0635 --- /dev/null +++ b/Mono.Addins.Setup/Mono.Addins.Setup/DownloadFileRequest.cs @@ -0,0 +1,129 @@ +// +// WebRequestWrapper.cs +// +// Author: +// Matt Ward +// +// Copyright (c) 2018 Microsoft +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Mono.Addins.Setup +{ + abstract class DownloadFileRequest : IDisposable + { + public abstract void Dispose (); + public abstract int ContentLength { get; } + public abstract Stream Stream { get; } + + public static Task DownloadFile (string url, bool noCache) + { + if (HttpClientProvider.HasCustomCreation || !WebRequestHelper.HasCustomRequestHandler) + return HttpClientDownloadFileRequest.Create (url, noCache); + + return WebRequestDownloadFileRequest.Create (url, noCache); + } + } + + class HttpClientDownloadFileRequest : DownloadFileRequest + { + HttpClient client; + HttpResponseMessage response; + Stream stream; + + public static Task Create (string url, bool noCache) + { + // Use Task.Run to avoid hanging the UI thread when waiting for the GetAsync method to return + // with the response for an .mpack file download. + return Task.Run (async () => { + var client = HttpClientProvider.CreateHttpClient (url); + if (noCache) + client.DefaultRequestHeaders.Add ("Pragma", "no-cache"); + + var response = await client.GetAsync (url, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait (false); + var stream = await response.Content.ReadAsStreamAsync ().ConfigureAwait (false); + + return new HttpClientDownloadFileRequest { + client = client, + response = response, + stream = stream + }; + }); + } + + public override int ContentLength { + get { return (int)response.Content.Headers.ContentLength; } + } + + public override Stream Stream { + get { return stream; } + } + + public override void Dispose () + { + stream?.Dispose (); + response?.Dispose (); + client.Dispose (); + } + } + + class WebRequestDownloadFileRequest : DownloadFileRequest + { + WebResponse response; + Stream stream; + + public static Task Create (string url, bool noCache) + { + var response = WebRequestHelper.GetResponse ( + () => (HttpWebRequest)WebRequest.Create (url), + r => { + if (noCache) + r.Headers ["Pragma"] = "no-cache"; + } + ); + + var request = new WebRequestDownloadFileRequest { + response = response, + stream = response.GetResponseStream () + }; + + return Task.FromResult (request); + } + + public override int ContentLength { + get { return (int)response.ContentLength; } + } + + public override Stream Stream { + get { return stream; } + } + + public override void Dispose () + { + stream?.Dispose (); + response.Dispose (); + } + } +} diff --git a/Mono.Addins.Setup/Mono.Addins.Setup/HttpClientProvider.cs b/Mono.Addins.Setup/Mono.Addins.Setup/HttpClientProvider.cs new file mode 100644 index 00000000..387cb59c --- /dev/null +++ b/Mono.Addins.Setup/Mono.Addins.Setup/HttpClientProvider.cs @@ -0,0 +1,65 @@ +// +// HttpClientProvider.cs +// +// Author: +// Matt Ward +// +// Copyright (c) 2018 Microsoft +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Net; +using System.Net.Http; + +namespace Mono.Addins.Setup +{ + /// + /// Creates a HttpClient with support for authenticated proxies. + /// + public static class HttpClientProvider + { + static Func httpClientFactory; + + public static void SetHttpClientFactory (Func factory) + { + httpClientFactory = factory; + } + + static internal bool HasCustomCreation { + get { return httpClientFactory != null; } + } + + /// + /// Creates a new HttpClient. + /// + /// The HttpClient. + /// The request url. + public static HttpClient CreateHttpClient (string uri) + { + if (httpClientFactory != null) + return httpClientFactory.Invoke (uri); + + var handler = new HttpClientHandler { + AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip + }; + return new HttpClient (handler); + } + } +} diff --git a/Mono.Addins.Setup/Mono.Addins.Setup/Repository.cs b/Mono.Addins.Setup/Mono.Addins.Setup/Repository.cs index 86abd8f8..5c28af8e 100644 --- a/Mono.Addins.Setup/Mono.Addins.Setup/Repository.cs +++ b/Mono.Addins.Setup/Mono.Addins.Setup/Repository.cs @@ -128,21 +128,27 @@ public IAsyncResult BeginDownloadSupportFile (string name, AsyncCallback cb, obj } res.FilePath = cachedFile; - WebRequestHelper.GetResponseAsync (() => (HttpWebRequest)WebRequest.Create (u)).ContinueWith (t => { + var request = DownloadFileRequest.DownloadFile (u.ToString (), false).ContinueWith (t => { try { - var resp = t.Result; - string dir = Path.GetDirectoryName (res.FilePath); - lock (this) { - if (!Directory.Exists (dir)) - Directory.CreateDirectory (dir); - } - byte[] buffer = new byte [8092]; - using (var s = resp.GetResponseStream ()) { - using (var f = File.OpenWrite (res.FilePath)) { - int nr = 0; - while ((nr = s.Read (buffer, 0, buffer.Length)) > 0) - f.Write (buffer, 0, nr); + using (var resp = t.Result) { + string dir = Path.GetDirectoryName (res.FilePath); + lock (this) { + if (!Directory.Exists (dir)) + Directory.CreateDirectory (dir); + } + if (File.Exists (res.FilePath)) { + res.SetDone (); + return; + } + byte [] buffer = new byte [8092]; + using (var s = resp.Stream) { + using (var f = File.OpenWrite (res.FilePath)) { + int nr = 0; + while ((nr = s.Read (buffer, 0, buffer.Length)) > 0) + f.Write (buffer, 0, nr); + } } + res.SetDone (); } } catch (Exception ex) { res.Error = ex; diff --git a/Mono.Addins.Setup/Mono.Addins.Setup/WebRequestHelper.cs b/Mono.Addins.Setup/Mono.Addins.Setup/WebRequestHelper.cs index 5685b5ac..f962c44e 100644 --- a/Mono.Addins.Setup/Mono.Addins.Setup/WebRequestHelper.cs +++ b/Mono.Addins.Setup/Mono.Addins.Setup/WebRequestHelper.cs @@ -46,6 +46,10 @@ public static void SetRequestHandler (Func, Action /// Gets the web response, using the request handler to handle proxy authentication /// if necessary.