Python多处理:如何从子进程可靠地重定向stdout?

时间:2011-10-10 15:10:50

标签: python windows stdout multiprocessing stderr

NB。我见过Log output of multiprocessing.Process - 不幸的是,它没有回答这个问题。

我正在通过多处理创建子进程(在Windows上)。我希望将子进程的stdout和stderr输出的 all 重定向到日志文件,而不是出现在控制台上。我看到的唯一建议是子进程将sys.stdout设置为文件。但是,由于Windows上的stdout重定向行为,这不能有效地重定向所有stdout输出。

要说明此问题,请使用以下代码构建Windows DLL

#include <iostream>

extern "C"
{
    __declspec(dllexport) void writeToStdOut()
    {
        std::cout << "Writing to STDOUT from test DLL" << std::endl;
    }
}

然后创建并运行如下所示的python脚本,该脚本导入此DLL并调用函数:

from ctypes import *
import sys

print
print "Writing to STDOUT from python, before redirect"
print
sys.stdout = open("stdout_redirect_log.txt", "w")
print "Writing to STDOUT from python, after redirect"

testdll = CDLL("Release/stdout_test.dll")
testdll.writeToStdOut()

为了看到与我相同的行为,可能需要针对不同于Python使用的不同C运行时构建DLL。在我的例子中,python是使用Visual Studio 2010构建的,但我的DLL是使用VS 2005构建的。

我看到的行为是控制台显示:

> stdout_test.py

Writing to STDOUT from python, before redirect

Writing to STDOUT from test DLL

文件stdout_redirect_log.txt最终包含:

Writing to STDOUT from python, after redirect

换句话说,设置sys.stdout无法重定向DLL生成的stdout输出。鉴于Windows中stdout重定向的基础API的性质,这并不令人惊讶。我以前在本机/ C ++级别遇到过这个问题,但从未找到一种方法可以在进程内可靠地重定向stdout。它必须在外部完成。

这实际上是我启动子进程的原因 - 这样我可以在外部连接到它的管道,从而保证我可以拦截它的所有输出。我可以通过使用pywin32手动启动进程来实现这一点,但我非常希望能够使用多处理功能,特别是通过多处理Pipe对象与子进程通信的能力,以便获得进步更新。问题是,是否有任何方法可以为其IPC工具使用多处理,以便可靠地将所有子项的stdout和stderr输出重定向到文件。

UPDATE:查看multiprocessing.Processs的源代码,它有一个静态成员_Popen,看起来它可以用来覆盖用于创建进程的类。如果它设置为None(默认值),它使用multiprocessing.forking._Popen,但它看起来像是说

multiprocessing.Process._Popen = MyPopenClass

我可以覆盖流程创建。然而,虽然我可以从multiprocessing.forking._Popen中得到这个,但看起来我必须将一堆内部东西复制到我的实现中,这听起来很脆弱,而且不太适合未来。如果这是唯一的选择我认为我可能会用pywin32手动完成整个事情。

5 个答案:

答案 0 :(得分:7)

您建议的解决方案是一个很好的解决方案:手动创建您的流程,以便您可以显式访问其stdout / stderr文件句柄。然后,您可以创建一个套接字以与子进程通信,并在该套接字上使用multiprocessing.connection(multiprocessing.Pipe创建相同类型的连接对象,因此这应该为您提供所有相同的IPC功能)。

这是一个双文件示例。

<强> master.py:

import multiprocessing.connection
import subprocess
import socket
import sys, os

## Listen for connection from remote process (and find free port number)
port = 10000
while True:
    try:
        l = multiprocessing.connection.Listener(('localhost', int(port)), authkey="secret")
        break
    except socket.error as ex:
        if ex.errno != 98:
            raise
        port += 1  ## if errno==98, then port is not available.

proc = subprocess.Popen((sys.executable, "subproc.py", str(port)), stdout=subprocess.PIPE, stderr=subprocess.PIPE)

