การไม่บล็อกการอ่านบน subprocess.PIPE ใน python


506

ฉันใช้โมดูลย่อยเพื่อเริ่มกระบวนการย่อยและเชื่อมต่อกับเอาต์พุตสตรีม (stdout) ฉันต้องการที่จะรันการอ่านที่ไม่บล็อกบน stdout มีวิธีที่จะทำให้. non-block หรือตรวจสอบว่ามีข้อมูลในสตรีมก่อนที่ฉันจะเรียกใช้.readlineหรือไม่ ฉันต้องการให้อุปกรณ์พกพาหรืออย่างน้อยต้องทำงานภายใต้ Windows และ Linux

นี่คือวิธีที่ฉันทำตอนนี้ (มันปิดกั้น.readlineหากไม่มีข้อมูล):

p = subprocess.Popen('myprogram.exe', stdout = subprocess.PIPE)
output_str = p.stdout.readline()

14
(มาจาก google?) PIPE ทั้งหมดจะหยุดชะงักเมื่อบัฟเฟอร์ของ PIPE ตัวใดตัวหนึ่งเต็มและไม่อ่าน เช่น stdout deadlock เมื่อเติม stderr ไม่เคยผ่าน PIPE ที่คุณไม่ได้ตั้งใจอ่าน
Nasser Al-Wohaibi

@ NasserAl-Wohaibi นี่หมายความว่ามันจะดีกว่าถ้าจะสร้างไฟล์เสมอ
Charlie Parker

สิ่งที่ฉันอยากรู้อยากเห็นคือทำไมมันบล็อกในตอนแรก ... ฉันถามเพราะฉันเคยเห็นความคิดเห็น:To avoid deadlocks: careful to: add \n to output, flush output, use readline() rather than read()
Charlie Charlie

มันคือ "โดยการออกแบบ" กำลังรอรับอินพุต
Mathieu

เกี่ยวข้อง: stackoverflow.com/q/19880190/240515
user240515

คำตอบ:


403

fcntl, select, asyncprocจะไม่ช่วยในกรณีนี้

วิธีที่เชื่อถือได้ในการอ่านสตรีมโดยไม่ปิดกั้นโดยไม่คำนึงถึงระบบปฏิบัติการคือการใช้Queue.get_nowait():

import sys
from subprocess import PIPE, Popen
from threading  import Thread

try:
    from queue import Queue, Empty
except ImportError:
    from Queue import Queue, Empty  # python 2.x

ON_POSIX = 'posix' in sys.builtin_module_names

def enqueue_output(out, queue):
    for line in iter(out.readline, b''):
        queue.put(line)
    out.close()

p = Popen(['myprogram.exe'], stdout=PIPE, bufsize=1, close_fds=ON_POSIX)
q = Queue()
t = Thread(target=enqueue_output, args=(p.stdout, q))
t.daemon = True # thread dies with the program
t.start()

# ... do other things here

# read line without blocking
try:  line = q.get_nowait() # or q.get(timeout=.1)
except Empty:
    print('no output yet')
else: # got line
    # ... do something with line

6
ใช่มันใช้งานได้สำหรับฉัน แต่ฉันก็ถอดออกมาก มันมีแนวปฏิบัติที่ดี แต่ไม่จำเป็นเสมอไป Python 3.x 2.X compat และ close_fds อาจถูกละเว้นมันจะยังใช้งานได้ แต่ต้องระวังสิ่งที่ทุกอย่างทำและไม่คัดลอกมันสุ่มสี่สุ่มห้าแม้ว่ามันจะใช้งานได้! (อันที่จริงวิธีแก้ปัญหาที่ง่ายที่สุดคือการใช้เธรดและทำ readline เหมือน Seb ทำ Qeues เป็นเพียงวิธีที่ง่ายต่อการรับข้อมูลมีคนอื่น ๆ เธรดคือคำตอบ!)
Aki

3
ภายในเธรดการเรียกไปยังout.readlineบล็อกเธรดและเธรดหลักและฉันต้องรอจนกว่า readline ส่งกลับก่อนที่ทุกอย่างจะดำเนินต่อไป มีวิธีง่าย ๆ ในเรื่องนี้ไหม? (ผมอ่านหลายบรรทัดจากกระบวนการของฉันซึ่งยังเป็นอีกหนึ่งไฟล์ .py ที่ทำฐานข้อมูลและสิ่ง)
จัสติน

3
@Justin: 'out.readline' ไม่ได้บล็อกเธรดหลักที่ถูกเรียกใช้ในเธรดอื่น
jfs

4
จะทำอย่างไรถ้าฉันไม่ปิดระบบย่อยเช่น เนื่องจากข้อยกเว้น? เธรด stdout-reader จะไม่ตายและ python จะหยุดทำงานแม้ว่าเธรดหลักจะออกแล้วใช่ไหม เราจะแก้ไขเรื่องนี้ได้อย่างไร? หลาม 2.x ไม่สนับสนุนการฆ่าเธรดสิ่งที่แย่กว่านั้นไม่สนับสนุนการขัดจังหวะ :( (เห็นได้ชัดว่าควรจัดการกับข้อยกเว้นเพื่อให้มั่นใจว่ากระบวนการย่อยถูกปิดลง แต่ในกรณีที่ไม่สามารถทำได้คุณจะทำอย่างไร)
n611x007

3
ฉันได้สร้างห่อบางเป็นมิตรของนี้ในแพคเกจshelljob pypi.python.org/pypi/shelljob
EDA-QA mort-Ora-Y

77

ฉันมักจะมีปัญหาที่คล้ายกัน โปรแกรม Python ที่ฉันเขียนบ่อยๆต้องมีความสามารถในการใช้งานฟังก์ชั่นหลักบางอย่างในขณะที่ยอมรับอินพุตของผู้ใช้จาก command line (stdin) เพียงแค่วางฟังก์ชั่นการจัดการอินพุตของผู้ใช้ในเธรดอื่นไม่สามารถแก้ปัญหาได้เนื่องจากreadline()บล็อกและไม่มีการหมดเวลา หากการทำงานหลักเสร็จสมบูรณ์และไม่จำเป็นต้องรออีกต่อไปสำหรับการป้อนข้อมูลของผู้ใช้ฉันมักต้องการให้โปรแกรมของฉันออก แต่ก็ทำไม่ได้เพราะreadline()ยังคงบล็อกในเธรดอื่นที่รอสายอยู่ วิธีแก้ปัญหาที่ฉันได้พบกับปัญหานี้คือการทำให้ stdin เป็นไฟล์ที่ไม่บล็อกโดยใช้โมดูล fcntl:

import fcntl
import os
import sys

# make stdin a non-blocking file
fd = sys.stdin.fileno()
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)

# user input handling thread
while mainThreadIsRunning:
      try: input = sys.stdin.readline()
      except: continue
      handleInput(input)

ในความคิดของฉันนี้ค่อนข้างสะอาดกว่าการใช้โมดูล select หรือ signal เพื่อแก้ปัญหานี้ แต่แล้วมันก็ใช้ได้กับ UNIX ...


1
ตามเอกสารนั้น fcntl () สามารถรับไฟล์ descriptor หรือวัตถุที่มีเมธอด. fileno ()
Denilson Sá Maia

10
คำตอบของเจสไม่ถูกต้อง จาก Guido, readline ทำงานไม่ถูกต้องกับโหมด non-blocking และมันจะไม่ทำงานก่อน Python 3000 bugs.python.org/issue1175#msg56041หากคุณต้องการใช้ fcntl เพื่อตั้งไฟล์เป็นโหมด non-blocking คุณต้องใช้ os.read () ระดับต่ำกว่าและแยกบรรทัดออกเอง การผสม fcntl กับการโทรระดับสูงที่ดำเนินการบัฟเฟอร์บรรทัดกำลังถามถึงปัญหา
anonnn

2
ดูเหมือนว่าการใช้ readline จะไม่ถูกต้องใน Python 2 ดูคำตอบของ anonnn stackoverflow.com/questions/375427/ …
Catalin Iacob

10
กรุณาอย่าใช้ลูปที่ยุ่ง ใช้แบบสำรวจความคิดเห็น () ด้วยระยะหมดเวลาเพื่อรอข้อมูล
Ivo Danihelka

@tefano สิ่งที่buffer_sizeกำหนดไว้เป็นอย่างไร
แมว

39

งูหลาม 3.4 เปิดตัวใหม่API ชั่วคราวสำหรับตรงกัน IO - โมดูลasyncio

วิธีการคล้ายกับtwistedคำตอบตาม @Bryan Ward - กำหนดโปรโตคอลและวิธีการที่เรียกว่าทันทีที่ข้อมูลพร้อม:

#!/usr/bin/env python3
import asyncio
import os

class SubprocessProtocol(asyncio.SubprocessProtocol):
    def pipe_data_received(self, fd, data):
        if fd == 1: # got stdout data (bytes)
            print(data)

    def connection_lost(self, exc):
        loop.stop() # end loop.run_forever()

if os.name == 'nt':
    loop = asyncio.ProactorEventLoop() # for subprocess' pipes on Windows
    asyncio.set_event_loop(loop)
else:
    loop = asyncio.get_event_loop()
try:
    loop.run_until_complete(loop.subprocess_exec(SubprocessProtocol, 
        "myprogram.exe", "arg1", "arg2"))
    loop.run_forever()
finally:
    loop.close()

ดู"Subprocess" ในเอกสาร

มีอินเตอร์เฟสระดับสูงasyncio.create_subprocess_exec()ที่ส่งคืนอProcessอบเจ็กต์ที่อนุญาตให้อ่านบรรทัดแบบอะซิงโครนัสโดยใช้StreamReader.readline()coroutine (ด้วยasync/ awaitPython 3.5+ ไวยากรณ์ ):

#!/usr/bin/env python3.5
import asyncio
import locale
import sys
from asyncio.subprocess import PIPE
from contextlib import closing

async def readline_and_kill(*args):
    # start child process
    process = await asyncio.create_subprocess_exec(*args, stdout=PIPE)

    # read line (sequence of bytes ending with b'\n') asynchronously
    async for line in process.stdout:
        print("got line:", line.decode(locale.getpreferredencoding(False)))
        break
    process.kill()
    return await process.wait() # wait for the child process to exit


if sys.platform == "win32":
    loop = asyncio.ProactorEventLoop()
    asyncio.set_event_loop(loop)
else:
    loop = asyncio.get_event_loop()

with closing(loop):
    sys.exit(loop.run_until_complete(readline_and_kill(
        "myprogram.exe", "arg1", "arg2")))

readline_and_kill() ดำเนินการงานต่อไปนี้:

  • เริ่มกระบวนการย่อยเปลี่ยนเส้นทาง stdout ไปยังไปป์
  • อ่านบรรทัดจาก stdout ของกระบวนการย่อยแบบอะซิงโครนัส
  • ฆ่ากระบวนการย่อย
  • รอให้มันออก

แต่ละขั้นตอนอาจถูก จำกัด ด้วยการหมดเวลาวินาทีหากจำเป็น


เมื่อฉันลองทำสิ่งนี้โดยใช้ python 3.4 coroutines ฉันจะได้รับผลลัพธ์ก็ต่อเมื่อสคริปต์ทั้งหมดทำงาน ฉันต้องการดูบรรทัดของเอาต์พุตที่พิมพ์ออกมาทันทีที่ subprocess พิมพ์บรรทัด นี่คือสิ่งที่ฉันมี: pastebin.com/qPssFGep
flutefreak7

1
@ flutefreak7: ปัญหาการบัฟเฟอร์ไม่เกี่ยวข้องกับคำถามปัจจุบัน ไปที่ลิงก์เพื่อดูวิธีแก้ไขที่เป็นไปได้
jfs

ขอบคุณ! การแก้ไขปัญหาสำหรับสคริปต์ของฉันโดยเพียงแค่ใช้เพื่อให้ข้อความที่พิมพ์จะสามารถใช้งานได้ทันทีที่โทรเฝ้าprint(text, flush=True) readlineเมื่อฉันทดสอบด้วยปฏิบัติการที่ใช้ Fortran ฉันต้องการห่อ / ดูจริง ๆ แล้วมันไม่ได้บัฟเฟอร์ผลลัพธ์ของมันดังนั้นจึงทำงานตามที่คาดไว้
flutefreak7

เป็นไปได้หรือไม่ที่จะยอมให้กระบวนการย่อยยังคงอยู่และดำเนินการอ่าน / เขียนต่อไป readline_and_killในสคริปต์ที่สองของคุณทำงานเหมือนอย่างมากsubprocess.comunicateในการที่จะยุติกระบวนการหลังจากการดำเนินการอ่าน / เขียนหนึ่งครั้ง ฉันเห็นด้วยเช่นกันว่าคุณกำลังใช้ไปป์เดียวstdoutซึ่ง subprocess จัดการนั้นไม่ใช่การบล็อก พยายามที่จะใช้ทั้งสองstdoutและฉันคิดว่าฉันท้ายการปิดกั้นstderr
Carel

@Careli รหัสในคำตอบทำงานตามที่อธิบายไว้ในคำตอบอย่างชัดเจน มันเป็นไปได้ที่จะใช้พฤติกรรมอื่น ๆ ถ้าต้องการ ทั้งท่อเท่าเทียมกัน nonblocking ถ้าใช้นี่คือตัวอย่างวิธีการอ่านจากท่อทั้งสองพร้อมกัน
jfs

19

ลองใช้โมดูลasyncproc ตัวอย่างเช่น:

import os
from asyncproc import Process
myProc = Process("myprogram.app")

while True:
    # check to see if process has ended
    poll = myProc.wait(os.WNOHANG)
    if poll != None:
        break
    # print any new output
    out = myProc.read()
    if out != "":
        print out

โมดูลดูแลเกลียวทั้งหมดตามที่แนะนำโดย S.Lott


1
ยอดเยี่ยมอย่างแน่นอน ง่ายกว่าโมดูลย่อยกระบวนการดิบ ทำงานได้อย่างสมบูรณ์แบบสำหรับฉันบน Ubuntu
Cerin

