Ruby'de harici sürecin STDOUT'tan sürekli olarak okuyun


86

Karıştırıcıyı komut satırından bir ruby ​​betiği aracılığıyla çalıştırmak istiyorum, bu daha sonra bir GUI'deki ilerleme çubuğunu güncellemek için blender tarafından verilen çıktıyı satır satır işleyecek. Blender'ın standartlarını okumam gereken harici süreç olması gerçekten önemli değil.

Blender işlemi hala çalışırken blender'ın normalde kabuğa yazdırdığı ilerleme mesajlarını yakalayamıyorum ve birkaç yol denedim. Her zaman blender çalışıyorken değil , blender kapandıktan sonra blenderin standart çıkışına erişiyor gibiyim .

İşte başarısız bir girişim örneği. Blender çıktısının ilk 25 satırını alır ve yazdırır, ancak yalnızca blender işleminden çıktıktan sonra:

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}

Düzenle:

Biraz daha net hale getirmek için, blenderi çağıran komut, kabukta ilerlemeyi gösteren bir çıktı akışı verir (1-16. Bölüm tamamlandı vb.). Görünüşe göre çıktıyı "almak" için yapılan herhangi bir çağrı blender çıkana kadar engelleniyor. Sorun, blender çıktısını kabuğa yazdırdığından, blender çalışırken hala bu çıktıya nasıl erişilebileceğidir.

Yanıtlar:


175

Bu problemimi çözmede biraz başarılı oldum. İşte benzer bir sorunu olan birinin bu sayfayı bulması durumunda bazı açıklamalarla birlikte ayrıntılar. Ancak ayrıntıları önemsemiyorsanız, işte kısa cevap :

PTY.spawn'ı aşağıdaki şekilde kullanın (elbette kendi komutunuzla):

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

Ve işte çok fazla ayrıntıyla uzun cevap :

Gerçek sorun, eğer bir süreç kendi stdout'unu açıkça boşaltmazsa, o zaman stdout'a yazılan her şeyin, işlem tamamlanıncaya kadar gerçekte gönderilmek yerine arabelleğe alınıp, IO'yu en aza indirgemek için ( görünüşe göre bu, birçoklarının uygulama detaylarından biridir). C kitaplıkları, iş hacminin daha az sıklıkta IO ile maksimize edilmesi için yapılmıştır). Süreci, stdout'u düzenli olarak temizleyecek şekilde kolayca değiştirebilirseniz, o zaman bu sizin çözümünüz olacaktır. Benim durumumda, bu bir karıştırıcıydı, bu yüzden benim gibi tam bir çaylak için kaynağı değiştirmek için biraz korkutucu.

Ancak bu işlemleri kabuktan çalıştırdığınızda, standart çıktıyı kabuğa gerçek zamanlı olarak gösterirler ve stdout arabelleğe alınmış gibi görünmez. Yalnızca inandığım başka bir süreçten çağrıldığında tamponlanıyor, ancak bir kabukla uğraşılıyorsa, stdout gerçek zamanlı olarak tamponlanmamış olarak görülüyor.

Bu davranış, çıktısı gerçek zamanlı olarak toplanması gereken alt süreç olarak bir yakut süreci ile bile gözlemlenebilir. Sadece aşağıdaki satırı içeren random.rb bir komut dosyası oluşturun:

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

Sonra onu çağırmak ve çıktısını döndürmek için bir Ruby betiği:

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

Sonucu beklediğiniz gibi gerçek zamanlı olarak almadığınızı göreceksiniz, ancak daha sonra hepsini birden alacaksınız. STDOUT, kendiniz random.rb'yi çalıştırsanız bile, arabelleğe alınmaz. Bu STDOUT.flush, bloğun içine random.rb'ye bir ifade eklenerek çözülebilir . Ancak kaynağı değiştiremiyorsanız, bunun üzerinde çalışmalısınız. İşlemin dışından onu temizleyemezsiniz.

Alt süreç gerçek zamanlı olarak kabuğa yazdırabiliyorsa, bunu Ruby ile gerçek zamanlı olarak yakalamanın bir yolu olmalıdır. Ve orada. Ruby core'da bulunan PTY modülünü kullanmanız gerektiğine inanıyorum (1.8.6 her durumda). Üzücü olan şey, belgelenmemiş olması. Ama neyse ki bazı kullanım örnekleri buldum.

İlk olarak, PTY'nin ne olduğunu açıklamak için, sözde terminal anlamına gelir . Temel olarak, ruby ​​betiğinin kendisini, komutu bir kabuğa henüz yazmış gerçek bir kullanıcıymış gibi alt işleme sunmasına izin verir. Bu nedenle, yalnızca bir kullanıcı işlemi bir kabuk aracılığıyla başlattığında (bu durumda STDOUT'un arabelleğe alınmaması gibi) meydana gelen herhangi bir değiştirilmiş davranış meydana gelecektir. Başka bir işlemin bu işlemi başlattığını gizlemek, STDOUT'u arabelleğe alınmadığı için gerçek zamanlı olarak toplamanıza olanak tanır.

Bunun, alt öğe olarak random.rb komut dosyasıyla çalışmasını sağlamak için aşağıdaki kodu deneyin:

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
Bu harika, ancak stdin ve stdout blok parametrelerinin değiştirilmesi gerektiğine inanıyorum. Bakınız: ruby-doc.org/stdlib-1.9.3/libdoc/pty/rdoc/…
Mike Conigliaro

