терминальное разбиение для каждого из подпроцессов печатает

Скажем, у нас есть несколько подпроцессов, подобных следующему, некоторые результаты которых выводятся в режиме реального времени в sys.stdout или sys.stderr.

proc1 = subprocess.Popen(['cmd1'],
                         env=venv1,
                         stdout=sys.stdout,
                         stderr=sys.stderr, 
                         )

proc2 = subprocess.Popen(['cmd2'],
                         env=venv2,
                         stdout=sys.stdout,
                         stderr=sys.stderr, 
                         )

Однако после выполнения этого скрипта в терминале, глядя на то, что печатается, непросто различить, какая печать из первого процесса, а какая из второго.

Есть ли решение для этого, чтобы увидеть стандартный вывод каждого процесса отдельно, например, если бы экран терминала мог быть разделен на разделы, и каждый раздел показывал бы результаты печати из каждого процесса?


person Azerila    schedule 30.05.2020    source источник


Ответы (1)


Я написал для вас curses-приложение, которое будет делать то, что вы просите: разделить окно терминала на несколько разделов, а затем просматривать различные потоки вывода в разных разделах.

Функция watch_fd_in_panes примет список списков, где подсписки указывают, какие файловые дескрипторы отслеживать внутри каждого раздела.

Вот как будет выглядеть ваш пример кода вызова:

import subprocess
from watcher import watch_fds_in_panes

proc1 = subprocess.Popen('for i in `seq 30`; do date; sleep 1 ; done',
                         shell=True,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE,
                         )

# this process also writes something on stderr
proc2 = subprocess.Popen('ls -l /asdf; for i in `seq 20`; do echo $i; sleep 0.5; done',
                         shell=True,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE,
                         )

proc3 = subprocess.Popen(['echo', 'hello'],
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE,
                         )

try:
    watch_fds_in_panes([[proc1.stdout.fileno(), proc1.stderr.fileno()],
                        [proc2.stdout.fileno(), proc2.stderr.fileno()],
                        [proc3.stdout.fileno(), proc3.stderr.fileno()]],
                       sleep_at_end=3.)
except KeyboardInterrupt:
    print("interrupted")
    proc1.kill()
    proc2.kill()
    proc3.kill()

Для запуска вам понадобятся эти два файла:

panes.py

import curses

class Panes:
    """
    curses-based app that divides the screen into a number of scrollable
    panes and lets the caller write text into them
    """

    def start(self, num_panes):
        "set up the panes and initialise the app"

        # curses init
        self.num = num_panes
        self.stdscr = curses.initscr()
        curses.noecho()
        curses.cbreak()

        # split the screen into number of panes stacked vertically,
        # drawing some horizontal separator lines
        scr_height, scr_width = self.stdscr.getmaxyx()
        div_ys = [scr_height * i // self.num for i in range(1, self.num)]
        for y in div_ys:
            self.stdscr.addstr(y, 0, '-' * scr_width)
        self.stdscr.refresh()

        # 'boundaries' contains y coords of separator lines including notional
        # separator lines above and below everything, and then the panes
        # occupy the spaces between these
        boundaries = [-1] + div_ys + [scr_height]
        self.panes = []
        for i in range(self.num):
            top = boundaries[i] + 1
            bottom = boundaries[i + 1] - 1
            height = bottom - top + 1
            width = scr_width
            # create a scrollable pad for this pane, of height at least
            # 'height' (could be more to retain some scrollback history)
            pad = curses.newpad(height, width)
            pad.scrollok(True)
            self.panes.append({'pad': pad,
                               'coords': [top, 0, bottom, width],
                               'height': height})

    def write(self, pane_num, text):
        "write text to the specified pane number (from 0 to num_panes-1)"

        pane = self.panes[pane_num]
        pad = pane['pad']
        y, x = pad.getyx()
        pad.addstr(y, x, text)
        y, x = pad.getyx()
        view_top = max(y - pane['height'], 0)
        pad.refresh(view_top, 0, *pane['coords'])

    def end(self):
        "restore the original terminal behaviour"

        curses.nocbreak()
        self.stdscr.keypad(0)
        curses.echo()
        curses.endwin()

и watcher.py

import os
import select
import time

from panes import Panes


def watch_fds_in_panes(fds_by_pane, sleep_at_end=0):
    """
    Use panes to watch output from a number of fds that are writing data.

    fds_by_pane contains a list of lists of fds to watch in each pane.
    """
    panes = Panes()
    npane = len(fds_by_pane)
    panes.start(npane)
    pane_num_for_fd = {}
    active_fds = []
    data_tmpl = {}
    for pane_num, pane_fds in enumerate(fds_by_pane):
        for fd in pane_fds:
            active_fds.append(fd)
            pane_num_for_fd[fd] = pane_num
            data_tmpl[fd] = bytes()
    try:
        while active_fds:
            all_data = data_tmpl.copy()
            timeout = None
            while True:
                fds_read, _, _ = select.select(active_fds, [], [], timeout)
                timeout = 0
                if fds_read:
                    for fd in fds_read:
                        data = os.read(fd, 1)
                        if data:
                            all_data[fd] += data
                        else:
                            active_fds.remove(fd)  # saw EOF
                else:
                    # no more data ready to read
                    break
            for fd, data in all_data.items():
                if data:
                    strng = data.decode('utf-8')
                    panes.write(pane_num_for_fd[fd], strng)
    except KeyboardInterrupt:
        panes.end()
        raise

    time.sleep(sleep_at_end)
    panes.end()

Наконец, вот скриншот приведенного выше кода в действии:

введите здесь описание изображения

В этом примере мы отслеживаем как stdout, так и stderr каждого процесса в соответствующем разделе. На снимке экрана строка, которую proc2 записала в stderr перед началом цикла (относительно /asdf), появилась после первой строки, которую proc2 записала в stdout во время первой итерации цикла (т. е. 1, которая с тех пор прокручивается сверху). раздела), но это невозможно контролировать, потому что они были записаны в разные каналы.

