traduccion - Incrustar un lenguaje de scripting de bajo rendimiento en Python
scripting python (8)
Jispy es el ajuste perfecto!
Es un intérprete de JavaScript en Python, creado principalmente para incrustar JS en Python.
En particular, proporciona controles y límites en la recursión y el bucle. Justo como es necesario.
Fácilmente te permite hacer que las funciones de Python estén disponibles para el código JavaScript.
De forma predeterminada, no expone el sistema de archivos del host ni ningún otro elemento sensible.
La divulgación completa:
- Jispy es mi proyecto. Obviamente estoy sesgado hacia ello.
- Sin embargo, aquí, realmente parece ser el ajuste perfecto.
PD:
- Esta respuesta se está escribiendo ~ 3 años después de esta pregunta.
- La motivación detrás de una respuesta tan tardía es simple:
Dado lo estrechamente que Jispy se limita a la pregunta en cuestión, los futuros lectores con requisitos similares deberían poder beneficiarse de ella.
Tengo una aplicación web. Como parte de esto, necesito que los usuarios de la aplicación puedan escribir (o copiar y pegar) scripts muy simples para ejecutarlos contra sus datos.
Los scripts realmente pueden ser muy simples, y el rendimiento es solo el problema más pequeño. Y un ejemplo de la sofisticación del guión que quiero decir es algo como:
ratio = 1.2345678
minimum = 10
def convert(money)
return money * ratio
end
if price < minimum
cost = convert(minimum)
else
cost = convert(price)
end
donde el precio y el costo son variables globales (algo que puedo incluir en el entorno y acceso después del cálculo).
Yo, sin embargo, necesito garantizar algunas cosas.
Cualquier secuencia de comandos ejecutada no puede obtener acceso al entorno de Python. No pueden importar cosas, métodos de llamada que no expongo explícitamente para ellos, leer o escribir archivos, generar hilos, etc. Necesito un bloqueo total.
Necesito poder poner un límite estricto en la cantidad de "ciclos" para los que se ejecuta un script. Ciclos es un término general aquí. podrían ser instrucciones de VM si el lenguaje está compilado por bytes. Aplicar-llamadas para un bucle Eval / Aplicar. O simplemente iteraciones a través de un bucle de procesamiento central que ejecuta el script. Los detalles no son tan importantes como mi capacidad para evitar que algo se ejecute después de un breve período de tiempo, enviar un correo electrónico al propietario y decirle que "sus scripts parecen estar haciendo más que sumar algunos números juntos, ordénelos".
Debe ejecutarse en Vainilla sin parche CPython.
Hasta ahora he estado escribiendo mi propio DSL para esta tarea. Yo puedo hacer eso. Pero me preguntaba si podría construir sobre los hombros de gigantes. ¿Hay un mini-idioma disponible para Python que haría esto?
Hay un montón de variantes Lisp hacky (incluso una que escribí en Github), pero preferiría algo con más sintaxis no especializada (más C o Pascal, por ejemplo), y como lo estoy considerando como una alternativa a la codificación Uno mismo me gustaría algo un poco más maduro.
¿Algunas ideas?
¿Por qué no el código de Python en pysandbox http://pypi.python.org/pypi/pysandbox/1.0.3 ?
Aquí está mi opinión sobre este problema. Requerir que los scripts de usuario se ejecuten dentro de vanilla CPython significa que debe escribir un intérprete para su mini idioma o compilarlo en el código de bytes de Python (o usar Python como su idioma de origen) y luego "sanear" el código de bytes antes de ejecutarlo.
He optado por un ejemplo rápido basado en el supuesto de que los usuarios pueden escribir sus scripts en Python, y que la fuente y el bytecode se pueden sanear suficientemente mediante una combinación de filtrado de sintaxis no segura del árbol de análisis y / o eliminación de códigos de operación no seguros de bytecode
La segunda parte de la solución requiere que el bytecode del script del usuario sea interrumpido periódicamente por una tarea de vigilancia que garantice que el script del usuario no exceda el límite de un código de operación, y que todo esto se ejecute en el CPython de vainilla.
Resumen de mi intento, que se centra principalmente en la segunda parte del problema.
- Los scripts de usuario están escritos en Python.
- Use byteplay para filtrar y modificar el bytecode.
- Instale el bytecode del usuario para insertar un contador de código de operación y llame a una función cuyo contexto cambia a la tarea de vigilancia.
- Use greenlet para ejecutar el bytecode del usuario, con el cambio de los rendimientos entre el script del usuario y el perro guardián.
- El watchdog impone un límite preestablecido en el número de códigos de operación que pueden ejecutarse antes de generar un error.
Esperemos que esto al menos vaya en la dirección correcta. Me interesa saber más acerca de su solución cuando llegue a ella.
Código fuente para 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])
Aquí hay un ejemplo de script de usuario 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)
Aquí hay una muestra de ejecución:
% 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
Echa un vistazo a LimPy . Significa Limited Python y fue construido exactamente para este propósito.
Había un entorno donde los usuarios necesitaban escribir lógica básica para controlar una experiencia de usuario. No sé cómo interactuará con los límites del tiempo de ejecución, pero imagino que puedes hacerlo si estás dispuesto a escribir un pequeño código.
He usado Python como un "mini idioma de configuración" para un proyecto anterior. Mi enfoque fue tomar el código, analizarlo usando el módulo parser
y luego recorrer el AST del código generado y eliminar las operaciones "no permitidas" (por ejemplo, definir clases, llamadas __
métodos, etc.).
Después de hacer esto, creé un entorno sintético con solo los módulos y las variables que estaban "permitidos" y evalué el código para obtener algo que pudiera ejecutar.
Funcionó muy bien para mí. No sé si es a prueba de balas, especialmente si quieres dar a tus usuarios más poder que yo para un lenguaje de configuración.
En cuanto al límite de tiempo, puede ejecutar su programa en un hilo o proceso separado y terminarlo después de un período de tiempo fijo.
La forma más sencilla de crear un DSL real es ANTLR, tiene plantillas de sintaxis para algunos idiomas populares.
Prueba Lua. La sintaxis que mencionaste es casi idéntica a la de Lua. Ver ¿Cómo podría incrustar Lua en Python 3.x?
Todavía no conozco nada que realmente resuelva este problema.
Creo que lo más simple que podrías hacer sería escribir tu propia versión de la máquina virtual de python en python.
A menudo he pensado en hacer eso en algo como Cython, así que simplemente podría importarlo como un módulo, y podría apoyarse en el tiempo de ejecución existente para la mayoría de los bits difíciles.
Es posible que ya pueda generar un intérprete de python-in-python con PyPy, pero la salida de PyPy es un tiempo de ejecución que hace TODO, incluida la implementación del equivalente de los PyObjects subyacentes para los tipos incorporados y todo eso, y creo que eso es un exceso. este tipo de cosas
Todo lo que realmente necesita es algo que funcione como un Marco en la pila de ejecución, y luego un método para cada código de operación. No creo que tengas que implementarlo tú mismo. Simplemente puede escribir un módulo que exponga los objetos de marco existentes al tiempo de ejecución.
De todos modos, entonces solo mantienes tu propia pila de objetos de marco y manejas los códigos de bytes, y puedes acelerarlos con códigos de bytes por segundo o lo que sea.