Ciągłe odczytywanie z STDOUT zewnętrznego procesu w Rubim

86

Chcę uruchomić blendera z wiersza poleceń za pomocą skryptu ruby, który następnie przetworzy dane wyjściowe podane przez blendera wiersz po wierszu, aby zaktualizować pasek postępu w GUI. Nie jest ważne, że blender jest procesem zewnętrznym, którego standardową wersję muszę przeczytać.

Wydaje się, że nie jestem w stanie złapać komunikatów o postępie, które blender normalnie drukuje do powłoki, gdy proces blendera nadal działa, a wypróbowałem kilka sposobów. Zawsze wydaje mi się, że mam dostęp do standardowego wyjścia blendera po jego zamknięciu, a nie wtedy, gdy nadal działa.

Oto przykład nieudanej próby. Pobiera i wyświetla pierwsze 25 wierszy danych wyjściowych blendera, ale dopiero po zakończeniu procesu blendera:

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}

Edytować:

Aby było trochę jaśniej, polecenie wywołujące blender zwraca strumień danych wyjściowych w powłoce, wskazując postęp (ukończono część 1-16 itp.). Wygląda na to, że każde wywołanie „pobiera” wyjście jest blokowane do zakończenia pracy blendera. Problem polega na tym, jak uzyskać dostęp do tego wyjścia, gdy blender nadal działa, ponieważ blender drukuje go do powłoki.

ehsanul
źródło

Odpowiedzi:

175

Odniosłem pewien sukces w rozwiązaniu tego mojego problemu. Oto szczegóły wraz z wyjaśnieniami, na wypadek gdyby ktoś miał podobny problem, znalazł tę stronę. Ale jeśli nie dbasz o szczegóły, oto krótka odpowiedź :

Używaj PTY.spawn w następujący sposób (oczywiście z własnym poleceniem):

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

A oto długa odpowiedź , ze zbyt wieloma szczegółami:

Prawdziwym problemem wydaje się być to, że jeśli proces nie opróżnia swojego standardowego wyjścia, to wszystko, co jest zapisywane na standardowe wyjście, jest buforowane, a nie wysyłane, dopóki proces nie zostanie zakończony, aby zminimalizować IO (jest to najwyraźniej szczegół implementacji wielu Biblioteki C, stworzone w taki sposób, że przepustowość jest maksymalizowana poprzez rzadsze IO). Jeśli możesz łatwo zmodyfikować proces tak, aby regularnie opróżniał standardowe wyjście, to byłoby to rozwiązanie. W moim przypadku był to blender, więc trochę onieśmielający dla kompletnego nooba, takiego jak ja, do modyfikowania źródła.

Ale kiedy uruchamiasz te procesy z powłoki, wyświetlają one standardowe wyjście do powłoki w czasie rzeczywistym, a standardowe wyjście nie wydaje się być buforowane. Są one buforowane tylko wtedy, gdy są wywoływane z innego procesu, jak sądzę, ale jeśli zajmujemy się powłoką, standardowe wyjście jest widoczne w czasie rzeczywistym, niebuforowane.

To zachowanie można nawet zaobserwować w przypadku procesu rubinowego jako procesu potomnego, którego dane wyjściowe muszą być gromadzone w czasie rzeczywistym. Po prostu utwórz skrypt, random.rb, z następującym wierszem:

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

Następnie skrypt ruby, aby go wywołać i zwrócić wynik:

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

Zobaczysz, że nie otrzymasz wyniku w czasie rzeczywistym, jak można by się spodziewać, ale wszystko naraz. STDOUT jest buforowany, nawet jeśli sam uruchomisz random.rb, nie jest buforowany. Można to rozwiązać, dodając STDOUT.flushinstrukcję wewnątrz bloku w random.rb. Ale jeśli nie możesz zmienić źródła, musisz to obejść. Nie można go przepłukać spoza procesu.

Jeśli podproces może drukować do powłoki w czasie rzeczywistym, musi istnieć sposób, aby przechwycić to również w Rubim w czasie rzeczywistym. I jest. Musisz użyć modułu PTY, zawartego w rdzeniu rubinowym, jak sądzę (w każdym razie 1.8.6). Smutne jest to, że nie jest to udokumentowane. Na szczęście znalazłem kilka przykładów użycia.

Po pierwsze, aby wyjaśnić, czym jest PTY, oznacza to pseudoterminal . Zasadniczo pozwala skryptowi ruby ​​zaprezentować się podprocesowi tak, jakby był prawdziwym użytkownikiem, który właśnie wpisał polecenie w powłoce. Zatem każde zmienione zachowanie, które występuje tylko wtedy, gdy użytkownik uruchomił proces za pośrednictwem powłoki (na przykład STDOUT nie jest buforowane, w tym przypadku) wystąpi. Ukrywanie faktu, że inny proces rozpoczął ten proces, umożliwia gromadzenie STDOUT w czasie rzeczywistym, ponieważ nie jest on buforowany.

