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

86

Я хочу запустить блендер из командной строки через скрипт ruby, который затем будет обрабатывать вывод, выданный блендером, построчно, чтобы обновить индикатор выполнения в графическом интерфейсе. На самом деле не важно, что 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}

Редактировать:

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

эхсанул
источник

Ответы:

175

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

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

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

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

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

Такое поведение можно наблюдать даже с процессом 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
эхсанул
источник
7
Это здорово, но я считаю, что параметры блока stdin и stdout следует поменять местами. См .: ruby-doc.org/stdlib-1.9.3/libdoc/pty/rdoc/…
Майк Конильяро, 08
1
Как закрыть pty? Убить пид?
Борис Б.
Отличный ответ. Вы помогли мне улучшить мой скрипт развёртывания рейка для heroku. Он отображает журнал git push в реальном времени и прерывает задачу, если обнаружено «fatal:» gist.github.com/sseletskyy/9248357
Серж Селецкий
1
Изначально я пытался использовать этот метод, но pty недоступен в Windows. Как оказалось, STDOUT.sync = trueэто все, что нужно (ответ Мвермана ниже). Вот еще один поток с примером кода .
Pakman
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
Синан Тайфур
источник
Я пробовал это. Проблема та же. После этого я получаю доступ к выходу. Я считаю, что IO.popen запускается с запуска первого аргумента в виде команды и ждет ее завершения. В моем случае результат выдается блендером, пока блендер все еще обрабатывает. А потом после вызывается блок, что мне не помогает.
ehsanul
Вот что я пробовал. Он возвращает результат после завершения работы блендера: IO.popen ("blender -b mball.blend // renders / -F JPEG -x 1 -f 1", "w +") do | blender | blender.each {| line | ставит линию; output + = line;} end
ehsanul
3
Я не уверен, что происходит в вашем случае. Я протестировал приведенный выше код с yesпомощью приложения командной строки, которое никогда не заканчивается , и оно сработало. Код был следующим: IO.popen('yes') { |p| p.each { |f| puts f } }. Я подозреваю, что это связано с блендером, а не с рубином. Вероятно, блендер не всегда сбрасывает свой STDOUT.
Sinan Taifour,
Хорошо, я просто попробовал это проверить с помощью внешнего процесса ruby, и вы правы. Кажется, проблема с блендером. В любом случае спасибо за ответ.
ehsanul
Оказывается, есть способ получить результат через Ruby, даже если блендер не сбрасывает свой стандартный вывод. Подробности в отдельном ответе, если вам интересно.
ehsanul
6

STDOUT.flush или STDOUT.sync = true

Мвеерман
источник
да, это был неудачный ответ. Ваш ответ был лучше.
mveerman
Не хромой! Работал у меня.
Clay Bridges
Точнее:STDOUT.sync = true; system('<whatever-command>')
caram
4

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

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

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

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

ххааму
источник
1
Я только что проверил вывод, и мне кажется, что блендер использует разрыв строки (\ n), так что проблема не в этом. В любом случае спасибо за совет, я буду иметь это в виду в следующий раз, когда буду отлаживать что-то вроде этого.
ehsanul
0

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

Я не понимаю, как ehsanul работает с Blender, поэтому я сделал другой пример с помощью 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
Condichoso
источник
0

Старый вопрос, но были похожие проблемы.

Без особого изменения моего кода Ruby, одна вещь, которая помогла, заключалась в том, чтобы обернуть мою трубу stdbuf , например:

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

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

В моем примере фактическая команда, с которой я хочу взаимодействовать, как если бы это была оболочка, - это openssl .

-oL -eL скажите ему буферизовать STDOUT и STDERR только до новой строки. Заменить Lна, 0чтобы полностью отключить буфер.

Однако это не всегда работает: иногда целевой процесс применяет собственный тип буфера потока, как указано в другом ответе.

Маркос
источник