• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

KSP-CKAN / CKAN / 18959752962

31 Oct 2025 01:19AM UTC coverage: 85.329% (+3.5%) from 81.873%
18959752962

Pull #4454

github

HebaruSan
Build on Windows, upload multi-platform coverage
Pull Request #4454: Build on Windows, upload multi-platform coverage

2005 of 2167 branches covered (92.52%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 1 file covered. (100.0%)

27 existing lines in 19 files now uncovered.

11971 of 14212 relevant lines covered (84.23%)

1.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.25
/Core/Net/NetAsyncDownloader.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Linq;
4
using System.Net;
5
using System.Security.Cryptography;
6
using System.Threading;
7

8
using Autofac;
9
using log4net;
10

11
namespace CKAN
12
{
13
    /// <summary>
14
    /// Download lots of files at once!
15
    /// </summary>
16
    public partial class NetAsyncDownloader
17
    {
18
        private static readonly ILog log = LogManager.GetLogger(typeof(NetAsyncDownloader));
2✔
19

20
        public  readonly IUser  User;
21
        private readonly string userAgent;
22
        private readonly Func<HashAlgorithm?> getHashAlgo;
23

24
        /// <summary>
25
        /// Raised when data arrives for a download
26
        /// </summary>
27
        public event Action<DownloadTarget, long, long>? TargetProgress;
28
        public event Action<ByteRateCounter>?            OverallProgress;
29

30
        private readonly object dlMutex = new object();
2✔
31
        private readonly List<DownloadPart> downloads       = new List<DownloadPart>();
2✔
32
        private readonly List<DownloadPart> queuedDownloads = new List<DownloadPart>();
2✔
33
        private int completed_downloads;
34

35
        private readonly ByteRateCounter rateCounter = new ByteRateCounter();
2✔
36

37
        // For inter-thread communication
38
        private volatile bool download_canceled;
39
        private readonly ManualResetEvent complete_or_canceled;
40
        private readonly CancellationToken cancelToken;
41

42
        /// <summary>
43
        /// Invoked when a download completes or fails.
44
        /// </summary>
45
        /// <param>The download that is done</param>
46
        /// <param>Exception thrown if failed</param>
47
        /// <param>ETag of the URL</param>
48
        public event Action<DownloadTarget, Exception?, string?, string>? onOneCompleted;
49

50
        /// <summary>
51
        /// Returns a perfectly boring NetAsyncDownloader
52
        /// </summary>
53
        public NetAsyncDownloader(IUser user,
2✔
54
                                  Func<HashAlgorithm?> getHashAlgo,
55
                                  string? userAgent = null,
56
                                  CancellationToken cancelToken = default)
57
        {
2✔
58
            User = user;
2✔
59
            this.userAgent = userAgent ?? Net.UserAgentString;
2✔
60
            this.getHashAlgo = getHashAlgo;
2✔
61
            this.cancelToken = cancelToken;
2✔
62
            complete_or_canceled = new ManualResetEvent(false);
2✔
63
        }
2✔
64

65
        public static void DownloadWithProgress(IReadOnlyCollection<DownloadTarget> downloadTargets,
66
                                                string?                             userAgent,
67
                                                IUser?                              user = null)
68
        {
×
69
            var downloader = new NetAsyncDownloader(user ?? new NullUser(), () => null, userAgent);
×
70
            downloader.onOneCompleted += (target, error, etag, hash) =>
×
71
            {
×
72
                if (error != null)
×
73
                {
×
74
                    user?.RaiseError("{0}", error.ToString());
×
75
                }
×
76
            };
×
77
            downloader.DownloadAndWait(downloadTargets);
×
78
        }
×
79

80
        /// <summary>
81
        /// Start a new batch of downloads
82
        /// </summary>
83
        /// <param name="targets">The downloads to begin</param>
84
        public void DownloadAndWait(IReadOnlyCollection<DownloadTarget> targets)
85
        {
2✔
86
            lock (dlMutex)
2✔
87
            {
2✔
88
                if (downloads.Count + queuedDownloads.Count > completed_downloads)
2✔
89
                {
×
90
                    // Some downloads are still in progress, add to the current batch
91
                    foreach (var target in targets)
×
92
                    {
×
93
                        DownloadModule(new DownloadPart(target, userAgent, SHA256.Create()));
×
94
                    }
×
95
                    // Wait for completion along with original caller
96
                    // so we can handle completion tasks for the added mods
97
                    complete_or_canceled.WaitOne();
×
98
                    return;
×
99
                }
100

101
                completed_downloads = 0;
2✔
102
                // Make sure we are ready to start a fresh batch
103
                complete_or_canceled.Reset();
2✔
104

105
                // Start the downloads!
106
                Download(targets);
2✔
107
            }
2✔
108

109
            rateCounter.Start();
2✔
110

111
            log.Debug("Waiting for downloads to finish...");
2✔
112
            complete_or_canceled.WaitOne();
2✔
113

114
            rateCounter.Stop();
2✔
115

116
            log.Debug("Downloads finished");
2✔
117

118
            var old_download_canceled = download_canceled;
2✔
119
            // Set up the inter-thread comms for next time. Can not be done at the start
120
            // of the method as the thread could pause on the opening line long enough for
121
            // a user to cancel.
122

123
            download_canceled = false;
2✔
124
            complete_or_canceled.Reset();
2✔
125

126
            log.Debug("Completion signal reset");
2✔
127

128
            // If the user cancelled our progress, then signal that.
129
            if (old_download_canceled || cancelToken.IsCancellationRequested)
2✔
130
            {
×
131
                log.DebugFormat("User clicked cancel, discarding {0} queued downloads: {1}", queuedDownloads.Count, string.Join(", ", queuedDownloads.SelectMany(dl => dl.target.urls)));
×
132
                // Ditch anything we haven't started
133
                queuedDownloads.Clear();
×
134
                // Abort all our traditional downloads, if there are any.
135
                var inProgress = downloads.Where(dl => dl.bytesLeft > 0 && dl.error == null).ToList();
×
136
                log.DebugFormat("Telling {0} in progress downloads to abort: {1}", inProgress.Count, string.Join(", ", inProgress.SelectMany(dl => dl.target.urls)));
×
137
                foreach (var download in inProgress)
×
138
                {
×
139
                    log.DebugFormat("Telling download of {0} to abort", string.Join(", ", download.target.urls));
×
140
                    download.Abort();
×
141
                    log.DebugFormat("Done requesting abort of {0}", string.Join(", ", download.target.urls));
×
142
                }
×
143

144
                log.Debug("Throwing cancellation kraken");
×
145
                // Signal to the caller that the user cancelled the download.
146
                throw new CancelledActionKraken(Properties.Resources.NetAsyncDownloaderCancelled);
×
147
            }
148

149
            // Check to see if we've had any errors. If so, then release the kraken!
150
            var exceptions = downloads.SelectMany(dl => dl.error switch
2✔
151
                                                        {
152
                                                            DownloadErrorsKraken dlKrak => dlKrak.Exceptions,
×
153
                                                            Exception => Enumerable.Repeat(
2✔
154
                                                                new KeyValuePair<DownloadTarget, Exception>(dl.target, dl.error), 1),
155
                                                            null => Enumerable.Empty<KeyValuePair<DownloadTarget, Exception>>(),
2✔
156
                                                        })
157
                                      .OfType<KeyValuePair<DownloadTarget, Exception>>()
158
                                      .ToList();
159

160
            if (exceptions.FirstOrDefault(pair => pair.Value is WebException
2✔
161
                                                  {
162
                                                      Status: WebExceptionStatus.SecureChannelFailure,
163
                                                  })
164
                is KeyValuePair<DownloadTarget, Exception>
165
                {
166
                    Key:   DownloadTarget { urls: { Count: > 0 } urls },
167
                    Value: WebException wex,
168
                })
169
            {
×
170
                throw new MissingCertificateKraken(urls.First(), null, wex);
×
171
            }
172

173
            var throttled = exceptions.Select(kvp => kvp.Value is WebException wex
2✔
174
                                                     && wex.Response is HttpWebResponse hresp
175
                                                     // Handle HTTP 403 used for throttling
176
                                                     && hresp.StatusCode == HttpStatusCode.Forbidden
177
                                                     && kvp.Key.urls.LastOrDefault() is Uri url
178
                                                     && url.IsAbsoluteUri
179
                                                     && Net.ThrottledHosts.TryGetValue(url.Host, out Uri? infoUrl)
180
                                                     && infoUrl is not null
181
                                                         ? new RequestThrottledKraken(url, infoUrl, wex)
182
                                                         : null)
183
                                      .OfType<RequestThrottledKraken>()
184
                                      .FirstOrDefault();
185
            if (throttled is not null)
2✔
186
            {
×
187
                throw throttled;
×
188
            }
189

190
            if (exceptions.Count > 0)
2✔
191
            {
2✔
192
                throw new DownloadErrorsKraken(exceptions);
2✔
193
            }
194

195
            // Yay! Everything worked!
196
            log.Debug("Done downloading");
2✔
197
        }
2✔
198

199
        /// <summary>
200
        /// Downloads our files.
201
        /// </summary>
202
        /// <param name="targets">A collection of DownloadTargets</param>
203
        private void Download(IReadOnlyCollection<DownloadTarget> targets)
204
        {
2✔
205
            downloads.Clear();
2✔
206
            queuedDownloads.Clear();
2✔
207
            foreach (var t in targets)
8✔
208
            {
2✔
209
                DownloadModule(new DownloadPart(t, userAgent, getHashAlgo?.Invoke()));
2✔
210
            }
2✔
211
        }
2✔
212

213
        private void DownloadModule(DownloadPart dl)
214
        {
2✔
215
            if (shouldQueue(dl.CurrentUri))
2✔
216
            {
×
217
                if (!queuedDownloads.Contains(dl))
×
218
                {
×
219
                    log.DebugFormat("Enqueuing download of {0}", dl.CurrentUri);
×
220
                    // Throttled host already downloading, we will get back to this later
221
                    queuedDownloads.Add(dl);
×
222
                }
×
223
            }
×
224
            else
225
            {
2✔
226
                log.DebugFormat("Beginning download of {0}", dl.CurrentUri);
2✔
227

228
                lock (dlMutex)
2✔
229
                {
2✔
230
                    if (!downloads.Contains(dl))
2✔
231
                    {
2✔
232
                        downloads.Add(dl);
2✔
233

234
                        // Schedule for us to get back progress reports.
235
                        dl.Progress += FileProgressReport;
2✔
236

237
                        // And schedule a notification if we're done (or if something goes wrong)
238
                        dl.Done += FileDownloadComplete;
2✔
239
                    }
2✔
240
                    queuedDownloads.Remove(dl);
2✔
241
                }
2✔
242

243
                // Encode spaces to avoid confusing URL parsers
244
                User.RaiseMessage(Properties.Resources.NetAsyncDownloaderDownloading,
2✔
245
                                  dl.CurrentUri.ToString().Replace(" ", "%20"));
246

247
                // Start the download!
248
                dl.Download(cancelToken);
2✔
249
            }
2✔
250
        }
2✔
251

252
        /// <summary>
253
        /// Check whether a given download should be deferred to be started later.
254
        /// Decision is made based on whether we're already downloading something
255
        /// else from the same host.
256
        /// </summary>
257
        /// <param name="url">A URL we want to download</param>
258
        /// <returns>
259
        /// true to queue, false to start immediately
260
        /// </returns>
261
        private bool shouldQueue(Uri url)
262
            => !url.IsFile && url.IsAbsoluteUri
2✔
263
               // Ignore inactive downloads
264
               && downloads.Except(queuedDownloads)
265
                           .Any(dl => dl.CurrentUri != url
×
266
                                      // Look for active downloads from the same host
267
                                      && (dl.CurrentUri.IsAbsoluteUri && dl.CurrentUri.Host == url.Host)
268
                                      // Consider done if no bytes left
269
                                      && dl.bytesLeft > 0
270
                                      // Consider done if already tried and failed
271
                                      && dl.error == null);
272

273
        private void triggerCompleted()
274
        {
2✔
275
            // Signal that we're done.
276
            complete_or_canceled.Set();
2✔
277
        }
2✔
278

279
        /// <summary>
280
        /// Generates a download progress report.
281
        /// </summary>
282
        /// <param name="download">The download that progressed</param>
283
        /// <param name="bytesDownloaded">The bytes downloaded</param>
284
        /// <param name="bytesToDownload">The total amount of bytes we expect to download</param>
285
        private void FileProgressReport(DownloadPart download, long bytesDownloaded, long bytesToDownload)
286
        {
2✔
287
            download.size      = bytesToDownload;
2✔
288
            download.bytesLeft = download.size - bytesDownloaded;
2✔
289
            TargetProgress?.Invoke(download.target, download.bytesLeft, download.size);
2✔
290

291
            lock (dlMutex)
2✔
292
            {
2✔
UNCOV
293
                var queuedSize = queuedDownloads.Sum(dl => dl.target.size);
×
294
                rateCounter.Size      = queuedSize + downloads.Sum(dl => dl.size);
2✔
295
                rateCounter.BytesLeft = queuedSize + downloads.Sum(dl => dl.bytesLeft);
2✔
296
            }
2✔
297

298
            OverallProgress?.Invoke(rateCounter);
2✔
299

300
            if (cancelToken.IsCancellationRequested)
2✔
301
            {
×
302
                download_canceled = true;
×
303
                triggerCompleted();
×
304
            }
×
305
        }
2✔
306

307
        private void PopFromQueue(string host)
308
        {
2✔
309
            // Make sure the threads don't trip on one another
310
            lock (dlMutex)
2✔
311
            {
2✔
312
                var next = queuedDownloads.FirstOrDefault(qDl =>
2✔
313
                    !qDl.CurrentUri.IsAbsoluteUri || qDl.CurrentUri.Host == host);
×
314
                if (next != null)
2✔
315
                {
×
316
                    log.DebugFormat("Attempting to start queued download {0}", string.Join(", ", next.target.urls));
×
317
                    // Start this host's next queued download
318
                    DownloadModule(next);
×
319
                }
×
320
            }
2✔
321
        }
2✔
322

323
        /// <summary>
324
        /// This method gets called back by `WebClient` when a download is completed.
325
        /// It in turncalls the onCompleted hook when *all* downloads are finished.
326
        /// </summary>
327
        private void FileDownloadComplete(DownloadPart dl,
328
                                          Exception?   error,
329
                                          bool         canceled,
330
                                          string?      etag,
331
                                          string       hash)
332
        {
2✔
333
            var doneUri = dl.CurrentUri;
2✔
334
            if (error != null)
2✔
335
            {
2✔
336
                log.InfoFormat("Error downloading {0}: {1}", doneUri, error.Message);
2✔
337

338
                // Check whether there are any alternate download URLs remaining
339
                if (!canceled && dl.HaveMoreUris)
2✔
340
                {
×
341
                    dl.NextUri();
×
342
                    // Either re-queue this or start the next one, depending on active downloads
343
                    DownloadModule(dl);
×
344
                    // Check the host that just failed for queued downloads
345
                    PopFromQueue(doneUri.Host);
×
346
                    // Short circuit the completion process so the fallback can run
347
                    return;
×
348
                }
349
                else
350
                {
2✔
351
                    dl.error = error;
2✔
352
                }
2✔
353
            }
2✔
354
            else
355
            {
2✔
356
                log.InfoFormat("Finished downloading {0}", string.Join(", ", dl.target.urls));
2✔
357
                dl.bytesLeft = 0;
2✔
358
                // Let calling code find out how big this file is
359
                dl.target.CalculateSize();
2✔
360
            }
2✔
361

362
            PopFromQueue(doneUri.Host);
2✔
363

364
            try
365
            {
2✔
366
                // Tell calling code that this file is ready
367
                onOneCompleted?.Invoke(dl.target, dl.error, etag, hash);
2✔
368
            }
2✔
369
            catch (Exception exc)
2✔
370
            {
2✔
371
                // Capture anything that goes wrong with the post-download process as well
372
                dl.error ??= exc;
2✔
373
            }
2✔
374

375
            // Make sure the threads don't trip on one another
376
            lock (dlMutex)
2✔
377
            {
2✔
378
                if (++completed_downloads >= downloads.Count + queuedDownloads.Count)
2✔
379
                {
2✔
380
                    log.DebugFormat("Triggering completion at {0} completed, {1} started, {2} queued", completed_downloads, downloads.Count, queuedDownloads.Count);
2✔
381
                    triggerCompleted();
2✔
382
                }
2✔
383
            }
2✔
384
        }
2✔
385
    }
386
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc