Постійно читаємо з STDOUT зовнішнього процесу в Ruby


86

Я хочу запустити blender з командного рядка через рубіновий скрипт, який потім оброблятиме вихідні дані, подані blender, рядок за рядком, щоб оновити індикатор прогресу в графічному інтерфейсі. Насправді не важливо, що блендер є зовнішнім процесом, який слід прочитати.

Здається, я не можу зрозуміти повідомлення про прогрес, які блендер зазвичай друкує в оболонці, коли процес змішування все ще працює, і я спробував кілька способів. Здається, я завжди отримую доступ до stdout блендера після того, як блендер вийшов, а не поки він ще працює.

Ось приклад невдалої спроби. Він отримує та друкує перші 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

І ось довга відповідь із забагато деталей:

Справжня проблема полягає в тому, що якщо процес явно не змиває свій stdout, тоді все, що записано в stdout, буферизується, а не фактично надсилається, доки процес не буде зроблено, щоб мінімізувати IO (це, очевидно , деталь реалізації багатьох C-бібліотеки, зроблені таким чином, щоб пропускна здатність була максимально збільшена завдяки менш частому вводу-виводу). Якщо ви можете легко модифікувати процес так, щоб він регулярно змивав stdout, тоді це буде вашим рішенням. У моєму випадку це був блендер, тому трохи залякує повного нуба, такого як я, змінити джерело.

Але коли ви запускаєте ці процеси з оболонки, вони відображають stdout до оболонки в режимі реального часу, і stdout, здається, не буферизується. На мою думку, він буферизується лише при виклику з іншого процесу, але якщо обробляється оболонка, stdout бачиться в режимі реального часу без буфера.

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

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

Потім рубіновий скрипт для його виклику і повернення результату:

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, це означає псевдотермінал . По суті, це дозволяє рубіновому скрипту представити себе в підпроцесі так, ніби це справжній користувач, який щойно ввів команду в оболонку. Отже, відбуватиметься будь-яка змінена поведінка, яка виникає лише тоді, коли користувач запускає процес через оболонку (наприклад, 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/…
Майк

1
Як закрити ящик? Вбити піда?
Борис Б.

Чудова відповідь. Ви допомогли мені вдосконалити сценарій розгортання граблів для heroku. Він відображає журнал 'git push' в режимі реального часу і перериває завдання, якщо 'фатально:' знайдено gist.github.com/sseletskyy/9248357
Серж Селецький

1
Спочатку я намагався використовувати цей метод, але "pty" недоступний у Windows. Як виявляється, STDOUT.sync = trueце все, що потрібно (відповідь mveerman нижче). Ось ще одна нитка з прикладом коду .
Пакман

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. кожен {| рядок | ставить лінію; вихід + = рядок;} кінець
ehsanul

3
Я не впевнений, що відбувається у вашому випадку. Я протестував наведений вище код за yesдопомогою програми командного рядка, яка ніколи не закінчується , і вона спрацювала. Код був наступним: IO.popen('yes') { |p| p.each { |f| puts f } }. Я підозрюю, що це стосується блендера, а не рубіну. Ймовірно, блендер не завжди змиває STDOUT.
Сінан Тайфур

Гаразд, я щойно спробував це із зовнішнім рубіновим процесом для тестування, і ти маєш рацію. Здається, це проблема блендера. Дякую за відповідь у будь-якому випадку.
ehsanul

Виявляється, все-таки є спосіб отримати вихід через рубін, хоча блендер не змиває свою потужність. Детально коротко в окремій відповіді, якщо ви зацікавлені.
ehsanul

6

STDOUT.flush або STDOUT.sync = true


так, це була кульгава відповідь. Ваша відповідь була кращою.
mveerman

Не кульгавий! Працював у мене.
Clay Bridges

Точніше:STDOUT.sync = true; system('<whatever-command>')
карам

4

Blender, ймовірно, не друкує розриви рядків, поки не закінчить програму. Натомість друкується символ повернення каретки (\ 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

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щоб повністю розблокувати буфер.

Однак це не завжди працює: іноді цільовий процес застосовує власний тип буфера потоку, як зазначено в іншій відповіді.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.