Lua/Java/LuaJ — обработка или прерывание бесконечных циклов и потоков

Я использую LuaJ для запуска пользовательских сценариев Lua на Java. Однако выполнение скрипта Lua, который никогда не возвращается, приводит к зависанию потока Java. Это также делает поток непрерывным. Я запускаю сценарий Lua с помощью:

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

badscript.lua содержит while true do end.

Я хотел бы иметь возможность автоматически завершать сценарии, которые застряли в непрекращающихся циклах, а также позволять пользователям вручную завершать свои сценарии Lua во время их работы. Я читал об debug.sethook и pcall, хотя я не уверен, как правильно использовать их для своих целей. Я также слышал, что песочница — лучшая альтернатива, хотя это немного не в моих силах.

Этот вопрос также может быть распространен только на потоки Java. Я не нашел какой-либо точной информации о прерывании потоков Java, застрявших в файле while (true);.

Онлайновая демонстрация Lua была очень многообещающей, но кажется обнаружение и завершение «плохих» сценариев выполняется в сценарии CGI, а не в Lua. Смогу ли я использовать Java для вызова сценария CGI, который, в свою очередь, вызывает сценарий Lua? Однако я не уверен, что это позволит пользователям вручную завершать работу своих скриптов. Я потерял ссылку на исходный код демо-версии Lua, но он у меня есть. Это волшебная строка:

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

Может ли кто-нибудь указать мне в правильном направлении?

Некоторые источники:


person xikkub    schedule 05.07.2013    source источник


Ответы (4)


Я боролся с той же проблемой, и после некоторого изучения реализации библиотеки отладки я создал решение, подобное предложенному Дэвидом Льюисом, но сделал это, предоставив свою собственную 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 при генерировании.

person Seldon    schedule 03.04.2015
comment
Фантастика. Это именно то, что я искал. Спасибо. - person xikkub; 05.04.2015

Есть проблемы, но это имеет большое значение для ответа на ваш вопрос.

Следующее доказательство концепции демонстрирует базовый уровень песочницы и регулирование произвольного пользовательского кода. Он запускает около 250 инструкций плохо обработанного «пользовательского ввода», а затем отбрасывает сопрограмму. Вы можете использовать механизм, подобный тому, что описан в этом ответе, для запроса Java и условного возврата внутри функции ловушки, вместо того, чтобы выдавать каждый время.

ПесочницаTest.java:

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

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

    chunk.call();
}

рез/test.lua:

function sandbox(fn)
    -- read script and set the environment
    f = loadfile(fn, "t")
    debug.setupvalue(f, 1, {print = print})

    -- create a coroutine and have it yield every 50 instructions
    local co = coroutine.create(f)
    debug.sethook(co, coroutine.yield, "", 50)

    -- demonstrate stepped execution, 5 'ticks'
    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

К сожалению, хотя поток управления работает как задумано, что-то в том, как «заброшенная» сопрограмма должна собирать мусор, работает неправильно. Соответствующий LuaThread в Java вечно зависает в цикле ожидания, поддерживая процесс в рабочем состоянии. Подробности здесь:

Как я могу отказаться от сопрограммы LuaJ LuaThread?

person David Lewis    schedule 05.07.2014

Я никогда раньше не пользовался Luaj, но не могли бы вы написать одну строчку

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

В новый собственный поток, который затем можно завершить из основного потока?

Это потребует от вас создания какого-то потока (класса) супервизора и передачи ему любых запущенных сценариев для наблюдения и, в конечном итоге, завершения, если они не завершатся сами по себе.

person user2555167    schedule 05.07.2013
comment
Я не знаю способа прервать LuaValue.call(). Независимо от того, где вы его поместите, любой поток, вызывающий его, станет непрерывным. - person xikkub; 06.07.2013
comment
Итак, я на самом деле закодировал то, что предложил, и, как упоминалось выше, он не отвечает на thread.interrupt(), что совершенно отстой. - person user2555167; 06.07.2013
comment
В примечаниях от Oracle говорится: В некоторых случаях вы можете использовать специальные приемы для приложений. Например, если поток ожидает в известном сокете, вы можете закрыть сокет, чтобы поток немедленно вернулся. К сожалению, на самом деле не существует какой-либо техники, которая работает в целом. Следует отметить, что во всех ситуациях, когда ожидающий поток не отвечает на Thread.interrupt, он не будет отвечать и на Thread.stop. К таким случаям относятся преднамеренные атаки типа «отказ в обслуживании» и операции ввода-вывода, для которых thread.stop и thread.interrupt не работают должным образом. - person user2555167; 06.07.2013

EDIT: я не нашел способа безопасно завершить потоки LuaJ без изменения самого LuaJ. Вот что я придумал, хотя это не работает с LuaJ. Однако его можно легко изменить, чтобы он выполнял свою работу на чистом Lua. Я могу переключиться на привязку Python для Java, поскольку многопоточность LuaJ настолько проблематична.

--- Я придумал следующее, но это не работает с LuaJ ---

