partisi terminal untuk setiap subproses yang dicetak

Katakanlah kita memiliki beberapa subproses seperti berikut ini yang beberapa hasilnya dicetak secara real time ke sys.stdout atau 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, 
                         )

Namun, setelah menjalankan skrip ini di terminal, sambil melihat apa yang sedang dicetak, tidak mudah untuk membedakan cetakan mana dari proses pertama dan mana dari proses kedua.

Apakah ada solusi untuk melihat stdout setiap proses secara terpisah, seperti jika layar terminal dapat dipartisi dan setiap partisi akan menampilkan hasil pencetakan dari setiap proses?


person Azerila    schedule 30.05.2020    source sumber


Jawaban (1)


Saya telah menulis untuk Anda sebuah aplikasi kutukan yang akan melakukan apa yang Anda minta: membagi jendela terminal menjadi beberapa partisi dan kemudian menonton aliran keluaran yang berbeda di partisi yang berbeda.

Fungsi watch_fd_in_panes akan mengambil daftar daftar, di mana sub-daftar menentukan deskriptor file mana yang harus diperhatikan di dalam setiap partisi.

Berikut tampilan contoh kode panggilan Anda:

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()

Untuk menjalankannya Anda memerlukan dua file ini:

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()

dan 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()

Terakhir, berikut adalah tangkapan layar dari kode di atas yang sedang beraksi:

masukkan deskripsi gambar di sini

Dalam contoh ini, kami memantau stdout dan stderr dari setiap proses di partisi yang relevan. Pada tangkapan layar, baris yang proc2 tulis ke stderr sebelum dimulainya perulangan (mengenai /asdf) telah muncul setelah baris pertama yang proc2 tulis ke stdout selama iterasi pertama perulangan (yaitu 1 yang sejak itu bergulir dari atas partisi), namun hal ini tidak dapat dikontrol karena ditulis ke pipa yang berbeda.

person alani    schedule 31.05.2020
comment
sepertinya sangat bagus, saya akan mencobanya. Hanya beberapa pertanyaan: 1_Apakah Anda juga tahu apakah ini brankas buntu? karena saya membaca ada kemungkinan terjadi kebuntuan saat menggunakan pembacaan subproses. 2_Menurut Anda juga mungkin ada batasan jumlah proses yang ingin kami lakukan - person Azerila; 31.05.2020
comment
3_Proses saya terus berjalan sementara saya melihat stdoutnya di terminal secara real-time dan sebelumnya ketika saya menggunakan komunikasi() atau stdout.read() itu akan tetap macet karena proses belum dihentikan atau tidak memberikan kesalahan apa pun . Apakah menurut Anda mungkin ada masalah dengan solusi di atas jika hal ini terjadi? (Saya akan tahu jawabannya ketika saya mencobanya) - person Azerila; 31.05.2020
comment
@Azerila Saya telah memodifikasi kodenya (untuk menghindari pemborosan CPU), jadi silakan ambil salinan baru. Saya akan mencoba menjawab pertanyaan Anda di komentar berikutnya. - person alani; 01.06.2020
comment
1. Saya tidak melihat alasan untuk menemui jalan buntu. Ini sangat konservatif, dan hanya membaca satu byte per deskriptor file yang select katakan dapat dibaca, sebelum memanggil select lagi, sehingga read tidak boleh diblokir. select kadang-kadang akan memblokir, tetapi hanya ketika diatur untuk menunggu keluaran pada deskriptor file apa pun yang belum melihat EOF, sehingga situasi tidak akan muncul di mana keluaran dari beberapa proses tidak sedang berjalan. membaca karena pembaca memblokir saat menunggu keluaran dari proses yang berbeda. (Jika proses memerlukan input apa pun, Anda memerlukan thread/proses penulis terpisah untuk itu.) - person alani; 01.06.2020
comment
2. Mengenai batasan jumlah, menurut saya batasan praktisnya adalah bagaimana Anda mengatur ruang layar. Itu hanya akan membagi layar secara vertikal (dengan garis horizontal), bukan berdampingan. Namun Anda dapat mengurangi jumlah partisi dengan mengelompokkan output dari proses beberapa ke dalam partisi yang sama, sambil melihat proses lainnya di partisi yang berbeda, hanya dengan cara Anda mendistribusikan deskriptor file di antara sub-daftar argumen ke watch_fds_in_panes. Contohnya setiap partisi digunakan untuk stdout dan stderr dari satu proses, tetapi ini sepenuhnya fleksibel. - person alani; 01.06.2020
comment
3. Watcher akan berhenti ketika telah menerima EOF pada semua deskriptor file yang terbuka. Sampai saat itu, ia akan terus mengawasi keluarannya (walaupun jika Anda menekan ctrl-C, ia akan keluar dengan baik). Setelah semua proses dihentikan (atau menutup stdout dan stderr), pengamat harus menulis data yang tersisa dan kemudian keluar. - person alani; 01.06.2020
comment
jadi itu berfungsi seperti yang diharapkan, tapi ada satu masalah. ketika proses sedang mencetak sesuatu yang berwarna, keluaran di terminal tidak berwarna tersebut dan sudah mencantumkan kode warna, misal print('\33[34m' + 'simulators_root_node start' + '\x1b[0m'), juga akan menulis \33[34m2 di terminal daripada menafsirkannya sebagai warna string berikut. - person Azerila; 01.06.2020
comment
beri tahu saya jika Anda juga punya solusi tentang masalah warna saat mencetak :) - person Azerila; 16.06.2020
comment
@Azerila Maaf saya tidak tahu bagaimana Anda akan menulis warna di dalam bantalan kutukan. Ini adalah aplikasi kutukan saya yang pertama dan satu-satunya! Coba ajukan pertanyaan baru. - person alani; 16.06.2020