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

时间:2021-02-04 21:01:29

I want to call a program from Python and make it believe that its stdout is a tty even when Python's process stdout is attached to a pipe. So I used the pty.spawn function to achieve that, which can be verified from the following :

我想从Python调用一个程序,并使其相信即使Python的进程stdout附加到管道,它的stdout也是一个tty。所以我使用了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

(We see that in the second command we have achieved our goal, i.e. the spawned process is tricked into thinking that its stdout is a tty.)

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

But the problem is that if we use pty.spawn then its input is not echoed, rather it is being redirected to the master's stdout. This can be seen by the following command :

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

$ 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

(But this problem does not exists when we use subprocess.call

(但是当我们使用subprocess.call时,这个问题不存在

$ python -c "import sys; from subprocess import call; call(sys.argv[1:])" cat > out1.txt
hello
$ cat out1.txt
hello

since its stdin and stdout are correctly attached to the master.)

因为它的stdin和stdout正确连接到主人。)

I could not find a way so that a program is called by Python, where it sees its stdout as a tty (similar to pty.spawn) but its input is echoed correctly (similar to subprocess.call). Any ideas?

我无法找到一种方法,以便Python调用一个程序,它将stdout视为tty(类似于pty.spawn),但其输入正确回显(类似于subprocess.call)。有任何想法吗?

1 个解决方案

#1


3  

You are creating a terminal with stdout connected to a file so the normal echo-back that terminals do is being sent to the file rather than screen.

您正在创建一个stdout连接到文件的终端,因此终端所执行的正常回显将被发送到文件而不是屏幕。

Im not sure that spawn is intended to be used directly like this: the pty library offers pty.fork() to create a child process and returns a file descriptor for the stdin/stdout. But you'll need a lot more code to use this.

我不确定spawn是否可以像这样直接使用:pty库提供了pty.fork()来创建子进程并返回stdin / stdout的文件描述符。但是你需要更多的代码才能使用它。

To overcome the current problem you are having with spawn, heres two easy options:

要克服当前与spawn有关的问题,还有两个简单的选择:

Option 1: If all you care about is sending the output of the spawned command to a file, then you can do (i prefer named pipes and here files for python one-liners):

选项1:如果你关心的只是将生成的命令的输出发送到文件,那么你可以这样做(我更喜欢命名管道和这里的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'

which will look like this when run:

运行时看起来像这样:

start to stdout only
hello
complete to stdout only

that shows that the input (I typed hello) and the result of print statements are going to the screen. The contents of out.txt will be:

这表明输入(我键入的hello)和print语句的结果将进入屏幕。 out.txt的内容将是:

$ cat out.txt
hello

That is, only what you typed.

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

Option 2: If on the other hand you want the out file to contain the python output around the spawned command output, then you need something a bit more complicated, like:

选项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

which will only have this output to the terminal when run (ie whatever you type):

运行时只输出到终端的输出(即输入的内容):

hello

but the out file will contain:

但是out文件将包含:

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

Background: python pty library is powerful: its creating a terminal device attached to the python processes stdout and stdin. Id imagine most uses of this will use the pty.fork() call so that real stdin/stdout are not affected.

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

However in your case, at your shell, you redirected the stdout of the python process to a file. The resulting pty also therefore had its stdout attached to the file so the normal action of echoing stdin back to stdout was being redirected. The regular stdout (screen) was still in place but not being used by the new pty.

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

The key difference for Option 1 above is to move the redirection of stdout to occur somewhere inside the pty.spawn call, so that the pty created still has a clear connection to the actual terminal stdout (for when it tries to echo stdin as you type)

上面选项1的关键区别是将stdout的重定向移动到pty.spawn调用内的某个位置,这样创建的pty仍然与实际的终端stdout有明确的连接(当它在你键入时尝试回显stdin时) )

The difference for Option 2 is to create a second channel on an arbitrary file descriptor (ie file descriptor 4) and use this in place of stdout, once you are inside python and when you create your spawned process (ie redirect the stdout of your spawned process to the same file descriptor)

