Запуск дочерних процессов в Ruby. Часть 3
В этой статье продолжаем запускать в виде дочернего процесса код:
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