Ver código fonte

Execution constraints (#709)

Sebastian Stehle 5 anos atrás
pai
commit
4e86924d7e

+ 56 - 0
Jint.Benchmark/TimeoutBenchmark.cs

@@ -0,0 +1,56 @@
+using BenchmarkDotNet.Attributes;
+using Jint.Constraints;
+using System;
+
+namespace Jint.Benchmark
+{
+    [MemoryDiagnoser]
+    public class TimeoutBenchmark
+    {
+        private const string Script = "var ret=[],tmp,num=100,i=256;for(var j1=0;j1<i*15;j1++){ret=[];ret.length=i}for(var j2=0;j2<i*10;j2++){ret=new Array(i)}ret=[];for(var j3=0;j3<i;j3++){ret.unshift(j3)}ret=[];for(var j4=0;j4<i;j4++){ret.splice(0,0,j4)}var a=ret.slice();for(var j5=0;j5<i;j5++){tmp=a.shift()}var b=ret.slice();for(var j6=0;j6<i;j6++){tmp=b.splice(0,1)}ret=[];for(var j7=0;j7<i*25;j7++){ret.push(j7)}var c=ret.slice();for(var j8=0;j8<i*25;j8++){tmp=c.pop()}var done = true;";
+
+        private Engine engineTimeout1;
+        private Engine engineTimeout2;
+
+        [GlobalSetup]
+        public void Setup()
+        {
+            engineTimeout1 = new Engine(options =>
+            {
+                options.Constraint(new TimeConstraint(TimeSpan.FromSeconds(5)));
+            });
+
+            engineTimeout2 = new Engine(options =>
+            {
+                options.Constraint(new TimeConstraint2(TimeSpan.FromSeconds(5)));
+            });
+        }
+
+        [Params(10)]
+        public virtual int N { get; set; }
+
+        [Benchmark]
+        public bool Timeout1()
+        {
+            bool done = false;
+            for (var i = 0; i < N; i++)
+            {
+                engineTimeout1.Execute(Script);
+            }
+
+            return done;
+        }
+
+        [Benchmark]
+        public bool Timeout2()
+        {
+            bool done = false;
+            for (var i = 0; i < N; i++)
+            {
+                engineTimeout2.Execute(Script);
+            }
+
+            return done;
+        }
+    }
+}

+ 32 - 0
Jint/Constraints/CancellationConstraint.cs

@@ -0,0 +1,32 @@
+using Jint.Runtime;
+using System.Threading;
+
+namespace Jint.Constraints
+{
+    public sealed class CancellationConstraint : IConstraint
+    {
+        private CancellationToken _cancellationToken;
+
+        public CancellationConstraint(CancellationToken cancellationToken)
+        {
+            _cancellationToken = cancellationToken;
+        }
+
+        public void Check()
+        {
+            if (_cancellationToken.IsCancellationRequested)
+            {
+                ExceptionHelper.ThrowStatementsCountOverflowException();
+            }
+        }
+
+        public void Reset(CancellationToken cancellationToken)
+        {
+            _cancellationToken = cancellationToken;
+        }
+
+        public void Reset()
+        {
+        }
+    }
+}

+ 53 - 0
Jint/Constraints/ConstraintsOptionsExtensions.cs

@@ -0,0 +1,53 @@
+using System;
+using System.Threading;
+using Jint.Constraints;
+
+namespace Jint
+{
+    public static class ConstraintsOptionsExtensions
+    {
+        public static Options MaxStatements(this Options options, int maxStatements = 0)
+        {
+            options.WithoutConstraint(x => x is MaxStatements);
+
+            if (maxStatements > 0 && maxStatements < int.MaxValue)
+            {
+                options.Constraint(new MaxStatements(maxStatements));
+            }
+            return options;
+        }
+
+        public static Options LimitMemory(this Options options, long memoryLimit)
+        {
+            options.WithoutConstraint(x => x is MemoryLimit);
+
+            if (memoryLimit > 0 && memoryLimit < int.MaxValue)
+            {
+                options.Constraint(new MemoryLimit(memoryLimit));
+            }
+            return options;
+        }
+
+        public static Options TimeoutInterval(this Options options, TimeSpan timeoutInterval)
+        {
+            options.WithoutConstraint(x => x is TimeConstraint);
+
+            if (timeoutInterval > TimeSpan.Zero && timeoutInterval < TimeSpan.MaxValue)
+            {
+                options.Constraint(new TimeConstraint2(timeoutInterval));
+            }
+            return options;
+        }
+
+        public static Options CancellationToken(this Options options, CancellationToken cancellationToken)
+        {
+            options.WithoutConstraint(x => x is CancellationConstraint);
+
+            if (cancellationToken != default)
+            {
+                options.Constraint(new CancellationConstraint(cancellationToken));
+            }
+            return options;
+        }
+    }
+}

+ 28 - 0
Jint/Constraints/MaxStatements.cs

@@ -0,0 +1,28 @@
+using Jint.Runtime;
+
+namespace Jint.Constraints
+{
+    internal sealed class MaxStatements : IConstraint
+    {
+        private readonly int _maxStatements;
+        private int _statementsCount;
+
+        public MaxStatements(int maxStatements)
+        {
+            _maxStatements = maxStatements;
+        }
+
+        public void Check()
+        {
+            if (_maxStatements > 0 && _statementsCount++ > _maxStatements)
+            {
+                ExceptionHelper.ThrowStatementsCountOverflowException();
+            }
+        }
+
+        public void Reset()
+        {
+            _statementsCount = 0;
+        }
+    }
+}

+ 54 - 0
Jint/Constraints/MemoryLimit.cs

@@ -0,0 +1,54 @@
+using Jint.Runtime;
+using System;
+
+namespace Jint.Constraints
+{
+    internal sealed class MemoryLimit : IConstraint
+    {
+        private static readonly Func<long> GetAllocatedBytesForCurrentThread;
+        private readonly long _memoryLimit;
+        private long _initialMemoryUsage;
+
+        static MemoryLimit()
+        {
+            var methodInfo = typeof(GC).GetMethod("GetAllocatedBytesForCurrentThread");
+
+            if (methodInfo != null)
+            {
+                GetAllocatedBytesForCurrentThread = (Func<long>)Delegate.CreateDelegate(typeof(Func<long>), null, methodInfo);
+            }
+        }
+
+        public MemoryLimit(long memoryLimit)
+        {
+            _memoryLimit = memoryLimit;
+        }
+
+        public void Check()
+        {
+            if (_memoryLimit > 0)
+            {
+                if (GetAllocatedBytesForCurrentThread != null)
+                {
+                    var memoryUsage = GetAllocatedBytesForCurrentThread() - _initialMemoryUsage;
+                    if (memoryUsage > _memoryLimit)
+                    {
+                        ExceptionHelper.ThrowMemoryLimitExceededException($"Script has allocated {memoryUsage} but is limited to {_memoryLimit}");
+                    }
+                }
+                else
+                {
+                    ExceptionHelper.ThrowPlatformNotSupportedException("The current platform doesn't support MemoryLimit.");
+                }
+            }
+        }
+
+        public void Reset()
+        {
+            if (GetAllocatedBytesForCurrentThread != null)
+            {
+                _initialMemoryUsage = GetAllocatedBytesForCurrentThread();
+            }
+        }
+    }
+}

+ 31 - 0
Jint/Constraints/TimeConstraint.cs

@@ -0,0 +1,31 @@
+using Jint.Runtime;
+using System;
+
+namespace Jint.Constraints
+{
+    internal sealed class TimeConstraint : IConstraint
+    {
+        private readonly long _maxTicks;
+        private long _timeoutTicks;
+
+        public TimeConstraint(TimeSpan timeout)
+        {
+            _maxTicks = timeout.Ticks;
+        }
+
+        public void Check()
+        {
+            if (_timeoutTicks > 0 && _timeoutTicks < DateTime.UtcNow.Ticks)
+            {
+                ExceptionHelper.ThrowTimeoutException();
+            }
+        }
+
+        public void Reset()
+        {
+            var timeoutIntervalTicks = _maxTicks;
+
+            _timeoutTicks = timeoutIntervalTicks > 0 ? DateTime.UtcNow.Ticks + timeoutIntervalTicks : 0;
+        }
+    }
+}

+ 35 - 0
Jint/Constraints/TimeConstraint2.cs

@@ -0,0 +1,35 @@
+using Jint.Runtime;
+using System;
+using System.Threading;
+
+namespace Jint.Constraints
+{
+    internal sealed class TimeConstraint2 : IConstraint
+    {
+        private readonly TimeSpan _timeout;
+        private CancellationTokenSource cts;
+
+        public TimeConstraint2(TimeSpan timeout)
+        {
+            _timeout = timeout;
+        }
+
+        public void Check()
+        {
+            if (cts.IsCancellationRequested)
+            {
+                ExceptionHelper.ThrowTimeoutException();
+            }
+        }
+
+        public void Reset()
+        {
+            cts?.Dispose();
+
+            // This cancellation token source is very likely not disposed property, but it only allocates a timer, so not a big deal.
+            // But using the cancellation token source is faster because we do not have to check the current time for each statement,
+            // which means less system calls.
+            cts = new CancellationTokenSource(_timeout);
+        }
+    }
+}

+ 9 - 69
Jint/Engine.cs

@@ -57,9 +57,6 @@ namespace Jint
 
         private readonly ExecutionContextStack _executionContexts;
         private JsValue _completionValue = JsValue.Undefined;
-        private int _statementsCount;
-        private long _initialMemoryUsage;
-        private long _timeoutTicks;
         internal INode _lastSyntaxNode;
 
         // lazy properties
@@ -74,11 +71,9 @@ namespace Jint
         private List<BreakPoint> _breakPoints;
 
         // cached access
+        private readonly List<IConstraint> _constraints;
         private readonly bool _isDebugMode;
         internal readonly bool _isStrict;
-        private readonly int _maxStatements;
-        private readonly long _memoryLimit;
-        internal readonly bool _runBeforeStatementChecks;
         internal readonly IReferenceResolver _referenceResolver;
         internal readonly ReferencePool _referencePool;
         internal readonly ArgumentsInstancePool _argumentsInstancePool;
@@ -152,16 +147,6 @@ namespace Jint
 
         internal readonly JintCallStack CallStack = new JintCallStack();
 
-        static Engine()
-        {
-            var methodInfo = typeof(GC).GetMethod("GetAllocatedBytesForCurrentThread");
-
-            if (methodInfo != null)
-            {
-                GetAllocatedBytesForCurrentThread =  (Func<long>)Delegate.CreateDelegate(typeof(Func<long>), null, methodInfo);
-            }
-        }
-
         public Engine() : this(null)
         {
         }
@@ -216,13 +201,8 @@ namespace Jint
             // gather some options as fields for faster checks
             _isDebugMode = Options.IsDebugMode;
             _isStrict = Options.IsStrict;
-            _maxStatements = Options._MaxStatements;
+            _constraints = Options._Constraints;
             _referenceResolver = Options.ReferenceResolver;
-            _memoryLimit = Options._MemoryLimit;
-            _runBeforeStatementChecks = (_maxStatements > 0 &&_maxStatements < int.MaxValue)
-                                        || Options._TimeoutInterval.Ticks > 0
-                                        || _memoryLimit > 0
-                                        || _isDebugMode;
 
             _referencePool = new ReferencePool();
             _argumentsInstancePool = new ArgumentsInstancePool(this);
@@ -304,8 +284,6 @@ namespace Jint
         }
         #endregion
 
