web-dev-qa-db-fra.com

Lire en continu depuis STDOUT du processus externe dans Ruby

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 est le processus externe dont je dois lire la sortie standard.

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

Voici un exemple d'une tentative échouée. 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}

Modifier:

Pour le rendre un peu plus clair, la commande invoquant le mélangeur 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 soit bloqué jusqu'à ce que le mélangeur se ferme. Le problème est de savoir comment accéder à cette sortie pendant que Blender est en cours d'exécution, car Blender imprime sa sortie dans Shell.

84
ehsanul

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 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 sa sortie standard, alors tout ce qui est écrit sur la sortie standard est tamponné plutôt que réellement envoyé, jusqu'à ce que le processus soit terminé, afin de minimiser IO (c'est apparemment un détail d'implémentation de nombreuses bibliothèques C, fait pour que le débit soit maximisé par des E/S moins fréquentes). Si vous pouvez facilement modifier le processus de manière à vider régulièrement la sortie standard, alors ce serait votre solution. Dans mon cas, c'était blender, donc un peu intimidant pour un noob complet comme moi pour modifier la source.

Mais lorsque vous exécutez ces processus à partir du shell, ils affichent la sortie standard vers le shell en temps réel, et la sortie standard ne semble pas être mise en mémoire tampon. Il n'est tamponné que lorsqu'il est appelé à partir d'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 processus 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:

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

Puis un script Ruby pour l'appeler et retourner 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 pouvez vous y attendre, mais tout de suite après. STDOUT est mis en mémoire tampon, 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 un STDOUT.flush instruction à l'intérieur du bloc dans random.rb. Mais si vous ne pouvez pas changer la source, vous devez contourner cela. Vous ne pouvez pas le vider de l'extérieur du processus.

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

Tout d'abord, pour expliquer ce qu'est PTY, cela signifie pseudo terminal . Fondamentalement, il permet au script Ruby de se présenter au sous-processus comme s'il s'agissait d'un véritable utilisateur qui vient 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 le STDOUT n'étant pas mis en mémoire tampon, dans ce cas) se produira. Le fait de masquer 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 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
173
ehsanul

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
12
Sinan Taifour

STDOUT.flush ou STDOUT.sync = true

5
mveerman

Blender n'imprime probablement pas les sauts de ligne jusqu'à la fin du programme. Au lieu de cela, il imprime le caractère de retour chariot (\ r). La solution la plus simple consiste probablement à 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 IO) utilisent le saut de ligne comme délimiteur. Ils liront le flux jusqu'à ce qu'ils atteignent le caractère "\ n" (qui blender n'envoie pas).

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

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

4
hhaamu

Je ne sais pas si au moment où ehsanul a répondu à la question, il y avait encore Open3::pipeline_rw() disponible, mais cela rend vraiment les choses plus simples.

Je ne comprends pas le travail d'ehsanul avec Blender, j'ai donc fait un autre exemple avec tar et xz. tar ajoutera le (s) fichier (s) d'entrée au flux stdout, puis xz prendra ce stdout et le compressera, à nouveau, dans 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
0
condichoso