Terus membaca dari STDOUT proses eksternal di Ruby


86

Saya ingin menjalankan blender dari baris perintah melalui skrip ruby, yang kemudian akan memproses keluaran yang diberikan oleh blender baris demi baris untuk memperbarui bilah kemajuan di GUI. Tidak terlalu penting bahwa blender adalah proses eksternal yang stdoutnya perlu saya baca.

Saya sepertinya tidak dapat menangkap pesan kemajuan yang biasanya dicetak oleh blender ke shell saat proses blender masih berjalan, dan saya sudah mencoba beberapa cara. Sepertinya saya selalu mengakses stdout blender setelah blender berhenti, bukan saat masih berjalan.

Berikut contoh upaya yang gagal. Itu memang mendapatkan dan mencetak 25 baris pertama dari output blender, tetapi hanya setelah proses blender selesai:

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:

Untuk membuatnya sedikit lebih jelas, perintah memanggil blender memberikan kembali aliran output di shell, menunjukkan kemajuan (bagian 1-16 selesai dll). Tampaknya setiap panggilan untuk "mendapatkan" output diblokir sampai blender berhenti. Masalahnya adalah bagaimana mendapatkan akses ke output ini saat blender masih berjalan, karena blender mencetak outputnya ke shell.

Jawaban:


175

Saya telah berhasil memecahkan masalah saya ini. Berikut adalah detailnya, dengan beberapa penjelasan, jika ada orang yang memiliki masalah serupa menemukan halaman ini. Tetapi jika Anda tidak peduli dengan detailnya, inilah jawaban singkatnya :

Gunakan PTY.spawn dengan cara berikut (dengan perintah Anda sendiri tentunya):

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

Dan inilah jawaban panjangnya , dengan terlalu banyak detail:

Masalah sebenarnya tampaknya adalah bahwa jika suatu proses tidak secara eksplisit menghapus stdout-nya, maka apa pun yang ditulis ke stdout di-buffer daripada benar-benar dikirim, sampai proses selesai, untuk meminimalkan IO (ini tampaknya merupakan detail implementasi dari banyak Library C, dibuat agar throughput dimaksimalkan melalui IO yang lebih jarang). Jika Anda dapat dengan mudah memodifikasi prosesnya sehingga stdout secara teratur, maka itu akan menjadi solusi Anda. Dalam kasus saya, ini adalah blender, jadi agak mengintimidasi untuk noob lengkap seperti saya untuk memodifikasi sumbernya.

Tetapi ketika Anda menjalankan proses ini dari shell, mereka menampilkan stdout ke shell secara real-time, dan stdout tampaknya tidak di-buffer. Ini hanya buffer ketika dipanggil dari proses lain yang saya percaya, tetapi jika shell sedang ditangani, stdout terlihat secara real time, tanpa buffer.

Perilaku ini bahkan dapat diamati dengan proses ruby ​​sebagai proses anak yang keluarannya harus dikumpulkan secara real time. Buat saja skrip, random.rb, dengan baris berikut:

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

Kemudian skrip ruby ​​untuk memanggilnya dan mengembalikan outputnya:

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

Anda akan melihat bahwa Anda tidak mendapatkan hasil secara real-time seperti yang Anda harapkan, tetapi sekaligus setelahnya. STDOUT sedang di-buffer, meskipun jika Anda menjalankan random.rb sendiri, itu tidak di-buffer. Ini dapat diselesaikan dengan menambahkan STDOUT.flushpernyataan di dalam blok secara random.rb. Tetapi jika Anda tidak dapat mengubah sumbernya, Anda harus mengatasinya. Anda tidak dapat membersihkannya dari luar proses.

Jika subproses dapat mencetak ke shell secara real-time, maka harus ada cara untuk menangkap ini dengan Ruby secara real-time juga. Dan ada. Anda harus menggunakan modul PTY, termasuk dalam ruby ​​core yang saya percaya (1.8.6 lagian). Yang menyedihkan adalah itu tidak didokumentasikan. Tapi untungnya saya menemukan beberapa contoh penggunaan.

Pertama, untuk menjelaskan apa itu PTY adalah singkatan dari pseudo terminal . Pada dasarnya, ini memungkinkan skrip ruby ​​untuk menampilkan dirinya sendiri ke subproses seolah-olah itu adalah pengguna sungguhan yang baru saja mengetik perintah ke dalam shell. Jadi setiap perubahan perilaku yang terjadi hanya ketika pengguna telah memulai proses melalui shell (seperti STDOUT tidak sedang buffer, dalam kasus ini) akan terjadi. Menyembunyikan fakta bahwa proses lain telah memulai proses ini memungkinkan Anda mengumpulkan STDOUT secara real-time, karena tidak sedang buffer.

Untuk membuat ini bekerja dengan skrip random.rb sebagai anak, coba kode berikut:

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
Ini bagus, tapi saya yakin parameter blok stdin dan stdout harus ditukar. Lihat: ruby-doc.org/stdlib-1.9.3/libdoc/pty/rdoc/…
Mike Conigliaro

