web-dev-qa-db-ja.com

Pythonマルチプロセッシング:子プロセスからstdoutを確実にリダイレクトするにはどうすればよいですか?

注意。 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()

私と同じ動作を確認するには、DLLは、Pythonが使用するものとは異なるCランタイムに対してビルドする必要があります。私の場合、pythonはVisualStudio 2010でビルドされていますが、私のDLLはVS2005でビルドされています。

私が見る動作は、コンソールが次のように表示することです。

> 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を使用してすべてを手動で行うためにおそらくふっくらと思います。

30
Tom

あなたが提案する解決策は良いものです:あなたがそれらのstdout/stderrファイルハンドルに明示的にアクセスできるようにあなたのプロセスを手動で作成してください。次に、サブプロセスと通信するソケットを作成し、そのソケットでmultiprocessing.connectionを使用できます(multiprocessing.Pipeは同じタイプの接続オブジェクトを作成するため、これによりすべて同じIPC =機能)。

これは2ファイルの例です。

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

サブプロセスから非ブロッキング読み取りを取得するには、 この質問 に対する最初の回答を確認することもできます。

8
Luke

コメントで述べたように、サブプロセスをファイルにリダイレクトするよりも良い選択肢はないと思います。

コンソールのstdin/out/errがWindowsで機能する方法は、プロセスが生まれたときの各プロセスで、 stdハンドル が定義されています。 SetStdHandle で変更できます。 Pythonのsys.stdoutを変更するときは、pythonが印刷する場所のみを変更し、他のDLLが印刷する場所は変更しません。 DLLのCRTの一部は、GetStdHandleを使用して印刷先を見つけています。必要に応じて、WindowsAPIのDLLまたはpythonスクリプトでpywin32を使用して任意のパイピングを実行できます。 subprocess の方が簡単だと思いますが。

1
ubershmekel

私はベースから外れていて何かが足りないと思いますが、ここで価値があるのは、あなたの質問を読んだときに頭に浮かんだことです。

Stdoutとstderrのすべてをインターセプトできる場合(あなたの質問からその印象を受けました)、各プロセスの周りにそのキャプチャ機能を追加またはラップしてみませんか?次に、キューを介してキャプチャされたものを、すべての出力で必要なことを実行できるコンシューマーに送信しますか?

0
KobeJohn

私の状況では、sys.stdout.writeをPySideQTextEditに書き込むように変更しました。 sys.stdoutから読み取ることができず、sys.stdoutを読み取り可能に変更する方法がわかりませんでした。 2本のパイプを作成しました。 1つはstdout用で、もう1つはstderr用です。別のプロセスで、sys.stdoutsys.stderrをマルチプロセッシングパイプの子接続にリダイレクトします。メインプロセスで、stdoutとstderrの親パイプを読み取り、パイプデータをsys.stdoutsys.stderrにリダイレクトする2つのスレッドを作成しました。

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()
0
justengel