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

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

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

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


person ehsanul    schedule 20.07.2009    source источник


Ответы (6)


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

Используйте 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, сделанная так, что пропускная способность максимизируется за счет менее частого ввода-вывода). Если вы можете легко изменить процесс, чтобы он регулярно сбрасывал стандартный вывод, то это было бы вашим решением. В моем случае это был блендер, поэтому для такого нуба, как я, было бы немного устрашать изменение исходного кода.

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

Такое поведение можно наблюдать даже с процессом 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
person ehsanul    schedule 22.07.2009
comment
Это здорово, но я считаю, что параметры блоков stdin и stdout следует поменять местами. См. ruby- doc.org/stdlib-1.9.3/libdoc/pty/rdoc/ - person Mike Conigliaro; 09.03.2012
comment
Как закрыть pty? Убить пид? - person Boris B.; 21.11.2013
comment
Отличный ответ. Вы помогли мне улучшить мой скрипт развертывания рейка для heroku. Он отображает журнал «git push» в реальном времени и прерывает задачу, если «fatal:» обнаружено gist.github.com / sseletskyy / 9248357 - person Serge Seletskyy; 17.03.2014
comment
Первоначально я пытался использовать этот метод, но pty недоступен в Windows. Как оказалось, STDOUT.sync = true - это все, что нужно (ответ Мвермана ниже). Вот еще один поток с примером кода. - person Pakman; 27.04.2016

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

Ваш код будет выглядеть примерно так:

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

STDOUT.flush или STDOUT.sync = true

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

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

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

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

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

person hhaamu    schedule 20.07.2009
comment
Я только что проверил вывод, и мне кажется, что блендер использует разрыв строки (\ n), так что проблема не в этом. В любом случае спасибо за совет, я буду иметь это в виду в следующий раз, когда буду отлаживать что-то вроде этого. - person ehsanul; 22.07.2009

Не знаю, отвечал ли эхсанул в то время на вопрос, было ли _ 1_ пока доступен, но это действительно упрощает работу.

Я не понимаю, как ehsanul работает с Blender, поэтому я сделал другой пример с tar и xz. tar добавит входной файл (ы) в поток стандартного вывода, затем 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
person condichoso    schedule 11.04.2015

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

Без особого изменения кода 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, чтобы полностью разблокировать буфер.

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

person Marcos    schedule 12.02.2020