Python pty.spawn stdin 未回显但重定向到 master 的 stdout

Python pty.spawn stdin not echoed but redirected to master's stdout

我想从 Python 调用一个程序并让它相信它的 stdout 是一个 tty,即使 Python 的进程标准输出连接到管道。所以我使用 pty.spawn 函数来实现,可以从以下验证:

$ python -c "import sys; from subprocess import call; call(sys.argv[1:])" python -c "import sys; print sys.stdout.isatty()" | cat
False

$ python -c "import sys; import pty; pty.spawn(sys.argv[1:])" python -c "import sys; print sys.stdout.isatty()" | cat
True

(我们看到在第二个命令中我们已经实现了我们的目标,即生成的进程被欺骗认为它的 stdout 是一个 tty。)

但问题是,如果我们使用 pty.spawn,那么它的输入不会被回显,而是被重定向到 master 的标准输出。这可以通过以下命令看到:

$ python -c "import sys; import pty; pty.spawn(sys.argv[1:])" cat > out.txt
$ # Typed "hello" in input, but that is not echoed (use ^D to exit). It is redirected to output.txt
$ cat out.txt
hello
hello

(但是我们使用subprocess.call

就不存在这个问题
$ python -c "import sys; from subprocess import call; call(sys.argv[1:])" cat > out1.txt
hello
$ cat out1.txt
hello

因为它的 stdin 和 stdout 正确地附加到 master。)

我找不到让程序被 Python 调用的方法,它将 stdout 视为 tty(类似于 pty.spawn)但其输入被回显正确(类似于 subprocess.call)。有什么想法吗?

您正在创建一个带有连接到文件的标准输出的终端,因此终端所做的正常 echo-back 被发送到文件而不是屏幕。

我不确定 spawn 是否打算像这样直接使用:pty 库提供 pty.fork() 创建 child 进程和 returns stdin/stdout 的文件描述符。但是您需要更多代码才能使用它。

要解决您目前在使用 spawn 时遇到的问题,这里有两个简单的选择:

选项 1: 如果您只关心将派生命令的输出发送到文件,那么您可以这样做(我更喜欢命名管道和 here python one-liners 的文件):

$ python <(cat << EOF
import sys
import pty
print 'start to stdout only'
pty.spawn(sys.argv[1:])
print 'complete to stdout only'
EOF
) bash -c 'cat > out.txt'

当 运行:

时看起来像这样
start to stdout only
hello
complete to stdout only

这表明输入(我输入了 hello)和打印语句的结果将显示在屏幕上。 out.txt 的内容将是:

$ cat out.txt
hello

也就是说,只有您输入的内容。

选项 2: 另一方面,如果您希望 out 文件在生成的命令输出周围包含 python 输出,那么您需要更多的东西复杂,例如:

python <(cat << EOF
import sys
import pty
import os
old_stdout = sys.stdout
sys.stdout = myfdout = os.fdopen(4,"w")
print 'start to out file only'
myfdout.flush()
pty.spawn(sys.argv[1:])
print 'complete to out file only'
sys.stdout = old_stdout
EOF
) bash -c 'cat >&4' 4>out.txt

只有当 运行(即无论您输入什么)时才会将此输出到终端:

hello

但输出文件将包含:

$ cat out.txt
start to out file only
hello
complete to out file only

背景: python pty 库功能强大:它创建了一个连接到 python 进程 stdout 和 stdin 的终端设备。我想这的大多数用途将使用 pty.fork() 调用,以便真正的 stdin/stdout 不受影响。

但是在您的情况下,在您的 shell,您将 python 进程的标准输出重定向到一个文件。因此,生成的 pty 也将其 stdout 附加到文件,因此将 stdin 回显到 stdout 的正常操作被重定向。常规标准输出(屏幕)仍然存在,但没有被新的 pty 使用。

上面选项 1 的主要区别是将标准输出的重定向移动到 pty.spawn 调用内部的某处,以便创建的 pty 仍然具有清晰的连接到实际的终端标准输出(当它尝试在您键入时回显标准输入时)

选项 2 的区别是在任意文件描述符(即文件描述符 4)上创建第二个通道,并在进入 python 并且当您创建衍生进程时(即将衍生进程的标准输出重定向到相同的文件描述符)

