【问题标题】:terminal partitioning for each of subprocesses prints每个子进程的终端分区打印
【发布时间】:2020-09-18 05:16:31
【问题描述】:

假设我们有多个子进程,如下所示,它们将一些结果实时打印到 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, 
                         )

但是,在终端执行这个脚本后,在查看正在打印的内容时,很难区分哪个打印来自第一个进程,哪个来自第二个进程。

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

【问题讨论】:

    标签: python terminal subprocess std sys


    【解决方案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,此后滚动到分区顶部),但这是无法控制的,因为它们被写入不同的管道。

    【讨论】:

    • 看起来很不错,我去试试。只是几个问题: 1_你也知道它是否是死锁安全的吗?因为我读到使用子进程读数时可能会出现死锁。 2_您是否还认为我们想要执行此操作的进程数量可能存在限制
    • 3_我的进程继续运行,而我在终端中实时看到它们的标准输出,而更早时,当我使用communicate() 或 stdout.read() 时,它会一直卡住,因为进程没有尚未终止或没有给出任何错误。如果是这种情况,您认为上述解决方案可能有问题吗?(我会在尝试时知道答案)
    • @Azerila 我已经修改了代码(以避免它浪费 CPU),所以请获取一个新的副本。我将尝试在下一条评论中回答您的问题。
    • 1.我认为没有理由陷入僵局。它非常保守,在select 表示可读的每个文件描述符中只读取一个字节,然后再次调用select,因此read 不应阻塞。 select 有时会阻塞,但仅当它被设置为等待尚未看到 EOF 的 any 文件描述符上的输出时,因此不应出现某些进程的输出为没有被读取,因为阅读器在等待来自不同进程的输出时被阻塞。 (如果进程需要任何输入,您将需要一个单独的编写器线程/进程。)
    • @Azerila 抱歉,我不知道你会如何在 curses pad 中写颜色。这是我的第一个也是唯一的诅咒应用程序!尝试提出一个新问题。
    猜你喜欢
    • 2022-01-06
    • 2016-08-01
    • 1970-01-01
    • 2014-12-03
    • 2016-04-12
    • 2020-01-16
    • 1970-01-01
    • 2018-09-24
    • 1970-01-01
    相关资源
    最近更新 更多