【问题标题】:Interrupt java thread running nashorn script中断运行 nashorn 脚本的 java 线程
【发布时间】:2014-09-11 08:57:16
【问题描述】:

在下面的代码中,我有 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 应该真正检查线程的中断状态,应该由Future.cancel(true) 调用设置,但它似乎没有这样做。您可以将此作为错误提交。
  • 是的,我会报告的...

标签: java javascript multithreading nashorn


【解决方案1】:

我有一个类似的问题,我让用户编写自己的脚本。 但在我允许执行脚本之前,我会解析脚本。 如果我发现以下任何一项 (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 字符串中;

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

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

userJSCode = safeWhile + userJSCode;

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

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

//第四步:执行自定义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 次,所以无论你设置什么限制。

【讨论】:

  • 不错的方法,你能发布一个工作示例吗?您是否使用某种检测工具来注入代码?
  • 我已经编辑了上面的代码作为示例,您需要涵盖所有其他循环等,但它是我们使用的“while”循环场景。
  • 这很有趣。这样我们甚至可以监听 java 中断。 String userJs = "while (true) { }"; String injectSsafeWHile = "while(!java.lang.Thread.interrupted() && "; userJs = userJs.replaceAll("while \(", injectSsafeWHile);
  • 这是一个糟糕的“建议”。尝试解析源代码并确定停机问题是愚蠢的。
【解决方案2】:

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

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

【讨论】:

  • 不错的文章,是否可以包含一个完整的工作示例? :)
  • 我把它放在我的清单上,以便将一个工作示例转储到某个地方,我会把它提高优先级 =)
【解决方案3】:

不幸的是,它不适用于简单的无限循环: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() 结束它,那么它确实有效。

所以这个故事的寓意是没有万无一失的方法来结束 Nashorn 中失控的脚本,但在大多数情况下都有一些方法。

我的实现发出Thread.interrupt(),然后礼貌地等待2 秒让线程终止,如果失败则发出Thread.stop() 两次。如果这不起作用,那么其他任何事情都不会。

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

【讨论】:

    【解决方案4】:

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

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

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

    Java Thread Primitive Deprecation

    另请参阅 Goetz,Java 并发实践,第 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() 方法中。这可以防止实例从构造函数中泄露给其他线程。在原始示例代码中没有明显的危险——事实上,几乎从来没有——但避免从构造函数中泄漏实例总是一个好习惯。

    【讨论】:

    • 感谢您详尽的回复。这一切对我来说都很有意义,我学到了一些新东西。现在,脚本线程的整个设计应该取决于您、用户或其他人可能没有足够的洞察力来优雅地检查中断标志 - 因此需要强制它关闭而不管损坏到一些数据结构——如果已经/允许/暴露给脚本。这就是为什么我使用空白的 while(true){} 来模拟一个在挂起时需要终止的阻塞操作。
    • 像这样设计虚拟机(在虚拟机中)是愚蠢的。有一种非常简单的方法可以终止自定义 VM 而不会丢失数据
    猜你喜欢
    • 2014-06-08
    • 1970-01-01
    • 2010-11-18
    • 2016-07-04
    • 1970-01-01
    • 1970-01-01
    • 2018-09-14
    • 2015-03-28
    • 1970-01-01
    相关资源
    最近更新 更多