Aby to zadziałało ze skryptem random.rb jako dzieckiem, wypróbuj następujący kod:

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
źródło
7
To świetnie, ale uważam, że parametry bloków stdin i stdout powinny zostać zamienione. Zobacz: ruby-doc.org/stdlib-1.9.3/libdoc/pty/rdoc/…
Mike Conigliaro
1
Jak zamknąć pty? Zabić pid?
Boris B.
Świetna odpowiedź. Pomogłeś mi ulepszyć mój skrypt wdrażania rake dla heroku. Wyświetla logowanie typu „git push” w czasie rzeczywistym i przerywa zadanie, jeśli zostanie znalezione „fatalne”: gist.github.com/sseletskyy/9248357
Serge Seletskyy
1
Początkowo próbowałem użyć tej metody, ale „pty” nie jest dostępne w systemie Windows. Jak się okazuje, STDOUT.sync = trueto wszystko, czego potrzeba (odpowiedź mveermana poniżej). Oto kolejny wątek z przykładowym kodem .
Pakman
12

używać IO.popen. To jest dobry przykład.

Twój kod wyglądałby tak:

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
źródło
Próbowałem tego. Problem jest ten sam. Potem uzyskuję dostęp do wyjścia. Myślę, że IO.popen zaczyna się od uruchomienia pierwszego argumentu jako polecenia i czeka na jego zakończenie. W moim przypadku wyjście jest podawane przez blender, podczas gdy blender nadal przetwarza. Następnie wywoływany jest blok, co mi nie pomaga.
ehsanul
Oto, czego próbowałem. Zwraca wynik po zakończeniu działania blendera: IO.popen ("blender -b mball.blend // renderuje / -F JPEG -x 1 -f 1", "w +") do | blender | blender.each {| line | stawia linię; output + = line;} end
ehsanul
3
Nie jestem pewien, co się dzieje w Twoim przypadku. Przetestowałem powyższy kod za yespomocą aplikacji wiersza poleceń, która nigdy się nie kończy i zadziałało. Kod był następujący: IO.popen('yes') { |p| p.each { |f| puts f } }. Podejrzewam, że ma to związek z blenderem, a nie rubinem. Prawdopodobnie blender nie zawsze opróżnia swój STDOUT.
Sinan Taifour
OK, właśnie wypróbowałem to z zewnętrznym procesem rubinowym do przetestowania i masz rację. Wydaje się, że jest to problem z blenderem. Mimo wszystko dzięki za odpowiedź.
ehsanul
Okazuje się, że mimo wszystko istnieje sposób na uzyskanie wyniku przez ruby, mimo że blender nie opróżnia swojego standardowego wyjścia. Szczegóły w osobnej odpowiedzi, na wypadek gdybyś był zainteresowany.
ehsanul
6

STDOUT.flush lub STDOUT.sync = true

mveerman
źródło
tak, to była kiepska odpowiedź. Twoja odpowiedź była lepsza.
mveerman
Nie kiepsko! Pracował dla mnie.
Clay Bridges
Dokładniej:STDOUT.sync = true; system('<whatever-command>')
Caram
4

Blender prawdopodobnie nie wypisuje znaków końca linii, dopóki nie zakończy programu. Zamiast tego wypisuje znak powrotu karetki (\ r). Najłatwiejszym rozwiązaniem jest prawdopodobnie wyszukanie magicznej opcji, która wyświetla podziały wierszy ze wskaźnikiem postępu.

Problem polega na tym, że IO#gets(i wiele innych metod IO) używają podziału wiersza jako separatora. Będą czytać strumień, dopóki nie trafią w znak "\ n" (który blender nie wysyła).

Spróbuj ustawić separator wejściowy $/ = "\r"lub użyj blender.gets("\r")zamiast niego.

Swoją drogą, w przypadku takich problemów należy zawsze sprawdzać puts someobj.inspectlub p someobj(oba robią to samo), aby zobaczyć ukryte znaki w ciągu.

hhaamu
źródło
1
Właśnie sprawdziłem podane dane wyjściowe i wygląda na to, że blender używa znaku końca wiersza (\ n), więc to nie był problem. Mimo wszystko dzięki za wskazówkę, będę o tym pamiętać następnym razem, gdy będę debugować coś takiego.
ehsanul
0

Nie wiem, czy w tamtym czasie ehsanul odpowiedział na pytanie, było Open3::pipeline_rw()jeszcze dostępne, ale to naprawdę upraszcza sprawę.

Nie rozumiem pracy ehsanula w Blenderze, więc zrobiłem kolejny przykład z tari xz. tardoda plik (i) wejściowe do strumienia standardowego, a następnie xzweźmie go stdouti skompresuje, ponownie, do innego wyjścia standardowego. Naszym zadaniem jest pobranie ostatniego standardowego wyjścia i zapisanie go w naszym ostatecznym pliku:

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
źródło
0

Stare pytanie, ale miał podobne problemy.

Bez rzeczywistej zmiany mojego kodu Ruby, jedną rzeczą, która pomogła, było owinięcie mojego potoku stdbuf , na przykład:

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

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

W moim przykładzie faktycznym poleceniem, z którym chcę wchodzić w interakcje tak, jakby była powłoką, jest openssl .

-oL -eL powiedz mu, aby buforował STDOUT i STDERR tylko do nowej linii. Wymienić Lz 0całkowicie unbuffer.

To jednak nie zawsze działa: czasami proces docelowy wymusza własny typ bufora strumienia, jak wskazano w innej odpowiedzi.

Marcos
źródło