Как сделать встроенный модуль python для удаленной обработки песочницы?
Я пытаюсь динамически добавлять код python в модуль песочницы для выполнения на удаленной машине. У меня возникла проблема с обработкой импортированных методов. Например, обычно видны сценарии, такие как:
from test_module import g
import other_module
def f():
g()
other_module.z()
Я знаю, что могу разбить f на g и потенциально z, но как сохранить область "other_module" для z? Если я помещу как f, так и g в песочницу, тогда z не будет корректно разрешено при вызове f. Можно ли использовать какой-либо встроенный модуль для правильного выбора z, т.е. Sandbox.other_module?
Моя цель для загрузки удаленного кода в песочницу - не загрязнять глобальное пространство имен. Например, если другой удаленный метод вызывается с собственным графиком зависимости, он не должен мешать другому набору удаленных кодов. Является ли реалистичным ожидать, что python будет стабильным с использованием модулей песочницы, входящих и выходящих из использования? Я говорю это из-за этого сообщения:
Как выгрузить (перезагрузить) модуль Python?
что заставляет меня чувствовать, что это может быть проблематичным удаление модулей, таких как различные песочницы в этом случае.
Ответы
Ответ 1
Другие модули могут быть импортированы в песочницу (вы подразумеваете модули, которые динамически создаются во время выполнения) на
sandbox.other_module = __import__('other_module')
или
exec 'import other_module' in sandbox.__dict__
Если вы вызываете модули "песочницы" из других модулей или других модулей песочницы, и вы хотите перезагрузить какой-либо новый код позже, проще импортировать только модуль, а не имена из него, как "из импорта песочницы f", и вызвать "sandbox.f" не "f". Тогда перезагрузка легко. (но команда naturarely reload для него не полезна)
Классы
>>> class A(object): pass
...
>>> a = A()
>>> A.f = lambda self, x: 2 * x # or a pickled function
>>> a.f(1)
2
>>> A.f = lambda self, x: 3 * x
>>> a.f(1)
3
Кажется, что способы перезагрузки могут быть легкими. Я помню, что классы перезагрузки, определенные в модифицированном исходном коде, могут быть сложными, потому что старый код класса может храниться каким-то экземпляром. Код экземпляра может/нуждается в обновлении индивидуально в худшем случае:
some_instance.__class__ = sandbox.SomeClass # that means the same reloaded class
Я использовал последний с помощью службы python, доступной через автоматизацию win32com, и перезагрузка кода классов была успешной без данных экземпляров потерь
Ответ 2
В текущем подходе я собираюсь включить как "импорт x", так и "из x import y". Одна обратная сторона этой текущей реализации заключается в том, что она создает копии методов в каждом используемом модуле, в отличие от исходного кода, где каждое использование является просто ссылкой на тот же метод в памяти (хотя у меня есть противоречивые результаты здесь - см. раздел после кода).
///analysis_script.py///(для краткости не определены ограничения)
import test_module
from third_level_module import z
def f():
for i in range(1,5):
test_module.g('blah string used by g')
z()
///driver.py///
import modutil
import analysis_script
modutil.serialize_module_with_dependencies(analysis_script)
///modutil.py///
import sys
import modulefinder
import os
import inspect
import marshal
def dump_module(funcfile, name, module):
functions_list = [o for o in inspect.getmembers(module) if inspect.isfunction(o[1])]
print 'module name:' + name
marshal.dump(name, funcfile)
for func in functions_list:
print func
marshal.dump(func[1].func_code, funcfile)
def serialize_module_with_dependencies(module):
python_path = os.environ['PYTHONPATH'].split(os.pathsep)
module_path = os.path.dirname(module.__file__)
#planning to search for modules only on this python path and under the current scripts working directory
#standard libraries should be expected to be installed on the target platform
search_dir = [python_path, module_path]
mf = modulefinder.ModuleFinder(search_dir)
#__file__ returns the pyc after first run
#in this case we use replace to get the py file since we need that for our call to mf.run_script
src_file = module.__file__
if '.pyc' in src_file:
src_file = src_file.replace('.pyc', '.py')
mf.run_script(src_file)
funcfile = open("functions.pickle", "wb")
dump_module(funcfile, 'sandbox', module)
for name, mod in mf.modules.iteritems():
#the sys module is included by default but has no file and we don't want it anyway, i.e. should
#be on the remote systems path. __main__ we also don't want since it should be virtual empty and
#just used to invoke this function.
if not name == 'sys' and not name == '__main__':
dump_module(funcfile, name, sys.modules[name])
funcfile.close()
///sandbox_reader.py///
import marshal
import types
import imp
sandbox_module = imp.new_module('sandbox')
dynamic_modules = {}
current_module = ''
with open("functions.pickle", "rb") as funcfile:
while True:
try:
code = marshal.load(funcfile)
except EOFError:
break
if isinstance(code,types.StringType):
print "module name:" + code
if code == 'sandbox':
current_module = "sandbox"
else:
current_module = imp.new_module(code)
dynamic_modules[code] = current_module
exec 'import '+code in sandbox_module.__dict__
elif isinstance(code,types.CodeType):
print "func"
if current_module == "sandbox":
func = types.FunctionType(code, sandbox_module.__dict__, code.co_name)
setattr(sandbox_module, code.co_name, func)
else:
func = types.FunctionType(code, current_module.__dict__, code.co_name)
setattr(current_module, code.co_name, func)
else:
raise Exception( "unknown type received")
#yaa! actually invoke the method
sandbox_module.f()
del sandbox_module
Например, перед сериализацией граф функции выглядит так:
module name:sandbox
('f', <function f at 0x15e07d0>)
('z', <function z at 0x7f47d719ade8>)
module name:test_module
('g', <function g at 0x15e0758>)
('z', <function z at 0x7f47d719ade8>)
module name:third_level_module
('z', <function z at 0x7f47d719ade8>)
В частности, глядя на функцию z, мы видим, что все ссылки указывают на один и тот же адрес, т.е. 0x7f47d719ade8.
В удаленном процессе после реконструкции песочницы мы имеем:
print sandbox_module.z
<function z at 0x1a071b8>
print sandbox_module.third_level_module.z
<function z at 0x1a072a8>
print sandbox_module.test_module.z
<function z at 0x1a072a8>
Это дует мой разум! Я бы подумал, что все адреса здесь будут уникальными после реконструкции, но по какой-то причине sandbox_module.test_module.z и sandbox_module.third_level_module.z имеют одинаковый адрес?
Ответ 3
- Вероятно, вы не хотите сериализовать импортированные функции из библиотеки Python, например. математических функций или больших пакетов, смешанных с Python + C, но ваш код сериализует его. Это может вызвать ненужные проблемы, в которых у них нет атрибута func_code и т.д.
- Вам не нужно многократно сериализовать функции, которые были сериализованы ранее. Вы можете отправить полное имя и импортировать их в соответствии с этим. Именно по этой причине у вас это было несколько раз в памяти.
- Исходный формат
<module_name> <serialized_func1> <serialized_func2>...
недостаточно общий. Функция может быть на локальном компьютере, импортированном под разными именами, в разделе "... import... as...". Вы можете сериализовать список кортежей смешанных строк и объектов кода.
Подсказка
def some_filter(module_name):
mod_path = sys.modules[module_name].__file__
# or if getattr(sys.modules[module_name], 'some_my_attr', None)
return not mod_path.startswith('/usr/lib/python2.7/')
dumped_funcs = {}
def dump_module(...
...
data = []
for func_name, func_obj in functions_list:
if some_filter(func_obj.__module__) and not func_obj in dumped_funcs and \
hasattr(func_obj, 'func_code'):
data.append((func_name, func_obj.func_code))
dumped_funcs[func_obj] = True # maybe later will be saved package.mod.fname
else:
data.append((func_name, '%s.%s' % (func_obj.__module__, \
func_obj.func_code.co_name)))
marshal.dump(data, funcfile)
Теперь нет важного различия между песочницей и другим сериализованным модулем. Условия "если песочница" может быть вскоре удалена.