web-dev-qa-db-fra.com

Lecture non bloquante sur un sous-processus.PIPE dans python

J'utilise le module de sous-processus pour démarrer un sous-processus et se connecter à son flux de sortie (stdout). Je veux pouvoir exécuter des lectures non bloquantes sur sa sortie standard. Existe-t-il un moyen de rendre .readline non bloquant ou de vérifier s’il existe des données sur le flux avant d’appeler .readline? J'aimerais que cela soit portable ou au moins fonctionne sous Windows et Linux.

voici comment je le fais pour le moment (Il bloque le .readline si aucune donnée n'est disponible):

p = subprocess.Popen('myprogram.exe', stdout = subprocess.PIPE)
output_str = p.stdout.readline()
469
Mathieu Pagé

fcntl , select , asyncproc ne vous aidera pas dans ce cas.

Un moyen fiable de lire un flux sans bloquer, quel que soit le système d'exploitation, consiste à utiliser 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
383
jfs

J'ai souvent eu un problème similaire; Les programmes Python que j'écris fréquemment doivent pouvoir exécuter certaines fonctionnalités principales tout en acceptant les entrées utilisateur à partir de la ligne de commande (stdin). Le simple fait de placer la fonctionnalité de gestion des entrées utilisateur dans un autre thread ne résout pas le problème, car readline() se bloque et n'a pas de délai d'expiration. Si la fonctionnalité principale est terminée et qu'il n'est plus nécessaire d'attendre l'entrée utilisateur supplémentaire, je souhaite généralement que mon programme se ferme, mais il ne le peut pas car readline() bloque toujours dans l'autre thread en attente d'une ligne. Une solution que j'ai trouvée à ce problème consiste à faire de stdin un fichier non bloquant à l'aide du module 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)

À mon avis, c'est un peu plus propre que d'utiliser les modules select ou signal pour résoudre ce problème, mais là encore, cela ne fonctionne que sous UNIX ...

73
Jesse

Python 3.4 introduit le nouveau API provisoire pour le module IO _ _ - asyncio asynchrone .

L’approche est similaire à réponse basée sur twisted de @Bryan Ward - définissez un protocole et ses méthodes sont appelées dès que les données sont prêtes:

#!/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()

Voir "Sous-processus" dans la documentation .

Il existe une interface de haut niveau asyncio.create_subprocess_exec() qui renvoie Process objets qui permet de lire une ligne de manière asynchrone à l'aide de StreamReader.readline() coroutine (avec async/await Python syntaxe 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() effectue les tâches suivantes:

  • démarrer un sous-processus, rediriger sa sortie standard vers un tuyau
  • lire une ligne de la sortie standard du sous-processus de manière asynchrone
  • tuer le sous-processus
  • attendez qu'il sorte

Chaque étape peut être limitée par un délai d'expiration en secondes si nécessaire.

37
jfs

Essayez le module asyncproc . Par exemple:

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

Le module s'occupe de tout le threading comme suggéré par S.Lott.

20
Noah

Vous pouvez le faire très facilement dans Twisted . En fonction de votre base de code existante, cela peut ne pas être aussi facile à utiliser, mais si vous construisez une application tordue, cela devient alors presque trivial. Vous créez une classe ProcessProtocol et remplacez la méthode outReceived(). Twisted (selon le réacteur utilisé) est généralement juste une grosse boucle select() avec des callbacks installés pour gérer les données de différents descripteurs de fichier (souvent des sockets réseau). La méthode outReceived() installe donc simplement un rappel pour le traitement des données provenant de STDOUT. Voici un exemple simple démontrant ce comportement:

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

Le documentation sur Twisted contient de bonnes informations à ce sujet.

Si vous construisez toute votre application autour de Twisted, la communication asynchrone avec d'autres processus, locaux ou distants, devient vraiment élégante, comme celle-ci. D'un autre côté, si votre programme n'est pas construit sur Twisted, cela ne sera pas vraiment utile. J'espère que cela pourra être utile aux autres lecteurs, même si cela ne s'applique pas à votre application particulière.

17
Bryan Ward

Utilisez select & read (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))

Pour readline () - like:

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
16
Andy Jackson

Une solution consiste à créer un autre processus pour effectuer votre lecture du processus ou à en créer une unité d'exécution avec un délai d'expiration.

Voici la version filetée d'une fonction de délai d'attente:

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

Cependant, avez-vous besoin de lire la sortie standard à mesure qu'elle arrive? Une autre solution peut être de vider la sortie dans un fichier et d’attendre la fin du processus avec 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()
8
monkut

Les solutions existantes ne fonctionnaient pas pour moi (détails ci-dessous). Ce qui a finalement fonctionné a été de mettre en œuvre readline en utilisant read (1) (basé sur cette réponse ). Ce dernier ne bloque pas:

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

