Отличить, объединить, stdout и stderr из подпроцесса.Popen () - PullRequest
1 голос
/ 27 июня 2019

Учитывая произвольный исполняемый файл, который может записывать в stdout или stderr произвольно быстро, например,

#include <stdio.h>

int main(void)
{
    fprintf(stdout, "OUT:1\n");
    fprintf(stdout, "OUT:2\n");
    fprintf(stderr, "ERR:3\n");
    fprintf(stderr, "ERR:4\n");
    fprintf(stdout, "OUT:5\n");
    fprintf(stderr, "ERR:6\n");
    fprintf(stdout, "OUT:7\n");

    return 0;
}

Можно ли получить ОБЪЕДИНЕННЫЙ stdout и stderr, т.е.

stdout_master_fd, stdout_slave_fd = pty.openpty()
subprocess.Popen(stdout=stdout_slave_fd, stderr=subprocess.STDOUT)

И stdout и stderr разделены.

stdout_master_fd, stdout_slave_fd = pty.openpty()
stderr_master_fd, stderr_slave_fd = pty.openpty()
subprocess.Popen(stdout=stdout_slave_fd, stderr=stderr_slave_fd)

Только с одним вызовом subprocess.Popen ()

Есть ряд похожих вопросов:

Первая ссылка использует временные метки, чтобы попытаться отсортировать stderr в stdout, но даже при использовании буферизации ptys, похоже, происходит:

stdout - 2019-06-27T23:43:55.337389 - OUT:1
stdout - 2019-06-27T23:43:55.337389 - OUT:2
stdout - 2019-06-27T23:43:55.337389 - OUT:5
stdout - 2019-06-27T23:43:55.337389 - OUT:7
stderr - 2019-06-27T23:43:55.337413 - ERR:3
stderr - 2019-06-27T23:43:55.337413 - ERR:4
stderr - 2019-06-27T23:43:55.337413 - ERR:6

Плюс, очевидно, что это не может обрабатывать произвольно быстрые записи.

Вторая ссылка использует poll (), но читает только две строки, и точное число меняется, и не упорядочено правильно:

STDOUT:root:OUT:1
STDERR:root:ERR:3
STDOUT:root:OUT:2

Я думаю, что второй метод имеет лучшую предпосылку, но в настоящее время ни один из них не выглядит слишком многообещающим.

Моя версия второго метода, модифицированная из jfs в комментариях ко второй ссылке.

def execute(cmd):
        import subprocess, pty, os, select, logging

        logging.basicConfig(level=logging.INFO)
        logging.addLevelName(logging.INFO+1, 'STDOUT')
        logging.addLevelName(logging.INFO+2, 'STDERR')
        logger = logging.getLogger()

        stdout_master_fd, stdout_slave_fd = pty.openpty()
        stderr_master_fd, stderr_slave_fd = pty.openpty()

        p = subprocess.Popen(cmd, stdout=stdout_slave_fd, stderr=stderr_slave_fd, close_fds=True)

        os.close(stdout_slave_fd)
        os.close(stderr_slave_fd)

        with os.fdopen(stdout_master_fd)as stdout, os.fdopen(stderr_master_fd) as stderr:
            poll = select.poll()
            poll.register(stdout, select.POLLIN)
            poll.register(stderr, select.POLLIN | select.EPOLLHUP)

            def cleanup(_done=[]):
                if _done:
                    return
                _done.append(1)
                poll.unregister(stderr)
                poll.unregister(stdout)
                assert p.poll() is not None

            read_write = {stdout.fileno(): (stdout.readline, lambda s: logger.log(logging.INFO+1, s)),
                          stderr.fileno(): (stderr.readline, lambda s: logger.log(logging.INFO+2, s))}

            while True:
                events = poll.poll(40)

                if not events and p.poll() is not None:
                    cleanup()
                    break

                for fd, event in events:
                    if event & select.POLLIN:
                        read, write = read_write[fd]
                        line = read()
                        if line:
                            write(line.rstrip())
                    elif event & select.POLLHUP:
                        cleanup()
                    else:
                        assert 0

        p.wait()
        return p

execute(['./test'])

Общий результат предпочтительно будет примерно таким:

outs['stdout'] == """
OUT:1
OUT:2
OUT:5
OUT:7
"""

outs['stderr'] == """
ERR:3
ERR:4
ERR:6
"""

outs['merged'] == """
OUT:1
OUT:2
ERR:3
ERR:4
OUT:5
ERR:6
OUT:7
"""

И прямой доступ к возвращаемому значению subprocess.Popen (), даже если была задействована библиотека.

...