Lire en continu à partir de STDOUT du processus externe dans Ruby

86

Je veux exécuter blender à partir de la ligne de commande via un script ruby, qui traitera ensuite la sortie donnée par blender ligne par ligne pour mettre à jour une barre de progression dans une interface graphique. Ce n'est pas vraiment important que blender soit le processus externe dont j'ai besoin de lire la sortie standard.

Je n'arrive pas à capter les messages de progression que Blender imprime normalement sur la coque lorsque le processus de Blender est toujours en cours d'exécution, et j'ai essayé plusieurs méthodes. Il me semble toujours accéder à la sortie standard du blender après l'arrêt du blender , pas tant qu'il est toujours en marche.

Voici un exemple de tentative infructueuse. Il obtient et imprime les 25 premières lignes de la sortie du mélangeur, mais seulement après la fin du processus du mélangeur:

blender = nil
t = Thread.new do
  blender = open "| blender -b mball.blend -o //renders/ -F JPEG -x 1 -f 1"
end
puts "Blender is doing its job now..."
25.times { puts blender.gets}

Éditer:

Pour rendre les choses un peu plus claires, la commande invoquant blender renvoie un flux de sortie dans le shell, indiquant la progression (partie 1-16 terminée, etc.). Il semble que tout appel à "obtient" la sortie est bloqué jusqu'à ce que le mélangeur se ferme. Le problème est de savoir comment accéder à cette sortie pendant que Blender est toujours en cours d'exécution, car Blender imprime sa sortie vers le shell.

ehsanul
la source

Réponses:

175

J'ai réussi à résoudre mon problème. Voici les détails, avec quelques explications, au cas où quelqu'un ayant un problème similaire trouverait cette page. Mais si vous ne vous souciez pas des détails, voici la réponse courte :

Utilisez PTY.spawn de la manière suivante (avec votre propre commande bien sûr):

require 'pty'
cmd = "blender -b mball.blend -o //renders/ -F JPEG -x 1 -f 1" 
begin
  PTY.spawn( cmd ) do |stdout, stdin, pid|
    begin
      # Do stuff with the output here. Just printing to show it works
      stdout.each { |line| print line }
    rescue Errno::EIO
      puts "Errno:EIO error, but this probably just means " +
            "that the process has finished giving output"
    end
  end
rescue PTY::ChildExited
  puts "The child process exited!"
end

Et voici la réponse longue , avec beaucoup trop de détails:

Le vrai problème semble être que si un processus ne vide pas explicitement son stdout, alors tout ce qui est écrit sur stdout est mis en mémoire tampon plutôt que réellement envoyé, jusqu'à ce que le processus soit terminé, afin de minimiser les E / S (c'est apparemment un détail d'implémentation de beaucoup Bibliothèques C, conçues pour que le débit soit maximisé grâce à des E / S moins fréquentes). Si vous pouvez facilement modifier le processus pour qu'il vide régulièrement stdout, alors ce serait votre solution. Dans mon cas, c'était blender, donc un peu intimidant pour un noob complet comme moi de modifier la source.

Mais lorsque vous exécutez ces processus depuis le shell, ils affichent stdout vers le shell en temps réel, et le stdout ne semble pas être mis en mémoire tampon. Il est uniquement mis en mémoire tampon lorsqu'il est appelé depuis un autre processus, je crois, mais si un shell est traité, la sortie standard est vue en temps réel, sans tampon.

Ce comportement peut même être observé avec un processus ruby ​​comme le processus enfant dont la sortie doit être collectée en temps réel. Créez simplement un script, random.rb, avec la ligne suivante:

5.times { |i| sleep( 3*rand ); puts "#{i}" }

Puis un script ruby ​​pour l'appeler et renvoyer sa sortie:

IO.popen( "ruby random.rb") do |random|
  random.each { |line| puts line }
end