Pourquoi les solutions existantes n'ont pas fonctionné:

  1. Les solutions nécessitant une ligne de lecture (y compris celles basées sur la file d'attente) sont toujours bloquées. Il est difficile (impossible?) De tuer le thread qui exécute readline. Il n'est tué que lorsque le processus qui l'a créé se termine, mais pas lorsque le processus produisant une sortie est tué.
  2. Le mélange de fcntl de bas niveau avec des appels readline de haut niveau risque de ne pas fonctionner correctement, comme cela a déjà été souligné.
  3. Utiliser select.poll () est simple, mais ne fonctionne pas sous Windows selon python docs.
  4. L'utilisation de bibliothèques tierces semble excessive pour cette tâche et ajoute des dépendances supplémentaires.
7
Vikram Pudi

Avertissement: cela ne fonctionne que pour la tornade

Vous pouvez le faire en définissant le fd sur non bloquant, puis utilisez ioloop pour enregistrer des rappels. Je l'ai emballé dans un oeuf appelé tornado_subprocess et vous pouvez l'installer via PyPI:

easy_install tornado_subprocess

maintenant, vous pouvez faire quelque chose comme ça:

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

vous pouvez également l'utiliser avec un gestionnaire de demandes

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()
7
Vukasin Toroman

Cette version de lecture non bloquante ne nécessite pas des modules spéciaux et fonctionnera immédiatement avec la majorité des distributions Linux.

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 __== '__main__':
    cmd = 'ping 8.8.8.8'
    sout, serr = Shell(cmd.split())
5
Tom Lime

J'ajoute ce problème pour lire un sous-processus.Popen stdout. Voici ma solution de lecture non bloquante:

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'
4
Sebastien Claeys

Voici mon code, utilisé pour capturer toutes les sorties du sous-processus ASAP, y compris les lignes partielles. Il pompe en même temps et stdout et stderr dans un ordre presque correct.

Testé et correctement travaillé sur Python 2.7 Linux et 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 __== '__main__':
    __main__()
4
datacompboy

Ajouter cette réponse ici car elle permet de définir des canaux non bloquants sous Windows et Unix.

Tous les détails ctypes sont dus à réponse de @ techtonik .

Il existe une version légèrement modifiée à utiliser sur les systèmes Unix et Windows.

  • Compatible Python3 (seule une modification mineure est nécessaire) .
  • Inclut la version posix et définit une exception à utiliser.

De cette façon, vous pouvez utiliser la même fonction et la même exception pour le code Unix et 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: https://stackoverflow.com/questions/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

Pour éviter de lire des données incomplètes, j'ai fini par écrire mon propre générateur de lignes de lecture (qui renvoie la chaîne d'octets pour chaque ligne).

C'est un générateur afin que vous puissiez par exemple ...

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)
2
ideasman42

J'ai le problème du questionneur d'origine, mais je n'ai pas voulu invoquer les discussions. J'ai mélangé la solution de Jesse avec une lecture directe () du tube et mon propre gestionnaire de tampons pour les lectures de lignes (cependant, mon sous-processus - ping - a toujours écrit des lignes entières <une taille de page système). J'évite les longues files d'attente en ne lisant que dans une montre io enregistrée par un gobjet. Ces jours-ci, je lance généralement du code dans un gobject MainLoop pour éviter les threads.

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

L'observateur est

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

Et le programme principal configure un ping, puis appelle la boucle de messagerie de gobject.

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

Tout autre travail est attaché aux rappels dans gobject.

2
Dave Kitchen

Dans mon cas, j'avais besoin d'un module de journalisation qui récupère la sortie des applications en arrière-plan et l'augmente (ajout d'horodatages, de couleurs, etc.).

Je me suis retrouvé avec un thread d'arrière-plan qui fait la réelle E/S. Le code suivant est uniquement destiné aux plateformes POSIX. J'ai dépouillé des parties non essentielles.

Si quelqu'un veut utiliser cette bête pendant de longues périodes, envisagez de gérer les descripteurs ouverts. Dans mon cas ce n'était pas un gros problème.

# -*- 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
Dmytro

pourquoi déranger fil et file d'attente? contrairement à readline (), BufferedReader.read1 () ne se bloque pas en attente de\r\n, il retourne ASAP s'il y a une sortie qui entre.

#!/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 __== '__main__':
    __main__()
1
mfmain

Le module select vous aide à déterminer où se trouve la prochaine entrée utile.

Cependant, vous êtes presque toujours plus heureux avec des threads séparés. L'un fait un blocage en lisant le stdin, un autre en tout lieu que vous ne voulez pas bloquer.

1
S.Lott

À partir de la réponse de J.F. Sebastian et de plusieurs autres sources, j'ai mis au point un gestionnaire de sous-processus simple. Il fournit la requête en lecture non bloquante, ainsi que l'exécution de plusieurs processus en parallèle. Il n'utilise aucun appel spécifique au système d'exploitation (à ma connaissance) et devrait donc fonctionner n'importe où.

