Как сделать встроенный модуль 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)

Теперь нет важного различия между песочницей и другим сериализованным модулем. Условия "если песочница" может быть вскоре удалена.