选项2的区别在于在任意文件描述符(即文件描述符4)上创建第二个通道,并使用它代替stdout,一旦你在python中并且你创建了生成的进程(即重定向你生成的stdout)进程到同一个文件描述符)

Both of these difference prevent the pty that pty.spawn creates from having its stdout changed or disconnected from the real terminal. This allows the echo-back of stdin to work properly.

这两个差异都会阻止pty.spawn通过更改stdout或从实际终端断开连接而创建的pty。这允许stdin的回显正常工作。

There are packages that use the pty library and give you more control but you'll find most of these use pty.fork() (and interestingly I havent found one yet that actually uses pty.spawn)

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

EDIT Heres an example of using pty.fork():

编辑下面是使用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

EDIT This question has some good links for pty.fork()

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

UPDATE: should have put some comments in the code How the pty.fork() example works:

更新:应该在代码中放置一些注释pty.fork()示例如何工作:

When the interpretor executes the call to pty.fork(), the processesing splits into two: there are now two threads that both appear to have just executed the pty.fork() call.

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

One thread is the thread you were originally in (the parent) and one is a new thread (the child).

一个线程是您最初的线程(父线程),一个是新线程(子线程)。

In the parent, the pid and fd are set to the process id of the child and a file decriptor connnected to teh child's stdin and stdout: in the parent, when you read from fd you are reading what has been written to the childs stdout; when you write to fd you are writing to the childs stdin. So now, in the parent we have a way of communicating with the other thread over its stdout/stdin.

在父级中,pid和fd被设置为子级的进程id和文件描述符连接到子级的stdin和stdout:在父级中,当您从fd读取时,您正在读取已写入childs stdout的内容;当你写到fd时,你正在给孩子们写信。所以现在,在父级中我们有一种通过其stdout / stdin与另一个线程进行通信的方式。

In the child, the pid is set to 0 and the fd is not set. If we want to talk to the parent thread, we can read and write over stdin/stdout knowing that the parent can and should do something with this.

在子节点中,pid设置为0并且未设置fd。如果我们想与父线程交谈,我们可以通过stdin / stdout读取和写入,知道父母可以而且应该对此做些什么。

The two threads are going to execute the same code from this point on, but we can tell if we are in the parent or the child thread based on the value in pid. If we want to do different things in the child and parent threads then we just need a conditional statement that sends the child down one code path and the parent down a different code path. Thats what this line does:

这两个线程将从此时开始执行相同的代码,但我们可以根据pid中的值判断我们是在父线程还是子线程中。如果我们想在子线程和父线程中执行不同的操作,那么我们只需要一个条件语句,将子进程发送到一个代码路径,将父进程发送到不同的代码路径。这就是这条线的作用:

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

In the child, we simply want to spawn the new command in a new pty. The os.execvp is used becuase we will have more control over the pty as a terminal with this method but essentially its the same as 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`

在孩子中,我们只想在新的pty中生成新命令。使用os.execvp是因为我们将使用此方法更多地控制pty作为终端,但基本上与pty.spawn()'相同。这意味着子stdin / stdout现在通过pty连接到您想要的命令。重要的是,通过读取fromfd,父线程可以使用命令的任何输入或输出(或者pty)。父母可以通过编写tofd`来写入命令

So now, in the parent, we need to connect the real stdin/stdout to the child stdin/stdout via reading and writing to fd. Thats what the parent code now does (the else part). Any data that turns up on the real stdin is written out to fd. Any data read from fd (by the parent) is written to stdout. So the only thing the parent thread is now doing is proxying between the real stdin/stdout and fd. If you wanted to do something with the input and output of you command programatically, this is where you would do it.

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

The only other thing that happens in the parent is this call:

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

tty.setraw(fd, termios.TCSANOW)

This is one way to tell the pty in the child to stop doing echo-back.

这是告诉孩子的pty停止回声的一种方法。