Il est disponible chez Pypi, donc juste pip install shelljob. Reportez-vous à la page du projet pour obtenir des exemples et une documentation complète.

0
edA-qa mort-ora-y

EDIT: Cette implémentation bloque toujours. Utilisez plutôt J. (Sébastien) réponse .

J'ai essayé le réponse du haut , mais le risque supplémentaire et la maintenance du code de thread étaient inquiétants.

En regardant à travers le module io (et étant limité à 2.6), j'ai trouvé BufferedReader. Ceci est ma solution sans thread, non bloquante.

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
0
romc

Les choses sont bien meilleures en Python moderne.

Voici un programme enfant simple, "hello.py":

#!/usr/bin/env python3

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

Et un programme pour interagir avec elle:

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

Cela imprime:

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

Notez que le modèle actuel, qui correspond également à presque toutes les réponses précédentes, ici et dans les questions connexes, consiste à définir le descripteur de fichier stdout de l'enfant sur non bloquant, puis à l'interroger dans une sorte de boucle de sélection. Ces jours-ci, bien sûr, cette boucle est fournie par asyncio.

0
user240515

Ceci est un exemple pour exécuter une commande interactive dans un sous-processus, et la sortie standard est interactive à l'aide d'un pseudo-terminal. Vous pouvez vous référer à: 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
Liao

Mon problème est un peu différent car je voulais collecter stdout et stderr à partir d'un processus en cours d'exécution, mais finalement le même depuis que je voulais restituer la sortie dans un widget telle que générée.

Je ne voulais pas recourir à de nombreuses solutions de contournement proposées en utilisant des files d'attente ou des threads supplémentaires, car ils ne devraient pas être nécessaires pour exécuter une tâche aussi courante que celle d'exécuter un autre script et de collecter sa sortie.

Après avoir lu les solutions proposées et python docs, j'ai résolu mon problème avec l'implémentation ci-dessous. Oui, cela ne fonctionne que pour POSIX car j'utilise l'appel de fonction select.

Je conviens que les documents sont déroutants et que la mise en œuvre est complexe pour une tâche de script commune. Je pense que les anciennes versions de python ont des valeurs par défaut différentes pour Popen et des explications différentes, ce qui a créé beaucoup de confusion. Cela semble bien fonctionner à la fois pour Python 2.7.12 et 3.5.2.

La clé consistait à définir bufsize=1 pour la mise en mémoire tampon de lignes, puis universal_newlines=True à traiter comme un fichier texte au lieu d'un fichier binaire, ce qui semble devenir la valeur par défaut lors du réglage de bufsize=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

ERROR, DEBUG et VERBOSE sont simplement des macros imprimant une sortie sur le terminal.

Cette solution est efficace à mon humble avis à 99,99%, car elle utilise toujours la fonction de blocage readline. Nous supposons donc que le sous-processus est Nice et génère des lignes complètes.

J'apprécie les commentaires pour améliorer la solution car je suis encore novice en Python.

0
Brooke Wallace

J'ai créé une bibliothèque basée sur solution de J.F. Sebastian . Tu peux l'utiliser.

https://github.com/cenkalti/what

0
Cenk Alti

Cette solution utilise le module select pour "lire toutes les données disponibles" d'un flux IO. Cette fonction bloque initialement jusqu'à ce que les données soient disponibles, mais ne lit ensuite que les données disponibles et ne bloque plus.

Étant donné qu'il utilise le module select, cela ne fonctionne que sous Unix.

Le code est entièrement conforme à 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
Bradley Odell

J'ai également fait face au problème décrit par Jesse et je l'ai résolu en utilisant "select" comme Bradley , Andy et d'autres l'ont fait mais dans un mode de blocage éviter une boucle occupée. Il utilise un faux tuyau comme faux stdin. Sélectionnez Block et attendez que stdin ou le canal soit prêt. Quand une touche est enfoncée, stdin débloque la sélection et la valeur de la clé peut être récupérée avec read (1). Lorsqu'un autre thread écrit dans le tuyau, celui-ci débloque la sélection et peut être considéré comme une indication que le besoin de stdin est terminé. Voici un code de référence:

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

Je suis récemment tombé sur le même problème. J'ai besoin de lire une ligne à la fois d'un flux (fin du sous-processus) en mode non bloquant. Je voulais éviter les problèmes suivants: ne pas graver de processeur, ne lisez pas le flux d'un octet ( comme l'a fait readline), etc.

Voici ma mise en œuvre https://Gist.github.com/grubberr/5501e1a9760c3eab5e0a il ne supporte pas Windows (sondage), ne gère pas EOF, mais cela fonctionne bien pour moi

0
grubberr