web-dev-qa-db-fra.com

attraper stdout en temps réel depuis un sous-processus

Je veux subprocess.Popen() rsync.exe dans Windows et imprimer la sortie standard en Python.

Mon code fonctionne, mais il ne détecte pas la progression tant qu'un transfert de fichier n'est pas terminé! Je veux imprimer la progression de chaque fichier en temps réel.

En utilisant Python 3.1 maintenant puisque j'ai entendu dire qu'il devrait être mieux gérer IO.

import subprocess, time, os, sys

cmd = "rsync.exe -vaz -P source/ dest/"
p, line = True, 'start'


p = subprocess.Popen(cmd,
                     Shell=True,
                     bufsize=64,
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()
64
John A

Quelques règles de base pour subprocess.

  • Jamais utilisez Shell=True. Il appelle inutilement un processus Shell supplémentaire pour appeler votre programme.
  • Lors de l'appel de processus, les arguments sont transmis sous forme de listes. sys.argv en python est une liste, tout comme argv en C. Vous passez donc un list à Popen pour appeler des sous-processus, pas une chaîne.
  • Ne redirigez pas stderr vers un PIPE lorsque vous ne le lisez pas. 
  • Ne redirigez pas stdin lorsque vous n'y écrivez pas.

Exemple:

import subprocess, time, os, sys
cmd = ["rsync.exe", "-vaz", "-P", "source/" ,"dest/"]

p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE,
                     stderr=subprocess.STDOUT)

for line in iter(p.stdout.readline, b''):
    print(">>> " + line.rstrip())

Cela dit, il est probable que rsync tamponne sa sortie lorsqu'il détecte qu'il est connecté à un canal plutôt qu'à un terminal. C'est le comportement par défaut - quand un programme est connecté à un tuyau, les programmes doivent explicitement vider stdout pour obtenir des résultats en temps réel, sinon la bibliothèque C standard utilisera un tampon.

Pour tester cela, essayez plutôt d'exécuter ceci:

cmd = [sys.executable, 'test_out.py']

et créez un fichier test_out.py avec le contenu:

import sys
import time
print ("Hello")
sys.stdout.flush()
time.sleep(10)
print ("World")

L'exécution de ce sous-processus devrait vous donner "Bonjour" et attendre 10 secondes avant de donner "Monde". Si cela se produit avec le code python ci-dessus et non pas avec rsync, cela signifie que rsync est lui-même la mise en mémoire tampon de la sortie.

Une solution serait de connecter directement à une pty, en utilisant quelque chose comme pexpect.

81
nosklo

Je sais que c'est un sujet ancien, mais il existe une solution maintenant. Appelez le rsync avec l'option --outbuf = L. Exemple:

cmd=['rsync', '-arzv','--backup','--outbuf=L','source/','dest']
p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE)
for line in iter(p.stdout.readline, b''):
    print '>>> {}'.format(line.rstrip())
29
Elvin

Sur Linux, j'ai eu le même problème pour me débarrasser de la mise en mémoire tampon. J'ai finalement utilisé "stdbuf -o0" (ou, comme prévu, pour supprimer le tampon PIPE).

proc = Popen(['stdbuf', '-o0'] + cmd, stdout=PIPE, stderr=PIPE)
stdout = proc.stdout

Je pourrais alors utiliser select.select sur stdout.

Voir aussi https://unix.stackexchange.com/questions/25372/

10
Ling
for line in p.stdout:
  ...

bloque toujours jusqu'au prochain saut de ligne.

Pour un comportement "en temps réel", vous devez faire quelque chose comme ceci:

while True:
  inchar = p.stdout.read(1)
  if inchar: #neither empty string nor None
    print(str(inchar), end='') #or end=None to flush immediately
  else:
    print('') #flush for implicit line-buffering
    break

La boucle while est laissée lorsque le processus enfant ferme sa sortie standard ou quitte .read()/read(-1) se bloque jusqu'à ce que le processus enfant ferme ou quitte sa sortie standard.

7
IBue

Votre problème c'est:

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()

l'itérateur lui-même dispose d'une mémoire tampon supplémentaire.

Essayez de faire comme ça:

while True:
  line = p.stdout.readline()
  if not line:
     break
  print line
6
zviadm

Vous ne pouvez pas obtenir stdout pour imprimer sans tampon dans un tuyau (à moins que vous ne puissiez réécrire le programme qui imprime sur stdout), voici donc ma solution:

Rediriger stdout vers sterr, qui n'est pas mis en mémoire tampon. '<cmd> 1>&2' devrait le faire. Ouvrez le processus comme suit: myproc = subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)
Vous ne pouvez pas distinguer stdout ou stderr, mais vous obtenez toutes les sorties immédiatement.

J'espère que cela aidera quiconque à résoudre ce problème.

5
Erik

Pour éviter la mise en cache de la sortie, vous pouvez essayer pexpect, 

child = pexpect.spawn(launchcmd,args,timeout=None)
while True:
    try:
        child.expect('\n')
        print(child.before)
    except pexpect.EOF:
        break

PS: Je sais que cette question est assez ancienne, fournissant toujours la solution qui a fonctionné pour moi.

PPS: a obtenu cette réponse d'une autre question

2
nithin
    p = subprocess.Popen(command,
                                bufsize=0,
                                universal_newlines=True)

J'écris une interface graphique pour rsync en python, et ai les mêmes probels. Ce problème me préoccupe depuis plusieurs jours jusqu'à ce que je trouve cela dans pyDoc.

Si universal_newlines a la valeur True, les objets de fichier stdout et stderr sont ouverts en tant que fichiers texte en mode de nouvelle ligne universel. Les lignes peuvent être terminées par «\ n», convention de fin de ligne Unix, «\ r», ancienne convention Macintosh ou «\ r\n», convention Windows. Toutes ces représentations externes sont considérées comme '\ n' par le programme Python.

Il semble que rsync affichera '\ r' lorsque la traduction est en cours.

2
xmc

Changez la sortie standard du processus rsync pour qu'elle ne soit plus masquée.

p = subprocess.Popen(cmd,
                     Shell=True,
                     bufsize=0,  # 0=unbuffered, 1=line-buffered, else buffer-size
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)
2
Will

Selon le cas d'utilisation, vous pouvez également désactiver la mise en mémoire tampon dans le sous-processus lui-même.

Si le sous-processus est un processus Python, vous pouvez le faire avant l'appel:

os.environ["PYTHONUNBUFFERED"] = "1"

Ou bien passez ceci dans l'argument env à Popen.

Sinon, si vous êtes sous Linux/Unix, vous pouvez utiliser l'outil stdbuf. Par exemple. comme:

cmd = ["stdbuf", "-oL"] + cmd

Voir aussi ici à propos de stdbuf ou d’autres options.

1
Albert

J'ai remarqué qu'il n'est pas question d'utiliser un fichier temporaire comme intermédiaire. Ce qui suit résout les problèmes de mise en mémoire tampon en générant dans un fichier temporaire et vous permet d'analyser les données provenant de rsync sans vous connecter à un pty. J'ai testé les éléments suivants sur une boîte Linux, et la sortie de rsync a tendance à être différente d'une plate-forme à l'autre. Les expressions régulières permettant d'analyser la sortie peuvent donc varier:

import subprocess, time, tempfile, re

pipe_output, file_name = tempfile.TemporaryFile()
cmd = ["rsync", "-vaz", "-P", "/src/" ,"/dest"]

p = subprocess.Popen(cmd, stdout=pipe_output, 
                     stderr=subprocess.STDOUT)
while p.poll() is None:
    # p.poll() returns None while the program is still running
    # sleep for 1 second
    time.sleep(1)
    last_line =  open(file_name).readlines()
    # it's possible that it hasn't output yet, so continue
    if len(last_line) == 0: continue
    last_line = last_line[-1]
    # Matching to "[bytes downloaded]  number%  [speed] number:number:number"
    match_it = re.match(".* ([0-9]*)%.* ([0-9]*:[0-9]*:[0-9]*).*", last_line)
    if not match_it: continue
    # in this case, the percentage is stored in match_it.group(1), 
    # time in match_it.group(2).  We could do something with it here...
0
MikeGM