【问题标题】:Run interactive Bash with popen and a dedicated TTY Python使用 popen 和专用的 TTY Python 运行交互式 Bash
【发布时间】:2017-05-23 09:58:24
【问题描述】:

我需要在 Python 中的一个单独进程中运行一个交互式 Bash 实例,并使用它自己的专用 TTY(我不能使用 pexpect)。 我使用了我在类似程序中经常看到的这段代码sn-p:

master, slave = pty.openpty()

p = subprocess.Popen(["/bin/bash", "-i"], stdin=slave, stdout=slave, stderr=slave)

os.close(slave)

x = os.read(master, 1026)

print x

subprocess.Popen.kill(p)
os.close(master)

但是当我运行它时,我得到以下输出:

$ ./pty_try.py
bash: cannot set terminal process group (10790): Inappropriate ioctl for device
bash: no job control in this shell

运行的 Strace 显示一些错误:

...
readlink("/usr/bin/python2.7", 0x7ffc8db02510, 4096) = -1 EINVAL (Invalid argument)
...
ioctl(3, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7ffc8db03590) = -1 ENOTTY (Inappropriate ioctl for device)
...
readlink("./pty_try.py", 0x7ffc8db00610, 4096) = -1 EINVAL (Invalid argument)

代码 sn-p 看起来很简单,Bash 没有得到它需要的东西吗?这可能是什么问题?

【问题讨论】:

  • 这很正常——你有一个没有作业控制的交互式shell。
  • 如果你也想要作业控制,你需要你的 shell 成为进程的领导者——也就是开始新的“会话”,它是通过 start_new_session=True 关键字参数到 Popen 来实现的(从 Python 3.2 开始) .如果您需要更多控制,请使用preexec_fn=...
  • 好吧,这听起来很合理。我了解start_new_session=True 仅与>3.2 相关。 2.7中是否有等价物?对不起,可能应该在问题中提到python版本。
  • 您可以通过 preexec_fn 中的 setsid() 通过 ctypes 手动调用

标签: python linux bash python-2.x tty


【解决方案1】:

这是在子进程中运行交互式命令的解决方案。它使用伪终端使 stdout 非阻塞(有些命令也需要 tty 设备,例如 bash)。它使用 select 来处理子进程的输入和输出。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import select
import termios
import tty
import pty
from subprocess import Popen

command = 'bash'
# command = 'docker run -it --rm centos /bin/bash'.split()

# save original tty setting then set it to raw mode
old_tty = termios.tcgetattr(sys.stdin)
tty.setraw(sys.stdin.fileno())

# open pseudo-terminal to interact with subprocess
master_fd, slave_fd = pty.openpty()


try:
    # use os.setsid() make it run in a new process group, or bash job control will not be enabled
    p = Popen(command,
              preexec_fn=os.setsid,
              stdin=slave_fd,
              stdout=slave_fd,
              stderr=slave_fd,
              universal_newlines=True)

    while p.poll() is None:
        r, w, e = select.select([sys.stdin, master_fd], [], [])
        if sys.stdin in r:
            d = os.read(sys.stdin.fileno(), 10240)
            os.write(master_fd, d)
        elif master_fd in r:
            o = os.read(master_fd, 10240)
            if o:
                os.write(sys.stdout.fileno(), o)
finally:
    # restore tty settings back
    termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)

【讨论】:

  • 来自文档,pty.spawn 似乎做了类似的事情,但界面更简单。 docs.python.org/3/library/pty.html#example他们也在做同样的事情吗?
  • @balki pty.spawn 正在阻塞:“预计在 pty 后面生成的进程最终将终止,并且当它生成时将返回。”
  • @balki 我查看了github.com/python/cpython/blob/master/Lib/pty.py#L151 上的源代码。我认为pty.spawn 与此代码 sn-p 所做的基本相同。所以是的,使用pty.spawn,因为它会使应用程序代码更简单。
  • 如果进程退出而没有输出,则挂起等待输入(在select.select 上设置超时或将其设置为非阻塞“修复”此问题)。这也需要一些调整窗口大小的工作(SIGWINCH 等)
  • stdout/stderr 可以分开吗?
【解决方案2】:

这是最后对我有用的解决方案(如 qarma 建议的那样):

libc = ctypes.CDLL('libc.so.6')

master, slave = pty.openpty()
p = subprocess.Popen(["/bin/bash", "-i"], preexec_fn=libc.setsid, stdin=slave, stdout=slave, stderr=slave)
os.close(slave)

... do stuff here ...

x = os.read(master, 1026)
print x

【讨论】:

  • 我怀疑你是否需要 -i 来处理所有其他事情,bash 应该检测到它的环境。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-25
  • 1970-01-01
  • 1970-01-01
  • 2018-05-13
  • 1970-01-01
  • 2013-04-16
  • 2020-10-04
相关资源
最近更新 更多