这两个差异防止 pty.spawn 创建的 pty 更改其标准输出或与真实终端断开连接。这允许 stdin 的 echo-back 正常工作。

有些软件包使用 pty 库并为您提供更多控制,但您会发现其中大部分使用 pty.fork()(有趣的是,我还没有找到一个实际使用 pty.spawn)

EDIT 下面是一个使用 pty.fork() 的例子:

import sys
import pty
import os
import select
import time
import tty
import termios

print 'start'
try:
    pid, fd = pty.fork()
    print 'forked'
except OSError as e:
    print e

if pid == pty.CHILD:
    cmd = sys.argv[1]
    args = sys.argv[1:]
    print cmd, args
    os.execvp(cmd,args)
else:
    tty.setraw(fd, termios.TCSANOW)
    try:
        child_file = os.fdopen(fd,'rw')
        read_list = [sys.stdin, child_file]
        while read_list:
            ready = select.select(read_list, [], [], 0.1)[0]
            if not ready and len(read_list) < 2:
                break
            elif not ready:
                time.sleep(1)
            else:
                for file in ready:
                    try:
                        line = file.readline()
                    except IOError as e:
                        print "Ignoring: ", e
                        line = None
                    if not line:
                        read_list.remove(file)
                    else:
                        if file == sys.stdin:
                            os.write(fd,line)
                        else:
                            print "from child:", line
    except KeyboardInterrupt:
        pass

编辑 这个 question 有一些很好的链接 pty.fork()

更新:应该在代码中添加一些注释 pty.fork() 示例的工作原理:

当解释器执行对 pty.fork() 的调用时,处理分为两个:现在有两个线程似乎都刚刚执行了 pty.fork() 调用。

一个线程是您原来所在的线程(parent),一个是新线程(child)。

在 parent 中,pidfd 设置为 child 的进程 ID 和连接到 child 的文件描述符的标准输入和标准输出:在 parent 中,当您从 fd 读取时,您正在读取已写入 childs 标准输出的内容;当您写入 fd 时,您正在写入 childs stdin。所以现在,在 parent 中,我们有一种通过 stdout/stdin.

与另一个线程通信的方法

在child中,pid设置为0,fd未设置。如果我们想与 parent 线程对话,我们可以读取和写入 stdin/stdout 知道 parent 可以而且应该对此做一些事情。

两个线程从此开始执行相同的代码,但是我们可以根据[=35]中的值判断我们是在parent还是child线程=].如果我们想在 child 和 parent 线程中做不同的事情,那么我们只需要一个条件语句,将 child 发送到一个代码路径,将 parent 发送到另一个不同的代码路径代码路径。这就是这一行的作用:

if pid == pty.CHILD:
  #child thread will execute this code
  ....
else
  #parent thread will execute this code
  ...

在 child 中,我们只想在新 pty 中生成新命令。使用 os.execvp 是因为我们可以通过此方法更好地控制 pty 作为终端,但本质上与 pty.spawn()'. This means the child stdin/stdout are now connected to the command you wanted via a pty. IMmportantly, any input or output from the command (or the pty for that matter) will be available to the parent thread by reading fromfd. And the parent can write to the command via pty by writing tofd`

相同

所以现在,在parent中,我们需要通过对fd的读写,将真正的stdin/stdout连接到childstdin/stdout。这就是 parent 代码现在所做的(else 部分)。出现在真实标准输入中的任何数据都被写入 fd。从中读取的任何数据fd(由 parent)写入标准输出。所以 parent 线程现在唯一做的就是在真正的 stdin/stdout 和 fd 之间进行代理。如果您想以编程方式对命令的输入和输出执行某些操作,可以在此处执行此操作。

parent 中唯一发生的另一件事是这个调用:

tty.setraw(fd, termios.TCSANOW)

这是告诉 child 中的 pty 停止做 echo-back 的一种方法。

这解决了您最初遇到的问题: - 您的本地终端仅连接到 parent 线程 - 正常 echo-back 到位(即在您的输入传递到流程之前) - 进程的标准输出可以被重定向 - 无论您对终端标准输出做什么,都不会影响 child 进程的 stdin/stdout - child 进程被告知不要执行其标准输入

的本地 echo-back

这似乎解释了很多 - 如果有人为了清楚起见进行任何编辑?