Skip to content

Commit e5d9dc1

Browse files
authored
fix: fix Downloader to dispose tempFile and use synchronous IO (#81)
Fixes coder/internal#598 There is a possible race where if the cancellation token is expired, `Download()` never gets called and the tempFile is never disposed of (at least until GC). We also switch to synchronous IO so that a pending overlapped write won't block the deletion. These issues can cause races in our tests when we try to clean up the directory.
1 parent 75cdfd0 commit e5d9dc1

File tree

2 files changed

+23
-20
lines changed

2 files changed

+23
-20
lines changed

Tests.Vpn.Service/DownloaderTest.cs

+11-6
Original file line numberDiff line numberDiff line change
@@ -442,23 +442,28 @@ public async Task CancelledWaitingForOther(CancellationToken ct)
442442
[CancelAfter(30_000)]
443443
public async Task CancelledInner(CancellationToken ct)
444444
{
445+
var httpCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
446+
var taskCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
445447
using var httpServer = new TestHttpServer(async ctx =>
446448
{
447449
ctx.Response.StatusCode = 200;
448-
await ctx.Response.OutputStream.WriteAsync("test"u8.ToArray(), ct);
449-
await ctx.Response.OutputStream.FlushAsync(ct);
450-
await Task.Delay(TimeSpan.FromSeconds(5), ct);
450+
await ctx.Response.OutputStream.WriteAsync("test"u8.ToArray(), httpCts.Token);
451+
await ctx.Response.OutputStream.FlushAsync(httpCts.Token);
452+
// wait up to 5 seconds.
453+
await Task.Delay(TimeSpan.FromSeconds(5), httpCts.Token);
451454
});
452455
var url = new Uri(httpServer.BaseUrl + "/test");
453456
var destPath = Path.Combine(_tempDir, "test");
454457

455458
var manager = new Downloader(NullLogger<Downloader>.Instance);
456459
// The "inner" Task should fail.
457-
var smallerCt = new CancellationTokenSource(TimeSpan.FromSeconds(1)).Token;
460+
var taskCt = taskCts.Token;
458461
var dlTask = await manager.StartDownloadAsync(new HttpRequestMessage(HttpMethod.Get, url), destPath,
459-
NullDownloadValidator.Instance, smallerCt);
462+
NullDownloadValidator.Instance, taskCt);
463+
await taskCts.CancelAsync();
460464
var ex = Assert.ThrowsAsync<TaskCanceledException>(async () => await dlTask.Task);
461-
Assert.That(ex.CancellationToken, Is.EqualTo(smallerCt));
465+
Assert.That(ex.CancellationToken, Is.EqualTo(taskCt));
466+
await httpCts.CancelAsync();
462467
}
463468

464469
[Test(Description = "Validation failure")]

Vpn.Service/Downloader.cs

+12-14
Original file line numberDiff line numberDiff line change
@@ -453,27 +453,25 @@ private async Task Start(CancellationToken ct = default)
453453
if (res.Content.Headers.ContentLength >= 0)
454454
TotalBytes = (ulong)res.Content.Headers.ContentLength;
455455

456-
FileStream tempFile;
457-
try
458-
{
459-
tempFile = File.Create(TempDestinationPath, BufferSize,
460-
FileOptions.Asynchronous | FileOptions.SequentialScan);
461-
}
462-
catch (Exception e)
463-
{
464-
_logger.LogError(e, "Failed to create temporary file '{TempDestinationPath}'", TempDestinationPath);
465-
throw;
466-
}
467-
468-
await Download(res, tempFile, ct);
456+
await Download(res, ct);
469457
return;
470458
}
471459

472-
private async Task Download(HttpResponseMessage res, FileStream tempFile, CancellationToken ct)
460+
private async Task Download(HttpResponseMessage res, CancellationToken ct)
473461
{
474462
try
475463
{
476464
var sha1 = res.Headers.Contains("ETag") ? SHA1.Create() : null;
465+
FileStream tempFile;
466+
try
467+
{
468+
tempFile = File.Create(TempDestinationPath, BufferSize, FileOptions.SequentialScan);
469+
}
470+
catch (Exception e)
471+
{
472+
_logger.LogError(e, "Failed to create temporary file '{TempDestinationPath}'", TempDestinationPath);
473+
throw;
474+
}
477475
await using (tempFile)
478476
{
479477
var stream = await res.Content.ReadAsStreamAsync(ct);

0 commit comments

Comments
 (0)