12
asyncproc ไม่ทำงานบน windows และ windows ไม่รองรับ os.WNOHANG :-(
Bryan Oakley

26
asyncproc เป็น GPL ซึ่ง จำกัด การใช้งานเพิ่มเติม :-(
Bryan Oakley

ขอบคุณ สิ่งเล็ก ๆ น้อย ๆ อย่างหนึ่ง: ดูเหมือนว่าการแทนที่แท็บด้วยช่องว่าง 8 ช่องใน asyncproc.py เป็นวิธีการที่จะไป :)
benjaoming

ดูเหมือนว่าคุณจะไม่ได้รับรหัสส่งคืนของกระบวนการที่คุณเรียกใช้ผ่านโมดูล asyncproc เฉพาะเอาต์พุตที่สร้างขึ้น
grayaii

17

คุณสามารถทำเช่นนี้ได้อย่างง่ายดายจริงๆในการบิด ขึ้นอยู่กับฐานรหัสที่มีอยู่ของคุณสิ่งนี้อาจไม่ใช่เรื่องง่ายที่จะใช้ แต่ถ้าคุณกำลังสร้างแอปพลิเคชันที่บิดเบี้ยวสิ่งต่าง ๆ เช่นนี้กลายเป็นเรื่องเล็กน้อย คุณสร้างProcessProtocolคลาสและแทนที่outReceived()วิธีการ Twisted (ขึ้นอยู่กับเครื่องปฏิกรณ์ที่ใช้) โดยปกติจะเป็นเพียงselect()ลูปขนาดใหญ่ที่ติดตั้ง callbacks เพื่อจัดการข้อมูลจาก file descriptors ต่างๆ (มักจะเป็นซ็อกเก็ตเครือข่าย) ดังนั้นวิธีการที่เพียงแค่ติดตั้งโทรกลับสำหรับการจัดการข้อมูลที่มาจากoutReceived() STDOUTตัวอย่างง่ายๆที่แสดงให้เห็นถึงพฤติกรรมนี้มีดังนี้:

from twisted.internet import protocol, reactor

class MyProcessProtocol(protocol.ProcessProtocol):

    def outReceived(self, data):
        print data

proc = MyProcessProtocol()
reactor.spawnProcess(proc, './myprogram', ['./myprogram', 'arg1', 'arg2', 'arg3'])
reactor.run()

เอกสาร Twistedมีบางข้อมูลที่ดีเกี่ยวกับเรื่องนี้

หากคุณสร้างแอปพลิเคชั่นทั้งหมดของคุณรอบ ๆ Twisted มันทำให้การสื่อสารแบบอะซิงโครนัสกับกระบวนการอื่น ๆ ในพื้นที่หรือระยะไกลสง่างามเช่นนี้ ในทางตรงกันข้ามถ้าโปรแกรมของคุณไม่ได้ถูกสร้างขึ้นบน Twisted สิ่งนี้จะไม่เป็นประโยชน์อย่างแท้จริง หวังว่านี่จะเป็นประโยชน์กับผู้อ่านคนอื่น ๆ แม้ว่ามันจะไม่เหมาะกับแอปพลิเคชันของคุณก็ตาม


ไม่ดี. selectไม่ควรทำงานบน windows ที่มี file descriptors, ตามเอกสาร
n611x007

2
@naxa ฉันไม่คิดว่าselect()เขาหมายถึงคนที่คุณเป็น ฉันสมมติว่าเป็นเช่นนี้เพราะTwistedทำงานบน windows ...
notbad.jpeg


1
"Twisted (ขึ้นอยู่กับเครื่องปฏิกรณ์ที่ใช้) โดยปกติจะเป็นเพียงห่วง select () ขนาดใหญ่" หมายความว่ามีเครื่องปฏิกรณ์หลายตัวให้เลือก select()หนึ่งเป็นคนแบบพกพามากที่สุดใน UNIXes และยูนิกซ์ชอบ แต่ก็ยังมีสองเครื่องปฏิกรณ์ใช้ได้กับ Windows: twistedmatrix.com/documents/current/core/howto/...
clacke

14

ใช้เลือก & อ่าน (1)

import subprocess     #no new requirements
def readAllSoFar(proc, retVal=''): 
  while (select.select([proc.stdout],[],[],0)[0]!=[]):   
    retVal+=proc.stdout.read(1)
  return retVal
p = subprocess.Popen(['/bin/ls'], stdout=subprocess.PIPE)
while not p.poll():
  print (readAllSoFar(p))

สำหรับ readline () - เช่น:

lines = ['']
while not p.poll():
  lines = readAllSoFar(p, lines[-1]).split('\n')
  for a in range(len(lines)-1):
    print a
lines = readAllSoFar(p, lines[-1]).split('\n')
for a in range(len(lines)-1):
  print a

6
ไม่ดี. selectไม่ควรทำงานบน windows ที่มี file descriptors, ตามเอกสาร
n611x007

พระเจ้าช่วย. อ่านเมกะไบต์หรืออาจเป็นหนึ่งตัวอักษรต่อครั้ง ... นั่นเป็นความคิดที่เลวร้ายที่สุดที่ฉันเคยเห็นมานานแล้ว ... ไม่ต้องพูดถึงรหัสนี้ใช้งานไม่ได้เพราะproc.stdout.read()ไม่ว่าอาร์กิวเมนต์จะมีขนาดเล็กเพียงใด การบล็อกการโทร
wvxvw

OSError: [WinError 10093] Either the application has not called WSAStartup, or WSAStartup failed
nmz787

8

วิธีแก้ไขปัญหาหนึ่งคือการสร้างกระบวนการอื่นเพื่อดำเนินการอ่านกระบวนการหรือทำเธรดของกระบวนการด้วยการหมดเวลา

นี่คือฟังก์ชั่นไทม์เอาต์ของเธรด:

http://code.activestate.com/recipes/473878/

อย่างไรก็ตามคุณจำเป็นต้องอ่าน stdout ขณะที่กำลังเข้ามาใช่ไหม ทางออกก็อาจจะมีการถ่ายโอนข้อมูลที่ส่งออกไปยังแฟ้มและรอให้กระบวนการเสร็จสิ้นการใช้p.wait ()

f = open('myprogram_output.txt','w')
p = subprocess.Popen('myprogram.exe', stdout=f)
p.wait()
f.close()


str = open('myprogram_output.txt','r').read()

ดูเหมือนว่าเธรดของ recpieจะไม่ออกหลังจากหมดเวลาและการฆ่ามันขึ้นอยู่กับความสามารถในการฆ่า subprocess (sg. ไม่เกี่ยวข้องกับเรื่องนี้) อ่าน (สิ่งที่คุณควรจะสามารถ แต่ในกรณีที่คุณไม่สามารถ .. ) .
n611x007

7

ข้อจำกัดความรับผิดชอบ: ใช้งานได้กับพายุทอร์นาโดเท่านั้น

คุณสามารถทำได้โดยการตั้งค่า fd เป็น nonblocking จากนั้นใช้ ioloop เพื่อลงทะเบียนการเรียกกลับ ฉันได้บรรจุสิ่งนี้ในไข่ที่เรียกว่าtornado_subprocessและคุณสามารถติดตั้งผ่าน PyPI:

easy_install tornado_subprocess

ตอนนี้คุณสามารถทำสิ่งนี้:

import tornado_subprocess
import tornado.ioloop

    def print_res( status, stdout, stderr ) :
    print status, stdout, stderr
    if status == 0:
        print "OK:"
        print stdout
    else:
        print "ERROR:"
        print stderr

t = tornado_subprocess.Subprocess( print_res, timeout=30, args=[ "cat", "/etc/passwd" ] )
t.start()
tornado.ioloop.IOLoop.instance().start()

คุณสามารถใช้กับ RequestHandler ได้

class MyHandler(tornado.web.RequestHandler):
    def on_done(self, status, stdout, stderr):
        self.write( stdout )
        self.finish()

    @tornado.web.asynchronous
    def get(self):
        t = tornado_subprocess.Subprocess( self.on_done, timeout=30, args=[ "cat", "/etc/passwd" ] )
        t.start()

ขอบคุณสำหรับคุณสมบัติที่ดี! เพื่ออธิบายให้ชัดเจนว่าทำไมเราจึงไม่สามารถใช้threading.Threadสร้างกระบวนการที่ไม่บล็อกใหม่ได้ ฉันใช้มันในon_messageอินสแตนซ์ของ Tornado websocket และทำงานได้ดี
VisioN

1
การทำเกลียวส่วนใหญ่จะท เหมาะสำหรับฟังก์ชั่นการวิ่งระยะสั้น คุณสามารถอ่านเกี่ยวกับเรื่องนี้ได้ที่นี่: stackoverflow.com/questions/7846323/tornado-web-and-threads github.com/facebook/tornado/wiki/Threading-and-concurrency
Vukasin Toroman

@VukasinToroman คุณช่วยฉันด้วยสิ่งนี้จริงๆ ขอบคุณมากสำหรับโมดูล tornado_subprocess :) ความ
เจมส์ Gentes

ใช้งานกับ windows ได้หรือไม่ (ทราบว่าselectมีอธิบายไฟล์ไม่ได้ )
n611x007

lib นี้ไม่ได้ใช้การselectโทร ฉันไม่ได้ลองใน Windows แต่คุณอาจจะประสบปัญหาเนื่องจาก lib ใช้fcntlโมดูล ดังนั้นในระยะสั้น: ไม่น่าจะไม่สามารถใช้งานได้กับ Windows
Vukasin Toroman

6

โซลูชันที่มีอยู่ใช้ไม่ได้สำหรับฉัน (รายละเอียดด้านล่าง) สิ่งที่ได้ผลที่สุดคือการใช้ readline โดยใช้ read (1) (ตามคำตอบนี้ ) หลังไม่ได้บล็อก:

from subprocess import Popen, PIPE
from threading import Thread
def process_output(myprocess): #output-consuming thread
    nextline = None
    buf = ''
    while True:
        #--- extract line using read(1)
        out = myprocess.stdout.read(1)
        if out == '' and myprocess.poll() != None: break
        if out != '':
            buf += out
            if out == '\n':
                nextline = buf
                buf = ''
        if not nextline: continue
        line = nextline
        nextline = None

        #--- do whatever you want with line here
        print 'Line is:', line
    myprocess.stdout.close()

myprocess = Popen('myprogram.exe', stdout=PIPE) #output-producing process
p1 = Thread(target=process_output, args=(dcmpid,)) #output-consuming thread
p1.daemon = True
p1.start()

#--- do whatever here and then kill process and thread if needed
if myprocess.poll() == None: #kill process; will automatically stop thread
    myprocess.kill()
    myprocess.wait()
if p1 and p1.is_alive(): #wait for thread to finish
    p1.join()

ทำไมโซลูชันที่มีอยู่ไม่ทำงาน:

  1. โซลูชันที่ต้องการ readline (รวมถึงคิวที่ใช้คิว) จะบล็อกตลอดเวลา เป็นการยากที่จะฆ่าเธรดที่เรียกใช้ readline มันจะถูกฆ่าเมื่อกระบวนการที่สร้างเสร็จ แต่ไม่ใช่เมื่อกระบวนการผลิตเอาต์พุตถูกฆ่า
  2. การผสม fcntl ระดับต่ำกับการโทร readline ระดับสูงอาจทำงานไม่ถูกต้องเนื่องจาก anonnn ชี้ให้เห็น
  3. การใช้ select.poll () นั้นเรียบร้อย แต่ไม่สามารถใช้งานได้กับ Windows ตามเอกสารของหลาม
  4. การใช้ไลบรารี่ของบุคคลที่สามนั้นเกินความจำเป็นสำหรับงานนี้และเพิ่มการพึ่งพาเพิ่มเติม

1
1. q.get_nowait()จากคำตอบของฉันต้องไม่บล็อกเคยเป็นจุดที่ใช้ 2. เธรดที่ดำเนินการ readline ( enqueue_output()ฟังก์ชัน ) ออกจาก EOF เช่นรวมถึงกรณีเมื่อกระบวนการผลิตเอาต์พุตถูกฆ่า หากคุณเชื่อว่ามันไม่เป็นเช่นนั้น โปรดระบุตัวอย่างรหัสขั้นต่ำที่สมบูรณ์ซึ่งแสดงเป็นอย่างอื่น (อาจเป็นคำถามใหม่ )
jfs

1
@ sebastian ฉันใช้เวลาหนึ่งชั่วโมงหรือมากกว่านั้นเพื่อลองหาตัวอย่างเล็ก ๆ น้อย ๆ ในที่สุดฉันต้องยอมรับว่าคำตอบของคุณจัดการกับทุกกรณี ฉันเดาว่ามันจะไม่ทำงานก่อนหน้านี้สำหรับฉันเพราะเมื่อฉันพยายามที่จะฆ่ากระบวนการผลิตออกมามันถูกฆ่าไปแล้วและให้ข้อผิดพลาดที่ยากต่อการดีบัก ชั่วโมงใช้งานได้ดีเพราะในขณะที่มีตัวอย่างน้อยที่สุดฉันสามารถหาวิธีแก้ปัญหาที่ง่ายกว่าได้
Vikram Pudi

คุณช่วยโพสต์คำตอบที่ง่ายขึ้นได้ไหม? :) (ถ้าแตกต่างจากของเซบาสเตียน)
n611x007

@ danger89: dcmpid = myprocessผมคิดว่า
ViFI

อยู่ในสภาพหลังจากอ่าน () โทร (หลังจากนั้นเป็น True): out จะไม่เป็นสตริงว่างเพราะคุณอ่านอย่างน้อยสตริง / ไบต์ที่มีความยาว 1
sergzach

6

นี่คือรหัสของฉันที่ใช้ในการจับทุกผลลัพธ์จาก subprocess ASAP รวมถึงบางส่วนของบรรทัด มันปั๊มในเวลาเดียวกันและ stdout และ stderr ในลำดับที่ถูกต้องเกือบ

ทดสอบและทำงานอย่างถูกต้องบน Python 2.7 linux & windows

#!/usr/bin/python
#
# Runner with stdout/stderr catcher
#
from sys import argv
from subprocess import Popen, PIPE
import os, io
from threading import Thread
import Queue
def __main__():
    if (len(argv) > 1) and (argv[-1] == "-sub-"):
        import time, sys
        print "Application runned!"
        time.sleep(2)
        print "Slept 2 second"
        time.sleep(1)
        print "Slept 1 additional second",
        time.sleep(2)
        sys.stderr.write("Stderr output after 5 seconds")
        print "Eol on stdin"
        sys.stderr.write("Eol on stderr\n")
        time.sleep(1)
        print "Wow, we have end of work!",
    else:
        os.environ["PYTHONUNBUFFERED"]="1"
        try:
            p = Popen( argv + ["-sub-"],
                       bufsize=0, # line-buffered
                       stdin=PIPE, stdout=PIPE, stderr=PIPE )
        except WindowsError, W:
            if W.winerror==193:
                p = Popen( argv + ["-sub-"],
                           shell=True, # Try to run via shell
                           bufsize=0, # line-buffered
                           stdin=PIPE, stdout=PIPE, stderr=PIPE )
            else:
                raise
        inp = Queue.Queue()
        sout = io.open(p.stdout.fileno(), 'rb', closefd=False)
        serr = io.open(p.stderr.fileno(), 'rb', closefd=False)
        def Pump(stream, category):
            queue = Queue.Queue()
            def rdr():
                while True:
                    buf = stream.read1(8192)
                    if len(buf)>0:
                        queue.put( buf )
                    else:
                        queue.put( None )
                        return
            def clct():
                active = True
                while active:
                    r = queue.get()
                    try:
                        while True:
                            r1 = queue.get(timeout=0.005)
                            if r1 is None:
                                active = False
                                break
                            else:
                                r += r1
                    except Queue.Empty:
                        pass
                    inp.put( (category, r) )
            for tgt in [rdr, clct]:
                th = Thread(target=tgt)
                th.setDaemon(True)
                th.start()
        Pump(sout, 'stdout')
        Pump(serr, 'stderr')

        while p.poll() is None:
            # App still working
            try:
                chan,line = inp.get(timeout = 1.0)
                if chan=='stdout':
                    print "STDOUT>>", line, "<?<"
                elif chan=='stderr':
                    print " ERROR==", line, "=?="
            except Queue.Empty:
                pass
        print "Finish"

if __name__ == '__main__':
    __main__()

หนึ่งในไม่กี่คำตอบที่ให้คุณอ่านเนื้อหาที่ไม่จำเป็นต้องลงท้ายด้วย newline
totaam

5

ฉันเพิ่มปัญหานี้เพื่ออ่าน subprocess.Popen stdout บางส่วน นี่คือโซลูชันการอ่านที่ไม่บล็อกของฉัน:

import fcntl

def non_block_read(output):
    fd = output.fileno()
    fl = fcntl.fcntl(fd, fcntl.F_GETFL)
    fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
    try:
        return output.read()
    except:
        return ""

# Use example
from subprocess import *
sb = Popen("echo test && sleep 1000", shell=True, stdout=PIPE)
sb.kill()

# sb.stdout.read() # <-- This will block
non_block_read(sb.stdout)
'test\n'

5
fcntl ไม่ทำงานบนหน้าต่างตามเอกสาร
n611x007

@anatolytechtonik ใช้msvcrt.kbhit()แทน
cat

4

การอ่านแบบไม่มีการบล็อกรุ่นนี้ไม่ต้องการโมดูลพิเศษและจะทำงานนอกกรอบบน Linux distros ส่วนใหญ่

import os
import sys
import time
import fcntl
import subprocess

def async_read(fd):
    # set non-blocking flag while preserving old flags
    fl = fcntl.fcntl(fd, fcntl.F_GETFL)
    fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
    # read char until EOF hit
    while True:
        try:
            ch = os.read(fd.fileno(), 1)
            # EOF
            if not ch: break                                                                                                                                                              
            sys.stdout.write(ch)
        except OSError:
            # waiting for data be available on fd
            pass

