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

KSP-CKAN / CKAN / 25198663652

01 May 2026 01:58AM UTC coverage: 87.472% (+1.6%) from 85.851%
25198663652

push

github

HebaruSan
Merge #4594 Windows dark mode in .NET 10 build

1982 of 2112 branches covered (93.84%)

Branch coverage included in aggregate %.

35 of 36 new or added lines in 7 files covered. (97.22%)

33 existing lines in 24 files now uncovered.

8491 of 9861 relevant lines covered (86.11%)

2.69 hits per line

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

74.0
/Core/Net/ResumingWebClient.cs
1
using System;
2
using System.IO;
3
using System.Net;
4
using System.ComponentModel;
5
using System.Security.Cryptography;
6
using System.Threading;
7
using System.Threading.Tasks;
8

9
using log4net;
10

11
using CKAN.Extensions;
12

13
// This WebClient child class does some complicated stuff, let's keep using it for now
14
#pragma warning disable SYSLIB0014
15

16
namespace CKAN
17
{
18
    public class ResumingWebClient : WebClient
19
    {
20
        public ResumingWebClient()
3✔
21
        {
22
            Encoding = System.Text.Encoding.UTF8;
3✔
23
        }
3✔
24

25
        /// <summary>
26
        /// A version of DownloadFileAsync that appends to its destination
27
        /// file if it already exists and skips downloading the bytes
28
        /// we already have.
29
        /// </summary>
30
        /// <param name="url">What to download</param>
31
        /// <param name="path">Where to save it</param>
32
        /// <param name="size">Size of the download</param>
33
        /// <param name="hasher">Hash algorithm to use, or null</param>
34
        /// <param name="cancelToken">Cancellation token to cancel the operation</param>
35
        public void DownloadFileAsyncWithResume(Uri url, string path, long size,
36
                                                HashAlgorithm? hasher,
37
                                                CancellationToken? cancelToken = default)
38
        {
39
            this.hasher = hasher;
3✔
40
            contentLength = 0;
3✔
41
            Task.Run(() =>
3✔
42
            {
43
                var fi = new FileInfo(path);
3✔
44
                if (fi.Exists)
3✔
45
                {
46
                    using (var stream = fi.OpenRead())
3✔
47
                    {
48
                        hasher?.PartialHash(stream,
3✔
49
                                            new ProgressImmediate<int>(percent =>
50
                                                DownloadProgress?.Invoke(percent,
3✔
51
                                                                         percent * size / 100, size)),
52
                                            cancelToken);
53

54
                    }
3✔
55
                    log.DebugFormat("File exists at {0}, {1} bytes", path, fi.Length);
3✔
56
                    bytesToSkip = fi.Length;
3✔
57
                }
58
                else
59
                {
60
                    // Reset in case we try multiple with same webclient
61
                    bytesToSkip = 0;
3✔
62
                }
63
                OpenReadAsync(url, path);
3✔
64
            });
3✔
65
        }
3✔
66

67
        public void DownloadFileAsyncWithResume(Uri url, Stream stream, HashAlgorithm? hasher)
68
        {
69
            this.hasher = hasher;
3✔
70
            contentLength = 0;
3✔
71
            Task.Run(() =>
3✔
72
            {
73
                bytesToSkip = 0;
3✔
74
                OpenReadAsync(url, stream);
3✔
75
            });
3✔
76
        }
3✔
77

78
        /// <summary>
79
        /// Same as DownloadProgressChanged, but usable by us.
80
        /// Called with percent, bytes received, total bytes to receive.
81
        ///
82
        /// DownloadProgressChangedEventArg has an internal constructor
83
        /// and readonly properties, and everyplace that does make one
84
        /// is private instead of protected, so we have to reinvent this wheel.
85
        /// (Meanwhile AsyncCompletedEventArgs has none of these problems.)
86
        /// </summary>
87
        public event Action<int, long, long>? DownloadProgress;
88

89
        /// <summary>
90
        /// CancelAsync isn't virtual, so we make another function
91
        /// </summary>
92
        public void CancelAsyncOverridden()
93
        {
94
            if (cancelTokenSrc != null)
×
95
            {
96
                log.Debug("Cancellation requested, going through token");
×
97
                cancelTokenSrc?.Cancel();
×
98
            }
99
            else
100
            {
101
                log.Debug("Cancellation requested, using non-token means");
×
102
                CancelAsync();
×
103
            }
104
        }
×
105

106
        protected override WebRequest GetWebRequest(Uri address)
107
        {
108
            var request = base.GetWebRequest(address);
3✔
109
            if (request is HttpWebRequest webRequest && bytesToSkip > 0)
3✔
110
            {
111
                log.DebugFormat("Skipping {0} bytes of {1}", bytesToSkip, address);
3✔
112
                webRequest.AddRange(bytesToSkip);
3✔
113
                webRequest.ReadWriteTimeout = timeoutMs;
3✔
114
            }
115
            return request;
3✔
116
        }
117

118
        protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
119
        {
120
            try
121
            {
122
                var response = base.GetWebResponse(request, result);
3✔
123
                contentLength = response.ContentLength;
3✔
124
                return response;
3✔
125
            }
126
            catch (WebException wexc)
127
            when (wexc.Status == WebExceptionStatus.ProtocolError
3✔
128
                  && wexc.Response is HttpWebResponse response
129
                  && response.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable)
130
            {
131
                log.DebugFormat("GetWebResponse failed with range error, closing stream for {0}", request.RequestUri);
×
132
                // Don't save the error page into a file
133
                response.Close();
×
134
                return response;
×
135
            }
136
        }
3✔
137

138
        protected override void OnOpenReadCompleted(OpenReadCompletedEventArgs e)
139
        {
140
            base.OnOpenReadCompleted(e);
3✔
141
            if (!e.Cancelled && e.Error == null)
3✔
142
            {
143
                var destination = e.UserState as string;
3✔
144
                using (var netStream = e.Result)
3✔
145
                {
146
                    if (!netStream.CanRead || contentLength == 0)
3✔
147
                    {
148
                        log.DebugFormat("OnOpenReadCompleted got closed stream or zero contentLength, skipping download to {0}",
×
149
                                        destination ?? "stream");
150
                        // Synthesize a progress update for 100% completion
151
                        if (!string.IsNullOrEmpty(destination)
×
152
                            && File.Exists(destination))
153
                        {
154
                            hasher?.TransformFinalBlock(new byte[16], 0, 0);
×
155
                            var fi = new FileInfo(destination);
×
156
                            DownloadProgress?.Invoke(100, fi.Length, fi.Length);
×
157
                        }
158
                    }
159
                    else
160
                    {
161
                        try
162
                        {
163
                            log.DebugFormat("OnOpenReadCompleted got open stream, appending to {0}",
3✔
164
                                            destination ?? "stream");
165
                            // file:// URLs don't support timeouts
166
                            if (netStream.CanTimeout)
3✔
167
                            {
168
                                log.DebugFormat("Default stream read timeout is {0}", netStream.ReadTimeout);
3✔
169
                                netStream.ReadTimeout = timeoutMs;
3✔
170
                            }
171
                            cancelTokenSrc = new CancellationTokenSource();
3✔
172
                            switch (e.UserState)
3✔
173
                            {
174
                                case string path:
175
                                    ToFile(netStream, path, cancelTokenSrc.Token);
3✔
176
                                    break;
3✔
177
                                case Stream stream:
178
                                    ToStream(netStream, stream, cancelTokenSrc.Token);
3✔
179
                                    break;
180
                            }
181
                            // Make sure caller knows we've finished
182
                            DownloadProgress?.Invoke(100, contentLength, contentLength);
3✔
183
                            cancelTokenSrc = null;
3✔
184
                        }
3✔
185
                        catch (OperationCanceledException exc)
×
186
                        {
187
                            log.Debug("Cancellation token threw, sending cancel completion");
×
188
                            OnDownloadFileCompleted(new AsyncCompletedEventArgs(exc, true, e.UserState));
×
189
                            return;
×
190
                        }
191
                        catch (Exception exc)
×
192
                        {
193
                            OnDownloadFileCompleted(new AsyncCompletedEventArgs(exc, false, e.UserState));
×
194
                            return;
×
195
                        }
196
                    }
UNCOV
197
                }
×
198
            }
199
            OnDownloadFileCompleted(new AsyncCompletedEventArgs(e.Error, e.Cancelled, e.UserState));
3✔
200
        }
3✔
201

202
        private void ToFile(Stream netStream, string path, CancellationToken token)
203
        {
204
            using (var outStream = new FileStream(path, FileMode.Append, FileAccess.Write))
3✔
205
            {
206
                ToStream(netStream, outStream, token);
3✔
207
            }
3✔
208
        }
3✔
209

210
        private void ToStream(Stream netStream, Stream outStream, CancellationToken token)
211
        {
212
            netStream.CopyTo(outStream, new ProgressImmediate<long>(bytesDownloaded =>
3✔
213
                {
214
                    DownloadProgress?.Invoke((int)(100 * bytesDownloaded / contentLength),
3✔
215
                                             bytesDownloaded, contentLength);
216
                }),
3✔
217
                TimeSpan.FromSeconds(5),
218
                hasher,
219
                token);
220
        }
3✔
221

222
        /// <summary>
223
        /// Ideally the bytes to skip would be passed in the userToken param of OpenReadAsync,
224
        /// but GetWebRequest can't access it, so we store it here.
225
        /// </summary>
226
        private long bytesToSkip   = 0;
227
        private long contentLength = 0;
228
        private CancellationTokenSource? cancelTokenSrc;
229
        private HashAlgorithm?           hasher;
230

231
        private const int timeoutMs = 30 * 1000;
232
        private static readonly ILog log = LogManager.GetLogger(typeof(ResumingWebClient));
3✔
233
    }
234
}
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