Python中的PIPE到popen stdin

4

我正在尝试与通过 stdout 和 PIPE 实时运行 subprocess.Popen非常相似的东西。

然而,我也想向正在运行的进程发送输入。

如果我使用单独的线程启动进程,则可以使用

process = subprocess.Popen(cmd,stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

我可以使用终端发送输入。

如果我想从其他来源发送输入,例如不在线程中的单独函数,该怎么办?

我无法使用Popen.communicate,因为正在运行的进程永远不会结束,因为我正在尝试与程序进行实时交互。

提前感谢你的帮助。

这是我的完整代码,我希望在点击发送按钮时将输入发送到子进程过程。

from Tkinter import *`
from ttk import *`
import subprocess
from threading import Thread

class Example(Frame):

    def __init__(self, parent):
       Frame.__init__(self, parent)   

        self.parent = parent
        self.initUI()


    def initUI(self):    

        self.parent.title("Test Client")
        self.style = Style()
        self.style.theme_use("default")
        self.pack(fill=BOTH, expand=1)

        #Label, doesnt change
        lbl = Label(self, text="Client:")
        lbl.grid(row=0, column=1, sticky=W )

        #when output from client is shown
        global display
        display = Text(self,width=50,height=20)
        display.grid(row=1, column=1, sticky=E+W+N+S)

        #where user input is taken
        global prompt
        prompt = Entry(self,width=50)
        prompt.grid(row=3, column=1, sticky=E+W+N+S)

        #Button that will send input to client
        send = Button(self,text="Send",command=self.send)
        send.grid(row=3, column=2, sticky=N)
        get = Button(self,text="Get",command=self.get)
        get.grid(row=2, column=2, sticky=S)

    def get(self):
        print foo

    def send(self):
        sent = prompt.get()


def MyThread():
     global sent
     sent = 2
     cmd = ['nc', '-l', '-p', '50000']

     process = subprocess.Popen(cmd,stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    while True:
        out = process.stdout.read(1)
        if out == '' and process.poll() != None:
            break
        if out != '':
            display.insert(INSERT, out)
            sys.stdout.write(out)
            sys.stdout.flush()

def main():
    root = Tk()
    root.geometry("500x410+300+300")
    app = Example(root)

    thread = Thread(target = MyThread, args=())
    thread.start()

    root.mainloop()

if __name__ == '__main__':
    main()  

你应该可以使用Popen.communicate()来实现,你试过了吗?当你这样做时会发生什么? - Lie Ryan
1
@LieRyan: communicate 等待子进程完成并读取其所有输出。由于“运行的进程永远不会结束”,这意味着 communicate 永远不会返回。因此,他不能使用它。 - abarnert
1
你不应该直接从后台线程调用GUI(它可能会或可能不会间歇性地工作)。你可以使用队列和root.after()在线程中安排对GUI的调用。避免不必要地使用global,而可以使用实例变量,例如self.prompt。你可以使用socketselect模块代替运行netcat - jfs
1
@J.F.Sebastian:关于直接使用套接字而不是编写“netcat”脚本的观点很好。(您可能仍需要一个单独的线程来服务该套接字...但比为子进程提供3-4个线程要好得多...而且更简单。 它将在具有BSD或Hobbit netcat的系统上运行,而不是GNU netcat,或者根本没有netcat等。等等。) - abarnert
3个回答

6
首先,显然需要在 Popen 构造函数中添加 stdin=subprocess.PIPE,然后就可以像使用 process.stdout.read 一样使用 process.stdin.write 写入。但是,如果子进程没有读取数据,则像 read 一样,write 也会阻塞。
此外,即使超越了明显的部分,要在与交互式程序使用 Popen 中两个方向的 PIPE 的情况下避免阻塞实际上非常困难。如果您真的想做到这一点,请查看 communicate 源代码 查看其工作原理(3.2 之前存在已知的错误,因此如果您在 2.x 上,则可能需要进行一些回溯)。您将不得不自己实现代码,并且如果您希望它跨平台,您将不得不完成 communicate 在内部执行的所有混乱(为管道生成读者和写者线程等),当然还要在每次尝试进行通信时添加另一个线程以不阻塞主线程,并添加某种机制来在子进程就绪时向主线程发送消息等。
或者,您可以查看 PyPI 上的各种“异步子进程”项目。我今天所知道的最简单的一个是 async_subprocess,它基本上只提供了一个无需阻塞即可使用的 communicate
或者,如果您可以使用 twisted(或可能是其他基于事件的网络框架),则有包装器包装 subprocess 并插入其事件循环中。 (如果您可以等待 Python 3.4,或者在 3.3 上使用正在进行的工作 tulip,则有人已经构建了类似于 tulip 的东西,可能会进入 3.4。)而且 twisted 甚至知道如何插入 Tkinter 中,因此您不必手动处理两个单独的事件循环并在它们之间通信。
如果您只关心现代 POSIX 系统(而非 Windows),则可以通过将管道置于非阻塞模式并将代码编写为处理套接字的方式使其更简单。

但最简单的解决方案可能是使用类似于pexpect这样的工具,而不是尝试手动编写脚本。(正如J.F. Sebastian所指出的那样,pexpect仅适用于Unix系统,但您可以使用Unix上的pexpect包装器和Windows上的winpexpect。)


我已经查看了通信,并且由于它是阻塞调用,所以它无法与我希望运行的命令配合使用,而pexpect也无法使用,因为我不是在尝试编写脚本,而是拥有一个交互式程序。 - Mr S
2
@MrS:我并没有说要使用“communicate”,我是说你必须查看源代码,以便了解你需要处理的所有事情,以便自己实现这个功能。这并不容易。 - abarnert
2
@MrS:另外,谁告诉你pexpect不能用于编写交互式程序脚本?这正是它的全部意义所在。 - abarnert
1
在Windows上也有winpexpect模块 - jfs

5
标准库中的select模块适用于这种情况:
process = subprocess.Popen(cmd,stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

while True:
   reads,writes,excs = select.select([process.stdout, process.stderr], [process.stdin], [], 1)
   for r in reads:
       out = r.read(1)
       display.insert(INSERT, out)
       sys.stdout.write(out)
       sys.stdout.flush()
   for w in writes:
       w.write('a')

你可以将文件对象或文件描述符的列表传递给 select(),它将返回那些准备好进行读/写操作的文件,或直到可选超时时间。

select 模块适用于 Windows 和类 Unix 系统(Linux、Mac 等等)。


"select" 是仅在 *nix 上可用的解决方案(只能在 Windows 上使用套接字,即 select 在管道上无法工作)。必须在某个地方调用 process.poll() 来检查进程是否完成。 - jfs
它可以在大多数*nix平台上运行,但不是所有平台都支持。在某些平台上,只有在您明确将管道设置为非阻塞模式后才能正常工作,而您在此处没有这样做。 - abarnert

0
一个简单的便携式解决方案,只需最小限度地更改代码,就可以创建一个写入线程,从队列中获取项目并将它们写入进程的标准输入,每当按下按钮时,就将值放入队列中:
from subprocess import Popen, PIPE, STDOUT
from Queue import Queue

class Example(Frame):
    def __init__(self, parent, queue):
       # ...
       self.queue = queue
    # ...
    def send(self): # should be call on the button press
        self.queue.put(prompt.get())

def writer(input_queue, output): 
    for item in iter(input_queue.get, None): # get items until None found
        output.write(item)
    output.close()

def MyThread(queue):
    # ...
    #NOTE: you must set `stdin=PIPE` if you wan't to write to process.stdin
    process = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
    Thread(target=writer, args=[queue, process.stdin]).start()
    # ...

def main():
    # ...
    queue = Queue()
    app = Example(root, queue)
    Thread(target=MyThread, args=[queue]).start()
    # ...
    root.mainloop()
    queue.put(None) # no more input for the subprocess

可能对某些人有用;但我必须在output.write()之后添加output.flush()才能使其正常工作。 - Raj
@Raj 在Python 2中,bufsize=0output=process.stdin因此在这种情况下output.flush()是无用的。 - jfs
我正在使用Python 3.x。有什么原因我必须使用flush()(相信缓冲区在推入流之前被填满了)...不确定是否有更好的方法。 - Raj
1
问题和我的回答都使用Python 2。在Python 3上,默认值不同,即bufsize=-1(块缓冲模式),并且output.flush()可能会有用。在Python 3上还有其他更改,例如文本与二进制模式。 - jfs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接