-        private static readonly Func<long> GetAllocatedBytesForCurrentThread;
-
         public void EnterExecutionContext(
             LexicalEnvironment lexicalEnvironment,
             LexicalEnvironment variableEnvironment,
@@ -364,25 +342,14 @@ namespace Jint
         /// <summary>
         /// Initializes the statements count
         /// </summary>
-        public void ResetStatementsCount()
+        public void ResetConstraints()
         {
-            _statementsCount = 0;
-        }
-
-        public void ResetMemoryUsage()
-        {
-            if (GetAllocatedBytesForCurrentThread != null)
+            for (var i = 0; i < _constraints.Count; i++)
             {
-                _initialMemoryUsage = GetAllocatedBytesForCurrentThread();
+                _constraints[i].Reset();
             }
         }
 
-        public void ResetTimeoutTicks()
-        {
-            var timeoutIntervalTicks = Options._TimeoutInterval.Ticks;
-            _timeoutTicks = timeoutIntervalTicks > 0 ? DateTime.UtcNow.Ticks + timeoutIntervalTicks : 0;
-        }
-
         /// <summary>
         /// Initializes list of references of called functions
         /// </summary>
@@ -404,14 +371,7 @@ namespace Jint
 
         public Engine Execute(Script program)
         {
-            ResetStatementsCount();
-
-            if (_memoryLimit > 0)
-            {
-                ResetMemoryUsage();
-            }
-
-            ResetTimeoutTicks();
+            ResetConstraints();
             ResetLastStatement();
             ResetCallStack();
 
@@ -452,30 +412,10 @@ namespace Jint
 
         internal void RunBeforeExecuteStatementChecks(Statement statement)
         {
-            if (_maxStatements > 0 && _statementsCount++ > _maxStatements)
-            {
-                ExceptionHelper.ThrowStatementsCountOverflowException();
-            }
-
-            if (_timeoutTicks > 0 && _timeoutTicks < DateTime.UtcNow.Ticks)
-            {
-                ExceptionHelper.ThrowTimeoutException();
-            }
-
-            if (_memoryLimit > 0)
+            // Avoid allocating the enumerator because we run this loop very often.
+            for (var i = 0; i < _constraints.Count; i++)
             {
-                if (GetAllocatedBytesForCurrentThread != null)
-                {
-                    CurrentMemoryUsage = GetAllocatedBytesForCurrentThread() - _initialMemoryUsage;
-                    if (CurrentMemoryUsage > _memoryLimit)
-                    {
-                        ExceptionHelper.ThrowMemoryLimitExceededException($"Script has allocated {CurrentMemoryUsage} but is limited to {_memoryLimit}");
-                    }
-                }
-                else
-                {
-                    ExceptionHelper.ThrowPlatformNotSupportedException("The current platform doesn't support MemoryLimit.");
-                }
+                _constraints[i].Check();
             }
 
             if (_isDebugMode)

+ 9 - 0
Jint/IConstraint.cs

@@ -0,0 +1,9 @@
+namespace Jint
+{
+    public interface IConstraint
+    {
+        void Reset();
+
+        void Check();
+    }
+}

+ 14 - 21
Jint/Options.cs

@@ -3,6 +3,8 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Reflection;
+using System.Threading;
+using Jint.Constraints;
 using Jint.Native;
 using Jint.Native.Object;
 using Jint.Runtime.Interop;
@@ -11,6 +13,7 @@ namespace Jint
 {
     public sealed class Options
     {
+        private readonly List<IConstraint> _constraints = new List<IConstraint>();
         private bool _discardGlobal;
         private bool _strict;
         private bool _allowDebuggerStatement;
@@ -18,11 +21,8 @@ namespace Jint
         private bool _allowClrWrite = true;
         private readonly List<IObjectConverter> _objectConverters = new List<IObjectConverter>();
         private Func<object, ObjectInstance> _wrapObjectHandler;
-        private int _maxStatements;
-        private long _memoryLimit;
         private int _maxRecursionDepth = -1;
-        private TimeSpan _timeoutInterval;
-        private TimeSpan? _regexTimeoutInterval;
+        private TimeSpan _regexTimeoutInterval = TimeSpan.FromSeconds(10);
         private CultureInfo _culture = CultureInfo.CurrentCulture;
         private TimeZoneInfo _localTimeZone = TimeZoneInfo.Local;
         private List<Assembly> _lookupAssemblies = new List<Assembly>();
@@ -129,20 +129,18 @@ namespace Jint
             return this;
         }
 
-        public Options MaxStatements(int maxStatements = 0)
+        public Options Constraint(IConstraint constraint)
         {
-            _maxStatements = maxStatements;
-            return this;
-        }
-        public Options LimitMemory(long memoryLimit)
-        {
-            _memoryLimit = memoryLimit;
+            if (constraint != null)
+            {
+                _constraints.Add(constraint);
+            }
             return this;
         }
 
-        public Options TimeoutInterval(TimeSpan timeoutInterval)
+        public Options WithoutConstraint(Predicate<IConstraint> predicate)
         {
-            _timeoutInterval = timeoutInterval;
+            _constraints.RemoveAll(predicate);
             return this;
         }
 
@@ -203,23 +201,18 @@ namespace Jint
 
         internal List<IObjectConverter> _ObjectConverters => _objectConverters;
 
-        internal Func<object, ObjectInstance> _WrapObjectHandler => _wrapObjectHandler;
-
-        internal long _MemoryLimit => _memoryLimit;
+        internal List<IConstraint> _Constraints => _constraints;
 
-        internal int _MaxStatements => _maxStatements;
+        internal Func<object, ObjectInstance> _WrapObjectHandler => _wrapObjectHandler;
 
         internal int MaxRecursionDepth => _maxRecursionDepth;
 
-        internal TimeSpan _TimeoutInterval => _timeoutInterval;
-
-        internal TimeSpan _RegexTimeoutInterval => _regexTimeoutInterval ?? _timeoutInterval;
+        internal TimeSpan _RegexTimeoutInterval => _regexTimeoutInterval;
 
         internal CultureInfo _Culture => _culture;
 
         internal TimeZoneInfo _LocalTimeZone => _localTimeZone;
 
         internal IReferenceResolver  ReferenceResolver => _referenceResolver;
-
     }
 }

+ 1 - 4
Jint/Runtime/Interpreter/JintStatementList.cs

@@ -52,10 +52,7 @@ namespace Jint.Runtime.Interpreter
             if (_statement != null)
             {
                 _engine._lastSyntaxNode = _statement;
-                if (_engine._runBeforeStatementChecks)
-                {
-                    _engine.RunBeforeExecuteStatementChecks(_statement);
-                }
+                _engine.RunBeforeExecuteStatementChecks(_statement);
             }
 
             JintStatement s = null;

+ 1 - 5
Jint/Runtime/Interpreter/Statements/JintStatement.cs

@@ -33,11 +33,7 @@ namespace Jint.Runtime.Interpreter.Statements
         public Completion Execute()
         {
             _engine._lastSyntaxNode = _statement;
-
-            if (_engine._runBeforeStatementChecks)
-            {
-                _engine.RunBeforeExecuteStatementChecks(_statement);
-            }
+            _engine.RunBeforeExecuteStatementChecks(_statement);
 
             if (!_initialized)
             {

+ 87 - 0
README.md

@@ -142,6 +142,93 @@ This example is using French as the default culture.
     engine.Execute("new Number(1.23).toLocaleString()"); // 1,23
 ```
 
+## Constraints 
+
+Constraints are used during script execution to ensure that requirements around resource consumption are met, for example:
+
+* Scripts should not use more than X memory.
+* Scripts should only run for a maximum amount of time.
+
+You can configure them via the options:
+
+```c#
+var engine = new Engine(options => {
+
+    // Limit memory allocations to MB
+    options.LimitMemory(4_000_000);
+
+    // Set a timeout to 4 seconds.
+    options.TimeoutInterval(TimeSpan.FromSeconds(4));
+
+    // Set limit of 1000 executed statements.
+    options.MaxStatements(1000);
+
+    // Use a cancellation token.
+    options.CancellationToken(cancellationToken);
+}
+```
+
+You can also write a custom constraint by implementing the `IConstraint` interface:
+
+```c#
+public interface IConstraint
+{
+    /// Called before a script is executed and useful when you us an engine object for multiple executions.
+    void Reset();
+
+    // Called before each statement to check if your requirements are met.
+    void Check();
+}
+```
+
+For example we can write a constraint that stops scripts when the CPU usage gets too high:
+
+```c#
+class MyCPUConstraint : IConstraint
+{
+    public void Reset()
+    {
+    }
+
+    public void Check()
+    {
+        var cpuUsage = GetCPUUsage();
+
+        if (cpuUsage > 0.8) // 80%
+        {
+            throw new OperationCancelledException();
+        }
+    }
+}
+
+var engine = new Engine(options =>
+{
+    options.Constraint(new MyCPUConstraint());
+});
+```
+
+When you reuse the engine you want to use cancellation tokens you have to reset the token before each call of `Execute`:
+
+```c#
+var constraint = new CancellationConstraint();
+
+var engine = new Engine(options =>
+{
+    options.Constraint(constraint);
+});
+
+for (var i = 0; i < 10; i++) 
+{
+    using (var tcs = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
+    {
+        constraint.Reset(tcs.Token);
+
+        engine.SetValue("a", 1);
+        engine.Execute("a++");
+    }
+}
+```
+
 ## Implemented features:
 
 ### ECMAScript 5.1