Vous verrez que vous n'obtenez pas le résultat en temps réel comme vous pourriez vous y attendre, mais tout d'un coup par la suite. STDOUT est mis en mémoire tampon, même si vous exécutez vous-même random.rb, il ne l'est pas. Cela peut être résolu en ajoutant une STDOUT.flushinstruction à l'intérieur du bloc dans random.rb. Mais si vous ne pouvez pas changer la source, vous devez contourner ce problème. Vous ne pouvez pas le vider de l'extérieur du processus.

Si le sous-processus peut imprimer sur le shell en temps réel, il doit également y avoir un moyen de capturer cela avec Ruby en temps réel. Et voici. Vous devez utiliser le module PTY, inclus dans le noyau ruby ​​je crois (1.8.6 de toute façon). Le plus triste, c'est que ce n'est pas documenté. Mais j'ai trouvé quelques exemples d'utilisation heureusement.

Premièrement, pour expliquer ce qu'est PTY, il signifie pseudo terminal . Fondamentalement, cela permet au script ruby ​​de se présenter au sous-processus comme s'il s'agissait d'un utilisateur réel qui venait de taper la commande dans un shell. Ainsi, tout comportement modifié qui se produit uniquement lorsqu'un utilisateur a démarré le processus via un shell (tel que STDOUT n'étant pas mis en mémoire tampon, dans ce cas) se produira. Dissimuler le fait qu'un autre processus a démarré ce processus vous permet de collecter le STDOUT en temps réel, car il n'est pas mis en mémoire tampon.

Pour que cela fonctionne avec le script random.rb en tant qu'enfant, essayez le code suivant:

require 'pty'
begin
  PTY.spawn( "ruby random.rb" ) do |stdout, stdin, pid|
    begin
      stdout.each { |line| print line }
    rescue Errno::EIO
    end
  end
rescue PTY::ChildExited
  puts "The child process exited!"
end
ehsanul
la source
7
C'est génial, mais je crois que les paramètres de bloc stdin et stdout devraient être échangés. Voir: ruby-doc.org/stdlib-1.9.3/libdoc/pty/rdoc
Mike Conigliaro
1
Comment fermer le pty? Tuer le pid?
Boris B.
Réponse géniale. Vous m'avez aidé à améliorer mon script de déploiement de rake pour heroku. Il affiche le journal 'git push' en temps réel et abandonne la tâche si 'fatal:' trouvé gist.github.com/sseletskyy/9248357
Serge Seletskyy
1
J'ai initialement essayé d'utiliser cette méthode mais «pty» n'est pas disponible sous Windows. En fait, STDOUT.sync = truec'est tout ce dont vous avez besoin (réponse de mveerman ci-dessous). Voici un autre fil avec un exemple de code .
Pakman
12

utiliser IO.popen. Ceci est un bon exemple.

Votre code deviendrait quelque chose comme:

blender = nil
t = Thread.new do
  IO.popen("blender -b mball.blend -o //renders/ -F JPEG -x 1 -f 1") do |blender|
    blender.each do |line|
      puts line
    end
  end
end
Sinan Taifour
la source
J'ai essayé ça. Le problème est le même. J'ai accès à la sortie par la suite. Je crois que IO.popen commence par exécuter le premier argument en tant que commande et attend la fin. Dans mon cas, la sortie est donnée par le mélangeur pendant que le mélangeur est toujours en train de traiter. Et puis le bloc est appelé après, ce qui ne m'aide pas.
ehsanul
Voici ce que j'ai essayé. Il renvoie la sortie une fois que Blender est terminé: IO.popen ("blender -b mball.blend // rend / -F JPEG -x 1 -f 1", "w +") do | blender | blender.each {| ligne | met la ligne; output + = line;} end
ehsanul
3
Je ne suis pas sûr de ce qui se passe dans votre cas. J'ai testé le code ci-dessus avec yes, une application de ligne de commande qui ne se termine jamais , et cela a fonctionné. Le code a été comme suit: IO.popen('yes') { |p| p.each { |f| puts f } }. Je soupçonne que c'est quelque chose qui a à voir avec le mélangeur, et non avec le rubis. Le mélangeur ne rince probablement pas toujours son STDOUT.
Sinan Taifour
Ok, je viens de l'essayer avec un processus Ruby externe à tester, et vous avez raison. Semble être un problème de mélangeur. Merci pour la réponse de toute façon.
ehsanul
Il s'avère qu'il existe un moyen d'obtenir la sortie via ruby ​​après tout, même si Blender ne vide pas sa sortie standard. Détails sous peu dans une réponse séparée, au cas où vous seriez intéressé.
ehsanul
6

