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

KSP-CKAN / CKAN / 18889376578

28 Oct 2025 09:13PM UTC coverage: 84.85% (+3.0%) from 81.873%
18889376578

Pull #4454

github

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

1974 of 2144 branches covered (92.07%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 3 files covered. (100.0%)

97 existing lines in 25 files now uncovered.

11904 of 14212 relevant lines covered (83.76%)

0.88 hits per line

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

81.33
/Core/Net/NetAsyncModulesDownloader.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Collections.Concurrent;
4
using System.Threading.Tasks;
5
using System.IO;
6
using System.Linq;
7
using System.Threading;
8
using System.Security.Cryptography;
9

10
using log4net;
11
using Autofac;
12

13
using CKAN.Configuration;
14
using CKAN.Extensions;
15

16
namespace CKAN
17
{
18
    /// <summary>
19
    /// Download lots of files at once!
20
    /// </summary>
21
    public class NetAsyncModulesDownloader : IDownloader
22
    {
23
        public event Action<CkanModule, long, long>? DownloadProgress;
24
        public event Action<ByteRateCounter>?        OverallDownloadProgress;
25
        public event Action<CkanModule, long, long>? StoreProgress;
26
        public event Action<CkanModule>?             OneComplete;
27
        public event Action?                         AllComplete;
28

29
        /// <summary>
30
        /// Returns a perfectly boring NetAsyncModulesDownloader.
31
        /// </summary>
32
        public NetAsyncModulesDownloader(IUser             user,
1✔
33
                                         NetModuleCache    cache,
34
                                         string?           userAgent   = null,
35
                                         CancellationToken cancelToken = default)
36
        {
1✔
37
            modules    = new List<CkanModule>();
1✔
38
            downloader = new NetAsyncDownloader(user, SHA256.Create, userAgent, cancelToken);
1✔
39
            // Schedule us to process each module on completion.
40
            downloader.onOneCompleted += ModuleDownloadComplete;
1✔
41
            downloader.TargetProgress += (target, remaining, total) =>
1✔
42
            {
1✔
43
                if (targetModules?[target].First() is CkanModule mod)
1✔
44
                {
1✔
45
                    DownloadProgress?.Invoke(mod, remaining, total);
1✔
46
                }
1✔
47
            };
1✔
48
            downloader.OverallProgress += brc => OverallDownloadProgress?.Invoke(brc);
1✔
49
            this.cache = cache;
1✔
50
            this.cancelToken = cancelToken;
1✔
51
        }
1✔
52

53
        internal NetAsyncDownloader.DownloadTarget TargetFromModuleGroup(
54
                HashSet<CkanModule> group,
55
                string?[]           preferredHosts)
56
            => TargetFromModuleGroup(group, group.OrderBy(m => m.identifier).First(), preferredHosts);
1✔
57

58
        private NetAsyncDownloader.DownloadTargetFile TargetFromModuleGroup(
59
                HashSet<CkanModule> group,
60
                CkanModule          first,
61
                string?[]           preferredHosts)
62
            => new NetAsyncDownloader.DownloadTargetFile(
1✔
63
                group.SelectMany(mod => mod.download ?? Enumerable.Empty<Uri>())
1✔
64
                     .Concat(group.Select(mod => mod.InternetArchiveDownload)
1✔
65
                                  .OfType<Uri>()
66
                                  .OrderBy(uri => uri.ToString()))
1✔
67
                     .Distinct()
68
                     .OrderBy(u => u,
1✔
69
                              new PreferredHostUriComparer(preferredHosts))
70
                     .ToList(),
71
                cache.GetInProgressFileName(first)?.FullName,
72
                first.download_size,
73
                string.IsNullOrEmpty(first.download_content_type)
74
                    ? defaultMimeType
75
                    : $"{first.download_content_type};q=1.0,{defaultMimeType};q=0.9");
76

77
        /// <summary>
78
        /// <see cref="IDownloader.DownloadModules"/>
79
        /// </summary>
80
        public void DownloadModules(IEnumerable<CkanModule> modules)
81
        {
1✔
UNCOV
82
            var activeURLs = this.modules.SelectMany(m => m.download ?? Enumerable.Empty<Uri>())
×
83
                                         .OfType<Uri>()
84
                                         .ToHashSet();
85
            var moduleGroups = CkanModule.GroupByDownloads(modules);
1✔
86
            // Make sure we have enough space to download and cache
87
            cache.CheckFreeSpace(moduleGroups.Sum(grp => grp.First().download_size));
1✔
88
            // Add all the requested modules
89
            this.modules.AddRange(moduleGroups.SelectMany(grp => grp));
1✔
90

91
            var preferredHosts = ServiceLocator.Container.Resolve<IConfiguration>().PreferredHosts;
1✔
92
            targetModules = moduleGroups
1✔
93
                // Skip any group that already has a URL in progress
94
                .Where(grp => grp.All(mod => mod.download?.All(dlUri => !activeURLs.Contains(dlUri)) ?? false))
1✔
95
                // Each group gets one target containing all the URLs
96
                .ToDictionary(grp => TargetFromModuleGroup(grp, preferredHosts),
1✔
97
                              grp => grp.ToArray());
1✔
98
            try
99
            {
1✔
100
                // Start the downloads!
101
                downloader.DownloadAndWait(targetModules.Keys);
1✔
102
                this.modules.Clear();
1✔
103
                targetModules.Clear();
1✔
104
                AllComplete?.Invoke();
1✔
105
            }
1✔
106
            catch (DownloadErrorsKraken kraken)
1✔
107
            {
1✔
108
                // Associate the errors with the affected modules
109
                var exc = new ModuleDownloadErrorsKraken(
1✔
110
                    kraken.Exceptions
111
                          .SelectMany(kvp => targetModules[kvp.Key]
1✔
112
                                             .Select(m => new KeyValuePair<CkanModule, Exception>(
1✔
113
                                                              m, kvp.Value.GetBaseException())))
114
                          .ToList());
115
                // Clear this.modules because we're done with these
116
                this.modules.Clear();
1✔
117
                targetModules.Clear();
1✔
118
                throw exc;
1✔
119
            }
120
        }
1✔
121

122
        public IEnumerable<CkanModule> ModulesAsTheyFinish(IReadOnlyCollection<CkanModule> cached,
123
                                                           IReadOnlyCollection<CkanModule> toDownload)
124
        {
1✔
125
            var (dlTask, blockingQueue) = DownloadsCollection(toDownload);
1✔
126
            return ModulesAsTheyFinish(cached, dlTask, blockingQueue);
1✔
127
        }
1✔
128

129
        private static IEnumerable<CkanModule> ModulesAsTheyFinish(IReadOnlyCollection<CkanModule> cached,
130
                                                                   Task                            dlTask,
131
                                                                   BlockingCollection<CkanModule>  blockingQueue)
132
        {
1✔
133
            foreach (var m in cached)
3✔
134
            {
×
135
                yield return m;
×
136
            }
×
137
            foreach (var m in blockingQueue.GetConsumingEnumerable())
4✔
138
            {
1✔
139
                yield return m;
1✔
140
            }
1✔
141
            blockingQueue.Dispose();
1✔
142
            try
143
            {
1✔
144
                dlTask.Wait();
1✔
145
                if (dlTask.Exception is AggregateException agExc)
1✔
146
                {
×
147
                    agExc.RethrowInner();
×
148
                }
×
149
            }
1✔
150
            catch (AggregateException agExc)
1✔
151
            {
1✔
152
                agExc.RethrowInner();
1✔
153
            }
×
154
        }
1✔
155

156
        private (Task dlTask, BlockingCollection<CkanModule> blockingQueue) DownloadsCollection(IReadOnlyCollection<CkanModule> toDownload)
157
        {
1✔
158
            var blockingQueue = new BlockingCollection<CkanModule>(new ConcurrentQueue<CkanModule>());
1✔
159
            Action<CkanModule> oneComplete = m => blockingQueue.Add(m);
1✔
160
            OneComplete += oneComplete;
1✔
161
            return (Task.Run(() => DownloadModules(toDownload))
1✔
162
                        .ContinueWith(t =>
163
                                      {
1✔
164
                                          blockingQueue.CompleteAdding();
1✔
165
                                          OneComplete -= oneComplete;
1✔
166
                                          if (t.Exception is AggregateException agExc)
1✔
167
                                          {
1✔
168
                                              agExc.RethrowInner();
1✔
169
                                          }
×
170
                                      }),
1✔
171
                    blockingQueue);
172
        }
1✔
173

174
        private void ModuleDownloadComplete(NetAsyncDownloader.DownloadTarget target,
175
                                            Exception?                        error,
176
                                            string?                           etag,
177
                                            string?                           sha256)
178
        {
1✔
179
            if (target is NetAsyncDownloader.DownloadTargetFile fileTarget && targetModules != null)
1✔
180
            {
1✔
181
                var url      = fileTarget.urls.First();
1✔
182
                var filename = fileTarget.filename;
1✔
183

184
                log.DebugFormat("Received download completion: {0}, {1}, {2}",
1✔
185
                                url, filename, error?.Message);
186
                if (error != null)
1✔
187
                {
1✔
188
                    // If there was an error in DOWNLOADING, keep the file so we can retry it later
189
                    log.Info(error.Message);
1✔
190
                }
1✔
191
                else
192
                {
1✔
193
                    // Cache if this download succeeded
194
                    CkanModule? module = null;
1✔
195
                    try
196
                    {
1✔
197
                        var completedMods = targetModules[fileTarget];
1✔
198
                        module = completedMods.First();
1✔
199

200
                        // Check hash if defined in module
201
                        if (module.download_hash?.sha256 != null
1✔
202
                            && sha256 != module.download_hash.sha256)
203
                        {
×
204
                            throw new InvalidModuleFileKraken(
×
205
                                module, filename,
206
                                string.Format(Properties.Resources.NetModuleCacheMismatchSHA256,
207
                                              module, filename,
208
                                              sha256, module.download_hash.sha256));
209
                        }
210

211
                        User.RaiseMessage(Properties.Resources.NetAsyncDownloaderValidating,
1✔
212
                                          module.name);
213
                        var fileSize = new FileInfo(filename).Length;
1✔
214
                        cache.Store(module, filename,
1✔
215
                                    new ProgressImmediate<long>(bytes => StoreProgress?.Invoke(module,
1✔
216
                                                                                               fileSize - bytes,
217
                                                                                               fileSize)),
218
                                    module.StandardName(),
219
                                    false,
220
                                    cancelToken);
221
                        File.Delete(filename);
1✔
222
                        foreach (var m in completedMods)
4✔
223
                        {
1✔
224
                            OneComplete?.Invoke(m);
1✔
225
                        }
1✔
226
                    }
1✔
227
                    catch (InvalidModuleFileKraken)
1✔
228
                    {
1✔
229
                        if (module != null)
1✔
230
                        {
1✔
231
                            // Finish out the progress bar
232
                            StoreProgress?.Invoke(module, 0, 100);
1✔
233
                        }
1✔
234
                        // If there was an error in STORING, delete the file so we can try it from scratch later
235
                        File.Delete(filename);
1✔
236

237
                        // Tell downloader there is a problem with this file
238
                        throw;
1✔
239
                    }
240
                    catch (OperationCanceledException exc)
×
241
                    {
×
242
                        log.WarnFormat("Cancellation token threw, validation incomplete: {0}", filename);
×
243
                        User.RaiseMessage("{0}", exc.Message);
×
244
                        if (module != null)
×
245
                        {
×
246
                            // Finish out the progress bar
247
                            StoreProgress?.Invoke(module, 0, 100);
×
248
                        }
×
249
                        // Don't delete because there might be nothing wrong
250
                    }
×
251
                    catch (FileNotFoundException e)
×
252
                    {
×
253
                        log.WarnFormat("cache.Store(): FileNotFoundException: {0}", e.Message);
×
254
                    }
×
255
                    catch (InvalidOperationException)
×
256
                    {
×
257
                        log.WarnFormat("No module found for completed URL: {0}", url);
×
258
                    }
×
259
                }
1✔
260
            }
1✔
261
        }
1✔
262

263
        private static readonly ILog log = LogManager.GetLogger(typeof(NetAsyncModulesDownloader));
1✔
264

265
        private const    string                  defaultMimeType = "application/octet-stream";
266

267
        private readonly List<CkanModule>         modules;
268
        private          Dictionary<NetAsyncDownloader.DownloadTarget, CkanModule[]>? targetModules;
269
        private readonly NetAsyncDownloader       downloader;
270
        private          IUser                    User => downloader.User;
1✔
271
        private readonly NetModuleCache           cache;
272
        private readonly CancellationToken        cancelToken;
273
    }
274
}
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

© 2026 Coveralls, Inc