Skip to content

Commit 272895b

Browse files
authored
fix: fix downloading different URLs to same destination (#70)
fixes #69 Fixes `Downloader` to remove the `DownloadTask` when it is done via a completion. This will allow changing Coder deployments, which use different URLs but whose files are downloaded to the same place.
1 parent 8f60b4d commit 272895b

File tree

2 files changed

+66
-39
lines changed

2 files changed

+66
-39
lines changed

Tests.Vpn.Service/DownloaderTest.cs

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,34 @@ public async Task Download(CancellationToken ct)
284284
Assert.That(await File.ReadAllTextAsync(destPath, ct), Is.EqualTo("test"));
285285
}
286286

287+
[Test(Description = "Perform 2 downloads with the same destination")]
288+
[CancelAfter(30_000)]
289+
public async Task DownloadSameDest(CancellationToken ct)
290+
{
291+
using var httpServer = EchoServer();
292+
var url0 = new Uri(httpServer.BaseUrl + "/test0");
293+
var url1 = new Uri(httpServer.BaseUrl + "/test1");
294+
var destPath = Path.Combine(_tempDir, "test");
295+
296+
var manager = new Downloader(NullLogger<Downloader>.Instance);
297+
var startTask0 = manager.StartDownloadAsync(new HttpRequestMessage(HttpMethod.Get, url0), destPath,
298+
NullDownloadValidator.Instance, ct);
299+
var startTask1 = manager.StartDownloadAsync(new HttpRequestMessage(HttpMethod.Get, url1), destPath,
300+
NullDownloadValidator.Instance, ct);
301+
var dlTask0 = await startTask0;
302+
await dlTask0.Task;
303+
Assert.That(dlTask0.TotalBytes, Is.EqualTo(5));
304+
Assert.That(dlTask0.BytesRead, Is.EqualTo(5));
305+
Assert.That(dlTask0.Progress, Is.EqualTo(1));
306+
Assert.That(dlTask0.IsCompleted, Is.True);
307+
var dlTask1 = await startTask1;
308+
await dlTask1.Task;
309+
Assert.That(dlTask1.TotalBytes, Is.EqualTo(5));
310+
Assert.That(dlTask1.BytesRead, Is.EqualTo(5));
311+
Assert.That(dlTask1.Progress, Is.EqualTo(1));
312+
Assert.That(dlTask1.IsCompleted, Is.True);
313+
}
314+
287315
[Test(Description = "Download with custom headers")]
288316
[CancelAfter(30_000)]
289317
public async Task WithHeaders(CancellationToken ct)
@@ -347,17 +375,17 @@ public async Task DownloadExistingDifferentContent(CancellationToken ct)
347375

348376
[Test(Description = "Unexpected response code from server")]
349377
[CancelAfter(30_000)]
350-
public void UnexpectedResponseCode(CancellationToken ct)
378+
public async Task UnexpectedResponseCode(CancellationToken ct)
351379
{
352380
using var httpServer = new TestHttpServer(ctx => { ctx.Response.StatusCode = 404; });
353381
var url = new Uri(httpServer.BaseUrl + "/test");
354382
var destPath = Path.Combine(_tempDir, "test");
355383

356384
var manager = new Downloader(NullLogger<Downloader>.Instance);
357-
// The "outer" Task should fail.
358-
var ex = Assert.ThrowsAsync<HttpRequestException>(async () =>
359-
await manager.StartDownloadAsync(new HttpRequestMessage(HttpMethod.Get, url), destPath,
360-
NullDownloadValidator.Instance, ct));
385+
// The "inner" Task should fail.
386+
var dlTask = await manager.StartDownloadAsync(new HttpRequestMessage(HttpMethod.Get, url), destPath,
387+
NullDownloadValidator.Instance, ct);
388+
var ex = Assert.ThrowsAsync<HttpRequestException>(async () => await dlTask.Task);
361389
Assert.That(ex.Message, Does.Contain("404"));
362390
}
363391

@@ -384,22 +412,6 @@ public async Task MismatchedETag(CancellationToken ct)
384412
Assert.That(ex.Message, Does.Contain("ETag does not match SHA1 hash of downloaded file").And.Contains("beef"));
385413
}
386414

387-
[Test(Description = "Timeout on response headers")]
388-
[CancelAfter(30_000)]
389-
public void CancelledOuter(CancellationToken ct)
390-
{
391-
using var httpServer = new TestHttpServer(async _ => { await Task.Delay(TimeSpan.FromSeconds(5), ct); });
392-
var url = new Uri(httpServer.BaseUrl + "/test");
393-
var destPath = Path.Combine(_tempDir, "test");
394-
395-
var manager = new Downloader(NullLogger<Downloader>.Instance);
396-
// The "outer" Task should fail.
397-
var smallerCt = new CancellationTokenSource(TimeSpan.FromSeconds(1)).Token;
398-
Assert.ThrowsAsync<TaskCanceledException>(
399-
async () => await manager.StartDownloadAsync(new HttpRequestMessage(HttpMethod.Get, url), destPath,
400-
NullDownloadValidator.Instance, smallerCt));
401-
}
402-
403415
[Test(Description = "Timeout on response body")]
404416
[CancelAfter(30_000)]
405417
public async Task CancelledInner(CancellationToken ct)
@@ -451,12 +463,10 @@ public async Task ValidationFailureExistingFile(CancellationToken ct)
451463
await File.WriteAllTextAsync(destPath, "test", ct);
452464