## open connection for remote process
conn = l.accept()
conn.send([1, "asd", None])
print(proc.stdout.readline())

<强> subproc.py:

import multiprocessing.connection
import subprocess
import sys, os, time

port = int(sys.argv[1])
conn = multiprocessing.connection.Client(('localhost', port), authkey="secret")

while True:
    try:
        obj = conn.recv()
        print("received: %s\n" % str(obj))
        sys.stdout.flush()
    except EOFError:  ## connection closed
        break

您可能还希望看到this question的第一个答案,以便从子流程中获取非阻塞读取。

答案 1 :(得分:1)

我认为您没有比在评论中提到的将子流程重定向到文件更好的选择。

控制台stdin / out / err在Windows中的工作方式是它出生时的每个进程都定义了std handles。您可以使用SetStdHandle更改它们。当你修改python的sys.stdout时,你只能修改python打印出来的东西,而不是其他DLL打印的东西。 DLL中的部分CRT使用GetStdHandle来查找要打印到的位置。如果你愿意,你可以在你的DLL或你的python 32脚本中使用pywin32做你想要的任何管道。虽然我确实认为subprocess会更简单。

答案 2 :(得分:0)

我认为我已经离开了基地,并且遗漏了一些东西,但是在我看到你的问题时,我想到的是什么。

如果您可以拦截所有stdout和stderr(我从您的问题中得到了这种印象),那么为什么不在每个进程周围添加或包装捕获功能呢?然后将通过队列捕获的内容发送给消费者,消费者可以使用所有输出执行任何操作吗?

答案 3 :(得分:0)

在我的情况下,我更改了sys.stdout.write以写到PySide QTextEdit。我无法读取sys.stdout,也不知道如何更改sys.stdout以使其可读。我创建了两个管道。一个用于stdout,另一个用于stderr。在单独的过程中,我将sys.stdoutsys.stderr重定向到多处理管道的子连接。在主进程上,我创建了两个线程来读取stdout和stderr父管道,并将管道数据重定向到sys.stdoutsys.stderr

import sys
import contextlib
import threading
import multiprocessing as mp
import multiprocessing.queues
from queue import Empty
import time


