Встраивание низкоэффективного скриптового языка в Python - PullRequest
17 голосов
/ 24 февраля 2011

У меня есть веб-приложение. В связи с этим мне необходимо, чтобы пользователи приложения могли писать (или копировать и вставлять) очень простые сценарии для выполнения своих данных.

Сценарии действительно могут быть очень простыми, а производительность - это только самая незначительная проблема. И пример сложности сценария, я имею в виду что-то вроде:

ratio = 1.2345678
minimum = 10

def convert(money)
    return money * ratio
end

if price < minimum
    cost = convert(minimum)
else
    cost = convert(price)
end

где цена и стоимость являются глобальными переменными (что-то, что я могу передать в среду и получить к ней доступ после вычисления).

Однако мне нужно кое-что гарантировать.

  1. Запущенные сценарии не могут получить доступ к среде Python. Они не могут импортировать вещи, вызывать методы, которые я им явно не выставляю, читать или записывать файлы, создавать потоки и т. Д. Мне нужна полная блокировка.

  2. Мне нужно иметь возможность жестко ограничить количество циклов, для которых выполняется скрипт. Циклы это общий термин здесь. может быть инструкциями VM, если язык скомпилирован. Применить-вызовы для цикла Eval / Apply. Или просто итерации через некоторый центральный цикл обработки, который запускает скрипт Детали не так важны, как моя способность остановить что-то работающее через некоторое время и отправить электронное письмо владельцу и сказать: «Ваши сценарии, кажется, делают больше, чем просто добавление нескольких чисел - разберитесь с ними».

  3. Он должен работать на незащищенном CPython для Vanilla.

До сих пор я писал свой собственный DSL для этой задачи. Я могу это сделать. Но мне было интересно, смогу ли я опираться на плечи гигантов. Есть ли мини-язык для Python, который бы делал это?

Существует множество хакерских вариантов Lisp (даже один, который я написал на Github), но я бы предпочел что-то с более неспецифическим синтаксисом (скажем, C или Pascal), и, поскольку я рассматриваю это как альтернатива самому кодированию Я хотел бы что-то более зрелое.

Есть идеи?

Ответы [ 8 ]

12 голосов
/ 04 марта 2011

Вот мой взгляд на эту проблему.Требование, чтобы пользовательские сценарии выполнялись внутри vanilla CPython, означает, что вам нужно либо написать интерпретатор для вашего мини-языка, либо скомпилировать его в байт-код Python (или использовать Python в качестве исходного языка), а затем «санировать» байт-код перед его выполнением.

Я взял быстрый пример, основанный на предположении, что пользователи могут писать свои скрипты на Python, и что исходный код и байт-код могут быть в достаточной степени очищены с помощью некоторой комбинации фильтрации небезопасного синтаксиса из дерева разбора и / или удалениянебезопасные коды операций из байт-кода.

Вторая часть решения требует, чтобы байт-код пользовательского сценария периодически прерывался сторожевой задачей, которая гарантирует, что пользовательский сценарий не превышает некоторого предела кода операции, и для всего этогодля запуска на ванильном CPython.

Резюме моей попытки, которая в основном фокусируется на 2-й части проблемы.

  • Пользовательские скрипты написаны на Python.
  • Используйте byteplay для фильтрации и моотличается от байт-кода.
  • Инструментирует байт-код пользователя для вставки счетчика кода операции и вызова функции, контекст которой переключается на задачу наблюдения.
  • Используйте greenlet для выполнения пользователембайт-код, с выходом, переключающимся между сценарием пользователя и сопрограммой сторожевого таймера.
  • сторожевой таймер вводит предварительно установленное ограничение на число кодов операций, которые могут быть выполнены до возникновения ошибки.

Надеемсяэто по крайней мере идет в правильном направлении.Мне интересно узнать больше о вашем решении, когда вы его получите.

Исходный код для lowperf.py:

# std
import ast
import dis
import sys
from pprint import pprint

# vendor
import byteplay
import greenlet

# bytecode snippet to increment our global opcode counter
INCREMENT = [
    (byteplay.LOAD_GLOBAL, '__op_counter'),
    (byteplay.LOAD_CONST, 1),
    (byteplay.INPLACE_ADD, None),
    (byteplay.STORE_GLOBAL, '__op_counter')
    ]

# bytecode snippet to perform a yield to our watchdog tasklet.
YIELD = [
    (byteplay.LOAD_GLOBAL, '__yield'),
    (byteplay.LOAD_GLOBAL, '__op_counter'),
    (byteplay.CALL_FUNCTION, 1),
    (byteplay.POP_TOP, None)
    ]

def instrument(orig):
    """
    Instrument bytecode.  We place a call to our yield function before
    jumps and returns.  You could choose alternate places depending on 
    your use case.
    """
    line_count = 0
    res = []
    for op, arg in orig.code:
        line_count += 1

        # NOTE: you could put an advanced bytecode filter here.

        # whenever a code block is loaded we must instrument it
        if op == byteplay.LOAD_CONST and isinstance(arg, byteplay.Code):
            code = instrument(arg)
            res.append((op, code))
            continue

        # 'setlineno' opcode is a safe place to increment our global 
        # opcode counter.
        if op == byteplay.SetLineno:
            res += INCREMENT
            line_count += 1

        # append the opcode and its argument
        res.append((op, arg))

        # if we're at a jump or return, or we've processed 10 lines of
        # source code, insert a call to our yield function.  you could 
        # choose other places to yield more appropriate for your app.
        if op in (byteplay.JUMP_ABSOLUTE, byteplay.RETURN_VALUE) \
                or line_count > 10:
            res += YIELD
            line_count = 0

    # finally, build and return new code object
    return byteplay.Code(res, orig.freevars, orig.args, orig.varargs,
        orig.varkwargs, orig.newlocals, orig.name, orig.filename,
        orig.firstlineno, orig.docstring)

