2009-07-20 18 views
80

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 je dois lire la sortie.Lecture continue de STDOUT du processus externe dans Ruby

Je n'arrive pas à saisir les messages de progression que Blender imprime normalement au shell lorsque le processus Blender est en cours d'exécution, et j'ai essayé de plusieurs façons. Je semble toujours accéder au stdout de blender après blender a quitté, pas pendant qu'il est encore en cours d'exécution.

Voici un exemple d'échec d'une tentative. Il ne se et imprimer les 25 premières lignes de la sortie du mélangeur, mais seulement après le processus du mélangeur a quitté:

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} 

Edit:

Pour le rendre un peu plus clair, la commande invoquant blender renvoie un flux de sortie dans le shell, indiquant les progrès (partie 1-16 terminée, etc.). Il semble que tout appel à "obtenir" la sortie est bloqué jusqu'à ce que Blender 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 la sortie sur shell.

Répondre

161

J'ai réussi à résoudre ce problème. Voici les détails, avec quelques explications, au cas où quelqu'un ayant un problème similaire trouve 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 est ici la réponse longue, avec beaucoup trop de détails:

La vraie question semble être que si un processus doesn « t rincer explicitement son stdout, alors quoi que ce soit par écrit à SDOUT tamponne plutôt que réellement envoyé, jusqu'à ce que le processus se fait, de manière à minimiser IO (ce qui est apparemment un détail de mise en œuvre de nombreuses bibliothèques C, en sorte que le débit est maximisé par des entrées-sorties moins fréquentes). Si vous pouvez facilement modifier le processus afin 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 à partir du shell, ils affichent stdout à la coquille en temps réel, et le stdout ne semble pas être en mémoire tampon. Il n'est tamponné que lorsqu'il est appelé depuis un autre processus, mais si un shell est traité, le fichier stdout est vu en temps réel, sans tampon.

Ce comportement peut même être observé avec un processus de rubis que le processus de l'enfant dont la sortie doit être collectée en temps réel. Il suffit de créer un script, random.rb, avec la ligne suivante:

Ensuite, un script Ruby pour appeler et retourner sa sortie:

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

Vous verrez que vous ne recevez pas le résultat en temps réel comme vous pourriez vous y attendre, mais tout de suite après. STDOUT est en cours de tamponnage, même si vous exécutez random.rb vous-même, il n'est pas mis en mémoire tampon. Cela peut être résolu en ajoutant une instruction STDOUT.flush à 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 à débourser en temps réel, alors il doit y avoir un moyen de capturer cela avec Ruby en temps réel aussi bien. Et voici. Vous devez utiliser le module PTY, inclus dans le noyau de ruby ​​je crois (1.8.6 de toute façon). Ce qui est triste, c'est que ce n'est pas documenté. Mais j'ai trouvé quelques exemples d'utilisation heureusement.

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

Pour faire ce travail avec le script random.rb que l'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 
+0

+1 - Agréable et bien fait! :) –

+6

C'est une bonne réponse. Fait ma journée. –

+7

C'est génial, mais je crois que les paramètres du bloc stdin et stdout devraient être permutés. Voir: http://www.ruby-doc.org/stdlib-1.9.3/libdoc/pty/rdoc/PTY.html#method-c-spawn –

4

STDOUT.flush ou STDOUT.sync = true

+0

ouais, c'était une réponse boiteuse. Votre réponse était meilleure. – mveerman

+0

'STDOUT.sync = true' travaillé pour comme un charme pour moi –

12

utilisation IO.popen. This 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 
+0

J'ai essayé cela. Le problème est le même. J'ai accès à la sortie par la suite. Je crois que IO.popen commence en exécutant le premier argument comme une commande, et attend qu'il se termine. Dans mon cas, la sortie est donnée par blender pendant que blender est en cours de traitement. Et puis le bloc est invoqué après, ce qui ne m'aide pas. – ehsanul

+0

Voici ce que j'ai essayé. Il renvoie la sortie une fois le blender terminé: IO.popen ("blender -b mball.blend // rendu/-F JPEG -x 1 -f 1", "w +") do | blender | blender.each {| ligne | met la ligne; sortie + = ligne;} fin – 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 en ligne de commande qui n'a jamais _ends_, et cela a fonctionné. Le code était comme suit: 'IO.popen ('yes') {| p | p.each {| f | met f}} '. Je soupçonne que c'est quelque chose qui a à voir avec un mélangeur, et non avec du rubis. Probablement blender ne vide pas toujours son STDOUT. –

4

Blender n'a probablement pas des sauts de ligne d'impression jusqu'à ce qu'il se termine le programme. Au lieu de cela, il imprime le caractère de retour chariot (\ r). La solution la plus simple est probablement la recherche de 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" (que blender n'envoie pas).

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

BTW, pour des problèmes tels que ceux-ci, vous devriez toujours vérifier puts someobj.inspect ou p someobj (qui font tous deux la même chose) pour voir tous les caractères cachés dans la chaîne.

+1

J'ai juste inspecté 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 ça à l'esprit la prochaine fois que je débuggerai quelque chose comme ça. – ehsanul

0

Je ne sais pas si au Ehsanul du temps répondu à la question, il y avait Open3::pipeline_rw() encore disponible, mais ça rend vraiment les choses plus simples.

Je ne comprends pas le travail d'ehsanul avec Blender, donc j'ai fait un autre exemple avec tar et xz. tar ajoutera fichier d'entrée (s) au flux stdout, puis xz prendre que stdout et compriment, encore une fois, à un autre stdout.Notre travail consiste à prendre la dernière sortie standard et à 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