This solves the problem you were originally having: - your local terminal is connected only to the parent thread - normal echo-back is in place (ie before your input is passed into the process) - stdout of the process can be redirected - whatever you do with your terminal stdout has no impact on the stdin/stdout of the child process - the child process has been told to not do local echo-back of its stdin

这解决了您最初遇到的问题: - 您的本地终端仅连接到父线程 - 正常回显就绪(即在您的输入传递到进程之前) - 进程的stdout可以重定向 - 无论你是什么使用你的终端stdout对子进程的stdin / stdout没有影响 - 子进程被告知不要对其stdin进行本地回显

That seems like a lot of explanation - if anyone has any edits for clarity?

这似乎有很多解释 - 如果有人为了清晰起见有任何修改?

#1


3  

You are creating a terminal with stdout connected to a file so the normal echo-back that terminals do is being sent to the file rather than screen.

您正在创建一个stdout连接到文件的终端,因此终端所执行的正常回显将被发送到文件而不是屏幕。

Im not sure that spawn is intended to be used directly like this: the pty library offers pty.fork() to create a child process and returns a file descriptor for the stdin/stdout. But you'll need a lot more code to use this.

我不确定spawn是否可以像这样直接使用:pty库提供了pty.fork()来创建子进程并返回stdin / stdout的文件描述符。但是你需要更多的代码才能使用它。

To overcome the current problem you are having with spawn, heres two easy options:

要克服当前与spawn有关的问题,还有两个简单的选择:

Option 1: If all you care about is sending the output of the spawned command to a file, then you can do (i prefer named pipes and here files for python one-liners):

选项1:如果你关心的只是将生成的命令的输出发送到文件,那么你可以这样做(我更喜欢命名管道和这里的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'

which will look like this when run:

运行时看起来像这样:

start to stdout only
hello
complete to stdout only

that shows that the input (I typed hello) and the result of print statements are going to the screen. The contents of out.txt will be:

这表明输入(我键入的hello)和print语句的结果将进入屏幕。 out.txt的内容将是:

$ cat out.txt
hello

That is, only what you typed.

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

Option 2: If on the other hand you want the out file to contain the python output around the spawned command output, then you need something a bit more complicated, like:

选项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

which will only have this output to the terminal when run (ie whatever you type):

运行时只输出到终端的输出(即输入的内容):

hello

but the out file will contain:

但是out文件将包含:

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

Background: python pty library is powerful: its creating a terminal device attached to the python processes stdout and stdin. Id imagine most uses of this will use the pty.fork() call so that real stdin/stdout are not affected.

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

However in your case, at your shell, you redirected the stdout of the python process to a file. The resulting pty also therefore had its stdout attached to the file so the normal action of echoing stdin back to stdout was being redirected. The regular stdout (screen) was still in place but not being used by the new pty.

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

The key difference for Option 1 above is to move the redirection of stdout to occur somewhere inside the pty.spawn call, so that the pty created still has a clear connection to the actual terminal stdout (for when it tries to echo stdin as you type)

上面选项1的关键区别是将stdout的重定向移动到pty.spawn调用内的某个位置,这样创建的pty仍然与实际的终端stdout有明确的连接(当它在你键入时尝试回显stdin时) )

The difference for Option 2 is to create a second channel on an arbitrary file descriptor (ie file descriptor 4) and use this in place of stdout, once you are inside python and when you create your spawned process (ie redirect the stdout of your spawned process to the same file descriptor)

选项2的区别在于在任意文件描述符(即文件描述符4)上创建第二个通道,并使用它代替stdout,一旦你在python中并且你创建了生成的进程(即重定向你生成的stdout)进程到同一个文件描述符)

Both of these difference prevent the pty that pty.spawn creates from having its stdout changed or disconnected from the real terminal. This allows the echo-back of stdin to work properly.

这两个差异都会阻止pty.spawn通过更改stdout或从实际终端断开连接而创建的pty。这允许stdin的回显正常工作。

There are packages that use the pty library and give you more control but you'll find most of these use pty.fork() (and interestingly I havent found one yet that actually uses pty.spawn)

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