STDOUT.flush ou STDOUT.sync = true

mveerman
la source
ouais, c'était une réponse boiteuse. Votre réponse était meilleure.
mveerman
Pas nul! A travaillé pour moi.
Clay Bridges
Plus précisément:STDOUT.sync = true; system('<whatever-command>')
caram le
4

Blender n'imprime probablement pas les sauts de ligne tant qu'il n'a pas terminé le programme. Au lieu de cela, il imprime le caractère de retour chariot (\ r). La solution la plus simple est probablement de rechercher l'option magique qui imprime les sauts de ligne avec l'indicateur de progression.

Le problème est que IO#gets(et diverses autres méthodes d'E / S) utilisent le saut de ligne comme délimiteur. Ils liront le flux jusqu'à ce qu'ils atteignent le caractère "\ n" (quel mélangeur n'envoie pas).

Essayez de définir le séparateur d'entrée $/ = "\r"ou utilisez à la blender.gets("\r")place.

BTW, pour de tels problèmes, vous devez toujours vérifier puts someobj.inspectou p someobj(les deux font la même chose) pour voir tous les caractères cachés dans la chaîne.

hhaamu
la source
1
Je viens d'inspecter la sortie donnée, et il semble que Blender utilise un saut de ligne (\ n), donc ce n'était pas le problème. Merci pour le conseil de toute façon, je garderai cela à l'esprit la prochaine fois que je déboguerai quelque chose comme ça.
ehsanul
0

Je ne sais pas si à l'époque ehsanul a répondu à la question, il y en avait Open3::pipeline_rw()encore, mais cela simplifie vraiment les choses.

Je ne comprends pas le travail d'ehsanul avec Blender, alors j'ai fait un autre exemple avec taret xz. tarajoutera le (s) fichier (s) d'entrée au flux stdout, puis le xzprendra stdoutet le compressera, à nouveau, dans un autre stdout. Notre travail est de prendre le dernier stdout et de l'écrire dans notre fichier final:

require 'open3'

if __FILE__ == $0
    cmd_tar = ['tar', '-cf', '-', '-T', '-']
    cmd_xz = ['xz', '-z', '-9e']
    list_of_files = [...]

    Open3.pipeline_rw(cmd_tar, cmd_xz) do |first_stdin, last_stdout, wait_threads|
        list_of_files.each { |f| first_stdin.puts f }
        first_stdin.close

        # Now start writing to target file
        open(target_file, 'wb') do |target_file_io|
            while (data = last_stdout.read(1024)) do
                target_file_io.write data
            end
        end # open
    end # pipeline_rw
end
condichoso
la source
0

Ancienne question, mais avait des problèmes similaires.

Sans vraiment changer mon code Ruby, une chose qui m'a aidé a été d' encapsuler mon tube avec stdbuf , comme ceci:

cmd = "stdbuf -oL -eL -i0  openssl s_client -connect #{xAPI_ADDRESS}:#{xAPI_PORT}"

@xSess = IO.popen(cmd.split " ", mode = "w+")  

Dans mon exemple, la commande réelle avec laquelle je souhaite interagir comme s'il s'agissait d'un shell est openssl .

-oL -eL dites-lui de mettre en mémoire tampon STDOUT et STDERR uniquement jusqu'à une nouvelle ligne. Remplacez complètement Lpar 0pour désamorcer

Cela ne fonctionne pas toujours, cependant: parfois, le processus cible applique son propre type de tampon de flux, comme l'a souligné une autre réponse.

Marcos
la source