如何从使用屏幕重绘的程序获取终端屏幕刮刀中的输出?

时间:2015-03-15 05:29:07

标签: python subprocess file-descriptor pty

我正在尝试获取全屏终端程序的输出,该程序使用重绘转义码来显示数据,并且需要tty(或pty)才能运行。

人类将遵循的基本程序是:

  1. 在终端中启动程序。
  2. 该程序使用重绘来显示和更新各种数据字段。
  3. 人类等待直到显示一致(可能使用诸如&#34之类的提示;它没有闪烁"或者#34;自上次更新以来已经过了0.5秒" ;)
  4. 人类会查看某些位置的字段并记住或记录数据。
  5. 人类退出计划。
  6. 然后人类根据该数据在程序外执行操作。
  7. 我想自动化这个过程。步骤4和5可以按任何顺序完成。虽然我的完美主义者担心屏幕状态的自我一致性,但我承认我并不确定如何正确定义这个(除了使用"它不仅仅是一定的自上次更新以来的超时时间")。

    似乎使用ptysubprocess后跟某种屏幕抓取器是一种可行的方法,但我还不清楚如何将它们全部一起使用,以及我使用的一些较低级别物体存在哪些危险。

    考虑这个程序:

    #!/usr/bin/env python2
    import os
    import pty
    import subprocess
    import time
    
    import pexpect.ANSI
    
    # Psuedo-terminal FDs
    fd_master, fd_slave = pty.openpty()
    
    # Start 'the_program'
    the_proc = subprocess.Popen(['the_program'], stdin=fd_master, stdout=fd_slave, stderr=fd_slave)
    
    # Just kill it after a couple of seconds
    time.sleep(2)
    the_proc.terminate()
    
    # Read output into a buffer
    output_buffer = b''
    read_size = None
    
    while (read_size is None) or (read_size > 0):
        chunk = os.read(fd_master, 1024)
        output_buffer += chunk
        read_size = len(chunk)
    
    print("output buffer size: {:d}".format(len(output_buffer)))
    
    # Feed output to screen scraper
    ansi_term = pexpect.ANSI.ANSI(24, 80)
    ansi_term.write(output_buffer)
    
    # Parse presented data... 
    

    一个问题是os.read()呼叫始终阻止。我也想知道是否有更好的方法来获取pty输出以供进一步使用。具体做法是:

    1. 有没有办法用更高级别的代码执行此操作(或部分操作)?我无法使用subprocess.PIPE进行Popen通话,因为目标程序无法正常工作。但是,我可以用一些更方便的方法将这些文件描述符包装成I / O吗?
    2. 如果没有,我如何避免始终阻止os.read来电?我更习惯于read()始终返回的类似文件的对象,如果到达流的末尾,则只返回一个空字符串。在这里,os.read无论如何最终都会阻止。
    3. 我小心翼翼地将这个脚本带到"只是工作"没有意识到潜在的危险(例如,一千次出现一次的竞争条件)。我还需要注意什么?
    4. 我还认为首先使用ptysubprocess并不是最好的方法。

2 个答案:

答案 0 :(得分:1)

您可以使用pexpect执行此操作。使用run()函数获取数据,并查看附带的VT100 emulator(或pyte)以进行渲染。

使用实用程序top作为示例:

import time
import pexpect
import pexpect.ANSI

# Start 'top' and quit after a couple of seconds
output_buffer = pexpect.run('top', timeout=2)

# For continuous reading/interaction, you would need to use the "events"
# arg, threading, or a framework for asynchronous communication.

ansi_term = pexpect.ANSI.ANSI(24, 80)
ansi_term.write(output_buffer)
print(str(ansi_term))

(请注意,有时会出现导致extra line spacings的错误。)

答案 1 :(得分:1)

如果程序没有产生太多输出;最简单的方法是使用pexpect.run()通过pty获取其输出:

import pexpect # $ pip install pexpect

output, status = pexpect.run('top', timeout=2, withexitstatus=1)

您可以检测输出是否已经稳定下来"通过将其与之前的输出进行比较:

import pexpect # $ pip install pexpect

def every_second(d, last=[None]):
    current = d['child'].before
    if last[0] == current: # "settled down"
        raise pexpect.TIMEOUT(None) # exit run
    last[0] = current

output, status =  pexpect.run('top', timeout=1, withexitstatus=1,
                              events={pexpect.TIMEOUT: every_second})

您可以使用与输出中的循环模式匹配的正则表达式而不是超时。目的是确定何时输出结束"。

用于比较直接使用subprocesspty模块的代码:

#!/usr/bin/env python
"""Start process; wait 2 seconds; kill the process; print all process output."""
import errno
import os
import pty
import select
from subprocess import Popen, STDOUT
try:
    from time import monotonic as timer
except ImportError:
    from time import time as timer

output = []
master_fd, slave_fd = pty.openpty() #XXX add cleanup on exception
p = Popen(["top"], stdin=slave_fd, stdout=slave_fd, stderr=STDOUT,
          close_fds=True)
os.close(slave_fd)
endtime = timer() + 2 # stop in 2 seconds
while True:
    delay = endtime - timer()
    if delay <= 0: # timeout
        break
    if select.select([master_fd], [], [], delay)[0]:
        try:
            data = os.read(master_fd, 1024)
        except OSError as e: #NOTE: no need for IOError here
            if e.errno != errno.EIO:
                raise
            break # EIO means EOF on some systems
        else:
            if not data: # EOF
                break
            output.append(data)
os.close(master_fd)
p.terminate()
returncode = p.wait()
print([returncode, b''.join(output)])

注意:

  • 子进程中的所有三个标准流都使用slave_fd,而不是代码中使用master_fd stdin
  • 的代码
  • 代码读取输出,而进程仍在运行。它允许接受大输出(超过内核中单个缓冲区的大小)
  • 代码不会丢失EIO错误数据(此处为EOF)

基于Python subprocess readlines() hangs