中断运行javascript脚本的java线程

Interrupt java thread running nashorn script

本文关键字:java 线程 脚本 运行 javascript 中断      更新时间:2023-09-26

在下面的代码中,我有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");
    }
}

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

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

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

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

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

Java线程原语废弃

参见Goetz, Java并发实践,第七章,取消和关闭

检查中断最简单的方法是调用Thread.interrupted()。您可以很容易地从JavaScript调用它。下面是重写的示例程序,它在5秒后取消正在运行的脚本:

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()方法中。这可以防止实例从构造函数内部泄露给其他线程。在最初的示例代码中,这样做没有明显的危险——事实上,几乎从来没有——但是避免从构造函数中泄漏实例始终是一种良好的做法。

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

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

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

javascript捕获java.lang.ThreadDeath异常并继续运行。我发现上面的样本是不可能中断的,即使有几个Thread.stop()相继发布。我为什么要用几个呢?希望它们中的一个能在异常处理代码中捕获线程并中止它。如果catch块中有像var i = "" + e;这样简单的东西要处理,足以导致第二个Thread.stop()结束它,那么它确实有效。

所以这个故事的寓意是,没有万无一失的方法来结束《纳什orn》中失控的剧本,但有一些方法可以在大多数情况下起作用。

我的实现发出Thread.interrupt(),然后礼貌地等待2秒线程终止,如果失败,那么它发出Thread.stop()两次。

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

我有一个类似的问题,我让用户编写自己的脚本。但是在允许执行脚本之前,我先对脚本进行解析。如果我发现以下任何一种情况(System.sleep。退出线程。睡觉,睡觉)等我甚至没有启动脚本,我给用户一个错误。

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

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

这样在每次循环迭代之前,我的方法都会被调用。我可以计算我被调用了多少次,或者检查内部计时器。然后我可以在checkForLoop();

中停止循环或计时器

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

有上百种安全机制可以应用于用户提交的代码,没有规则说你需要按原样运行他们的代码。

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

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

步骤2//在代码的开头注入代码

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

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

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

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

nashhorn.execute(injectSsafeWHile);

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

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次,所以无论您将限制设置为什么。