Вопрос по process, ruby, shell, stdout, stdin – Непрерывно читайте из STDOUT внешнего процесса в Ruby

83

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

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

Вот пример неудачной попытки. Он получает и печатает первые 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 печатает его вывод в оболочку.

Ваш Ответ

3   ответа
12

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
Хорошо, я только что попробовал его с внешним процессом ruby для проверки, и вы правы. Кажется, проблема с блендером. В любом случае, спасибо за ответ. ehsanul
Я пробовал это. Проблема та же. Я получаю доступ к выходу позже. Я полагаю, что IO.popen начинается с запуска первого аргумента в виде команды и ожидает его завершения. В моем случае выходные данные выдаются blender, пока blender все еще обрабатывает. И затем блок вызывается после, что мне не помогает. ehsanul
Оказывается, в конце концов, есть способ получить выходные данные через ruby, даже если blender не очищает свой стандартный вывод. Подробности коротко в отдельном ответе, на случай, если вы заинтересованы. ehsanul
Я не уверен, что происходит в вашем случае. Я тестировал код выше сyesприложение командной строки, которое никогдаendsи это сработало. Код был следующим:IO.popen('yes') { |p| p.each { |f| puts f } }, Я подозреваю, что это связано с блендером, а не с рубином. Возможно, Blender не всегда сбрасывает свой STDOUT.
Вот что я попробовал. Он возвращает выходные данные после завершения работы blender: IO.popen (& quot; blender -b mball.blend // отображает / -F JPEG -x 1 -f 1 & quot ;, "qu +") do | blender | blender.each {| line | ставит линию; выход + = строка;} конец ehsanul
168

сти, с некоторыми пояснениями, на случай, если кто-то с подобной проблемой найдет эту страницу. Но если вы не заботитесь о деталях,here's the short answer:

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

А такжеhere's the long answerсо слишком большим количеством деталей:

По-видимому, реальная проблема заключается в том, что если процесс явно не очищает свой стандартный вывод, то все, что записано в стандартный вывод, буферизуется, а не фактически отправляется, до тех пор, пока процесс не будет выполнен, чтобы минимизировать IO (этоapparently деталь реализации многих библиотек 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
Первоначально я пытался использовать этот метод, но «pty» не доступен в Windows. Как выясняется,STDOUT.sync = true это все, что нужно (ответ mveerman ниже).Here's another thread with some example code.
Как закрыть pty? Убить пид?
Это здорово, но я считаю, что параметры блоков stdin и stdout следует поменять местами. Увидеть:ruby-doc.org/stdlib-1.9.3/libdoc/pty/rdoc/…
Потрясающий ответ. Вы помогли мне улучшить мой скрипт rake deploy для heroku. Он отображает «git push» войти в систему в реальном времени и прервать задание, если «фатально:» найденныйgist.github.com/sseletskyy/9248357
0

Open3::pipeline_rw() доступно, но это действительно делает вещи проще.

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

Похожие вопросы