Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 63 additions & 11 deletions src/ui/Features/Shared/DownloadFfmpegLibsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public partial class DownloadFfmpegLibsViewModel : ObservableObject, IClosingCle
private readonly IFfmpegLibsDownloadService _downloadService;
private Task? _downloadTask;
private readonly Timer _timer;
private bool _done;
private volatile bool _done;
private int _cleanupStarted;
private readonly CancellationTokenSource _cancellationTokenSource;
private IndeterminateProgressHelper? _indeterminateProgressHelper;
private readonly Lock _lockObj = new();
Expand Down Expand Up @@ -93,14 +94,7 @@ private void OnTimerOnElapsed(object? sender, ElapsedEventArgs args)
}
finally
{
try
{
File.Delete(_tempFileName);
}
catch
{
// temp file, best effort
}
TryDeleteTempFile(_tempFileName);
}

StopIndeterminateProgress();
Expand Down Expand Up @@ -138,6 +132,11 @@ internal static void ExtractLibraries(string zipFileName, string targetFolder, C

private void StartIndeterminateProgress()
{
if (_cancellationTokenSource.IsCancellationRequested)
{
return;
}

_indeterminateProgressHelper?.Dispose();
_indeterminateProgressHelper = new IndeterminateProgressHelper(
value => ProgressValue = value,
Expand All @@ -159,14 +158,67 @@ private void Close()
[RelayCommand]
private void CommandCancel()
{
_cancellationTokenSource.Cancel();
_done = true;
CancelPendingWork();
Close();
}

private void CancelPendingWork()
{
_done = true;
_cancellationTokenSource.Cancel();
StopIndeterminateProgress();
}

public void OnClosingCleanup()
{
if (Interlocked.Exchange(ref _cleanupStarted, 1) != 0)
{
return;
}

// Closed can come from the Cancel button, Escape, the title-bar X, or normal success.
// Make the first three equivalent: stop all work before detaching the polling timer.
CancelPendingWork();
_timer.StopAndDispose(OnTimerOnElapsed);
_ = DeleteTempFileWhenTaskCompletesAsync(_downloadTask, _tempFileName);
}

internal static async Task DeleteTempFileWhenTaskCompletesAsync(Task? downloadTask, string tempFileName)
{
if (downloadTask != null)
{
try
{
await downloadTask.ConfigureAwait(false);
}
catch
{
// Cancellation/download failure is already the reason cleanup is running.
}
}

TryDeleteTempFile(tempFileName);
}

private static void TryDeleteTempFile(string tempFileName)
{
if (string.IsNullOrWhiteSpace(tempFileName))
{
return;
}

try
{
if (File.Exists(tempFileName))
{
File.Delete(tempFileName);
}
}
catch
{
// Best effort. During unpacking the timer callback may still own the ZIP and its
// finally block will retry deletion once that operation observes cancellation.
}
}

public void StartDownload()
Expand Down
79 changes: 79 additions & 0 deletions tests/UI/Features/Shared/DownloadFfmpegLibsViewModelTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using Nikse.SubtitleEdit.Features.Shared;
using Nikse.SubtitleEdit.Logic.Download;

namespace UITests.Features.Shared;

public class DownloadFfmpegLibsViewModelTests
{
[Fact]
public void OnClosingCleanup_CancelsPendingDownload()
{
var service = new BlockingFfmpegLibsDownloadService();
var vm = new DownloadFfmpegLibsViewModel(service);

vm.StartDownload();
Assert.True(service.Started.Wait(2000, TestContext.Current.CancellationToken));
Assert.False(service.CapturedToken.IsCancellationRequested);

vm.OnClosingCleanup();

Assert.True(service.CapturedToken.IsCancellationRequested);
}

[Fact]
public async Task DeleteTempFileWhenTaskCompletesAsync_WaitsThenDeletes()
{
var fileName = Path.Combine(Path.GetTempPath(), "se-ffmpeg-close-" + Guid.NewGuid().ToString("N") + ".zip");
File.WriteAllText(fileName, "partial");
var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

try
{
var cleanup = DownloadFfmpegLibsViewModel.DeleteTempFileWhenTaskCompletesAsync(completion.Task, fileName);
Assert.False(cleanup.IsCompleted);
Assert.True(File.Exists(fileName));

completion.SetResult();
await cleanup;

Assert.False(File.Exists(fileName));
}
finally
{
File.Delete(fileName);
}
}

[Fact]
public async Task DeleteTempFileWhenTaskCompletesAsync_FaultStillDeletes()
{
var fileName = Path.Combine(Path.GetTempPath(), "se-ffmpeg-close-" + Guid.NewGuid().ToString("N") + ".zip");
File.WriteAllText(fileName, "partial");

try
{
await DownloadFfmpegLibsViewModel.DeleteTempFileWhenTaskCompletesAsync(
Task.FromException(new IOException("download failed")),
fileName);

Assert.False(File.Exists(fileName));
}
finally
{
File.Delete(fileName);
}
}

private sealed class BlockingFfmpegLibsDownloadService : IFfmpegLibsDownloadService
{
internal ManualResetEventSlim Started { get; } = new(false);
internal CancellationToken CapturedToken { get; private set; }

public async Task DownloadFfmpegLibs(string destinationFileName, IProgress<float>? progress, CancellationToken cancellationToken)
{
CapturedToken = cancellationToken;
Started.Set();
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
}
}
}
Loading