Python 每个子流程的终端分区

Python 每个子流程的终端分区,python,terminal,subprocess,std,sys,Python,Terminal,Subprocess,Std,Sys,假设我们有多个子流程,如下所示,其中一些结果实时打印到sys.stdout或sys.stderr proc1 = subprocess.Popen(['cmd1'], env=venv1, stdout=sys.stdout, stderr=sys.stderr, ) proc2 = subpr

假设我们有多个子流程,如下所示,其中一些结果实时打印到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, 
                         )
然而,在终端中执行该脚本之后,在查看正在打印的内容时,不容易区分哪个打印来自第一个过程,哪个来自第二个过程


是否有一种解决方案,可以分别查看每个流程的标准输出,例如,如果终端屏幕可以分区,并且每个分区都会显示每个进程的打印结果?

我为您编写了一个curses应用程序,它将按照您的要求执行:将终端窗口划分为多个分区,然后在不同的分区中查看不同的输出流

函数
watch\u fd\u 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()
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()
watcher.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()
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
,该行从分区顶部滚动),但这是无法控制的,因为它们被写入了不同的管道。

似乎很好,我要试试。只有几个问题:1_你也知道它是否是死锁安全的吗?因为我读过,所以在使用子流程读数时可能会出现死锁。2_另外,您是否认为我们要执行此操作的进程数量可能有限制3_我的进程在终端中实时看到其标准输出时继续运行,如果我使用communicate()或标准输出.read()的话,它将一直处于停滞状态,因为进程尚未终止或未出现任何错误。如果是这样的话,你认为上面的解决方案可能会有问题吗?(我会知道答案的,尽管我会尝试)@Azerila我已经修改了代码(以避免浪费CPU),所以请获取一个新的副本。我将在下一篇评论中回答您的问题。1。我看不出它有什么理由陷入僵局。它非常保守,在再次调用
select
之前,每个
select
表示可读的文件描述符只读取一个字节,因此
read
不应阻塞。
select
有时会阻塞,但只有当它被设置为等待尚未看到EOF的任何文件描述符上的输出时,才会阻塞,因此不会出现某个进程的输出未被读取的情况,因为读卡器在等待另一个进程的输出时正在阻塞。(如果进程需要任何输入,您将需要一个单独的写入线程/进程。)@Azeria抱歉,我不知道您如何在诅咒板中写入颜色。这是我的第一个也是唯一一个诅咒应用程序!试着问一个新问题。