person alani    schedule 31.05.2020
comment
кажется очень хорошим, я собираюсь попробовать это. Всего несколько вопросов: 1_Знаете ли вы также, является ли он сейфом с врезным замком? потому что я читал, что при использовании показаний подпроцесса может возникнуть тупиковая ситуация. 2_Как вы думаете, может быть ограничение на количество процессов, с которыми мы хотим это сделать? - person Azerila; 31.05.2020
comment
3_Мои процессы продолжают работать, пока я вижу их stdout в терминале в режиме реального времени, и раньше, когда я использовал communi() или stdout.read(), он оставался бы зависшим, потому что процесс еще не завершился или не выдал никакой ошибки . Как вы думаете, может ли быть проблема с вышеуказанным решением, если это может быть так? (Я узнаю ответ, когда попробую) - person Azerila; 31.05.2020
comment
@Azerila Я изменил код (чтобы он не тратил ресурсы процессора), поэтому, пожалуйста, возьмите новую копию. Я постараюсь ответить на ваши вопросы в следующих комментариях. - person alani; 01.06.2020
comment
1. Не вижу причин для тупиковой ситуации. Он очень консервативен и считывает только один байт на файловый дескриптор, который select назвал доступным для чтения, перед повторным вызовом select, поэтому read не должен блокироваться. select иногда блокируется, но только когда он настроен на ожидание вывода на любой файловый дескриптор, который еще не видел EOF, поэтому не должно возникать ситуации, когда выходные данные какого-то процесса не обрабатываются. read, потому что читатель блокируется, ожидая вывода от другого процесса. (Если процессам нужны какие-либо входные данные, для этого вам понадобится отдельный поток/процесс записи.) - person alani; 01.06.2020
comment
2. Что касается ограничения по количеству, я думаю, что практическое ограничение заключается в том, как вы управляете пространством на экране. Он будет разделять экран только по вертикали (с горизонтальными линиями), а не бок о бок. Но вы можете уменьшить количество разделов, сгруппировав выходные данные некоторых процессов в один и тот же раздел, а другие просматривая в разных разделах, просто за счет того, как вы распределяете файловые дескрипторы среди подсписков аргумента. до watch_fds_in_panes. В примере каждый раздел используется для stdout и stderr одного процесса, но это полностью гибко. - person alani; 01.06.2020
comment
3. Наблюдатель завершит работу, когда получит EOF для всех дескрипторов открытых файлов. До тех пор он будет продолжать следить за выводом (хотя, если вы нажмете ctrl-C, он корректно завершится). После завершения всех процессов (или закрытия их stdout и stderr) наблюдатель должен записать все оставшиеся данные, а затем выйти. - person alani; 01.06.2020
comment
так что это сработало, как и ожидалось, но была одна проблема. когда процесс печатал что-то с цветом, вывод в терминале был не с этим цветом и включал код цвета, например, print('\33[34m' + 'simulators_root_node start' + '\x1b[0m'), также записал бы \33[34m2 в терминал, а не интерпретировал бы его как цвет следующей строки. - person Azerila; 01.06.2020
comment
дайте мне знать, если у вас также было решение проблемы с цветом при печати :) - person Azerila; 16.06.2020
comment
@Azerila Извините, я не знаю, как бы вы написали цвета в блокноте проклятий. Это мое первое и единственное приложение curses! Попробуйте задать новый вопрос. - person alani; 16.06.2020