Compare commits

...

4 commits

Author SHA1 Message Date
Sergey Nechaev
65cbc5552c Update the ThrowIfExitCodeNotZero() to check the exit code before handling cancellation.
This preserves the original semantics and contract (throw only if the ffprobe exits with a non-zero code).
2025-10-24 18:06:55 +02:00
Sergey Nechaev
8f9930ad2a Additional test to verify that FFProbeHelper still throws FFMpegException when FFProbe exits with non-zero code and no cancellation was requested.
Ref.: #594
2025-10-22 21:24:32 +02:00
Sergey Nechaev
c3e80a7af6 FFProbe: Do not throw FFMpegException if cancellation was requested.
Throw OperationCancelledException in this case to provide more uniform and expected behavior.

Fixes #594
2025-10-22 20:44:50 +02:00
Sergey Nechaev
5f906af57f Add test to verify unexpected exception on FFProbe operations cancellation.
Ref.: #594
2025-10-22 20:44:50 +02:00
3 changed files with 76 additions and 6 deletions

View file

@ -1,4 +1,5 @@
using FFMpegCore.Test.Resources;
using FFMpegCore.Exceptions;
using FFMpegCore.Test.Resources;
namespace FFMpegCore.Test;
@ -285,4 +286,70 @@ public class FFProbeTests
var info = FFProbe.Analyse(TestResources.Mp4Video, customArguments: "-headers \"Hello: World\"");
Assert.AreEqual(3, info.Duration.Seconds);
}
[TestMethod]
[Timeout(10000, CooperativeCancellation = true)]
public async Task Parallel_FFProbe_Cancellation_Should_Throw_Only_OperationCanceledException()
{
// Warm up FFMpegCore environment
Helpers.FFProbeHelper.VerifyFFProbeExists(GlobalFFOptions.Current);
var mp4 = TestResources.Mp4Video;
if (!File.Exists(mp4))
{
Assert.Inconclusive($"Test video not found: {mp4}");
return;
}
var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.CancellationToken);
var token = cts.Token;
using var semaphore = new SemaphoreSlim(Environment.ProcessorCount, Environment.ProcessorCount);
var tasks = Enumerable.Range(0, 50).Select(x => Task.Run(async () =>
{
await semaphore.WaitAsync(token);
try
{
var analysis = await FFProbe.AnalyseAsync(mp4, cancellationToken: token);
return analysis;
}
finally
{
semaphore.Release();
}
}, token)).ToList();
// Wait for 2 tasks to finish, then cancel all
await Task.WhenAny(tasks);
await Task.WhenAny(tasks);
await cts.CancelAsync();
cts.Dispose();
var exceptions = new List<Exception>();
foreach (var task in tasks)
{
try
{
await task;
}
catch (Exception e)
{
exceptions.Add(e);
}
}
Assert.IsNotEmpty(exceptions, "No exceptions were thrown on cancellation. Test was useless. " +
".Try adjust cancellation timings to make cancellation at the moment, when ffprobe is still running.");
// Check that all exceptions are OperationCanceledException
CollectionAssert.AllItemsAreInstancesOfType(exceptions, typeof(OperationCanceledException));
}
[TestMethod]
[Timeout(10000, CooperativeCancellation = true)]
public async Task FFProbe_Should_Throw_FFMpegException_When_Exits_With_Non_Zero_Code()
{
var input = TestResources.SrtSubtitle; //non media file
await Assert.ThrowsAsync<FFMpegException>(async () => await FFProbe.AnalyseAsync(input,
cancellationToken: TestContext.CancellationToken, customArguments: "--some-invalid-argument"));
}
}

View file

@ -84,7 +84,7 @@ public static class FFProbe
var instance = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false);
ThrowIfExitCodeNotZero(result);
ThrowIfExitCodeNotZero(result, cancellationToken);
return ParseOutput(result);
}
@ -123,7 +123,7 @@ public static class FFProbe
{
var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments);
var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false);
ThrowIfExitCodeNotZero(result);
ThrowIfExitCodeNotZero(result, cancellationToken);
return ParseOutput(result);
}
@ -150,7 +150,7 @@ public static class FFProbe
}
var result = await task.ConfigureAwait(false);
ThrowIfExitCodeNotZero(result);
ThrowIfExitCodeNotZero(result, cancellationToken);
pipeArgument.Post();
return ParseOutput(result);
@ -212,10 +212,13 @@ public static class FFProbe
}
}
private static void ThrowIfExitCodeNotZero(IProcessResult result)
private static void ThrowIfExitCodeNotZero(IProcessResult result, CancellationToken cancellationToken = default)
{
if (result.ExitCode != 0)
{
// if cancellation requested, then we are not interested in the exit code, just throw the cancellation exception
// to get consistent and expected behavior.
cancellationToken.ThrowIfCancellationRequested();
var message = $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})";
throw new FFMpegException(FFMpegExceptionType.Process, message, null, string.Join("\n", result.ErrorData));
}

View file

@ -13,6 +13,6 @@ public static class ProcessArgumentsExtensions
public static async Task<IProcessResult> StartAndWaitForExitAsync(this ProcessArguments processArguments, CancellationToken cancellationToken = default)
{
using var instance = processArguments.Start();
return await instance.WaitForExitAsync(cancellationToken);
return await instance.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
}
}