การแบ่งพาร์ติชันเทอร์มินัลสำหรับการพิมพ์แต่ละกระบวนการย่อย

สมมติว่าเรามีกระบวนการย่อยหลายกระบวนการดังต่อไปนี้ ซึ่งมีผลลัพธ์บางส่วนพิมพ์แบบเรียลไทม์ไปยัง 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, 
                         )

อย่างไรก็ตาม หลังจากรันสคริปต์นี้ในเทอร์มินัล ในขณะที่ดูสิ่งที่กำลังพิมพ์ ไม่ใช่เรื่องง่ายที่จะแยกแยะว่าการพิมพ์ใดมาจากกระบวนการแรกและสิ่งใดจากขั้นตอนที่สอง

มีวิธีแก้ไขในการดู stdout ของแต่ละกระบวนการแยกกันหรือไม่ เช่น หน้าจอเทอร์มินัลสามารถแบ่งพาร์ติชันได้ และแต่ละพาร์ติชันจะแสดงผลการพิมพ์จากแต่ละกระบวนการหรือไม่


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 ในเทอร์มินัลแบบเรียลไทม์และก่อนหน้านี้เมื่อฉันใช้ Communicate() หรือ stdout.read() มันจะยังคงติดอยู่เนื่องจากกระบวนการยังไม่ยุติหรือไม่ได้รับข้อผิดพลาดใดๆ . คุณคิดว่าอาจมีปัญหากับวิธีแก้ปัญหาข้างต้นหรือไม่หากเป็นกรณีนี้ (ฉันจะรู้คำตอบเมื่อฉันลอง) - person Azerila; 31.05.2020
comment
@Azerila ฉันได้แก้ไขรหัสแล้ว (เพื่อหลีกเลี่ยงการสิ้นเปลือง CPU) ดังนั้นโปรดหยิบสำเนาใหม่ ฉันจะพยายามตอบคำถามของคุณในความคิดเห็นถัดไป - person alani; 01.06.2020
comment
1. ฉันไม่เห็นเหตุผลที่มันจะหยุดชะงัก เป็นแบบอนุรักษ์นิยมมากและอ่านเพียงหนึ่งไบต์ต่อตัวอธิบายไฟล์ที่ select บอกว่าสามารถอ่านได้ ก่อนที่จะเรียก select อีกครั้ง ดังนั้น read ไม่ควรบล็อก บางครั้ง select จะบล็อก แต่เมื่อตั้งค่าให้รอเอาต์พุตบนตัวอธิบายไฟล์ ใดๆ ที่ยังไม่เห็น EOF ดังนั้น สถานการณ์ไม่ควรเกิดขึ้นว่าเอาต์พุตจากกระบวนการบางอย่างไม่ได้ถูกบล็อก อ่านเนื่องจากตัวอ่านกำลังบล็อกขณะรอเอาต์พุตจากกระบวนการอื่น (หากกระบวนการต้องการ อินพุต คุณจะต้องมีเธรด/กระบวนการตัวเขียนแยกต่างหากสำหรับสิ่งนั้น) - 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 beginning' + '\x1b[0m') จะต้องเขียน \33[34m2 ในเทอร์มินัลด้วยแทนที่จะตีความว่าเป็นสีของสตริงต่อไปนี้ - person Azerila; 01.06.2020
comment
แจ้งให้เราทราบหากคุณมีวิธีแก้ไขปัญหาเกี่ยวกับสีเมื่อพิมพ์ด้วย :) - person Azerila; 16.06.2020
comment
@Azerila ขออภัยฉันไม่รู้ว่าคุณจะเขียนสีลงในแผ่นคำสาปได้อย่างไร นี่เป็นแอปพลิเคชั่นคำสาปแรกและแห่งเดียวของฉัน! ลองถามคำถามใหม่ - person alani; 16.06.2020