diff --git a/README.rest b/README.rest index 46e5ca2..e336583 100644 --- a/README.rest +++ b/README.rest @@ -131,6 +131,13 @@ or if you don't need the internal parameters:: (js/set! scope "add" (js/wrap-plain-fn add)) (js/eval scope "add(1, 2)") 3.0))) +excecute code with timeout:: + + (def scope (js/new-safe-scope)) + (js/eval-timeout scope "while (true);" 1000) + + ; will throw a Error exception + license? -------- diff --git a/project.clj b/project.clj index 4097e3c..ad24734 100644 --- a/project.clj +++ b/project.clj @@ -4,4 +4,7 @@ :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.4.0"] - [org.mozilla/rhino "1.7R4"]]) + [org.mozilla/rhino "1.7R4"]] + + :source-paths ["src/clj"] + :java-source-paths ["src/java"]) diff --git a/src/clj_rhino.clj b/src/clj/clj_rhino.clj similarity index 95% rename from src/clj_rhino.clj rename to src/clj/clj_rhino.clj index d8ed648..769bf41 100644 --- a/src/clj_rhino.clj +++ b/src/clj/clj_rhino.clj @@ -1,7 +1,8 @@ (ns clj-rhino (:refer-clojure :exclude (eval get get-in set!)) (:import [org.mozilla.javascript Context UniqueTag NativeArray NativeObject - BaseFunction])) + BaseFunction] + [org.marianoguerra.rhino TimedContextFactory])) (defprotocol RhinoConvertible (-to-rhino [object scope ctx] "convert a value to a rhino compatible type")) @@ -135,6 +136,15 @@ (or filename "") (or line-number 1) sec-domain)))) +(defn eval-timeout [scope code timeout-ms & {:keys [filename line-number sec-domain]}] + (let [filename (or filename "") + line-number (or line-number 1) + factory (TimedContextFactory. timeout-ms) + ctx (.enterContext factory)] + (try + (.evaluateString ctx scope code filename line-number sec-domain) + (finally (Context/exit))))) + (defn undefined? [value] "return true if value is undefined" (= value (. UniqueTag NOT_FOUND))) diff --git a/src/java/org/marianoguerra/rhino/TimedContextFactory.java b/src/java/org/marianoguerra/rhino/TimedContextFactory.java new file mode 100644 index 0000000..467fa1f --- /dev/null +++ b/src/java/org/marianoguerra/rhino/TimedContextFactory.java @@ -0,0 +1,61 @@ +package org.marianoguerra.rhino; + +import org.mozilla.javascript.*; + +public class TimedContextFactory extends ContextFactory { + // Custom Context to store execution time. + private static class TimedContext extends Context { + long startTime; + + public TimedContext(ContextFactory factory) { + super(factory); + } + } + + private long timeoutMillis; + private int ticksInterval; + + public TimedContextFactory(long timeoutMillis) { + this(timeoutMillis, 10000); + } + + public TimedContextFactory(long timeoutMillis, int ticksInterval) { + this.timeoutMillis = timeoutMillis; + this.ticksInterval = ticksInterval; + } + + // Override makeContext() + protected Context makeContext() { + TimedContext cx = new TimedContext(this); + // Make Rhino runtime to call observeInstructionCount + // each ticksInterval bytecode instructions + cx.setInstructionObserverThreshold(this.ticksInterval); + return cx; + } + + // Override observeInstructionCount(Context, int) + protected void observeInstructionCount(Context cx, + int instructionCount) { + TimedContext mcx = (TimedContext)cx; + long currentTime = System.currentTimeMillis(); + + if (currentTime - mcx.startTime > this.timeoutMillis) { + // More then timeoutMillis from Context creation time: + // it is time to stop the script. + // Throw Error instance to ensure that script will never + // get control back through catch or finally. + throw new Error(); + } + } + + // Override doTopCall(Callable, Context, Scriptable, Scriptable, Object[]) + protected Object doTopCall(Callable callable, + Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + + TimedContext mcx = (TimedContext)cx; + mcx.startTime = System.currentTimeMillis(); + + return super.doTopCall(callable, cx, scope, thisObj, args); + } +} diff --git a/test/clj_rhino/core_test.clj b/test/clj_rhino/core_test.clj index cfc9c61..bfd1693 100644 --- a/test/clj_rhino/core_test.clj +++ b/test/clj_rhino/core_test.clj @@ -167,6 +167,10 @@ (js/set! scope "add" (js/wrap-plain-fn add)) (is (= (js/eval scope "add(1, 2)") 3.0)))))) + (testing "eval-timeout times out on infinite loop" + (let [scope (js/new-safe-scope)] + (is (thrown? Error (js/eval-timeout scope "while (true);" 1000))))) + (testing "native clojure functions can be added" (js/with-context (fn [ctx]