Lettura continua da STDOUT del processo esterno in Ruby


86

Voglio eseguire blender dalla riga di comando tramite uno script ruby, che elaborerà quindi l'output fornito da blender riga per riga per aggiornare una barra di avanzamento in una GUI. Non è molto importante che Blender sia il processo esterno di cui ho bisogno di leggere lo stdout.

Non riesco a rilevare i messaggi di avanzamento normalmente stampati da blender sulla shell quando il processo di blender è ancora in esecuzione, e ho provato alcuni modi. Mi sembra sempre di accedere allo stdout di Blender dopo che Blender si è chiuso, non mentre è ancora in esecuzione.

Ecco un esempio di tentativo fallito. Ottiene e stampa le prime 25 righe dell'output di blender, ma solo dopo che il processo di blender è terminato:

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}

Modificare:

Per renderlo un po 'più chiaro, il comando che richiama blender restituisce un flusso di output nella shell, indicando lo stato di avanzamento (parte 1-16 completata, ecc.). Sembra che qualsiasi chiamata a "gets" l'output sia bloccata fino alla chiusura di Blender. Il problema è come accedere a questo output mentre Blender è ancora in esecuzione, poiché Blender stampa il suo output nella shell.

Risposte:


175

Ho avuto un certo successo nel risolvere questo mio problema. Ecco i dettagli, con alcune spiegazioni, nel caso in cui qualcuno che ha un problema simile trovi questa pagina. Ma se non ti interessano i dettagli, ecco la risposta breve :

Usa PTY.spawn nel modo seguente (con il tuo comando ovviamente):

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

Ed ecco la risposta lunga , con troppi dettagli:

Il vero problema sembra essere che se un processo non scarica esplicitamente il suo stdout, allora qualsiasi cosa scritta su stdout viene bufferizzata piuttosto che effettivamente inviata, fino a quando il processo non è terminato, in modo da ridurre al minimo l'IO (questo è apparentemente un dettaglio di implementazione di molti Librerie C, realizzate in modo che il throughput sia massimizzato attraverso IO meno frequenti). Se puoi facilmente modificare il processo in modo che scarichi regolarmente lo stdout, allora questa sarebbe la tua soluzione. Nel mio caso, era un frullatore, quindi un po 'intimidatorio per un noob completo come me modificare la fonte.

Ma quando esegui questi processi dalla shell, visualizzano lo stdout nella shell in tempo reale e lo stdout non sembra essere bufferizzato. È bufferizzato solo quando viene chiamato da un altro processo, credo, ma se viene gestita una shell, lo stdout viene visto in tempo reale, senza buffer.

Questo comportamento può anche essere osservato con un processo Ruby come processo figlio il cui output deve essere raccolto in tempo reale. Basta creare uno script, random.rb, con la seguente riga:

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

Quindi uno script ruby ​​per chiamarlo e restituirne l'output:

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

Vedrai che non ottieni il risultato in tempo reale come potresti aspettarti, ma tutto in una volta dopo. STDOUT viene bufferizzato, anche se esegui random.rb da solo, non è bufferizzato. Questo può essere risolto aggiungendo STDOUT.flushun'istruzione all'interno del blocco in random.rb. Ma se non puoi cambiare la fonte, devi aggirare questo problema. Non puoi lavarlo dall'esterno del processo.

Se il sottoprocesso può stampare sulla shell in tempo reale, allora ci deve essere un modo per catturarlo anche con Ruby in tempo reale. E c'è. Devi usare il modulo PTY, incluso in ruby ​​core credo (1.8.6 comunque). La cosa triste è che non è documentata. Ma ho trovato fortunatamente alcuni esempi di utilizzo.

Innanzitutto, per spiegare cos'è PTY, sta per pseudo terminale . Fondamentalmente, consente allo script ruby ​​di presentarsi al sottoprocesso come se fosse un utente reale che ha appena digitato il comando in una shell. Quindi si verificherà qualsiasi comportamento alterato che si verifica solo quando un utente ha avviato il processo tramite una shell (come lo STDOUT non bufferizzato, in questo caso). Nascondere il fatto che un altro processo ha avviato questo processo consente di raccogliere lo STDOUT in tempo reale, poiché non viene bufferizzato.

Per fare in modo che funzioni con lo script random.rb come figlio, prova il codice seguente:

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

7
Questo è fantastico, ma credo che i parametri dei blocchi stdin e stdout dovrebbero essere scambiati. Vedi: ruby-doc.org/stdlib-1.9.3/libdoc/pty/rdoc/…
Mike Conigliaro

1
Come chiudere il pty? Uccidere il pid?
Boris B.

