I'm running into a problem with long startup times when I try to run an IronPython script engine in an AppDomain with restricted permissions. Can someone provide a canonical example of how to do this in a performant way?
No AppDomain no standard library
Time taken: 36.1022ms
No AppDomain with standard library
Time taken: 37.8556ms
I saw it mentioned somewhere that creating AppDomain instances is expensive. Would it be a best practice to create a single restricted AppDomain and reuse it?
[assembly: AllowPartiallyTrustedCallers]
public class ScriptEvaluator : MarshalByRefObject
{
public ScriptEngine ScriptEngine { get; set; }
public String PythonStandardLibaryPath { get; set; }
public String PythonApplicationCodePath { get; set; }
public ScriptEvaluator()
{
PythonStandardLibaryPath = Path.GetFullPath(@"..\..\..\MyProject\standard_library");
PythonApplicationCodePath = Path.GetFullPath(@"..\..\..\MyProject\python_code");
}
public ScriptEvaluator InstantiateInAppDomain()
{
/* create app domain setup */
var appDomainSetup = new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory };
//appDomainSetup.ApplicationBase = Path.GetDirectoryName(Environment.CurrentDirectory);
//appDomainSetup.ApplicationBase = Environment.CurrentDirectory;
//appDomainSetup.DisallowBindingRedirects = false;
//appDomainSetup.DisallowCodeDownload = true;
//appDomainSetup.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
/* assign permissions */
var permissionSet = new PermissionSet(PermissionState.None);
permissionSet.AddPermission(new ReflectionPermission(PermissionState.Unrestricted));
permissionSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
permissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read, PythonStandardLibaryPath));
permissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read, PythonApplicationCodePath));
if (Debugger.IsAttached)
{
permissionSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.UnmanagedCode));
}
/* create a script evaluator instance in the app domain and return a reference */
var appDomain = AppDomain.CreateDomain("Sandbox", null, appDomainSetup, permissionSet);
var scriptEvaluator = (ScriptEvaluator)appDomain.CreateInstanceAndUnwrap("MyProject", typeof(ScriptEvaluator).FullName);
scriptEvaluator.CreateScriptEngine();
return scriptEvaluator;
}
public ScriptEngine CreateScriptEngine()
{
/* create the iron IronPython engine */
var options = new Dictionary<string, object> { { "Debug", Debugger.IsAttached } };
ScriptEngine = Python.CreateEngine(options);
/* add library directories to the search paths */
var searchPaths = ScriptEngine.GetSearchPaths();
searchPaths.Add(PythonStandardLibaryPath);
searchPaths.Add(PythonApplicationCodePath);
ScriptEngine.SetSearchPaths(searchPaths);
return ScriptEngine;
}
}
Note: I had to use AllowPartiallyTrustedCallers to get around security errors, I'm not sure if this a desirable approach.
Usage:
var scriptEvaluator = new ScriptEvaluator().InstantiateInAppDomain();
var scriptScope = scriptEvaluator.ScriptEngine.CreateScope();
script = @"from mymodule import *; a = 1 + 1; b = 2 + 2; c = a + b";
foreach (var number in Enumerable.Range(1, 10))
{
Stopwatch stopwatch = Stopwatch.StartNew();
scriptEvaluator.ScriptEngine.Execute(script, scriptScope);
stopwatch.Stop(); Console.WriteLine("Time taken: {0}ms", stopwatch.Elapsed.TotalMilliseconds);
}
Did some profiling and it looks like the first execution of ScriptEngine.Execute is more expensive than subsequent executions.
// Time taken: 8.937ms -- InstantiateInAppDomain
// Time taken: 429.9291ms -- CreateScriptEngine
// Time taken: 9.1289ms -- CreateScope
// Time taken: 2080.1795ms -- ScriptEngine.Execute first
// Time taken: 5.2469ms -- ScriptEngine.Execute second
// Time taken: 7.9255ms -- ScriptEngine.Execute third
// Time taken: 3.1206ms -- ScriptEngine.Execute fourth
// Time taken: 3.1316ms -- ScriptEngine.Execute fifth
// Time taken: 3.0643ms -- ScriptEngine.Execute sixth
Does anyone know what permission is missing to be able to import the os module in a restricted AppDomain? Adding every permission I could find didn't work.