Взаимодействие с интерпретатором PyPy в песочнице — вторичный .communicate() возвращает пустой кортеж

Я пытаюсь создать способ взаимодействия с изолированным интерпретатором PyPy из обычного (не изолированного) скрипта cPython или PyPy.

Мне удалось скомпилировать изолированный интерпретатор PyPy, следуя этим инструкциям http://doc.pypy.org/en/latest/sandbox.html, и у меня есть файл pypy-c-sandbox, который работает с pypy_interact.py для создания интерактивного изолированного интерпретатора.

Теперь я хочу сделать очень похожую вещь, но вместо того, чтобы использовать stdin/stdout в качестве моего ввода-вывода, я хочу использовать скрипт python для взаимодействия с процессом pypy-sandbox. Я получил это, чтобы работать по большей части. Я могу использовать функцию .communicate() с объектами cStringIO в качестве ввода, вывода и ошибки и получать доступ к этим данным из обычного python.

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

Я собрал уродливый хак, чтобы продемонстрировать свою проблему:

import sys, os
import autopath
from pypy.translator.sandbox.sandlib import SimpleIOSandboxedProc
from pypy.translator.sandbox.sandlib import VirtualizedSandboxedProc
from pypy.translator.sandbox.vfs import Dir, RealDir, RealFile
import pypy


LIB_ROOT = os.path.dirname(os.path.dirname(pypy.__file__))

class PyPySandboxedProc(VirtualizedSandboxedProc, SimpleIOSandboxedProc):
    argv0 = '/bin/pypy-c'
    virtual_cwd = '/tmp'
    virtual_env = {}
    virtual_console_isatty = True
    arguments = ['../goal/pypy-c', '-u']

    def __init__(self, executable, arguments, tmpdir=None, debug=True):
        self.executable = executable = os.path.abspath(executable)
        self.tmpdir = tmpdir
        self.debug = debug
        super(PyPySandboxedProc, self).__init__([self.argv0] + arguments,
                                                executable=executable)

    def build_virtual_root(self):
        # build a virtual file system:
        # * can access its own executable
        # * can access the pure Python libraries
        # * can access the temporary usession directory as /tmp
        exclude = ['.pyc', '.pyo']
        if self.tmpdir is None:
            tmpdirnode = Dir({})
        else:
            tmpdirnode = RealDir(self.tmpdir, exclude=exclude)
        libroot = str(LIB_ROOT)

        return Dir({
            'bin': Dir({
                'pypy-c': RealFile(self.executable),
                'lib-python': RealDir(os.path.join(libroot, 'lib-python'),
                                      exclude=exclude),
                'lib_pypy': RealDir(os.path.join(libroot, 'lib_pypy'),
                                      exclude=exclude),
                }),
             'tmp': tmpdirnode,
             })

# run test
arguments = ['../goal/pypy-c', '-u']

sandproc = PyPySandboxedProc(arguments[0], arguments[1:],
                             tmpdir=None, debug=True)

#start the proc
code1 = "print 'started'\na = 5\nprint a"
code2 = "b = a\nprint b\nprint 'code 2 was run'"

output, error = sandproc.communicate(code1)
print "output: %s\n error: %s\n" % (output, error)

output, error = sandproc.communicate(code2)
print "output: %s\n error: %s\n" % (output, error)

Мне бы очень хотелось, чтобы code2 запускался одним и тем же экземпляром sandproc, но его ввод/вывод возвращались отдельно. Если я соединю весь свой код вместе и запущу его сразу, это сработает, но синтаксический анализ вывода для заданного ввода будет немного болезненным.


person ehc    schedule 07.12.2012    source источник
comment
сообщение заканчивается, чтобы процесс завершился. Посмотрите, как реализован handle_until_return в sandlib, чтобы понять, как это сделать по частям.   -  person fijal    schedule 07.12.2012


Ответы (1)


from rpython.translator.sandbox.sandlib import SimpleIOSandboxedProc

Вы импортируете и расширяете SimpleIOSandboxedProc в PyPySandboxedProc. В исходнике (исходник sandlib.py) , вы увидите, что sandproc.communicate() отправляет данные и возвращает результат после завершения дочернего процесса.

