在下面的代码中,我有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();
}
}
由于我们正在启动一个线程池,不妨将其设置为一个调度线程池,以便我们可以将其用于脚本任务和超时。这样我们就可以避免Timer
和TimerTask
,它们基本上都被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次,所以无论您将限制设置为什么。