Просмотр stdout подпроцесса. Открытые объекты ведут себя неправильно, я что-то упустил?
Точнее, он не обновляется до тех пор, пока все, что он содержит, не будет прочитано (но только если поток был прочитан хотя бы один раз), что делает его фактически дисфункциональным.
Простите за странный пример, но в настоящее время я пытаюсь написать простой графический монитор ping:
import tkinter as tk
from subprocess import Popen, PIPE, STDOUT
import shlex, re
from sys import stdout, platform
class Ping(object):
def __init__(self):
if platform == "win32":
command = shlex.split("ping -w 999 -t 8.8.8.8")
elif platform == "linux" or platform == "osx":
command = shlex.split("ping -W 1 8.8.8.8")
self.ping = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True)
self.ping.stdout.readline()
self.ping.stdout.readline()
def get_next_ping(self):
has_line = str.find(self.ping.stdout.peek().decode("ascii", "ignore"), "\n") != -1
if not has_line:
print(self.ping.stdout.peek()) # Debug statement
return None
else:
line = self.ping.stdout.readline().decode("ascii", "ignore")
print(line) # Debug statement
try: return int(float(re.findall("([0-9]+)[^m]?ms", line)[0]))
except IndexError: return -1
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.pingmon = Ping()
self.bind("<ButtonPress-1>", self.check_buffer)
def check_buffer(self, event):
print(self.pingmon.get_next_ping())
app=App()
app.mainloop()
В этом примере, когда вы нажимаете, подпроцесс опрашивается, чтобы узнать, доступна ли новая строка (содержащая выходной пинг или сообщение о тайм-ауте). Если вы запустите проект и начнете щелкать немедленно, вы заметите, что вывод peek()
прекратил обновление и всегда b'Reply from 8.8.8.8: '
,
Я также попробовал альтернативный метод проверки длины peek
Это результат, но он, очевидно, никогда не равен нулю, так что это тоже ничего не стоит.
Далее я попытался вызвать flush()
метод потока, но он, похоже, никак не помогает ситуации
Конечный результат заключается в том, что subprocess.Popen.stdout.peek()
кажется неработоспособным и не пригодным для использования по назначению - заглянуть в выходной буфер, но Python - это зрелый язык, и я не ожидал бы найти в нем такого рода ошибку, есть ли что-то, чего мне не хватает? Если нет, как я могу обойти эту проблему?
2 ответа
Ответ
просто используйте readline()
метод. если строка не существует, он возвращает пустой объект байтов - b''
пример использования readline()
:
from subprocess import Popen, PIPE, STDOUT
curdir = Popen(['pwd'], stdout=PIPE, stderr=STDOUT)
print(curdir.stdout.readline())
print(curdir.stdout.readline())
print(curdir.stdout.readline())
это выведет (на python3):
b'/home/shmulik\n'
b''
b''
для вашего случая это обновляется get_next_ping()
func (также немного изменил регулярное выражение)
def get_next_ping(self):
line = self.ping.stdout.readline()
if not line:
return
line = line.decode('utf-8', 'ignore')
print(line) # Debug statement
try:
return int(float(re.search(r'([0-9.]+)[^m]?ms', line).group(1)))
except (IndexError, AttributeError):
return -1
Неблокируемому
если вам небезразлична блокирующая операция, посмотрите на это, так что ответьте
Вы можете использовать select
модуль в unix для чтения из stdout неблокирующим способом или запуска фонового потока для обновления буфера для чтения.
Пример использования потока для буферизации
class Ping(object):
def __init__(self):
if platform == "win32":
command = shlex.split("ping -w 999 -t 8.8.8.8")
elif platform == "linux" or platform == "osx":
command = shlex.split("ping -W 1 8.8.8.8")
self.ping = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True)
self.ping.stdout.readline()
self.ping.stdout.readline()
self.lines = [] # lines will be added here by the background thread
self.lines_lock = Lock() # avoid race conditions on .pop()
self.lines_reader_thread = Thread(target=self._readlines) # start the background thread
self.lines_reader_thread.daemon = True
self.lines_reader_thread.start()
def _readlines(self):
line = None
while line or line is None:
line = self.ping.stdout.readline().decode()
with self.lines_lock:
self.lines.append(line)
def get_next_ping(self):
with self.lines_lock:
if not self.lines:
return
line = self.lines.pop()
print(line) # Debug statement
try:
return int(float(re.search(r'([0-9.]+)[^m]?ms', line).group(1)))
except (IndexError, AttributeError):
return -1
Предложения
- используйте существующую библиотеку Python для проверки связи вместо анализа stdout. некоторым библиотекам требуется запускать с правами root под linux, это может быть ограничением для вас.
- отправлять один пинг за раз вместо продолжительного фонового процесса пинга. таким образом, вы можете использовать
subprocess.check_output()
, - избегать использования
shell=True
наpopen()
Передача в него неанизированного ввода может привести к внедрению команды.
@Llamageddon Я думаю, что указатель файла должен быть перемещен, чтобы обновить буфер в проверке if not has_line, используя readline()
, Peek не продвигает указатель, поэтому вы, по сути, получили ошибку, которая будет "пиковаться" в пустом буфере файлов.
if not has_line:
print(self.ping.stdout.peek()) # Debug statement
self.ping.stdout.readline() # Should refresh the filebuffer.
return None
Re: peek()
может использоваться для просмотра больших файловых буферов и, возможно, не подходит для вашей работы, учитывая размер ответа; Тем не менее, я думаю, что хороший пример, когда peek()
не является "дисфункциональным и не пригодным для использования":), когда строка в буфере имеет длину 100 000 символов, и просмотра первых 100 символов будет достаточно, чтобы оценить, что делать со строкой (т.е. пропустить ее или применить дополнительную логику). Пик позволил бы нам выполнить просмотр и оценку всего, минимизируя время блока.