Вот возможное решение. Я регистрирую хук с помощью debug.sethook, который срабатывает при событиях "count" (эти события происходят даже в while true do end). Я также передаю созданный мной пользовательский объект Java "ScriptState", который содержит логический флаг, указывающий, должен ли сценарий завершаться или нет. Объект Java запрашивается в хуке Lua, который выдает ошибку закрытия скрипта, если установлен флаг (редактировать: выдача ошибки фактически не завершает скрипт). Флаг завершения также может быть установлен внутри Lua-скрипта.

Если вы хотите автоматически завершать непрекращающиеся бесконечные циклы, достаточно просто реализовать систему таймеров, которая записывает время последнего вызова ScriptState, а затем автоматически завершает скрипт, если проходит достаточное время без вызова API (редактировать: это работает, только если поток можно прервать). Если вы хотите отключить бесконечные циклы, но не прерывать определенные блокирующие операции, вы можете настроить объект ScriptState, включив в него другую информацию о состоянии, которая позволит вам временно приостановить автоматическое завершение и т. д.

Вот мой interpreter.lua, который можно использовать для вызова другого скрипта и прерывания его, если/когда это необходимо. Он вызывает методы Java, поэтому он не будет работать без LuaJ (или какой-либо другой библиотеки Lua-Java), если только он не будет модифицирован (редактировать: опять же, его можно легко изменить для работы в чистом Lua).

function hook_line(e)
    if jthread:getDone() then
        -- I saw someone else use error(), but an infinite loop still seems to evade it.
        -- os.exit() seems to take care of it well.
        os.exit()
    end
end

function inithook()
    -- the hook will run every 100 million instructions.
    -- the time it takes for 100 million instructions to occur
    --   is based on computer speed and the calling environment
    debug.sethook(hook_line, "", 1e8)
    local ret = dofile(jLuaScript)
    debug.sethook()
    return ret
end

args = { ... }
if jthread == nil then
    error("jthread object is nil. Please set it in the Java environment.",2)
elseif jLuaScript == nil then
    error("jLuaScript not set. Please set it in the Java environment.",2)
else
    local x,y = xpcall(inithook, debug.traceback)
end

Вот класс ScriptState, в котором хранится флаг и main() для демонстрации:

public class ScriptState {

    private AtomicBoolean isDone = new AtomicBoolean(true);
    public boolean getDone() { return isDone.get(); }
    public void setDone(boolean v) { isDone.set(v); }

    public static void main(String[] args) {
        Thread t = new Thread() {
            public void run() {
                System.out.println("J: Lua script started.");
                ScriptState s = new ScriptState();
                Globals g = JsePlatform.debugGlobals();
                g.set("jLuaScript", "res/main.lua");
                g.set("jthread", CoerceJavaToLua.coerce(s));
                try {
                    g.loadFile("res/_interpreter.lua").call();
                } catch (Exception e) {
                    System.err.println("There was a Lua error!");
                    e.printStackTrace();
                }
            }
        };
        t.start();
        try { t.join(); } catch (Exception e) { System.err.println("Error waiting for thread"); }
        System.out.println("J: End main");
    }
}

res/main.lua содержит целевой код Lua для запуска. Используйте переменные среды или параметры для передачи дополнительной информации сценарию, как обычно. Не забудьте использовать JsePlatform.debugGlobals() вместо JsePlatform.standardGlobals(), если вы хотите использовать библиотеку debug в Lua.

EDIT: я только что заметил, что os.exit() завершает не только сценарий Lua, но и вызывающий процесс. Кажется, это эквивалент System.exit(). error() выдаст ошибку, но не приведет к завершению Lua-скрипта. Сейчас я пытаюсь найти решение этой проблемы.

person xikkub    schedule 07.07.2013
comment
Это выглядит многообещающе. Вы пытались использовать coroutine.yield()? Не уверен, что это приведет к текущему потоку, если вы не настроите сопрограммы, но я надеюсь. Я попробую ваше решение с выходом позже и сообщу, работает ли оно. - person Sardtok; 16.07.2013
comment
Глядя на код, использование сопрограмм не помогло бы. Когда поток запускается, он просто вечно запускает код в новом потоке, в то время как поток, запустивший его, ждет его завершения. Прерывание начального потока предназначено для уничтожения виртуальной машины из-за ошибки OrphanedThread, но это не сильно отличается от os.exit(). - person Sardtok; 16.07.2013
comment
Да, я заметил. Я не успел его обновить. Я могу отказаться от LuaJ и вместо этого попробовать что-то вроде Jython. - person xikkub; 17.07.2013
comment
Было бы неплохо, если бы LuaJ показал мне свои потоки или предоставил способ их чистого завершения, но я думаю, что это не вариант. - person xikkub; 17.07.2013
comment
Я бы предпочел, чтобы os.exit просто убил виртуальную машину Lua, а не окружающую JVM. Для этого может быть настройка в Globals, так как большинство людей, вероятно, ожидают, что она будет работать, как всегда. Я собираюсь опубликовать запрос функции на SourceForge LuaJ. - person Sardtok; 22.07.2013
comment
Я взял на себя смелость создать билет самостоятельно. Спасибо за вашу помощь. Я надеюсь, что это будет рассмотрено. Мне очень нравится LuaJ. sourceforge.net/p/luaj/feature-requests/5 - person xikkub; 24.07.2013