中断运行nashorn脚本的java线程

在下面的代码中,我有一个javascript运行在与主要的一个单独的线程中。 该脚本是一个无限循环,所以它需要以某种方式终止。 怎么样?

脚本开始运行后,调用.cancel()不起作用。 但是如果我在线程初始化之后调用.cancel(),它将终止它(注释掉的行)。

package testscriptterminate; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.Timer; import java.util.TimerTask; public class TestScriptTerminate extends TimerTask{ private ExecutorService threads; private Future runScript; private Timer t; public TestScriptTerminate(){ t = new Timer(); t.schedule(this, 6000); //let the script run for a while before attempt to cancel threads = Executors.newFixedThreadPool(1); runScript = threads.submit(new ScriptExec()); //runScript.cancel(true); //will cancel here, before the script had a change to run, but useless, i want to cancel at any time on demand } @Override public void run(){ //after script has fully initialized and ran for a while - attempt to cancel. //DOESN'T WORK, thread still active System.out.println("Canceling now..."); runScript.cancel(true); } public static void main(String[] args) { new TestScriptTerminate(); } } class ScriptExec implements Runnable{ private ScriptEngine js; private ScriptEngineManager scriptManager; public ScriptExec(){ init(); } @Override public void run() { try { js.eval("while(true){}"); } catch (ScriptException ex) { System.out.println(ex.toString()); } } private void init(){ scriptManager = new ScriptEngineManager(); js = scriptManager.getEngineByName("nashorn"); } } 

与Java一样,JavaScript(在Nashorn下)不会在紧密循环中响应中断。 脚本需要轮询中断并自动终止循环,或者它可以调用检查中断的内容并让InterruptedException传播。

你可能会认为Nashorn“只是在运行一个脚本”并且应该立即中断。 这不适用,原因与Java中不适用的原因相同:异步中断可能会损害应用程序数据结构的损坏,并且基本上没有办法避免它或从中恢复。

异步中断带来了与长期弃用的Thread.stop方法相同的问题。 本文档对此进行了解释,该文档是评论中链接的文档的更新版本。

Java Thread Primitive Deprecation

另请参阅Goetz, Java Concurrency In Practice ,第7章, 取消和关闭

检查中断的最简单方法是调用Thread.interrupted() 。 您可以通过JavaScript轻松调用它。 这是重写示例程序,在五秒后取消正在运行的脚本:

 public class TestScriptTerminate { ScheduledExecutorService pool = Executors.newScheduledThreadPool(2); void script() { ScriptEngineManager scriptManager = new ScriptEngineManager(); ScriptEngine js = scriptManager.getEngineByName("nashorn"); try { System.out.println("Script starting."); js.eval("while (true) { if (java.lang.Thread.interrupted()) break; }"); System.out.println("Script finished."); } catch (ScriptException ex) { ex.printStackTrace(); } } void init() throws Exception { Future scriptTask = pool.submit(this::script); pool.schedule(() -> { System.out.println("Canceling now..."); scriptTask.cancel(true); }, 5, TimeUnit.SECONDS); pool.shutdown(); } public static void main(String[] args) throws Exception { new TestScriptTerminate().init(); } } 

由于我们正在启动一个线程池,所以不妨使它成为一个预定的线程池,以便我们可以将它用于脚本任务和超时。 这样我们就可以避免TimerTimerTask ,它们大多数被ScheduledExecutorService取代。

处理和中断时的通常惯例是恢复中断位或让InterruptedException传播。 ( 永远不要忽略一个中断。)由于断开循环可以认为已经完成了中断的处理,所以两者都没有必要,只是让脚本正常退出似乎就足够了。

这种重写还将构造函数中的大量工作转移到init()方法中。 这可以防止实例从构造函数中泄漏到其他线程。 在原始的示例代码中没有明显的危险 – 实际上,几乎从来没有 – 但是避免从构造函数中泄漏实例总是好的做法。

所以这是旧的,但我刚刚写了这篇文章并认为分享它可能很有价值。 默认情况下,没有什么可以阻止Nashorn脚本执行, .cancel() Thread.stop() Thread.interrupt()什么也不做,但是如果你愿意付出一些努力并且可以重写一些字节码,它是可以实现的。 细节:

http://blog.getsandbox.com/2018/01/15/nashorn-interupt/

不幸的是,它不适用于简单的无限循环: while (true) { } 。 我试过Thread.cancel(); 不会导致线程退出。 我想在IntelliJ插件中运行脚本是万无一失的,用户可以犯错误导致无限循环,挂起插件。

我发现在大多数情况下唯一Thread.stop()Thread.stop() 。 即使这对于这样的脚本也不起作用:

 while(true) { try { java.lang.Thread.sleep(100); } catch (e) { } } 

javascript捕获java.lang.ThreadDeathexception并继续运行。 我发现即使一个接一个地发出几个Thread.stop() ,上面的例子也不可能中断。 我为什么要用几个? 希望其中一个将在其exception处理代码中捕获该线程并中止它。 如果catch块中有某些东西像var i = "" + e;那样简单,那么哪个有效var i = "" + e; 这足以导致第二个Thread.stop()结束它。

所以故事的寓意是在Nashorn结束一个失控的剧本并没有失败的安全方法,但是在大多数情况下都有一些可行的东西。

我的实现发出一个Thread.interrupt() ,然后礼貌地等待2秒钟让线程终止,如果失败那么它会发出两次Thread.stop() 。 如果这不起作用,那么也没有别的。

希望它能帮助某人消除数小时的实验,找到一种更可靠的方法来阻止nashorn失控脚本,而不是希望运行脚本的合作能够尊重Thread.cancel()

我有一个类似的问题,我让用户编写自己的脚本。 但在我允许执行脚本之前,我解析脚本。 如果我发现以下任何一个(System.sleep.Exit,Thread.sleep,goto)等我甚至没有启动脚本,我给用户一个错误。

然后我搜索所有(for,loops,while,doWhile),然后我注入一个方法。
checkForLoop()紧跟在循环标识符之后。 我注入了checkForLoop(); 进入允许用户提交的脚本。

  while(users code) { } becomes while ( checkForLoop() && users code ) { } 

这种方式在循环的每次迭代之前,我的方法被调用。 我可以算一下我被叫多少次或检查内部计时器。 我可以从checkForLoop()内部停止循环或计时器;

老实说,无论如何,我认为这是一个很大的安全问题,只是盲目地让用户编写脚本并执行它。 您需要构建一个将代码注入其代码循环的系统。 哪个不那么难。

您可以将100多种安全机制应用于用户提交的代码,没有规则表明您需要按原样运行其代码。

我编辑了这个答案,包括一个非常简单的例子。

//步骤1将用户提交的JS代码放入名为userJSCode的Java String中;

第2步//在代码开头注入代码。

 String safeWhile ="var sCount=0; var sMax=10; function safeWhileCheck(){ sCount++; if ( return ( sCount > sMax )}"; userJSCode = safeWhile + userJSCode; 

//第3步:在代码中注入自定义

 String injectSsafeWHile = "while( safeWhileCheck() && "; userJSCode = userJSCode.replace("while(", injectSsafeWHile); 

//第4步:执行自定义JS代码

 nashhorn.execute(injectSsafeWHile); 

//这里是用户提交的错误代码,请注意循环中没有增量,它会永远持续下去。

 var i=0; while ( i <1000 ) console.log("I am number " + i); 

使用上面的步骤,我们最终得到了

 var sCount=0;var sMax=10; function safeWhileCheck(){ sCount++; return ( sCount > sMax )}; var i=0; while ( safeWhileCheck() && i <1000 ) console.log("I am number " + i)" 

这里while循环最多只执行10次,所以无论你设置限制为什么。