def shell(args, async=True):
    # merge stderr and stdout
    proc = subprocess.Popen(args, shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    if async: async_read(proc.stdout)
    sout, serr = proc.communicate()
    return (sout, serr)

if __name__ == '__main__':
    cmd = 'ping 8.8.8.8'
    sout, serr = shell(cmd.split())

3

นี่เป็นวิธีการแก้ปัญหาอย่างง่ายตามกระทู้ซึ่ง:

  • ทำงานได้ทั้ง Linux และ Windows (ไม่ต้องพึ่งพาselect)
  • อ่านทั้งสองstdoutและstderrไม่ตรงกัน
  • ไม่ต้องพึ่งพาการลงคะแนนแบบแอ็คทีฟด้วยเวลารอคอยโดยพลการ (เป็นมิตรกับ CPU)
  • ไม่ใช้asyncio(ซึ่งอาจขัดแย้งกับห้องสมุดอื่น ๆ )
  • ทำงานจนกว่ากระบวนการลูกจะสิ้นสุดลง

printer.py

import time
import sys

sys.stdout.write("Hello\n")
sys.stdout.flush()
time.sleep(1)
sys.stdout.write("World!\n")
sys.stdout.flush()
time.sleep(1)
sys.stderr.write("That's an error\n")
sys.stderr.flush()
time.sleep(2)
sys.stdout.write("Actually, I'm fine\n")
sys.stdout.flush()
time.sleep(1)

reader.py

import queue
import subprocess
import sys
import threading


def enqueue_stream(stream, queue, type):
    for line in iter(stream.readline, b''):
        queue.put(str(type) + line.decode('utf-8'))
    stream.close()


def enqueue_process(process, queue):
    process.wait()
    queue.put('x')


p = subprocess.Popen('python printer.py', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
q = queue.Queue()
to = threading.Thread(target=enqueue_stream, args=(p.stdout, q, 1))
te = threading.Thread(target=enqueue_stream, args=(p.stderr, q, 2))
tp = threading.Thread(target=enqueue_process, args=(p, q))
te.start()
to.start()
tp.start()

while True:
    line = q.get()
    if line[0] == 'x':
        break
    if line[0] == '2':  # stderr
        sys.stdout.write("\033[0;31m")  # ANSI red color
    sys.stdout.write(line[1:])
    if line[0] == '2':
        sys.stdout.write("\033[0m")  # reset ANSI code
    sys.stdout.flush()

tp.join()
to.join()
te.join()

2

การเพิ่มคำตอบนี้ที่นี่เนื่องจากมีความสามารถในการตั้งค่าไปป์ที่ไม่มีการบล็อกบน Windows และ Unix

ทุกctypesรายละเอียดด้วยการใช้@ คำตอบของ

มีรุ่นที่ปรับเปลี่ยนเล็กน้อยที่จะใช้ทั้งกับระบบ Unix และ Windows

  • Python3 เข้ากันได้(เฉพาะการเปลี่ยนแปลงเล็กน้อยจำเป็น)
  • รวมถึงเวอร์ชัน posix และกำหนดข้อยกเว้นที่จะใช้สำหรับทั้งสอง

วิธีนี้คุณสามารถใช้ฟังก์ชั่นและข้อยกเว้นเดียวกันสำหรับรหัส Unix และ Windows

# pipe_non_blocking.py (module)
"""
Example use:

    p = subprocess.Popen(
            command,
            stdout=subprocess.PIPE,
            )

    pipe_non_blocking_set(p.stdout.fileno())

    try:
        data = os.read(p.stdout.fileno(), 1)
    except PortableBlockingIOError as ex:
        if not pipe_non_blocking_is_error_blocking(ex):
            raise ex
"""


__all__ = (
    "pipe_non_blocking_set",
    "pipe_non_blocking_is_error_blocking",
    "PortableBlockingIOError",
    )

import os


if os.name == "nt":
    def pipe_non_blocking_set(fd):
        # Constant could define globally but avoid polluting the name-space
        # thanks to: /programming/34504970
        import msvcrt

        from ctypes import windll, byref, wintypes, WinError, POINTER
        from ctypes.wintypes import HANDLE, DWORD, BOOL

        LPDWORD = POINTER(DWORD)

        PIPE_NOWAIT = wintypes.DWORD(0x00000001)

        def pipe_no_wait(pipefd):
            SetNamedPipeHandleState = windll.kernel32.SetNamedPipeHandleState
            SetNamedPipeHandleState.argtypes = [HANDLE, LPDWORD, LPDWORD, LPDWORD]
            SetNamedPipeHandleState.restype = BOOL

            h = msvcrt.get_osfhandle(pipefd)

            res = windll.kernel32.SetNamedPipeHandleState(h, byref(PIPE_NOWAIT), None, None)
            if res == 0:
                print(WinError())
                return False
            return True

        return pipe_no_wait(fd)

    def pipe_non_blocking_is_error_blocking(ex):
        if not isinstance(ex, PortableBlockingIOError):
            return False
        from ctypes import GetLastError
        ERROR_NO_DATA = 232

        return (GetLastError() == ERROR_NO_DATA)

    PortableBlockingIOError = OSError
else:
    def pipe_non_blocking_set(fd):
        import fcntl
        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
        return True

    def pipe_non_blocking_is_error_blocking(ex):
        if not isinstance(ex, PortableBlockingIOError):
            return False
        return True

    PortableBlockingIOError = BlockingIOError

เพื่อหลีกเลี่ยงการอ่านข้อมูลที่ไม่สมบูรณ์ฉันสิ้นสุดการเขียนตัวสร้าง readline ของฉันเอง (ซึ่งส่งกลับสตริงไบต์สำหรับแต่ละบรรทัด)

มันเป็นเครื่องกำเนิดไฟฟ้าเพื่อให้คุณสามารถเช่น ...

def non_blocking_readlines(f, chunk=1024):
    """
    Iterate over lines, yielding b'' when nothings left
    or when new data is not yet available.

    stdout_iter = iter(non_blocking_readlines(process.stdout))

    line = next(stdout_iter)  # will be a line or b''.
    """
    import os

    from .pipe_non_blocking import (
            pipe_non_blocking_set,
            pipe_non_blocking_is_error_blocking,
            PortableBlockingIOError,
            )

    fd = f.fileno()
    pipe_non_blocking_set(fd)

    blocks = []

    while True:
        try:
            data = os.read(fd, chunk)
            if not data:
                # case were reading finishes with no trailing newline
                yield b''.join(blocks)
                blocks.clear()
        except PortableBlockingIOError as ex:
            if not pipe_non_blocking_is_error_blocking(ex):
                raise ex

            yield b''
            continue

        while True:
            n = data.find(b'\n')
            if n == -1:
                break

            yield b''.join(blocks) + data[:n + 1]
            data = data[n + 1:]
            blocks.clear()
        blocks.append(data)

(1) ความคิดเห็นนี้บ่งบอกว่าreadline()ไม่สามารถใช้งานกับท่อที่ไม่มีการบล็อก (เช่นการตั้งค่าการใช้fcntl) บน Python 2 - คุณคิดว่ามันไม่ถูกต้องอีกต่อไปหรือไม่? (คำตอบของฉันมีลิงค์ ( fcntl) ที่ให้ข้อมูลเหมือนกัน แต่ดูเหมือนว่าจะถูกลบตอนนี้) (2) ดูวิธีmultiprocessing.connection.Pipeใช้SetNamedPipeHandleState
jfs

ฉันทดสอบสิ่งนี้กับ Python3 เท่านั้น แต่เห็นข้อมูลนี้ด้วยและคาดว่าจะยังคงใช้ได้ ฉันยังเขียนโค้ดของตัวเองเพื่อใช้แทน readline ฉันได้อัปเดตคำตอบเพื่อรวมไว้
ideasman42

2

ฉันมีปัญหาของผู้ถามดั้งเดิม แต่ไม่ต้องการเรียกเธรด ฉันผสมโซลูชันของ Jesse กับการอ่านโดยตรง () จากไปป์และตัวจัดการบัฟเฟอร์ของฉันสำหรับการอ่านบรรทัด (อย่างไรก็ตามกระบวนการย่อยของฉัน - ping - เขียนบรรทัดเต็ม <ขนาดหน้าระบบเสมอ) ฉันหลีกเลี่ยงการยุ่งโดยการอ่านเฉพาะในนาฬิกา io ที่ลงทะเบียนกับ Gobject วันนี้ฉันมักจะเรียกใช้รหัสภายใน gobject MainLoop เพื่อหลีกเลี่ยงกระทู้

def set_up_ping(ip, w):
# run the sub-process
# watch the resultant pipe
p = subprocess.Popen(['/bin/ping', ip], stdout=subprocess.PIPE)
# make stdout a non-blocking file
fl = fcntl.fcntl(p.stdout, fcntl.F_GETFL)
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fl | os.O_NONBLOCK)
stdout_gid = gobject.io_add_watch(p.stdout, gobject.IO_IN, w)
return stdout_gid # for shutting down

ผู้เฝ้าดูคือ

def watch(f, *other):
print 'reading',f.read()
return True

และโปรแกรมหลักตั้งค่า ping แล้วเรียก gobject mail loop

def main():
set_up_ping('192.168.1.8', watch)
# discard gid as unused here
gobject.MainLoop().run()

งานอื่นใดที่แนบมากับการโทรกลับใน gobject


2

สิ่งต่างๆมากมายใน Python สมัยใหม่

นี่เป็นโปรแกรมลูกง่าย "hello.py":

#!/usr/bin/env python3

while True:
    i = input()
    if i == "quit":
        break
    print(f"hello {i}")

และโปรแกรมที่จะโต้ตอบกับมัน:

import asyncio


async def main():
    proc = await asyncio.subprocess.create_subprocess_exec(
        "./hello.py", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE
    )
    proc.stdin.write(b"bob\n")
    print(await proc.stdout.read(1024))
    proc.stdin.write(b"alice\n")
    print(await proc.stdout.read(1024))
    proc.stdin.write(b"quit\n")
    await proc.wait()


asyncio.run(main())

พิมพ์ออกมา:

b'hello bob\n'
b'hello alice\n'

โปรดทราบว่ารูปแบบที่แท้จริงซึ่งเกือบทั้งหมดเป็นคำตอบก่อนหน้านี้ทั้งที่นี่และในคำถามที่เกี่ยวข้องคือการตั้งค่าตัวอธิบายไฟล์ stdout ของเด็กเป็นไม่บล็อกและโพลในลูปที่เลือกบางประเภท แน่นอนว่าทุกวันนี้วงนั้นให้บริการโดย asyncio


1

การเลือกโมดูลจะช่วยให้คุณกำหนดว่าการป้อนข้อมูลที่เป็นประโยชน์ต่อไปคือ

อย่างไรก็ตามคุณมักจะมีความสุขกับการแยกกระทู้ หนึ่งจะทำการปิดกั้นการอ่าน stdin, อื่นจะทำทุกที่ที่คุณไม่ต้องการถูกบล็อก


11
ฉันคิดว่าคำตอบนี้ไม่ช่วยเหลือด้วยเหตุผลสองประการ: (a) โมดูลที่เลือกจะไม่ทำงานบนไพพ์ภายใต้ Windows (เนื่องจากลิงก์ที่ระบุไว้อย่างชัดเจน) ซึ่งเอาชนะความตั้งใจของ OP ที่จะมีโซลูชันแบบพกพา (b) เธรดแบบอะซิงโครนัสไม่อนุญาตให้มีการสนทนาแบบซิงโครนัสระหว่างผู้ปกครองและกระบวนการลูก เกิดอะไรขึ้นถ้ากระบวนการผู้ปกครองต้องการส่งการกระทำถัดไปตามบรรทัดถัดไปที่อ่านจากเด็ก!
ThomasH

4
การเลือกยังไม่มีประโยชน์ในการอ่านของงูใหญ่ที่จะปิดกั้นแม้หลังจากการเลือกเพราะมันไม่มีความหมายมาตรฐาน C และจะไม่ส่งคืนข้อมูลบางส่วน
Helmut Grohne

ข้อความแยกต่างหากสำหรับการอ่านจากผลลัพธ์ของเด็ก ๆ แก้ปัญหาของฉันซึ่งคล้ายกับสิ่งนี้ หากคุณต้องการการโต้ตอบแบบซิงโครนัสฉันเดาว่าคุณไม่สามารถใช้โซลูชันนี้ได้ ฉันจะยอมรับคำตอบนี้
Emiliano

1

ทำไมรบกวนเธรดและคิว ซึ่งแตกต่างจาก readline () BufferedReader.read1 () จะไม่บล็อคการรอ \ r \ n โดยจะส่งคืน ASAP หากมีเอาต์พุตใด ๆ เข้ามา

#!/usr/bin/python
from subprocess import Popen, PIPE, STDOUT
import io

def __main__():
    try:
        p = Popen( ["ping", "-n", "3", "127.0.0.1"], stdin=PIPE, stdout=PIPE, stderr=STDOUT )
    except: print("Popen failed"); quit()
    sout = io.open(p.stdout.fileno(), 'rb', closefd=False)
    while True:
        buf = sout.read1(1024)
        if len(buf) == 0: break
        print buf,

if __name__ == '__main__':
    __main__()

มันจะกลับมาโดยเร็วถ้าไม่มีอะไรมาทำอยู่เหรอ? หากไม่ได้เป็นการปิดกั้น
Mathieu Pagé

@ MathieuPagéถูกต้อง read1จะบล็อกหากบล็อกการอ่านที่มีการขีดเส้นใต้แรกซึ่งเกิดขึ้นเมื่อไพพ์ยังคงเปิดอยู่ แต่ไม่มีอินพุตให้ใช้
Jack O'Connor

1

ในกรณีของฉันฉันต้องการโมดูลการบันทึกที่จับเอาท์พุทจากการใช้งานพื้นหลังและเพิ่มมัน (เพิ่มการประทับเวลา, สี, ฯลฯ )

ฉันลงเอยด้วยเธรดพื้นหลังที่เป็น I / O จริง รหัสต่อไปนี้ใช้สำหรับแพลตฟอร์ม POSIX เท่านั้น ฉันถอดชิ้นส่วนที่ไม่จำเป็นออก

หากใครบางคนกำลังใช้สัตว์ร้ายตัวนี้เป็นเวลานานลองพิจารณาการจัดการคำอธิบายเปิด ในกรณีของฉันมันไม่ใช่ปัญหาใหญ่

# -*- python -*-
import fcntl
import threading
import sys, os, errno
import subprocess

class Logger(threading.Thread):
    def __init__(self, *modules):
        threading.Thread.__init__(self)
        try:
            from select import epoll, EPOLLIN
            self.__poll = epoll()
            self.__evt = EPOLLIN
            self.__to = -1
        except:
            from select import poll, POLLIN
            print 'epoll is not available'
            self.__poll = poll()
            self.__evt = POLLIN
            self.__to = 100
        self.__fds = {}
        self.daemon = True
        self.start()

    def run(self):
        while True:
            events = self.__poll.poll(self.__to)
            for fd, ev in events:
                if (ev&self.__evt) != self.__evt:
                    continue
                try:
                    self.__fds[fd].run()
                except Exception, e:
                    print e

    def add(self, fd, log):
        assert not self.__fds.has_key(fd)
        self.__fds[fd] = log
        self.__poll.register(fd, self.__evt)

class log:
    logger = Logger()

    def __init__(self, name):
        self.__name = name
        self.__piped = False

    def fileno(self):
        if self.__piped:
            return self.write
        self.read, self.write = os.pipe()
        fl = fcntl.fcntl(self.read, fcntl.F_GETFL)
        fcntl.fcntl(self.read, fcntl.F_SETFL, fl | os.O_NONBLOCK)
        self.fdRead = os.fdopen(self.read)
        self.logger.add(self.read, self)
        self.__piped = True
        return self.write

    def __run(self, line):
        self.chat(line, nl=False)

    def run(self):
        while True:
            try: line = self.fdRead.readline()
            except IOError, exc:
                if exc.errno == errno.EAGAIN:
                    return
                raise
            self.__run(line)

    def chat(self, line, nl=True):
        if nl: nl = '\n'
        else: nl = ''
        sys.stdout.write('[%s] %s%s' % (self.__name, line, nl))

def system(command, param=[], cwd=None, env=None, input=None, output=None):
    args = [command] + param
    p = subprocess.Popen(args, cwd=cwd, stdout=output, stderr=output, stdin=input, env=env, bufsize=0)
    p.wait()

ls = log('ls')
ls.chat('go')
system("ls", ['-l', '/'], output=ls)

date = log('date')
date.chat('go')
system("date", output=date)

1

ปัญหาของฉันแตกต่างกันเล็กน้อยเนื่องจากฉันต้องการรวบรวมทั้ง stdout และ stderr จากกระบวนการที่กำลังทำงานอยู่ แต่ในที่สุดก็เหมือนกันเพราะฉันต้องการแสดงผลเอาต์พุตในวิดเจ็ตตามที่สร้างขึ้น

ฉันไม่ต้องการใช้วิธีแก้ปัญหาที่เสนอจำนวนมากโดยใช้ Queues หรือเธรดเพิ่มเติมเนื่องจากไม่จำเป็นต้องทำงานทั่วไปเช่นใช้สคริปต์อื่นและรวบรวมผลลัพธ์

หลังจากอ่านวิธีแก้ปัญหาที่เสนอและเอกสารหลามฉันแก้ไขปัญหาด้วยการใช้งานด้านล่าง ใช่ใช้งานได้กับ POSIX เท่านั้นเมื่อฉันใช้selectเรียกฟังก์ชัน

ฉันยอมรับว่าเอกสารนั้นสร้างความสับสนและการใช้งานนั้นไม่สะดวกสำหรับงานสคริปต์ทั่วไป ฉันเชื่อว่าหลามเวอร์ชันเก่ามีค่าเริ่มต้นต่างกันPopenแตกต่างกันและคำอธิบายที่แตกต่างกันทำให้เกิดความสับสนอย่างมาก ดูเหมือนว่าจะทำงานได้ดีสำหรับทั้ง Python 2.7.12 และ 3.5.2

ที่สำคัญคือการตั้งค่าbufsize=1สำหรับสายบัฟเฟอร์แล้วที่จะดำเนินการเป็นไฟล์ข้อความแทนไบนารีซึ่งดูเหมือนว่าจะกลายเป็นค่าเริ่มต้นเมื่อการตั้งค่าuniversal_newlines=Truebufsize=1

class workerThread(QThread):
   def __init__(self, cmd):
      QThread.__init__(self)
      self.cmd = cmd
      self.result = None           ## return code
      self.error = None            ## flag indicates an error
      self.errorstr = ""           ## info message about the error

   def __del__(self):
      self.wait()
      DEBUG("Thread removed")

   def run(self):
      cmd_list = self.cmd.split(" ")   
      try:
         cmd = subprocess.Popen(cmd_list, bufsize=1, stdin=None
                                        , universal_newlines=True
                                        , stderr=subprocess.PIPE
                                        , stdout=subprocess.PIPE)
      except OSError:
         self.error = 1
         self.errorstr = "Failed to execute " + self.cmd
         ERROR(self.errorstr)
      finally:
         VERBOSE("task started...")
      import select
      while True:
         try:
            r,w,x = select.select([cmd.stdout, cmd.stderr],[],[])
            if cmd.stderr in r:
               line = cmd.stderr.readline()
               if line != "":
                  line = line.strip()
                  self.emit(SIGNAL("update_error(QString)"), line)
            if cmd.stdout in r:
               line = cmd.stdout.readline()
               if line == "":
                  break
               line = line.strip()
               self.emit(SIGNAL("update_output(QString)"), line)
         except IOError:
            pass
      cmd.wait()
      self.result = cmd.returncode
      if self.result < 0:
         self.error = 1
         self.errorstr = "Task terminated by signal " + str(self.result)
         ERROR(self.errorstr)
         return
      if self.result:
         self.error = 1
         self.errorstr = "exit code " + str(self.result)
         ERROR(self.errorstr)
         return
      return

ข้อผิดพลาด DEBUG และ VERBOSE เป็นเพียงแมโครที่พิมพ์เอาต์พุตไปยังเทอร์มินัล

โซลูชันนี้มีประสิทธิภาพ IMHO 99.99% เนื่องจากยังคงใช้readlineฟังก์ชันการบล็อกดังนั้นเราจึงถือว่ากระบวนการย่อยนั้นดีและให้ผลลัพธ์ที่สมบูรณ์

ฉันยินดีรับข้อเสนอแนะเพื่อปรับปรุงการแก้ปัญหาเนื่องจากฉันยังใหม่กับ Python


ในกรณีพิเศษนี้คุณสามารถตั้งค่า stderr = subprocess.STDOUT ในตัวสร้าง Popen และรับเอาต์พุตทั้งหมดจาก cmd.stdout.readline ()
แอรอน

เป็นตัวอย่างที่ชัดเจนดี มีปัญหากับ select.select () แต่นี่แก้ไขได้สำหรับฉัน
maharvey67


0

ทำงานจากคำตอบของ JF Sebastian และแหล่งข้อมูลอื่น ๆ ฉันได้รวบรวมผู้จัดการระบบย่อยที่เรียบง่ายไว้ด้วยกัน มันให้การร้องขอที่ไม่ปิดกั้นการอ่านเช่นเดียวกับการทำงานหลายกระบวนการในแบบคู่ขนาน มันไม่ได้ใช้การเรียกเฉพาะระบบปฏิบัติการใด ๆ (ที่ฉันรู้) และควรทำงานได้ทุกที่

มันมีอยู่จาก pypi pip install shelljobดังนั้นเพียงแค่ อ้างถึงหน้าโครงการสำหรับตัวอย่างและเอกสารฉบับเต็ม


0

แก้ไข: การใช้งานนี้ยังคงบล็อก ใช้คำตอบของ JFSebastian แทน

ฉันลองคำตอบยอดนิยมแต่ความเสี่ยงและการบำรุงรักษาเพิ่มเติมของเธรดโค้ดนั้นน่าเป็นห่วง

เมื่อมองผ่านโมดูล io (และ จำกัด อยู่ที่ 2.6) ฉันพบ BufferedReader นี่เป็นวิธีแก้ปัญหาแบบไม่บล็อกไม่มีเกลียวของฉัน

import io
from subprocess import PIPE, Popen

p = Popen(['myprogram.exe'], stdout=PIPE)

SLEEP_DELAY = 0.001

# Create an io.BufferedReader on the file descriptor for stdout
with io.open(p.stdout.fileno(), 'rb', closefd=False) as buffer:
  while p.poll() == None:
      time.sleep(SLEEP_DELAY)
      while '\n' in bufferedStdout.peek(bufferedStdout.buffer_size):
          line = buffer.readline()
          # do stuff with the line

  # Handle any remaining output after the process has ended
  while buffer.peek():
    line = buffer.readline()
    # do stuff with the line

คุณเคยลองfor line in iter(p.stdout.readline, ""): # do stuff with the lineไหม มันเป็นเธรด (เธรดเดี่ยว) และบล็อกเมื่อรหัสของคุณบล็อก
jfs

@ jf-sebastian ใช่ในที่สุดฉันก็กลับคำตอบของคุณ การใช้งานของฉันยังคงถูกบล็อกเป็นครั้งคราว ฉันจะแก้ไขคำตอบเพื่อเตือนผู้อื่นไม่ให้ลงเส้นทางนี้
romc

0

ฉันเพิ่งพบปัญหาเดียวกันฉันต้องอ่านหนึ่งบรรทัดในเวลาเดียวจากกระแส (หางทำงานใน subprocess) ในโหมดไม่บล็อคฉันต้องการหลีกเลี่ยงปัญหาต่อไป: ไม่เผาซีพียูไม่อ่านกระแสข้อมูลโดยหนึ่งไบต์ ( เหมือนที่ Readline ทำ) เป็นต้น

นี่คือการใช้งานของฉัน https://gist.github.com/grubberr/5501e1a9760c3eab5e0a มันไม่รองรับ windows (โพล) ไม่จัดการ EOF แต่มันใช้ได้ดีสำหรับฉัน


คำตอบกระทู้ตามไม่ได้เผาซีพียู (คุณสามารถระบุโดยพลการtimeoutเช่นเดียวกับในการแก้ปัญหาของคุณ) และ.readline()อ่านเพิ่มเติมมากกว่าหนึ่งไบต์ในเวลา ( bufsize=1หมายถึงเส้น -buffered (เฉพาะที่เกี่ยวข้องกับการเขียน)) คุณพบปัญหาอะไรอีกบ้าง คำตอบสำหรับลิงค์เท่านั้นไม่มีประโยชน์มาก
jfs

0

นี่คือตัวอย่างการรันคำสั่งแบบโต้ตอบในกระบวนการย่อยและ stdout เป็นแบบโต้ตอบโดยใช้เทอร์มินัลหลอก คุณสามารถอ้างถึง: https://stackoverflow.com/a/43012138/3555925

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import select
import termios
import tty
import pty
from subprocess import Popen

command = 'bash'
# command = 'docker run -it --rm centos /bin/bash'.split()

# save original tty setting then set it to raw mode
old_tty = termios.tcgetattr(sys.stdin)
tty.setraw(sys.stdin.fileno())

# open pseudo-terminal to interact with subprocess
master_fd, slave_fd = pty.openpty()

# use os.setsid() make it run in a new process group, or bash job control will not be enabled
p = Popen(command,
          preexec_fn=os.setsid,
          stdin=slave_fd,
          stdout=slave_fd,
          stderr=slave_fd,
          universal_newlines=True)

while p.poll() is None:
    r, w, e = select.select([sys.stdin, master_fd], [], [])
    if sys.stdin in r:
        d = os.read(sys.stdin.fileno(), 10240)
        os.write(master_fd, d)
    elif master_fd in r:
        o = os.read(master_fd, 10240)
        if o:
            os.write(sys.stdout.fileno(), o)

# restore tty settings back
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)

