Lua / Java / LuaJ - 处理或中断无限循环和线程。

我正在使用 LuaJ 在 Java 中运行用户创建的 Lua 脚本。但是,运行一个永远不返回的 Lua 脚本会导致 Java 线程冻结。这也使线程不可中断。我使用以下命令运行 Lua 脚本:

JsePlatform.standardGlobals().loadFile("badscript.lua").call();

badscript.lua 包含 while true do end

我希望能够自动终止那些陷入无法挽回的循环的脚本,并在用户运行它们时允许手动终止他们的 Lua 脚本。我阅读了有关 debug.sethookpcall 的信息,但我不确定如何正确地将它们用于我的目的。我也听说沙盒是更好的选择,但这有点超出了我的能力范围。

这个问题也可能扩展到 Java 线程。我没有找到任何关于中断 while (true); 中的 Java 线程的明确信息。

在线的 Lua demo 是非常有前途的,但似乎“坏”脚本的检测和终止是在 CGI 脚本中完成的,而不是 Lua。我能否使用 Java 调用一个 CGI 脚本,然后再调用 Lua 脚本?我不确定这会允许用户手动终止他们的脚本。我丢失了 Lua demo 源代码的链接,但我手头有它。这是魔法般的一行:

tee -a $LOG | (ulimit -t 1 ; $LUA demo.lua 2>&1 | head -c 8k)

有人能指引我正确的方向吗?

一些来源:

点赞
用户2555167
用户2555167

我以前从未使用过 Luaj,但是你能将你的一行代码

JsePlatform.standardGlobals().loadFile("badscript.lua").call();

放进一个新的线程中,然后你可以从主线程中终止它吗?

这将需要你创建某种监督线程(类),并将任何启动的脚本传递给它进行监督,最终在它们自己不关闭时终止它们。

2013-07-05 22:28:35
用户1553860
用户1553860

有些问题,但这对回答你的问题有一定的帮助。

以下的概念证明演示了基本的沙箱和限流任意用户代码的功能。它运行了大约250条错误的“用户输入”指令,然后丢弃协程。你可以使用像这个答案中的机制查询Java并在钩子函数中有条件地引出产生作用,而不是每次都进行引出。

SandboxTest.java:

public static void main(String[] args) {
    Globals globals = JsePlatform.debugGlobals();

    LuaValue chunk = globals.loadfile("res/test.lua");

    chunk.call();
}

res/test.lua:

function sandbox(fn)
    -- 读取脚本并设置环境变量
    f = loadfile(fn, "t")
    debug.setupvalue(f, 1, {print = print})

    -- 创建一个协程,并让它每50个指令引出产生作用
    local co = coroutine.create(f)
    debug.sethook(co, coroutine.yield, "", 50)

    -- 演示分步执行,5个“tick”
    for i = 1, 5 do
        print("tick")
        coroutine.resume(co)
    end
end

sandbox("res/badfile.lua")

res/badfile.lua:

while 1 do
    print("", "badfile")
end

不幸的是,尽管控制流按预期工作,但废弃协程应该被垃圾收集的方式并没有起作用。Java中相应的LuaThread将永远挂在一个等待循环中,保持进程的存活。详见这里:

如何放弃一个LuaJ协同程序LuaThread?

2014-07-05 10:36:00
用户1478302
用户1478302

我曾经也遇到了同样的问题,在阅读过 debug 库的实现后,我创建了一个类似于 David Lewis 所提出方案的解决方案,但这个方案中我提供了自己的 DebugLibrary

package org.luaj.vm2.lib;

import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;

public class CustomDebugLib extends DebugLib {
    public boolean interrupted = false;

    @Override
    public void onInstruction(int pc, Varargs v, int top) {
        if (interrupted) {
            throw new ScriptInterruptException();
        }
        super.onInstruction(pc, v, top);
    }

    public static class ScriptInterruptException extends RuntimeException {}
}

只需从一个新线程中执行你的脚本,并将 interrupted 设置为 true 就可以停止脚本运行。当抛出异常时,它将被封装为 LuaError 的原因。

2015-04-03 19:30:04
用户4525236
用户4525236

感谢 @Seldon 建议使用自定义 DebugLib。我通过仅在每个指令之前检查预定义的时间量来实现了一个简化版本。当然,这不是非常精确,因为在类创建和脚本执行之间存在一些时间。不需要单独的线程。

class DebugLibWithTimeout(
    timeout: Duration,
) : DebugLib() {

    private val timeoutOn = Instant.now() + timeout

    override fun onInstruction(pc: Int, v: Varargs, top: Int) {
        val timeoutElapsed = Instant.now() > timeoutOn
        if (timeoutElapsed)
            throw Exception("Timeout")
        super.onInstruction(pc, v, top)
    }
}

重要提示:如果你使用 load 函数在 Lua 代码中调用未受信任的脚本,并向其传递一个独立的环境,则此方法无法使用。onInstruction() 似乎只会在函数环境引用 _G 时被调用。我通过剥离 _G 中的一切,然后将白名单项目添加回去来解决了这个问题。

-- 白名单项目
local sandbox_globals = {
    print = print
}

local original_globals = {}
for key, value in pairs(_G) do
    original_globals[key] = value
end

local sandbox_env = _G
-- 从 _G 中去除一切
for key, _ in pairs(sandbox_env) do
    sandbox_env[key] = nil
end

-- 将白名单项目添加回去。
-- 现在无法使用全局 pairs 函数。
for key, value in original_globals.pairs(sandbox_globals) do
    sandbox_env[key] = value
end

local function run_user_script(script)
    local script_function, message = original_globals.load(script, nil, 't', sandbox_env)
    if not script_function then
        return false, message
    end

    return pcall(script_function)
end
2023-01-10 10:59:28