Skip to content

Commit 5b57d97

Browse files
Merge pull request #81 from hudl/ImplementTimeOuts
Implement timeout functionality and process monitoring
2 parents 6e48964 + 785842d commit 5b57d97

28 files changed

+493
-44
lines changed

Hudl.FFmpeg.Core/Command/BaseTypes/ICommand.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ ICommandProcessor ExecuteWith<TProcessorType, TBuilderType>()
1212
where TProcessorType : class, ICommandProcessor, new()
1313
where TBuilderType : class, ICommandBuilder, new();
1414

15-
ICommandProcessor ExecuteWith<TProcessorType, TBuilderType>(TProcessorType commandProcessor)
15+
ICommandProcessor ExecuteWith<TProcessorType, TBuilderType>(int? timeoutMilliseconds)
16+
where TProcessorType : class, ICommandProcessor, new()
17+
where TBuilderType : class, ICommandBuilder, new();
18+
19+
ICommandProcessor ExecuteWith<TProcessorType, TBuilderType>(TProcessorType commandProcessor, int? timeoutMilliseconds)
1620
where TProcessorType : class, ICommandProcessor
1721
where TBuilderType : class, ICommandBuilder, new();
1822
}

Hudl.FFmpeg.Core/Command/BaseTypes/ICommandProcessor.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ public interface ICommandProcessor
3737
/// </summary>
3838
bool Send(string command);
3939

40-
40+
/// <summary>
41+
/// processes the given command string against the processor engine
42+
/// </summary>
43+
bool Send(string command, int? timeoutMilliseconds);
4144
}
4245
}

Hudl.FFmpeg.Core/Command/Models/FFcommandBase.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
namespace Hudl.FFmpeg.Command.Models
66
{
7-
public abstract class FFcommandBase : ICommand
7+
public abstract class FFCommandBase : ICommand
88
{
9-
protected FFcommandBase()
9+
protected FFCommandBase()
1010
{
1111
PreExecutionAction = EmptyOperation;
1212
PostExecutionAction = EmptyOperation;
@@ -27,6 +27,13 @@ protected FFcommandBase()
2727
public ICommandProcessor ExecuteWith<TProcessorType, TBuilderType>()
2828
where TProcessorType : class, ICommandProcessor, new()
2929
where TBuilderType : class, ICommandBuilder, new()
30+
{
31+
return ExecuteWith<TProcessorType, TBuilderType>(null);
32+
}
33+
34+
public ICommandProcessor ExecuteWith<TProcessorType, TBuilderType>(int? timeoutMilliseconds)
35+
where TProcessorType : class, ICommandProcessor, new()
36+
where TBuilderType : class, ICommandBuilder, new()
3037
{
3138
var commandProcessor = new TProcessorType();
3239

@@ -35,7 +42,7 @@ public ICommandProcessor ExecuteWith<TProcessorType, TBuilderType>()
3542
throw new FFmpegRenderingException(commandProcessor.Error);
3643
}
3744

38-
var returnType = ExecuteWith<TProcessorType, TBuilderType>(commandProcessor);
45+
var returnType = ExecuteWith<TProcessorType, TBuilderType>(commandProcessor, timeoutMilliseconds);
3946

4047
if (!commandProcessor.Close())
4148
{
@@ -45,7 +52,7 @@ public ICommandProcessor ExecuteWith<TProcessorType, TBuilderType>()
4552
return returnType;
4653
}
4754

48-
public ICommandProcessor ExecuteWith<TProcessorType, TBuilderType>(TProcessorType commandProcessor)
55+
public ICommandProcessor ExecuteWith<TProcessorType, TBuilderType>(TProcessorType commandProcessor, int? timeoutMilliseconds)
4956
where TProcessorType : class, ICommandProcessor
5057
where TBuilderType : class, ICommandBuilder, new()
5158
{
@@ -59,7 +66,7 @@ public ICommandProcessor ExecuteWith<TProcessorType, TBuilderType>(TProcessorTyp
5966

6067
PreExecutionAction(Owner, this, true);
6168

62-
if (!commandProcessor.Send(commandBuilder.ToString()))
69+
if (!commandProcessor.Send(commandBuilder.ToString(), timeoutMilliseconds))
6370
{
6471
PostExecutionAction(Owner, this, false);
6572

Hudl.FFmpeg.Core/Command/Models/FFcommandBuilderBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
namespace Hudl.FFmpeg.Command.Models
44
{
5-
public abstract class FFcommandBuilderBase
5+
public abstract class FFCommandBuilderBase
66
{
77
protected readonly StringBuilder BuilderBase;
88

9-
protected FFcommandBuilderBase()
9+
protected FFCommandBuilderBase()
1010
{
1111
BuilderBase = new StringBuilder(100);
1212
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using Hudl.FFmpeg.Logging;
2+
using System;
3+
using System.Diagnostics;
4+
using System.Text;
5+
using System.Threading;
6+
7+
namespace Hudl.FFmpeg.Command.StreamReaders
8+
{
9+
public abstract class BaseStandardStreamReader
10+
{
11+
private static readonly LogUtility Log = LogUtility.GetLogger(typeof(BaseStandardStreamReader));
12+
private const int OutputBuilderLimit = 10000;
13+
14+
protected BaseStandardStreamReader(Process processToListenTo)
15+
{
16+
OutputBuilder = new StringBuilder();
17+
ProcessToListenTo = processToListenTo;
18+
_stopSignal = new ManualResetEvent(false);
19+
}
20+
21+
protected Thread MonitoringThread;
22+
protected StringBuilder OutputBuilder;
23+
protected readonly Process ProcessToListenTo;
24+
protected volatile bool _stopped;
25+
protected ManualResetEvent _stopSignal;
26+
27+
public string LastLineReceived { get; set; }
28+
29+
public void Listen()
30+
{
31+
//workaround for a bug in the mono process when attempting to read async from console output events
32+
// - link http://mono.1490590.n4.nabble.com/System-Diagnostic-Process-and-event-handlers-td3246096.html
33+
if (ResourceManagement.IsMonoRuntime())
34+
{
35+
ListenAsThread();
36+
}
37+
else
38+
{
39+
ListenAsAsync();
40+
}
41+
}
42+
protected abstract void ListenAsAsync();
43+
protected abstract void ListenAsThread();
44+
45+
public void Stop()
46+
{
47+
_stopped = true;
48+
_stopSignal.WaitOne(1000);
49+
if (ResourceManagement.IsMonoRuntime())
50+
{
51+
try
52+
{
53+
if (MonitoringThread.ThreadState != System.Threading.ThreadState.Stopped)
54+
{
55+
MonitoringThread.Abort();
56+
}
57+
}
58+
catch (Exception e)
59+
{
60+
Log.Error("Unable to abort the monitoring thread", e);
61+
}
62+
}
63+
}
64+
65+
private void HandleDataReceived(string data)
66+
{
67+
if (!string.IsNullOrWhiteSpace(data))
68+
{
69+
LastLineReceived = data;
70+
}
71+
72+
//there is no specific reason behind this number other than, it seems like a pretty good number baseline.
73+
//ultimately ffmpeg can blow up the standard output/error stream with logs. we dont want to create
74+
//an OOM exception. so we will trash and reset.
75+
if (OutputBuilder.Length > OutputBuilderLimit)
76+
{
77+
var newBuilder = new StringBuilder(OutputBuilderLimit);
78+
newBuilder.Append(OutputBuilder.ToString(), OutputBuilderLimit / 2, OutputBuilderLimit / 2);
79+
OutputBuilder = newBuilder;
80+
}
81+
82+
OutputBuilder.AppendLine(data);
83+
}
84+
protected void HandleDataReceivedAsAsync(object sender, DataReceivedEventArgs dataReceivedEventArgs)
85+
{
86+
HandleDataReceived(dataReceivedEventArgs.Data);
87+
}
88+
protected void HandleDataReceivedAsThread(string data)
89+
{
90+
HandleDataReceived(data);
91+
}
92+
93+
public override string ToString()
94+
{
95+
return OutputBuilder.ToString();
96+
}
97+
}
98+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System.Diagnostics;
2+
using System.Threading;
3+
4+
namespace Hudl.FFmpeg.Command.StreamReaders
5+
{
6+
public class StandardErrorAsyncStreamReader : BaseStandardStreamReader
7+
{
8+
private StandardErrorAsyncStreamReader(Process processToListenTo)
9+
: base(processToListenTo)
10+
{
11+
ProcessToListenTo.StartInfo.RedirectStandardError = true;
12+
if (!ResourceManagement.IsMonoRuntime())
13+
{
14+
ProcessToListenTo.ErrorDataReceived += HandleDataReceivedAsAsync;
15+
}
16+
}
17+
18+
public static StandardErrorAsyncStreamReader AttachReader(Process processToListenTo)
19+
{
20+
return new StandardErrorAsyncStreamReader(processToListenTo);
21+
}
22+
23+
protected override void ListenAsAsync()
24+
{
25+
ProcessToListenTo.BeginErrorReadLine();
26+
}
27+
28+
protected override void ListenAsThread()
29+
{
30+
MonitoringThread = new Thread(AsyncStdErrorMonitor);
31+
MonitoringThread.Start();
32+
}
33+
34+
private void AsyncStdErrorMonitor()
35+
{
36+
try
37+
{
38+
do
39+
{
40+
string line = ProcessToListenTo.StandardError.ReadLine();
41+
if (line != null)
42+
{
43+
HandleDataReceivedAsThread(line);
44+
}
45+
}
46+
while (!_stopped && !ProcessToListenTo.HasExited);
47+
_stopSignal.Set();
48+
}
49+
catch
50+
{ }
51+
}
52+
}
53+
54+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System.Diagnostics;
2+
using System.Threading;
3+
4+
namespace Hudl.FFmpeg.Command.StreamReaders
5+
{
6+
public class StandardOutputAsyncStreamReader : BaseStandardStreamReader
7+
{
8+
private StandardOutputAsyncStreamReader(Process processToListenTo)
9+
: base(processToListenTo)
10+
{
11+
ProcessToListenTo.StartInfo.RedirectStandardOutput = true;
12+
if (!ResourceManagement.IsMonoRuntime())
13+
{
14+
ProcessToListenTo.ErrorDataReceived += HandleDataReceivedAsAsync;
15+
}
16+
}
17+
18+
public static StandardOutputAsyncStreamReader AttachReader(Process processToListenTo)
19+
{
20+
return new StandardOutputAsyncStreamReader(processToListenTo);
21+
}
22+
23+
protected override void ListenAsAsync()
24+
{
25+
ProcessToListenTo.BeginErrorReadLine();
26+
}
27+
28+
protected override void ListenAsThread()
29+
{
30+
MonitoringThread = new Thread(AsyncStdErrorMonitor);
31+
MonitoringThread.Start();
32+
}
33+
34+
private void AsyncStdErrorMonitor()
35+
{
36+
try
37+
{
38+
do
39+
{
40+
string line = ProcessToListenTo.StandardError.ReadLine();
41+
if (line != null)
42+
{
43+
HandleDataReceivedAsThread(line);
44+
}
45+
}
46+
while (!_stopped && !ProcessToListenTo.HasExited);
47+
_stopSignal.Set();
48+
}
49+
catch
50+
{ }
51+
}
52+
}
53+
54+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
3+
namespace Hudl.FFmpeg.Exceptions
4+
{
5+
/// <summary>
6+
/// exception that is thrown when FFmpeg commands have passed the timeout period
7+
/// </summary>
8+
public class FFmpegTimeoutException : Exception
9+
{
10+
public FFmpegTimeoutException(string arguments)
11+
: base("FFmpeg timed out during processing")
12+
{
13+
base.Data["Arguments"] = arguments;
14+
15+
Arguments = arguments;
16+
}
17+
18+
public string Arguments { get; private set; }
19+
}
20+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Threading;
4+
5+
namespace Hudl.FFmpeg.Extensions
6+
{
7+
public static class ProcessExtensions
8+
{
9+
public static bool WaitForProcessStart(this Process process)
10+
{
11+
return process.WaitForProcessStart(null);
12+
}
13+
public static bool WaitForProcessStart(this Process process, int? timeoutMilliseconds)
14+
{
15+
var processTimeout = TimeSpan.FromMilliseconds(timeoutMilliseconds ?? 10000);
16+
return process.WaitForProcessStart(processTimeout);
17+
}
18+
public static bool WaitForProcessStart(this Process process, TimeSpan processTimeout)
19+
{
20+
var processStopwatch = Stopwatch.StartNew();
21+
var isProcessRunning = false;
22+
23+
while (processStopwatch.ElapsedMilliseconds <= processTimeout.TotalMilliseconds && !isProcessRunning)
24+
{
25+
//give a little bit of breathing room here....
26+
Thread.Sleep(20.Milliseconds());
27+
28+
try
29+
{
30+
isProcessRunning = (process != null && process.HasExited && process.Id != 0);
31+
}
32+
catch
33+
{
34+
isProcessRunning = false;
35+
}
36+
}
37+
38+
return isProcessRunning;
39+
}
40+
41+
public static bool WaitForProcessStop(this Process process)
42+
{
43+
return process.WaitForProcessStop(null);
44+
}
45+
public static bool WaitForProcessStop(this Process process, int? timeoutMilliseconds)
46+
{
47+
var processTimeout = TimeSpan.FromMilliseconds(timeoutMilliseconds ?? 0);
48+
return process.WaitForProcessStop(processTimeout);
49+
}
50+
public static bool WaitForProcessStop(this Process process, TimeSpan processTimeout)
51+
{
52+
var processStopwatch = Stopwatch.StartNew();
53+
54+
while (!process.HasExited)
55+
{
56+
Thread.Sleep(1.Seconds());
57+
58+
if (processTimeout.TotalMilliseconds > 0 && processStopwatch.ElapsedMilliseconds > processTimeout.TotalMilliseconds)
59+
{
60+
process.Kill();
61+
62+
process.WaitForExit((int)5.Seconds().TotalMilliseconds);
63+
64+
return false;
65+
}
66+
}
67+
68+
return true;
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)