0

วิธีนี้ใช้วิธี selectโมดูลเพื่อ "อ่านข้อมูลใด ๆ ที่มี" จากกระแส IO ฟังก์ชั่นนี้จะเริ่มต้นจนกว่าจะมีข้อมูล แต่จะอ่านเฉพาะข้อมูลที่มีอยู่และไม่ได้ปิดกั้นเพิ่มเติม

เมื่อพิจารณาจากความจริงที่ว่ามันใช้selectโมดูลสิ่งนี้ใช้งานได้กับ Unix เท่านั้น

รหัสนี้เป็นไปตามข้อกำหนด PEP8 อย่างสมบูรณ์

import select


def read_available(input_stream, max_bytes=None):
    """
    Blocks until any data is available, then all available data is then read and returned.
    This function returns an empty string when end of stream is reached.

    Args:
        input_stream: The stream to read from.
        max_bytes (int|None): The maximum number of bytes to read. This function may return fewer bytes than this.

    Returns:
        str
    """
    # Prepare local variables
    input_streams = [input_stream]
    empty_list = []
    read_buffer = ""

    # Initially block for input using 'select'
    if len(select.select(input_streams, empty_list, empty_list)[0]) > 0:

        # Poll read-readiness using 'select'
        def select_func():
            return len(select.select(input_streams, empty_list, empty_list, 0)[0]) > 0

        # Create while function based on parameters
        if max_bytes is not None:
            def while_func():
                return (len(read_buffer) < max_bytes) and select_func()
        else:
            while_func = select_func

        while True:
            # Read single byte at a time
            read_data = input_stream.read(1)
            if len(read_data) == 0:
                # End of stream
                break
            # Append byte to string buffer
            read_buffer += read_data
            # Check if more data is available
            if not while_func():
                break

    # Return read buffer
    return read_buffer

