Непрерывно читайте из STDOUT внешнего процесса в Ruby - PullRequest
84 голосов
/ 20 июля 2009

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

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

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

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 завершена и т. Д.). Кажется, что любой вызов «получает» вывод блокируется, пока блендер не выйдет. Вопрос в том, как получить доступ к этому выводу, когда blender все еще работает, так как blender печатает его вывод в оболочку.

Ответы [ 5 ]

172 голосов
/ 22 июля 2009

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

Используйте 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

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

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

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

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

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

Затем скрипт ruby ​​для его вызова и возврата:

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

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

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

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

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

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
12 голосов
/ 20 июля 2009

используйте 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
5 голосов
/ 20 июля 2009

STDOUT.flush или STDOUT.sync = true

4 голосов
/ 21 июля 2009

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

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

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

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

0 голосов
/ 11 апреля 2015

Я не знаю, ответил ли ehsanul на вопрос, было ли еще Open3::pipeline_rw(), но это действительно упрощает ситуацию.

Я не понимаю работу Эхсанула с Блендером, поэтому я сделал еще один пример с tar и xz. tar добавит входной файл (ы) в поток stdout, затем xz возьмет 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
...