def transform(path):
    """
    Transform the Python source into a form safe to execute and return
    the bytecode.
    """
    # NOTE: you could call ast.parse(data, path) here to get an
    # abstract syntax tree, then filter that tree down before compiling
    # it into bytecode.  i've skipped that step as it is pretty verbose.
    data = open(path, 'rb').read()
    suite = compile(data, path, 'exec')
    orig = byteplay.Code.from_code(suite)
    return instrument(orig)

def execute(path, limit = 40):
    """
    This transforms the user's source code into bytecode, instrumenting
    it, then kicks off the watchdog and user script tasklets.
    """
    code = transform(path)
    target = greenlet.greenlet(run_task)

    def watcher_task(op_count):
        """
        Task which is yielded to by the user script, making sure it doesn't
        use too many resources.
        """
        while 1:
            if op_count > limit:
                raise RuntimeError("script used too many resources")
            op_count = target.switch()

    watcher = greenlet.greenlet(watcher_task)
    target.switch(code, watcher.switch)

def run_task(code, yield_func):
    "This is the greenlet task which runs our user's script."
    globals_ = {'__yield': yield_func, '__op_counter': 0}
    eval(code.to_code(), globals_, globals_)

execute(sys.argv[1])

Вот пример пользовательского сценария user.py:

def otherfunc(b):
    return b * 7

def myfunc(a):
    for i in range(0, 20):
        print i, otherfunc(i + a + 3)

myfunc(2)

Вот примерный прогон:

% python lowperf.py user.py

0 35
1 42
2 49
3 56
4 63
5 70
6 77
7 84
8 91
9 98
10 105
11 112
Traceback (most recent call last):
  File "lowperf.py", line 114, in <module>
    execute(sys.argv[1])
  File "lowperf.py", line 105, in execute
    target.switch(code, watcher.switch)
  File "lowperf.py", line 101, in watcher_task
    raise RuntimeError("script used too many resources")
RuntimeError: script used too many resources
4 голосов
/ 17 ноября 2014

Jispy идеально подходит!

  • Это интерпретатор JavaScript в Python, созданный в основном для встраивания JS в Python.

  • Примечательно, что он обеспечивает проверки и ограничения на рекурсию и циклы. Так же, как необходимо.

  • Он позволяет легко сделать функции Python доступными для кода JavaScript.

  • По умолчанию он не раскрывает файловую систему хоста или любой другой чувствительный элемент.

Полное раскрытие информации:

  • Jispy - мой проект. Я явно склонен к этому.
  • Тем не менее, здесь, это действительно, кажется, идеально подходит.

PS:

  • Этот ответ пишется через 3 года после того, как этот вопрос был задан.
  • Мотивация такого запоздалого ответа проста:
    Учитывая, насколько тесно Jispy ограничивается рассматриваемым вопросом, будущие читатели с аналогичными требованиями должны быть в состоянии извлечь из этого пользу.
2 голосов
/ 24 февраля 2011

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

Я думаю, что самое простое, что вы могли бы сделать, - это написать собственную версию виртуальной машины python на python.

Я часто думал о том, чтобы сделать это в чем-то вроде Cython, чтобы вы могли просто импортировать его как модуль, и вы могли бы использовать существующую среду выполнения для большинства жестких битов.

Выможет уже быть в состоянии генерировать интерпретатор python-in-python с PyPy, но вывод PyPy - это среда выполнения, которая выполняет ВСЕ, включая реализацию эквивалента базовых объектов PyObject для встроенных типов и всего такого, и я думаю, что это излишне для этоготакие вещи.

Все, что вам действительно нужно, это то, что работает как фрейм в стеке выполнения, а затем метод для каждого кода операции.Я не думаю, что вы даже должны реализовать это самостоятельно.Вы могли бы просто написать модуль, который бы выставлял существующие объекты фрейма во время выполнения.

В любом случае, тогда вы просто поддерживаете свой собственный стек объектов фрейма и обрабатываете байт-коды, и вы можете регулировать его с помощью байт-кодов в секунду или любого другого.

2 голосов
/ 24 февраля 2011

Попробуйте Луа. Синтаксис, который вы упомянули, почти идентичен синтаксису Lua. Смотрите Как я могу встроить Lua в Python 3.x?

1 голос
/ 24 февраля 2011

Я использовал Python в качестве «языка мини-конфигурации» для более раннего проекта. Мой подход состоял в том, чтобы взять код, проанализировать его с помощью модуля parser, а затем пройти AST сгенерированного кода и запустить «не разрешенные» операции (например, определение классов, называемых __ методами и т. Д.).

После того, как я это сделал, была создана синтетическая среда, в которой были только те модули и переменные, которые были «разрешены», и оценивали код внутри, чтобы получить то, что я мог запустить.

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

Что касается ограничения по времени, вы можете запустить вашу программу в отдельном потоке или процессе и завершить ее через фиксированный промежуток времени.

1 голос
/ 24 февраля 2011

Взгляните на LimPy .Он расшифровывается как Limited Python и был создан именно для этой цели.

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

1 голос
/ 24 февраля 2011

Почему бы не код python в pysandbox http://pypi.python.org/pypi/pysandbox/1.0.3?

0 голосов
/ 24 февраля 2011

Самый простой способ создать настоящий DSL - это ANTLR, он имеет шаблоны синтаксиса для некоторых популярных языков.

...