0

ฉันยังประสบปัญหาที่อธิบายโดยJesseและแก้ไขโดยใช้ "select" เช่นBradley , Andyและคนอื่น ๆ ทำ แต่อยู่ในโหมดบล็อกเพื่อหลีกเลี่ยงการวนซ้ำไม่ว่าง มันใช้ Dummy Pipe เป็น stdin ปลอม บล็อกที่เลือกและรอ stdin หรือไปป์ที่จะพร้อม เมื่อกดปุ่ม stdin จะปลดบล็อกการเลือกและสามารถดึงค่าของคีย์ด้วยการอ่าน (1) เมื่อเธรดอื่นเขียนไปยังไพพ์ไพพ์นั้นจะปลดบล็อกการเลือกและสามารถนำมาเป็นเครื่องบ่งชี้ว่า stdin นั้นจำเป็นที่จะต้องจบลง นี่คือรหัสอ้างอิงบางส่วน:

import sys
import os
from select import select

# -------------------------------------------------------------------------    
# Set the pipe (fake stdin) to simulate a final key stroke
# which will unblock the select statement
readEnd, writeEnd = os.pipe()
readFile = os.fdopen(readEnd)
writeFile = os.fdopen(writeEnd, "w")

# -------------------------------------------------------------------------
def getKey():

    # Wait for stdin or pipe (fake stdin) to be ready
    dr,dw,de = select([sys.__stdin__, readFile], [], [])

    # If stdin is the one ready then read it and return value
    if sys.__stdin__ in dr:
        return sys.__stdin__.read(1)   # For Windows use ----> getch() from module msvcrt

    # Must finish
    else:
        return None

