Запуск дочерних процессов в Ruby. Часть 3

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

В этой статье продолжаем запускать в виде дочернего процесса код:

require 'rbconfig'

$stdout.sync = true

def hello(source, expect_input)

  puts "[child] Hello from #{source}"

  if expect_input

    puts "[child] Standard input contains: \"#{$stdin.readline.chomp}\""

  else

    puts "[child] No stdin, or stdin is same as parent's"

  end

  $stderr.puts "[child] Hello, standard error"

  puts "[child] DONE"

end

THIS_FILE = File.expand_path(__FILE__)



RUBY = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name'])

#hello — метод, который будет выполняться в дочернем процессе. Он читает текст из потока stdin и пишет данные в поток stdout и stderr. Переменные THIS_FILE и RUBY содержат полный путь к этому файлу и интерпретатору Ruby соответственно.


Method #6: Open3

 

Библиотека Open3 предоставляет метод Open3#popen3(). Этот метод ведет себя аналогично методу Kernel#popen(). Отличие состоит в том, что метод Kernel#popen() не позволяет считывать данные из потока stderr дочернего процесса. Метод Open3#popen3() лишён этого недостатка. Пример использования:

puts "6. Open3"

require 'open3'

include Open3

popen3(RUBY, '-r', THIS_FILE, '-e', 'hello("Open3", true)') do

  |stdin, stdout, stderr|

  stdin.write("hello from parent")

  stdin.close_write

  stdout.read.split("\n").each do |line|

    puts "[parent] stdout: #{line}"

  end

  stderr.read.split("\n").each do |line|

    puts "[parent] stderr: #{line}"

  end

end

puts "---"

Результат выполнения:

6. Open3

[parent] stdout: [child] Hello from Open3

[parent] stdout: [child] Standard input contains: "hello from parent"

[parent] stdout: [child] DONE

[parent] stderr: [child] Hello, standard error

---

Метод #7: PTY

Все ранее рассмотренные методы имеют одно существенное ограничение: они малопригодны для интенсивных обменов данными с подпроцессами. Они хорошо подходят для 'filter-style' команд, которые читают данные, обрабатывают их, выдают результат и затем завершают выполнение. При интенсивном обмене данными с дочерними процессами, которые ожидают входных данных, выдают выходные и затем опять ожидают входные данные, такая ситуация может привести к дедлокам (deadlock). В типичном сценарии взаимодействия ожидаемые выходные данные могут не быть получены из-за того, что они не «вытолкнуты» из внутренних буферов, и вызывающая программа «зависает». Для недопущения такой ситуации в предыдущих примерах для очистки буфера вызывался метод #close_write. В состав Ruby входит одна малоизвестная и плохо документированная библиотека — pty. Pty — интерфейс к BSD pty устройствам или, другими словами, псевдотерминал, не присоединённый к физическому терминалу. Этот терминал используется эмуляторами xterm, GNOME Terminal и Terminal.app для взаимодействия с операционной системой. Что это означает для нас ? Это значит, что под операционной системой Linux есть возможность запуска дочерних процессов в виртуальном терминале. В этот терминал родительский процесс может читать и писать как обычный пользователь в интерактивном режиме. Вот как это можно использовать:

puts "7. PTY"

require 'pty'

PTY.spawn(RUBY, '-r', THIS_FILE, '-e', 'hello("PTY", true)') do

  |output, input, pid|

  input.write("hello from parent\n")

  buffer = ""

  output.readpartial(1024, buffer) until buffer =~ /DONE/

  buffer.split("\n").each do |line|

    puts "[parent] output: #{line}"

  end

end

puts "---"

Результат:
7. PTY

[parent] output: [child] Hello from PTY

[parent] output: hello from parent

[parent] output: [child] Standard input contains: "hello from parent"

[parent] output: [child] Hello, standard error

[parent] output: [child] DONE

---

В этом примере есть ряд особенностей. Во-первых, мы не вызывали методы #close_write и #flush для очистки буферов. Однако, в данному случае обязательно использование символа '\n', при отсутствии которого дочерний процесс будет бесконечно ждать окончания ввода данных. Во-вторых, так как дочерний процесс выполняются асинхронно и независимо и мы не знаем когда он считает данные и завершит их обработку, мы накапливаем присылаемые данные в буфере до получения маркера ("DONE"). В-третьих, строка «hello from parent» появляется в выходном потоке дважды: первый раз как output родительского процесса и второй раз как output дочернего процесса. Это происходит потому, что UNIX-терминалы отсылают назад пользователю все данные, полученные от него. Такое поведение можно изменить используя гем termios. Обратите внимание, что данные из stdout и stderr попали в выходной поток дочернего процесса. С точки зрения пользователя pty stdout и stderr неразличимы. Использование pty — единственный способ запустить дочерний процесс и получить данные потоков stdio и stderr в одном потоке. В зависимости от разрабатываемого приложения это может быть достоинством или недостатком. Метод PTY.spawn() можно использовать без блока. В этом случае он возвращает массив из входных данных, выходных и PID. При использовании метода надо обрабатывать исключение PTY::ChildExited, выбрасываемое в момент завершения дочернего процесса. Ruby Standard Library включает в себя библиотеку expect.rb — реализация на языке Ruby утилиты expect, написанной с использованием pty.

Метод #8: Shell

 

Ruby-библиотека Shell еще менее известна, чем pty. Shell практически недокументирована и редко используется. Shell — это попытка сэмулировать UNIX-style среду как DSL, написанный на Ruby. Вот пример запуска дочернего процесса на Shell:

puts "8. Shell"

require 'shell'

Shell.def_system_command :ruby, RUBY

shell = Shell.new

input  = 'Hello from parent'

process = shell.transact do

  echo(input) | ruby('-r', THIS_FILE, '-e', 'hello("shell.rb", true)')

end

output = process.to_s

output.split("\n").each do |line|

  puts "[parent] output: #{line}"

end

puts "---"

Результат:
8. Shell

[child] Hello, standard error

[parent] output: [child] Hello from shell.rb

[parent] output: [child] Standard input contains: "Hello from parent"

[parent] output: [child] DONE

---

Сначала определяем интерпретатор Ruby как shell-команду с помощью вызова Shell.def_system_command. Дочерний процесс создаётся методов Shell#transact. Для того, чтобы передать дочернему процессу данные используется pipeline между командой echo и командой запуска интерпретатора Ruby. Далее дожидаемся завершения процесса и запрашиваем возвращаемые данные с помощью метода #to_s. Обратите внимание, что stderr родительского и дочернего процессов один и тот же. Библиотека Shell содержит реализацию множества команд UNIX, в том числе и средства координации потоков между процессами. Однако эту библиотеку следует использовать с осторожностью, т.к. она, насколько я знаю, не поддерживается и я столкнулся с парой багов при подготовке примеров. Библиотеку лучше не использовать в production-коде.

Подготовлено КОМТЕТ komtet.ru по материалам: devver.net

Вам также может помочь