Как подключить отладчик к подпроцессу Python?

Мне нужно отладить дочерний процесс, порожденный multiprocessing.Process(), pdb Похоже, что degugger не знает о разветвлении и не может подключиться к уже запущенным процессам.

Существуют ли более умные отладчики Python, которые можно подключить к подпроцессу?

10 ответов

Решение

Winpdb - это определение более умного отладчика Python. Он явно поддерживает переход на развилку, но не уверен, что multiprocessing.Process() но стоит попробовать.

Список кандидатов для проверки поддержки вашего варианта использования см. В списке отладчиков Python в вики.

Я искал простое решение этой проблемы и придумал это:

import sys
import pdb

class ForkedPdb(pdb.Pdb):
    """A Pdb subclass that may be used
    from a forked multiprocessing child

    """
    def interaction(self, *args, **kwargs):
        _stdin = sys.stdin
        try:
            sys.stdin = open('/dev/stdin')
            pdb.Pdb.interaction(self, *args, **kwargs)
        finally:
            sys.stdin = _stdin

Используйте его так же, как и классический Pdb:

ForkedPdb().set_trace()

Это разработка ответа Ромуальда, который восстанавливает исходный стандартный ввод с помощью файлового дескриптора. Это позволяет readline работать внутри отладчика. Кроме того, в pdb специальное управление KeyboardInterrupt отключено, чтобы не мешать многопроцессорному обработчику sigint.

class ForkablePdb(pdb.Pdb):

    _original_stdin_fd = sys.stdin.fileno()
    _original_stdin = None

    def __init__(self):
        pdb.Pdb.__init__(self, nosigint=True)

    def _cmdloop(self):
        current_stdin = sys.stdin
        try:
            if not self._original_stdin:
                self._original_stdin = os.fdopen(self._original_stdin_fd)
            sys.stdin = self._original_stdin
            self.cmdloop()
        finally:
            sys.stdin = current_stdin

remote-pdb можно использовать для отладки подпроцессов. После установки добавьте в код, который нужно отладить, следующие строки:

import remote_pdb
remote_pdb.set_trace()

remote-pdb напечатает номер порта, который будет принимать соединение Telnet для отладки этого конкретного процесса. Есть некоторые предостережения в отношении порядка запуска воркеров, где stdout используется при использовании различных интерфейсов и т. Д. Чтобы гарантировать использование определенного порта (должен быть свободным и доступным для текущего пользователя), используйте вместо этого следующее:

from remote_pdb import RemotePdb
RemotePdb('127.0.0.1', 4444).set_trace()

remote-pdb также можно запустить черезbreakpoint()команда в Python 3.7.

Опираясь на идею @memplex, мне пришлось изменить ее, чтобы она работала с joblib установив sys.stdin в конструкторе, а также передавая его напрямую через joblib.

<!-- language: lang-py -->
import os
import pdb
import signal
import sys
import joblib

_original_stdin_fd = None

class ForkablePdb(pdb.Pdb):
    _original_stdin = None
    _original_pid = os.getpid()

    def __init__(self):
        pdb.Pdb.__init__(self)
        if self._original_pid != os.getpid():
            if _original_stdin_fd is None:
                raise Exception("Must set ForkablePdb._original_stdin_fd to stdin fileno")

            self.current_stdin = sys.stdin
            if not self._original_stdin:
                self._original_stdin = os.fdopen(_original_stdin_fd)
            sys.stdin = self._original_stdin

    def _cmdloop(self):
        try:
            self.cmdloop()
        finally:
            sys.stdin = self.current_stdin

def handle_pdb(sig, frame):
    ForkablePdb().set_trace(frame)

def test(i, fileno):
    global _original_stdin_fd
    _original_stdin_fd = fileno
    while True:
        pass    

if __name__ == '__main__':
    print "PID: %d" % os.getpid()
    signal.signal(signal.SIGUSR2, handle_pdb)
    ForkablePdb().set_trace()
    fileno = sys.stdin.fileno()
    joblib.Parallel(n_jobs=2)(joblib.delayed(test)(i, fileno) for i in range(10))

Просто используйте PuDB , который дает вам потрясающий TUI (GUI на терминале) и поддерживает многопроцессорность следующим образом:

      from pudb import forked; forked.set_trace()

Проблема здесь в том, что Python всегда подключается в дочернем процессе кos.devnullчтобы избежать конкуренции за поток. Но это означает, что когда отладчик (или простойinput()) пытается подключиться к стандартному вводу, чтобы получить ввод от пользователя, он сразу достигает конца файла и сообщает об ошибке.

Одним из решений, по крайней мере, если вы не ожидаете одновременного запуска нескольких отладчиков, является повторное открытие stdin в дочернем процессе . Это можно сделать, установивsys.stdinкopen(0), который всегда открывает активный терминал. Это на самом деле то, чтоForkedPdbрешение делает, но это можно сделать более простым и независимым от ОС способом, например:

      import multiprocessing, sys

def main():
    process = multiprocessing.Process(target=worker)
    process.start()
    process.join()

def worker():
    # Python automatically closes sys.stdin for the subprocess, so we reopen
    # stdin. This enables pdb to connect to the terminal and accept commands.
    # See https://stackoverflow.com/a/30149635/3830997.
    sys.stdin = open(0) # or os.fdopen(0)
    print("Hello from the subprocess.")
    breakpoint()  # or import pdb; pdb.set_trace()
    print("Exited from breakpoint in the subprocess.")

if __name__ == '__main__':
    main()

У меня была идея создать "фиктивные" классы, чтобы имитировать реализацию методов, которые вы используете из многопроцессорной обработки:

from multiprocessing import Pool


class DummyPool():
    @staticmethod
    def apply_async(func, args, kwds):
        return DummyApplyResult(func(*args, **kwds))

    def close(self): pass
    def join(self): pass


class DummyApplyResult():
    def __init__(self, result):
        self.result = result

    def get(self):
        return self.result


def foo(a, b, switch):
    # set trace when DummyPool is used
    # import ipdb; ipdb.set_trace()
    if switch:
        return b - a
    else:
        return a - b


if __name__ == '__main__':
    xml = etree.parse('C:/Users/anmendoza/Downloads/jim - 8.1/running-config.xml')
    pool = DummyPool()  # switch between Pool() and DummyPool() here
    results = []
    results.append(pool.apply_async(foo, args=(1, 100), kwds={'switch': True}))
    pool.close()
    pool.join()
    results[0].get()

Вот версия ForkedPdb(решение Ромуальда), которая будет работать для систем на базе Windows и *nix.

import sys
import pdb
import win32console


class MyHandle():
    def __init__(self):
        self.screenBuffer = win32console.GetStdHandle(win32console.STD_INPUT_HANDLE)
    
    def readline(self):
        return self.screenBuffer.ReadConsole(1000)

class ForkedPdb(pdb.Pdb):
    def interaction(self, *args, **kwargs):
        _stdin = sys.stdin
        try:
            if sys.platform == "win32":
                sys.stdin = MyHandle()
            else:
                sys.stdin = open('/dev/stdin')
            pdb.Pdb.interaction(self, *args, **kwargs)
        finally:
            sys.stdin = _stdin

Если вы на поддерживаемой платформе, попробуйте DTrace. Большинство семейства BSD / Solaris / OS X поддерживают DTrace.

Вот вступление автора. Вы можете использовать Dtrace для отладки чего угодно.

Вот ТАК сообщение об обучении DTrace.

Другие вопросы по тегам