# -------------------------------------------------------------------------
def breakStdinRead():
    writeFile.write(' ')
    writeFile.flush()

# -------------------------------------------------------------------------
# MAIN CODE

# Get key stroke
key = getKey()

# Keyboard input
if key:
    # ... do your stuff with the key value

# Faked keystroke
else:
    # ... use of stdin finished

# -------------------------------------------------------------------------
# OTHER THREAD CODE

breakStdinRead()

หมายเหตุ: เพื่อให้สามารถใช้งานได้กับ Windows ท่อจะถูกแทนที่ด้วยซ็อกเก็ต ฉันยังไม่ได้ลอง แต่ควรใช้งานได้ตามเอกสาร
gonzaedu61

0

ลองwexpectซึ่งเป็นหน้าต่างทางเลือกของการpexpect

import wexpect

p = wexpect.spawn('myprogram.exe')
p.stdout.readline('.')               // regex pattern of any character
output_str = p.after()

0

บนระบบที่เหมือน Unix และ Python 3.5 ขึ้นไปมีos.set_blockingสิ่งใดที่จะบอกได้อย่างแน่นอน

import os
import time
import subprocess

cmd = 'python3', '-c', 'import time; [(print(i), time.sleep(1)) for i in range(5)]'
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
os.set_blocking(p.stdout.fileno(), False)
start = time.time()
while True:
    # first iteration always produces empty byte string in non-blocking mode
    for i in range(2):    
        line = p.stdout.readline()
        print(i, line)
        time.sleep(0.5)
    if time.time() > start + 5:
        break
p.terminate()

ผลลัพธ์นี้:

1 b''
2 b'0\n'
1 b''
2 b'1\n'
1 b''
2 b'2\n'
1 b''
2 b'3\n'
1 b''
2 b'4\n'

ด้วยos.set_blockingความเห็นมัน:

0 b'0\n'
1 b'1\n'
0 b'2\n'
1 b'3\n'
0 b'4\n'
1 b''

-2

นี่คือโมดูลที่รองรับการอ่านที่ไม่ปิดกั้นและการเขียนเบื้องหลังในหลาม:

https://pypi.python.org/pypi/python-nonblock

ให้ฟังก์ชั่น

nonblock_read ซึ่งจะอ่านข้อมูลจากสตรีมหากมีอยู่มิฉะนั้นจะส่งคืนสตริงว่าง (หรือไม่มีหากปิดสตรีมที่อีกด้านหนึ่งและอ่านข้อมูลที่เป็นไปได้ทั้งหมดแล้ว)

คุณอาจพิจารณาโมดูล python-subprocess2

https://pypi.python.org/pypi/python-subprocess2

ซึ่งเพิ่มไปยังโมดูลกระบวนการย่อย ดังนั้นบนวัตถุที่ส่งคืนจาก "subprocess.Popen" จะถูกเพิ่มวิธีการเพิ่มเติม runInBackground สิ่งนี้จะเริ่มเธรดและส่งคืนวัตถุซึ่งจะถูกเติมโดยอัตโนมัติเมื่อข้อมูลถูกเขียนไปยัง stdout / stderr โดยไม่ปิดกั้นเธรดหลักของคุณ

สนุก!


ฉันต้องการลองโมดูลnonblockนี้แต่ฉันค่อนข้างใหม่ในบางขั้นตอนของ Linux ฉันจะติดตั้งรูทีนเหล่านี้ได้อย่างไร ฉันใช้ Raspbian Jessie รสชาติของ Debian Linux สำหรับ Raspberry Pi ฉันลอง 'sudo apt-get install nonblock' และ python-nonblock และทั้งคู่เกิดข้อผิดพลาด - ไม่พบ ฉันได้ดาวน์โหลดไฟล์ zip จากไซต์นี้pypi.python.org/pypi/python-nonblockแล้ว แต่ไม่รู้จะทำอย่างไรกับมัน ขอบคุณ .... RDK
RDK
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.