1
Pty nasıl kapatılır? Pidi öldürmek mi?
Boris B.

Harika cevap. Heroku için tırmık konuşlandırma betiğimi geliştirmeme yardım ettin. Gerçek zamanlı olarak 'git push' günlüğünü görüntüler ve 'ölümcül:' ise görevi iptal eder gist.github.com/sseletskyy/9248357
Serge Seletskyy

1
Başlangıçta bu yöntemi kullanmayı denedim, ancak 'pty' Windows'ta mevcut değil. Görünüşe göre, STDOUT.sync = truegerekli olan tek şey (mveerman'ın cevabı aşağıda). İşte bazı örnek kodlu başka bir iş parçacığı .
Pakman

12

kullanın IO.popen. Bu iyi bir örnek.

Kodunuz şöyle bir şeye dönüşür:

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

Bunu denedim. Sorun aynı. Daha sonra çıktıya erişiyorum. IO.popen'in ilk argümanı bir komut olarak çalıştırarak başladığına ve bitmesini beklediğine inanıyorum. Benim durumumda çıktı, blender hala işlenirken blender tarafından verilir. Ve sonra blok devreye giriyor, bu bana yardımcı olmuyor.
ehsanul

İşte denediğim şey. Karıştırıcı tamamlandıktan sonra çıktıyı döndürür: IO.popen ("blender -b mball.blend // işler / -F JPEG -x 1 -f 1", "w +") do | blender | blender.each {| line | satır koyar; çıktı + = satır;} uç
ehsanul

3
Senin durumunda ne olduğundan emin değilim. Yukarıdaki kodu yeshiç bitmeyen bir komut satırı uygulamasıyla test ettim ve işe yaradı. Kod şu şekilde oldu: IO.popen('yes') { |p| p.each { |f| puts f } }. Bunun yakut değil blender ile ilgisi olduğunu sanıyorum. Muhtemelen blender, STDOUT'u her zaman temizlemiyor.
Sinan Taifour

Tamam, test etmek için harici bir yakut işlemiyle denedim ve haklısın. Bir blender sorunu gibi görünüyor. Cevabınız için yine de teşekkürler.
ehsanul

Blender stdout'unu temizlemese bile, sonuçta çıktıyı Ruby'den almanın bir yolu olduğu ortaya çıktı. Ayrıntılar, ilgilenmeniz durumunda kısaca ayrı bir cevapla.
ehsanul

6

STDOUT.flush veya STDOUT.sync = true


evet, bu kötü bir cevaptı. Cevabınız daha iyiydi.
mveerman

Topal değil! Benim için çalıştı.
Clay Bridges

Daha doğrusu:STDOUT.sync = true; system('<whatever-command>')
caram

4

Blender muhtemelen programı sonlandırana kadar satır sonları yazdırmıyor. Bunun yerine, satır başı karakterini (\ r) yazdırıyor. En kolay çözüm muhtemelen ilerleme göstergesiyle satır sonları yazdıran sihirli seçeneği aramaktır.

Sorun, IO#gets(ve diğer çeşitli GÇ yöntemlerinin) satır sonunu sınırlayıcı olarak kullanmasıdır. Akışı "\ n" karakterine (blender göndermeyen) ulaşana kadar okuyacaklar.

Giriş ayırıcısını ayarlamayı $/ = "\r"veya blender.gets("\r")bunun yerine kullanmayı deneyin .

BTW, bunun gibi sorunlar için , dizedeki gizli karakterleri görmek için her zaman kontrol etmelisiniz puts someobj.inspectveya p someobj(ikisi de aynı şeyi yapar).


1
Verilen çıktıyı az önce inceledim ve görünen o ki blender bir satır sonu (\ n) kullanıyor, bu yüzden sorun bu değildi. Her neyse, ipucu için teşekkürler, bir dahaki sefere böyle bir şeyde hata ayıklarken bunu aklımda tutacağım.
ehsanul

0

Ehsanul soruyu cevapladığında mı bilmiyorum, Open3::pipeline_rw()henüz mevcuttu, ama gerçekten işleri daha basit hale getiriyor.

Ehsanul'un Blender'daki işini anlamıyorum, bu yüzden tarve ile başka bir örnek yaptım xz. tarstdout'u akışına girdi dosyası (ler) ekleyin, sonra xzo almak stdoutve başka Stdout'a yine sıkıştırın. Bizim işimiz son stdout'u alıp son dosyamıza yazmak:

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

Eski soru, ama benzer sorunlar vardı.

Ruby kodumu gerçekten değiştirmeden, borumu stdbuf ile sarmalamaya yardımcı olan tek şey şuydu :

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

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

Örneğimde , bir kabukmuş gibi etkileşimde bulunmak istediğim asıl komut openssl .

-oL -eL STDOUT ve STDERR'ı yalnızca bir satırsonu satırına kadar arabelleğe almasını söyleyin. Arabelleği tamamen kaldırmak için Lile değiştirin 0.

Yine de bu her zaman işe yaramaz: bazen hedef süreç, başka bir yanıtın işaret ettiği gibi kendi akış arabellek türünü uygular.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.