1
Bagaimana cara menutup pty? Bunuh pid?
Boris B.

Jawaban yang luar biasa. Anda membantu saya meningkatkan skrip penerapan penggaruk untuk heroku. Ini menampilkan log 'git push' secara real time dan membatalkan tugas jika 'fatal:' ditemukan gist.github.com/sseletskyy/9248357
Serge Seletskyy

1
Saya awalnya mencoba menggunakan metode ini tetapi 'pty' tidak tersedia di Windows. Ternyata, STDOUT.sync = trueitulah yang dibutuhkan (jawaban mveerman di bawah). Berikut utas lain dengan beberapa kode contoh .
Pakman

12

gunakan IO.popen. Ini adalah contoh yang bagus.

Kode Anda akan menjadi seperti ini:

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

Saya sudah mencoba ini. Masalahnya sama. Saya mendapatkan akses ke output sesudahnya. Saya percaya IO.popen mulai dengan menjalankan argumen pertama sebagai perintah, dan menunggu sampai selesai. Dalam kasus saya, output diberikan oleh blender saat blender masih diproses. Dan kemudian pemblokiran dipanggil setelahnya, yang tidak membantu saya.
ehsanul

Inilah yang saya coba. Ia mengembalikan keluaran setelah blender selesai: IO.popen ("blender -b mball.blend // renders / -F JPEG -x 1 -f 1", "w +") do | blender | blender. setiap {| baris | menempatkan garis; output + = line;} akhir
ehsanul

3
Saya tidak yakin apa yang terjadi dalam kasus Anda. Saya menguji kode di atas dengan yes, aplikasi baris perintah yang tidak pernah berakhir , dan berhasil. Kode adalah sebagai berikut: IO.popen('yes') { |p| p.each { |f| puts f } }. Saya curiga ini ada hubungannya dengan blender, dan bukan ruby. Mungkin blender tidak selalu membilas STDOUT-nya.
Sinan Taifour

Oke, saya baru saja mencobanya dengan proses ruby ​​eksternal untuk diuji, dan Anda benar. Tampaknya menjadi masalah blender. Terima kasih atas jawabannya.
ehsanul

Ternyata ada cara untuk mendapatkan output melalui ruby, meskipun blender tidak membersihkan stdout-nya. Detail segera dalam jawaban terpisah, jika Anda tertarik.
ehsanul

6

STDOUT.flush atau STDOUT.sync = true


ya, ini adalah jawaban yang payah. Jawaban Anda lebih baik.
mveerman

Tidak timpang! Bekerja untuk saya.
Clay Bridges

Lebih tepatnya:STDOUT.sync = true; system('<whatever-command>')
karam

4

Blender mungkin tidak mencetak jeda baris sampai program berakhir. Sebagai gantinya, ini mencetak karakter carriage return (\ r). Solusi termudah mungkin mencari opsi ajaib yang mencetak jeda baris dengan indikator kemajuan.

Masalahnya adalah IO#gets(dan berbagai metode IO lainnya) menggunakan jeda baris sebagai pemisah. Mereka akan membaca aliran sampai mereka mencapai karakter "\ n" (yang tidak dikirim oleh blender).

Coba atur pemisah input $/ = "\r"atau gunakan blender.gets("\r")sebagai gantinya.

BTW, untuk masalah seperti ini, Anda harus selalu memeriksa puts someobj.inspectatau p someobj(keduanya melakukan hal yang sama) untuk melihat karakter tersembunyi di dalam string.


1
Saya baru saja memeriksa output yang diberikan, dan tampaknya blender menggunakan jeda baris (\ n), jadi bukan itu masalahnya. Terima kasih atas tipnya, saya akan mengingatnya saat saya men-debug sesuatu seperti ini lagi.
ehsanul

0

Saya tidak tahu apakah pada saat itu ehsanul menjawab pertanyaan itu, masih Open3::pipeline_rw()ada, tapi itu sangat mempermudah.

Saya tidak mengerti pekerjaan ehsanul dengan Blender, jadi saya membuat contoh lain dengan tardan xz. tarakan menambahkan file masukan ke aliran stdout, lalu xzmengambilnya stdoutdan memampatkannya, sekali lagi, ke stdout lain. Tugas kita adalah mengambil stdout terakhir dan menulisnya ke file terakhir kita:

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

Pertanyaan lama, tapi punya masalah serupa.

Tanpa benar-benar mengubah kode Ruby saya, satu hal yang membantu adalah membungkus pipa saya dengan stdbuf , seperti:

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

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

Dalam contoh saya, perintah sebenarnya yang ingin saya gunakan seolah-olah itu adalah shell, adalah openssl .

-oL -eL katakan untuk buffer STDOUT dan STDERR hanya sampai baris baru. Ganti Ldengan 0untuk melepas buffer sepenuhnya.

Ini tidak selalu berhasil, meskipun: terkadang proses target memberlakukan jenis buffer alirannya sendiri, seperti jawaban lain yang ditunjukkan.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.