Ottima risposta. Mi hai aiutato a migliorare il mio script di distribuzione rake per heroku. Visualizza il log "git push" in tempo reale e interrompe l'attività se "fatale:" trovato gist.github.com/sseletskyy/9248357
Serge Seletskyy

1
Inizialmente ho provato a utilizzare questo metodo ma "pty" non è disponibile in Windows. A quanto pare, STDOUT.sync = trueè tutto ciò che serve (risposta di Mveerman di seguito). Ecco un altro thread con un codice di esempio .
Pakman

12

uso IO.popen . Questo è un buon esempio.

Il tuo codice diventerebbe qualcosa di simile:

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

L'ho provato. Il problema è lo stesso. Successivamente ottengo l'accesso all'output. Credo che IO.popen inizi eseguendo il primo argomento come comando e attenda che finisca. Nel mio caso, l'output è dato da Blender mentre Blender è ancora in elaborazione. E poi il blocco viene richiamato dopo, il che non mi aiuta.
ehsanul

Ecco cosa ho provato. Restituisce l'output dopo che Blender è terminato: IO.popen ("blender -b mball.blend // rende / -F JPEG -x 1 -f 1", "w +") do | blender | blender.each {| line | mette la linea; output + = line;} end
ehsanul

3
Non sono sicuro di cosa stia succedendo nel tuo caso. Ho testato il codice sopra con yesun'applicazione a riga di comando che non finisce mai e ha funzionato. Il codice era come segue: IO.popen('yes') { |p| p.each { |f| puts f } }. Sospetto che sia qualcosa che ha a che fare con il frullatore e non con il rubino. Probabilmente il frullatore non lava sempre il suo STDOUT.
Sinan Taifour

Ok, l'ho appena provato con un processo rubino esterno per testarlo e hai ragione. Sembra essere un problema con il frullatore. Grazie comunque per la risposta.
ehsanul

Alla fine c'è un modo per ottenere l'output tramite Ruby, anche se Blender non scarica lo stdout. Dettagli a breve in una risposta separata, nel caso tu sia interessato.
ehsanul

6

STDOUT.flush o STDOUT.sync = true


sì, questa era una risposta scadente. La tua risposta è stata migliore.
mveerman

Non zoppo! Ha funzionato per me.
Clay Bridges

Più precisamente:STDOUT.sync = true; system('<whatever-command>')
caramello

4

Probabilmente Blender non stampa interruzioni di riga fino a quando non termina il programma. Invece, stampa il carattere di ritorno a capo (\ r). La soluzione più semplice è probabilmente cercare l'opzione magica che stampa interruzioni di riga con l'indicatore di avanzamento.

Il problema è che IO#gets (e vari altri metodi IO) utilizzano l'interruzione di riga come delimitatore. Leggeranno il flusso finché non premeranno il carattere "\ n" (che frullatore non sta inviando).

Prova a impostare il separatore di input $/ = "\r"o a utilizzareblender.gets("\r") invece.

A proposito, per problemi come questi, dovresti sempre controllare puts someobj.inspecto p someobj(entrambi fanno la stessa cosa) per vedere eventuali caratteri nascosti all'interno della stringa.


1
Ho appena controllato l'output fornito e sembra che Blender utilizzi un'interruzione di riga (\ n), quindi non era quello il problema. Grazie comunque per il suggerimento, lo terrò a mente la prossima volta che eseguo il debug di qualcosa di simile.
ehsanul

0

Non so se all'epoca ehsanul avesse risposto alla domanda, c'era Open3::pipeline_rw()ancora disponibile, ma rende davvero le cose più semplici.

Non capisco il lavoro di ehsanul con Blender, quindi ho fatto un altro esempio con tare xz. taraggiungerà i file di input allo stream stdout, quindi lo xzprenderà stdoute lo comprimerà, di nuovo, in un altro stdout. Il nostro compito è prendere l'ultimo stdout e scriverlo nel nostro file finale:

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

Vecchia domanda, ma ha avuto problemi simili.

Senza cambiare davvero il mio codice Ruby, una cosa che mi ha aiutato è stata avvolgere la mia pipa con stdbuf , in questo modo:

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

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

Nel mio esempio, il comando effettivo con cui voglio interagire come se fosse una shell, è openssl .

-oL -eL digli di bufferizzare STDOUT e STDERR solo fino a una nuova riga. Sostituire Lcon 0per rimuovere completamente il buffer.

Tuttavia, non sempre funziona: a volte il processo di destinazione impone il proprio tipo di buffer di flusso, come sottolineato da un'altra risposta.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.