Постоянно читается из STDOUT внешнего процесса в Ruby

Я хочу запустить blender из командной строки через ruby ​​ script, который затем обработает вывод, заданный blender по строкам, чтобы обновить индикатор выполнения в графическом интерфейсе. Это не очень важно, что блендер - это внешний процесс, для которого мне нужно читать.

Кажется, я не могу поймать сообщения о выполнении, которые blender обычно печатает в оболочке, когда процесс блендера все еще запущен, и я пробовал несколько способов. Кажется, я всегда получаю доступ к выходу блендера после того, как блендер ушел, а пока он все еще работает.

Вот пример неудачной попытки. Он получает и печатает первые 25 строк вывода блендера, но только после выхода процесса блендера:

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:

Чтобы сделать его более понятным, команда, вызывающая blender, возвращает поток вывода в оболочке, указывая на прогресс (часть 1-16 завершена и т.д.). Кажется, что любой вызов "получает" выход блокируется, пока блендер не уйдет. Проблема заключается в том, как получить доступ к этому выводу, пока блендер все еще запущен, так как блендер выводит его в оболочку.

Ответ 1

У меня был некоторый успех в решении этой проблемы. Вот подробности, с некоторыми пояснениями, в случае, если кто-то, у кого есть аналогичная проблема, находит эту страницу. Но если вам не нужны детали, здесь короткий ответ:

Используйте PTY.spawn следующим образом (с вашей собственной командой, конечно):

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

И здесь длинный ответ, со слишком большим количеством деталей:

Реальная проблема заключается в том, что если процесс явно не очищает его stdout, то все, что написано в stdout, буферизуется, а не отправляется, пока процесс не будет выполнен, чтобы минимизировать IO (это, по-видимому, реализация деталь многих библиотек C, сделанных так, чтобы пропускная способность была максимизирована за счет менее частого ввода-вывода). Если вы можете легко изменить процесс, чтобы он регулярно менял stdout, это было бы вашим решением. В моем случае это был блендер, поэтому немного пугайтесь для полной нуб, такой как я, чтобы изменить источник.

Но когда вы запускаете эти процессы из оболочки, они отображают stdout для оболочки в режиме реального времени, а stdout не похоже на буферизацию. Он только буферизуется при вызове из другого процесса, который, как мне кажется, но если оболочка рассматривается, то stdout отображается в реальном времени, небуферизованный.

Такое поведение можно даже наблюдать с рубиновым процессом в качестве дочернего процесса, выход которого должен собираться в реальном времени. Просто создайте script, random.rb со следующей строкой:

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

Затем ruby ​​ script, чтобы вызвать его и вернуть его вывод:

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

Вы увидите, что вы не получите результат в режиме реального времени, как вы могли ожидать, но все сразу после этого. STDOUT буферизуется, хотя, если вы запускаете random.rb самостоятельно, он не буферизируется. Это можно решить, добавив оператор STDOUT.flush внутри блока в random.rb. Но если вы не можете изменить источник, вам придется обойти это. Вы не можете удалить его из-за пределов процесса.

Если подпроцесс может печатать в оболочку в режиме реального времени, тогда должен быть способ захвата этого с Ruby в режиме реального времени. И есть. Вы должны использовать модуль PTY, включенный в рубиновый ядро, я считаю (1.8.6 в любом случае). Печально то, что он не задокументирован. Но, к счастью, я нашел несколько примеров использования.

Во-первых, чтобы объяснить, что такое PTY, он обозначает псевдотерминал. В принципе, он позволяет ruby ​​ script представить себя подпроцессу, как если бы он был настоящим пользователем, который только что ввел команду в оболочку. Таким образом, произойдет любое измененное поведение, которое происходит только тогда, когда пользователь начал процесс через оболочку (например, STDOUT, не буферизуемый в этом случае). Скрывая тот факт, что другой процесс начал этот процесс, вы можете собирать STDOUT в режиме реального времени, поскольку он не буферизуется.

Чтобы сделать эту работу с random.rb script в качестве дочернего элемента, попробуйте следующий код:

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

Ответ 2

используйте IO.popen. Это хороший пример.

Ваш код станет чем-то вроде:

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

Ответ 3

STDOUT.flush или STDOUT.sync = true

Ответ 4

Блендер, вероятно, не печатает разрывы строк, пока не закончит программу. Вместо этого он печатает символ возврата каретки (\ r). Самое простое решение - это, вероятно, поиск волшебной опции, которая печатает разрывы строк с индикатором прогресса.

Проблема заключается в том, что IO#gets (и различные другие методы ввода-вывода) используют разрывы строк в качестве разделителя. Они будут читать поток до тех пор, пока они не нажмут символ "\n" (который не отправляет блендер).

Попробуйте установить разделитель ввода $/ = "\r" или вместо blender.gets("\r").

Кстати, для таких проблем вы всегда должны проверять puts someobj.inspect или p someobj (оба они делают то же самое), чтобы увидеть какие-либо скрытые символы внутри строки.

Ответ 5

Я не знаю, ответил ли в то время эшанул на вопрос: Open3::pipeline_rw() пока еще есть, но это действительно упрощает,

Я не понимаю работу ehsanul с Blender, поэтому я сделал еще один пример с tar и xz. tar добавит входной файл к потоку stdout, затем xz возьмет это stdout и снова сжимает его на другой stdout. Наша задача - взять последний stdout и записать его в наш окончательный файл:

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