def communicate(self, input=None):
    """Send data to stdin. Read data from stdout and stderr,
    until end-of-file is reached. Wait for process to terminate.
    """
    import cStringIO
    if input:
        if isinstance(input, str):
            input = cStringIO.StringIO(input)
        self._input = input
    self._output = cStringIO.StringIO()
    self._error = cStringIO.StringIO()
    self.handle_forever()
    output = self._output.getvalue()
    self._output = None
    error = self._error.getvalue()
    self._error = None
    return (output, error)

В приведенном выше коде вызывается self.handle_forever():

def handle_until_return(self):
    child_stdin  = self.popen.stdin
    child_stdout = self.popen.stdout
    if self.os_level_sandboxing and sys.platform.startswith('linux'):
        # rationale: we wait until the child process started completely,
        # letting the C library do any system calls it wants for
        # initialization.  When the RPython code starts up, it quickly
        # does its first system call.  At this point we turn seccomp on.
        import select
        select.select([child_stdout], [], [])
        f = open('/proc/%d/seccomp' % self.popen.pid, 'w')
        print >> f, 1
        f.close()
    while True:
        try:
            fnname = read_message(child_stdout)
            args   = read_message(child_stdout)
        except EOFError, e:
            break
        if self.log and not self.is_spam(fnname, *args):
            self.log.call('%s(%s)' % (fnname,
                                 ', '.join([shortrepr(x) for x in args])))
        try:
            answer, resulttype = self.handle_message(fnname, *args)
        except Exception, e:
            tb = sys.exc_info()[2]
            write_exception(child_stdin, e, tb)
            if self.log:
                if str(e):
                    self.log.exception('%s: %s' % (e.__class__.__name__, e))
                else:
                    self.log.exception('%s' % (e.__class__.__name__,))
        else:
            if self.log and not self.is_spam(fnname, *args):
                self.log.result(shortrepr(answer))
            try:
                write_message(child_stdin, 0)  # error code - 0 for ok
                write_message(child_stdin, answer, resulttype)
                child_stdin.flush()
            except (IOError, OSError):
                # likely cause: subprocess is dead, child_stdin closed
                if self.poll() is not None:
                    break
                else:
                    raise
    returncode = self.wait()
    return returncode

Как видно, это «в то время как True:», что означает, что эта функция не вернется, пока мы не получим исключение. Таким образом, sandproc.communicate() не сможет достичь того, чего вы хотели бы добиться.

У вас есть два варианта.

Жесткий вариант — разветвить основной процесс. Используйте один процесс для запуска sandproc.interact() с некоторыми переданными os.pipes. И используйте другой процесс для чтения и записи в указанные каналы. Это неэффективно, так как требует 3 процесса (один для изолированного приложения, один для основного процесса и один для разветвленного процесса).

Простой вариант — переопределить некоторые функции в классе SimpleIOSandboxedProc. Все, что вам нужно сделать, это переопределить эти функции в PyPySandboxedProc.

def do_ll_os__ll_os_read(self, fd, size):
    if fd == 0:
        if self._input is None:
            return ""
        elif (getattr(self, 'virtual_console_isatty', False) or
              self._input.isatty()):
            # don't wait for all 'size' chars if reading from a tty,
            # to avoid blocking.  Instead, stop after reading a line.

            # For now, waiting at the interactive console is the
            # only time that counts as idle.
            self.enter_idle()
            try:
                inputdata = self._input.readline(size) #TODO: THIS IS WHERE YOU HANDLE READING FROM THE SANDBOXED PROCESS
            finally:
                self.leave_idle()
        else:
            inputdata = self._input.read(size)
        if self.inputlogfile is not None:
            self.inputlogfile.write(inputdata)
        return inputdata
    raise OSError("trying to read from fd %d" % (fd,))

def do_ll_os__ll_os_write(self, fd, data):
    if fd == 1:
        self._output.write(data) #TODO: THIS IS WHERE YOU WRITE TO THE SANDBOXED PROCESS
        return len(data)
    if fd == 2:
        self._error.write(data)
        return len(data)
    raise OSError("trying to write to fd %d" % (fd,))

Строка с пометкой #TODO: — это место, где данные вводятся и считываются из изолированного процесса.

Что вам нужно сделать, так это реализовать логику, в которой вы записываете некоторые данные (когда процесс в песочнице читает), ждете ответа (пока процесс не записывает), записываете больше данных и снова читаете ответ (когда процесс пишет второй раз) .

person Hesam Rabeti    schedule 31.05.2013