453465
var manager = new Downloader(NullLogger<Downloader>.Instance);
454-
// The "outer" Task should fail because the inner task never starts.
455-
var ex = Assert.ThrowsAsync<Exception>(async () =>
456-
{
457-
await manager.StartDownloadAsync(new HttpRequestMessage(HttpMethod.Get, url), destPath,
458-
new TestDownloadValidator(new Exception("test exception")), ct);
459-
});
466+
var dlTask = await manager.StartDownloadAsync(new HttpRequestMessage(HttpMethod.Get, url), destPath,
467+
new TestDownloadValidator(new Exception("test exception")), ct);
468+
// The "inner" Task should fail.
469+
var ex = Assert.ThrowsAsync<Exception>(async () => { await dlTask.Task; });
460470
Assert.That(ex.Message, Does.Contain("Existing file failed validation"));
461471
Assert.That(ex.InnerException, Is.Not.Null);
462472
Assert.That(ex.InnerException!.Message, Is.EqualTo("test exception"));

Vpn.Service/Downloader.cs

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Formats.Asn1;
44
using System.Net;
55
using System.Runtime.CompilerServices;
6+
using System.Runtime.ExceptionServices;
67
using System.Security.Cryptography;
78
using System.Security.Cryptography.X509Certificates;
89
using Coder.Desktop.Vpn.Utilities;
@@ -288,7 +289,26 @@ public async Task<DownloadTask> StartDownloadAsync(HttpRequestMessage req, strin
288289
{
289290
var task = _downloads.GetOrAdd(destinationPath,
290291
_ => new DownloadTask(_logger, req, destinationPath, validator));
291-
await task.EnsureStartedAsync(ct);
292+
// EnsureStarted is a no-op if we didn't create a new DownloadTask.
293+
// So, we will only remove the destination once for each time we start a new task.
294+
task.EnsureStarted(tsk =>
295+
{
296+
// remove the key first, before checking the exception, to ensure
297+
// we still clean up.
298+
_downloads.TryRemove(destinationPath, out _);
299+
if (tsk.Exception == null)
300+
{
301+
return;
302+
}
303+
304+
if (tsk.Exception.InnerException != null)
305+
{
306+
ExceptionDispatchInfo.Capture(tsk.Exception.InnerException).Throw();
307+
}
308+
309+
// not sure if this is hittable, but just in case:
310+
throw tsk.Exception;
311+
}, ct);
292312

293313
// If the existing (or new) task is for the same URL, return it.
294314
if (task.Request.RequestUri == req.RequestUri)
@@ -357,21 +377,19 @@ internal DownloadTask(ILogger logger, HttpRequestMessage req, string destination
357377
".download-" + Path.GetRandomFileName());
358378
}
359379

360-
internal async Task<Task> EnsureStartedAsync(CancellationToken ct = default)
380+
internal void EnsureStarted(Action<Task> continuation, CancellationToken ct = default)
361381
{
362-
using var _ = await _semaphore.LockAsync(ct);
382+
using var _ = _semaphore.Lock();
363383
if (Task == null!)
364-
Task = await StartDownloadAsync(ct);
365-
366-
return Task;
384+
Task = Start(ct).ContinueWith(continuation, ct);
367385
}
368386

369387
/// <summary>
370388
/// Starts downloading the file. The request will be performed in this task, but once started, the task will complete
371389
/// and the download will continue in the background. The provided CancellationToken can be used to cancel the
372390
/// download.
373391
/// </summary>
374-
private async Task<Task> StartDownloadAsync(CancellationToken ct = default)
392+
private async Task Start(CancellationToken ct = default)
375393
{
376394
Directory.CreateDirectory(_destinationDirectory);
377395

@@ -398,8 +416,7 @@ private async Task<Task> StartDownloadAsync(CancellationToken ct = default)
398416
throw new Exception("Existing file failed validation after 304 Not Modified", e);
399417
}
400418

401-
Task = Task.CompletedTask;
402-
return Task;
419+
return;
403420
}
404421

405422
if (res.StatusCode != HttpStatusCode.OK)
@@ -432,11 +449,11 @@ private async Task<Task> StartDownloadAsync(CancellationToken ct = default)
432449
throw;
433450
}
434451

435-
Task = DownloadAsync(res, tempFile, ct);
436-
return Task;
452+
await Download(res, tempFile, ct);
453+
return;
437454
}
438455

439-
private async Task DownloadAsync(HttpResponseMessage res, FileStream tempFile, CancellationToken ct)
456+
private async Task Download(HttpResponseMessage res, FileStream tempFile, CancellationToken ct)
440457
{
441458
try
442459
{

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy