OK, I've spent some time on this one and I think I have a solution that will work for you without having to change your code all that much.
The following is how you would use the Timebox
class that I created.
public void MyMethod( ... ) {
// some stuff
// instead of this
// using(...){ /* your code here */ }
// you can use this
var timebox = new Timebox(TimeSpan.FromSeconds(1));
timebox.Execute(() =>
{
/* your code here */
});
// some more stuff
}
Here's how Timebox
works.
- A
Timebox
object is created with a given Timespan
- When
Execute
is called, the Timebox
creates a child AppDomain
to hold a TimeboxRuntime
object reference, and returns a proxy to it
- The
TimeboxRuntime
object in the child AppDomain
takes an Action
as input to execute within the child domain
Timebox
then creates a task to call the TimeboxRuntime
proxy
- The task is started (and the action execution starts), and the "main" thread waits for for as long as the given
TimeSpan
- After the given
TimeSpan
(or when the task completes), the child AppDomain
is unloaded whether the Action
was completed or not.
- A
TimeoutException
is thrown if action
times out, otherwise if action
throws an exception, it is caught by the child AppDomain
and returned for the calling AppDomain
to throw
A downside is that your program will need elevated enough permissions to create an AppDomain
.
Here is a sample program which demonstrates how it works (I believe you can copy-paste this, if you include the correct using
s). I also created this gist if you are interested.
public class Program
{
public static void Main()
{
try
{
var timebox = new Timebox(TimeSpan.FromSeconds(1));
timebox.Execute(() =>
{
// do your thing
for (var i = 0; i < 1000; i++)
{
Console.WriteLine(i);
}
});
Console.WriteLine("Didn't Time Out");
}
catch (TimeoutException e)
{
Console.WriteLine("Timed Out");
// handle it
}
catch(Exception e)
{
Console.WriteLine("Another exception was thrown in your timeboxed function");
// handle it
}
Console.WriteLine("Program Finished");
Console.ReadLine();
}
}
public class Timebox
{
private readonly TimeSpan _ts;
public Timebox(TimeSpan ts)
{
_ts = ts;
}
public void Execute(Action func)
{
AppDomain childDomain = null;
try
{
// Construct and initialize settings for a second AppDomain. Perhaps some of
// this is unnecessary but perhaps not.
var domainSetup = new AppDomainSetup()
{
ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile,
ApplicationName = AppDomain.CurrentDomain.SetupInformation.ApplicationName,
LoaderOptimization = LoaderOptimization.MultiDomainHost
};
// Create the child AppDomain
childDomain = AppDomain.CreateDomain("Timebox Domain", null, domainSetup);
// Create an instance of the timebox runtime child AppDomain
var timeboxRuntime = (ITimeboxRuntime)childDomain.CreateInstanceAndUnwrap(
typeof(TimeboxRuntime).Assembly.FullName, typeof(TimeboxRuntime).FullName);
// Start the runtime, by passing it the function we're timboxing
Exception ex = null;
var timeoutOccurred = true;
var task = new Task(() =>
{
ex = timeboxRuntime.Run(func);
timeoutOccurred = false;
});
// start task, and wait for the alloted timespan. If the method doesn't finish
// by then, then we kill the childDomain and throw a TimeoutException
task.Start();
task.Wait(_ts);
// if the timeout occurred then we throw the exception for the caller to handle.
if(timeoutOccurred)
{
throw new TimeoutException("The child domain timed out");
}
// If no timeout occurred, then throw whatever exception was thrown
// by our child AppDomain, so that calling code "sees" the exception
// thrown by the code that it passes in.
if(ex != null)
{
throw ex;
}
}
finally
{
// kill the child domain whether or not the function has completed
if(childDomain != null) AppDomain.Unload(childDomain);
}
}
// don't strictly need this, but I prefer having an interface point to the proxy
private interface ITimeboxRuntime
{
Exception Run(Action action);
}
// Need to derive from MarshalByRefObject... proxy is returned across AppDomain boundary.
private class TimeboxRuntime : MarshalByRefObject, ITimeboxRuntime
{
public Exception Run(Action action)
{
try
{
// Nike: just do it!
action();
}
catch(Exception e)
{
// return the exception to be thrown in the calling AppDomain
return e;
}
return null;
}
}
}
EDIT:
The reason I went with an AppDomain
instead of Thread
s or Task
s only, is because there is no bullet proof way for terminating Thread
s or Task
s for arbitrary code [1][2][3]. An AppDomain, for your requirements, seemed like the best approach to me.