читать с таймаутом из локального процесса в псевдо-терминале - PullRequest
1 голос
/ 23 апреля 2020

Я хочу, например, прочитать первую строку, напечатанную "tcpdump":

tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes

, используя "ptyprocess" (контекст: локальный процесс, вовлеченный терминал) и выберите () для ожидания новых данных с время ожидания:

import logging
from ptyprocess import PtyProcess
from select import select

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s %(name)s %(message)s")

pty_process = PtyProcess.spawn(
    argv=["sudo", "tcpdump", "-w", "capture.pcap", "-i", "enp0s3"],
    echo=True)
while True:
    rlist, _, _ = select([pty_process.fd], [], [], 1)
    if pty_process.fd in rlist:
        try:
            data = pty_process.read(1)
        except EOFError:
            logging.debug("EOF")
            break
        logging.debug("read: %r", data)
    else:
        logging.debug("timeout")

Для Python 3.x (протестировано с 3.6.10 и 3.8.1) этот код читает вышеупомянутую строку, напечатанную "tcpdump".

Для Python 2.x (протестировано с 2.7.17) этот код считывает только первый символ "t", а после этого select () останавливается. Я также заметил, что для первого запуска было прочитано более одного символа, но не все.

Протестировано в Debian 10.

Как я могу использовать select () с таймаутом (или что-то похожее) с "ptyprocess" для ожидания новых данных, прежде чем я прочитал следующий символ в Python 2?

Обновление 1:

strace показывает следующую разницу:

Python 2:

select(6, [5], [], [], {tv_sec=1, tv_usec=0}) = 1 (in [5], left {tv_sec=0, tv_usec=999993})
read(5, "tcpdump: listening on enp0s3, li"..., 8192) = 86

Python 3:

select(6, [5], [], [], {tv_sec=1, tv_usec=0}) = 1 (in [5], left {tv_sec=0, tv_usec=999994})
read(5, "t", 1)                         = 1

I. е. для Python 2 вызывается чтение (..., 8192), а для Python 3 - чтение (..., 1). Как можно добиться, чтобы для Python 2 также вызывалось чтение (..., 1)?

Обновление 2:

Проблема не зависит от "tcpdump" и также может быть воспроизводится так:

import logging
from ptyprocess import PtyProcess
from select import select

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s %(name)s %(message)s")

pty_process = PtyProcess.spawn(
    argv=["bash", "-c", "echo 123 ; sleep 3"],
    echo=True)
while True:
    rlist, _, _ = select([pty_process.fd], [], [], 1)
    if pty_process.fd in rlist:
        try:
            data = pty_process.read(1)
        except EOFError:
            logging.debug("EOF")
            break
        logging.debug("read: %r", data)
    else:
        logging.debug("timeout")

Python 2 вывод:

2020-04-23 12:51:27,126 root read: '1'
2020-04-23 12:51:28,193 root timeout
2020-04-23 12:51:29,204 root timeout
2020-04-23 12:51:30,129 root read: '2'
2020-04-23 12:51:30,129 root read: '3'
2020-04-23 12:51:30,129 root read: '\r'
2020-04-23 12:51:30,130 root read: '\n'
2020-04-23 12:51:30,130 root EOF

Python 3 вывод:

2020-04-23 12:51:23,106 root read: b'1'
2020-04-23 12:51:23,107 root read: b'2'
2020-04-23 12:51:23,107 root read: b'3'
2020-04-23 12:51:23,107 root read: b'\r'
2020-04-23 12:51:23,107 root read: b'\n'
2020-04-23 12:51:24,109 root timeout
2020-04-23 12:51:25,109 root timeout
2020-04-23 12:51:26,109 root EOF

1 Ответ

1 голос
/ 26 апреля 2020

PtyProcess.read() звонки self.fileobj.read1(). PtyProcess.fileobj имеет тип BufferedRWPair. BufferedRWPair.read1() делегатов на BufferedRWPair.reader.read1(). Конструктор BufferedRWPair создает объект BufferedReader из параметра reader.

В Python 2.7.16 Modules/_io/bufferedio.c/buffered_read1() звонки _bufferedreader_fill_buffer(self), что:

len = self->buffer_size - start;
n = _bufferedreader_raw_read(self, self->buffer + start, len);

В Python 3.8.1 Modules/_io/bufferedio.c/_io__Buffered_read1_impl() звонки:

r = _bufferedreader_raw_read(self, PyBytes_AS_STRING(res), n);

Другими словами, в Python 3 BufferedReader.read1(n) raw-чтение n байтов, тогда как в Python 2 он читает больше байтов для заполнения буфера.

Невозможно использовать чтение (1), который работает с буфером, в сочетании с select(), который работает с базовым файловым дескриптором, так же, как и код, опубликованный в вопросе.

Следующий код, который вместо этого использует pexpect ptyprocess, позволяет читать с таймаутом:

import logging
import pexpect

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s %(name)s %(message)s")

child = pexpect.spawn("bash -c 'echo 123 ; sleep 3'")
while True:
    try:
        data = child.read_nonblocking(size=1, timeout=1)
        logging.debug("read: %r", data)
    except pexpect.TIMEOUT:
        logging.debug("timeout")
    except pexpect.EOF:
        logging.debug("EOF")
        break

Вывод:

2020-04-26 14:54:56,006 root read: '1'
2020-04-26 14:54:56,007 root read: '2'
2020-04-26 14:54:56,007 root read: '3'
2020-04-26 14:54:56,007 root read: '\r'
2020-04-26 14:54:56,007 root read: '\n'
2020-04-26 14:54:57,009 root timeout
2020-04-26 14:54:58,010 root timeout
2020-04-26 14:54:59,008 root EOF
...