class PipeProcess(mp.Process):
    """Process to pipe the output of the sub process and redirect it to this sys.stdout and sys.stderr.

    Note:
        The use_queue = True argument will pass data between processes using Queues instead of Pipes. Queues will
        give you the full output and read all of the data from the Queue. A pipe is more efficient, but may not
        redirect all of the output back to the main process.
    """
    def __init__(self, group=None, target=None, name=None, args=tuple(), kwargs={}, *_, daemon=None,
                 use_pipe=None, use_queue=None):
        self.read_out_th = None
        self.read_err_th = None
        self.pipe_target = target
        self.pipe_alive = mp.Event()

        if use_pipe or (use_pipe is None and not use_queue):  # Default
            self.parent_stdout, self.child_stdout = mp.Pipe(False)
            self.parent_stderr, self.child_stderr = mp.Pipe(False)
        else:
            self.parent_stdout = self.child_stdout = mp.Queue()
            self.parent_stderr = self.child_stderr = mp.Queue()

        args = (self.child_stdout, self.child_stderr, target) + tuple(args)
        target = self.run_pipe_out_target

        super(PipeProcess, self).__init__(group=group, target=target, name=name, args=args, kwargs=kwargs,
                                          daemon=daemon)

    def start(self):
        """Start the multiprocess and reading thread."""
        self.pipe_alive.set()
        super(PipeProcess, self).start()

        self.read_out_th = threading.Thread(target=self.read_pipe_out,
                                            args=(self.pipe_alive, self.parent_stdout, sys.stdout))
        self.read_err_th = threading.Thread(target=self.read_pipe_out,
                                            args=(self.pipe_alive, self.parent_stderr, sys.stderr))
        self.read_out_th.daemon = True
        self.read_err_th.daemon = True
        self.read_out_th.start()
        self.read_err_th.start()

    @classmethod
    def run_pipe_out_target(cls, pipe_stdout, pipe_stderr, pipe_target, *args, **kwargs):
        """The real multiprocessing target to redirect stdout and stderr to a pipe or queue."""
        sys.stdout.write = cls.redirect_write(pipe_stdout)  # , sys.__stdout__)  # Is redirected in main process
        sys.stderr.write = cls.redirect_write(pipe_stderr)  # , sys.__stderr__)  # Is redirected in main process

        pipe_target(*args, **kwargs)

    @staticmethod
    def redirect_write(child, out=None):
        """Create a function to write out a pipe and write out an additional out."""
        if isinstance(child, mp.queues.Queue):
            send = child.put
        else:
            send = child.send_bytes  # No need to pickle with child_conn.send(data)

        def write(data, *args):
            try:
                if isinstance(data, str):
                    data = data.encode('utf-8')

                send(data)
                if out is not None:
                    out.write(data)
            except:
                pass
        return write

    @classmethod
    def read_pipe_out(cls, pipe_alive, pipe_out, out):
        if isinstance(pipe_out, mp.queues.Queue):
            # Queue has better functionality to get all of the data
            def recv():
                return pipe_out.get(timeout=0.5)

            def is_alive():
                return pipe_alive.is_set() or pipe_out.qsize() > 0
        else:
            # Pipe is more efficient
            recv = pipe_out.recv_bytes  # No need to unpickle with data = pipe_out.recv()
            is_alive = pipe_alive.is_set

        # Loop through reading and redirecting data
        while is_alive():
            try:
                data = recv()
                if isinstance(data, bytes):
                    data = data.decode('utf-8')
                out.write(data)
            except EOFError:
                break
            except Empty:
                pass
            except:
                pass

    def join(self, *args):
        # Wait for process to finish (unless a timeout was given)
        super(PipeProcess, self).join(*args)

        # Trigger to stop the threads
        self.pipe_alive.clear()

        # Pipe must close to prevent blocking and waiting on recv forever
        if not isinstance(self.parent_stdout, mp.queues.Queue):
            with contextlib.suppress():
                self.parent_stdout.close()
            with contextlib.suppress():
                self.parent_stderr.close()

        # Close the pipes and threads
        with contextlib.suppress():
            self.read_out_th.join()
        with contextlib.suppress():
            self.read_err_th.join()

def run_long_print():
    for i in range(1000):
        print(i)
        print(i, file=sys.stderr)

    print('finished')


if __name__ == '__main__':
    # Example test write (My case was a QTextEdit)
    out = open('stdout.log', 'w')
    err = open('stderr.log', 'w')

    # Overwrite the write function and not the actual stdout object to prove this works
    sys.stdout.write = out.write
    sys.stderr.write = err.write

    # Create a process that uses pipes to read multiprocess output back into sys.stdout.write
    proc = PipeProcess(target=run_long_print, use_queue=True)  # If use_pipe=True Pipe may not write out all values
    # proc.daemon = True  # If daemon and use_queue Not all output may be redirected to stdout
    proc.start()

    # time.sleep(5)  # Not needed unless use_pipe or daemon and all of stdout/stderr is desired

    # Close the process
    proc.join()  # For some odd reason this blocks forever when use_queue=False

    # Close the output files for this test
    out.close()
    err.close()

答案 4 :(得分:0)

这是捕获multiprocessing.Process的标准输出的简单明了的方法:

import app
import io
import sys
from multiprocessing import Process


def run_app(some_param):
    sys.stdout = io.TextIOWrapper(open(sys.stdout.fileno(), 'wb', 0), write_through=True)
    app.run()

app_process = Process(target=run_app, args=('some_param',))
app_process.start()
# Use app_process.termninate() for python <= 3.7.
app_process.kill()