EDIT Heres an example of using pty.fork():

编辑下面是使用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

EDIT This question has some good links for pty.fork()

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

UPDATE: should have put some comments in the code How the pty.fork() example works:

更新:应该在代码中放置一些注释pty.fork()示例如何工作:

When the interpretor executes the call to pty.fork(), the processesing splits into two: there are now two threads that both appear to have just executed the pty.fork() call.

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

One thread is the thread you were originally in (the parent) and one is a new thread (the child).

一个线程是您最初的线程(父线程),一个是新线程(子线程)。

In the parent, the pid and fd are set to the process id of the child and a file decriptor connnected to teh child's stdin and stdout: in the parent, when you read from fd you are reading what has been written to the childs stdout; when you write to fd you are writing to the childs stdin. So now, in the parent we have a way of communicating with the other thread over its stdout/stdin.

在父级中,pid和fd被设置为子级的进程id和文件描述符连接到子级的stdin和stdout:在父级中,当您从fd读取时,您正在读取已写入childs stdout的内容;当你写到fd时,你正在给孩子们写信。所以现在,在父级中我们有一种通过其stdout / stdin与另一个线程进行通信的方式。

In the child, the pid is set to 0 and the fd is not set. If we want to talk to the parent thread, we can read and write over stdin/stdout knowing that the parent can and should do something with this.

在子节点中,pid设置为0并且未设置fd。如果我们想与父线程交谈,我们可以通过stdin / stdout读取和写入,知道父母可以而且应该对此做些什么。

The two threads are going to execute the same code from this point on, but we can tell if we are in the parent or the child thread based on the value in pid. If we want to do different things in the child and parent threads then we just need a conditional statement that sends the child down one code path and the parent down a different code path. Thats what this line does:

这两个线程将从此时开始执行相同的代码,但我们可以根据pid中的值判断我们是在父线程还是子线程中。如果我们想在子线程和父线程中执行不同的操作,那么我们只需要一个条件语句,将子进程发送到一个代码路径,将父进程发送到不同的代码路径。这就是这条线的作用:

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

In the child, we simply want to spawn the new command in a new pty. The os.execvp is used becuase we will have more control over the pty as a terminal with this method but essentially its the same as 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`

在孩子中,我们只想在新的pty中生成新命令。使用os.execvp是因为我们将使用此方法更多地控制pty作为终端,但基本上与pty.spawn()'相同。这意味着子stdin / stdout现在通过pty连接到您想要的命令。重要的是,通过读取fromfd,父线程可以使用命令的任何输入或输出(或者pty)。父母可以通过编写tofd`来写入命令

So now, in the parent, we need to connect the real stdin/stdout to the child stdin/stdout via reading and writing to fd. Thats what the parent code now does (the else part). Any data that turns up on the real stdin is written out to fd. Any data read from fd (by the parent) is written to stdout. So the only thing the parent thread is now doing is proxying between the real stdin/stdout and fd. If you wanted to do something with the input and output of you command programatically, this is where you would do it.

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

The only other thing that happens in the parent is this call:

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

tty.setraw(fd, termios.TCSANOW)

This is one way to tell the pty in the child to stop doing echo-back.

这是告诉孩子的pty停止回声的一种方法。

This solves the problem you were originally having: - your local terminal is connected only to the parent thread - normal echo-back is in place (ie before your input is passed into the process) - stdout of the process can be redirected - whatever you do with your terminal stdout has no impact on the stdin/stdout of the child process - the child process has been told to not do local echo-back of its stdin

这解决了您最初遇到的问题: - 您的本地终端仅连接到父线程 - 正常回显就绪(即在您的输入传递到进程之前) - 进程的stdout可以重定向 - 无论你是什么使用你的终端stdout对子进程的stdin / stdout没有影响 - 子进程被告知不要对其stdin进行本地回显

That seems like a lot of explanation - if anyone has any edits for clarity?

这似乎有很多解释 - 如果有人为了清晰起见有任何修改?