Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
# :PROCESS: ruby, "ruby %f 2>&1"
# :BRACKET_CODE: "[ruby]", "[/ruby]"
# :TEXT:
#
# In the <a
# href="http://devver.net/blog/2009/06/a-dozen-or-so-ways-to-start-sub-processes-in-ruby-part-1/">previous
# article</a> we looked at some basic methods for starting subprocesses in Ruby.
# One thing all those methods had in common was that they didn't permit a lot of
# communication between parent process and child. In this article we'll examine
# a few built-in Ruby methods which give us the ability to have a two-way
# conversation with our subprocesses.
#
# The complete source code for this article can be found at <a
# href="http://gist.github.com/146199">http://gist.github.com/146199</a>.
#
# <h3>Method #4: Opening a pipe</h3>
#
# As you know, the <code>Kernel#open</code> method allows you to open files for
# reading and writing (and, with addition of the open-uri library, HTTP sockets
# as well). What you may not know is that <code>Kernel.open</code> can also
# open processes as if they were files.
#
# :INSERT: @open_with_pipe
#
# By passing a pipe ("|") as the first character in the command, we signal to
# <code>open</code> that we want to start a process, not open a file. For a
# command, we're starting another Ruby process and calling our trusty
# <code>hello</code> method (see the first article or the <a
# href="http://gist.github.com/146199">source code</a> for this article for the
# definition of the <code>hello</code> method <code>RUBY</code> and
# <code>THIS_FILE</code> constants).
#
# <code>open</code> yields an IO object which enables us to communicate with the
# subprocess. Anything written to the object is piped to the process' STDIN, and
# the anything the process writes to its STDOUT can be read back as if reading
# from a file. In the example above we write a line to the child, read some
# text back from the child, and then end the block.
#
# Note the call to <code>close_write</code> on line 5. This call is important.
# Because the OS buffers input and output, it is possible to write to a
# subprocess, attempt to read back, and wait forever because the data is still
# sitting in the buffer. In addition, filter-style programs typically wait
# until they see an EOF on their STDIN to exit. By calling
# <code>close_write</code>, we cause the buffer to be flushed and an EOF to be
# sent. Once the subprocess exits, its output buffer wil be flushed and any
# <code>read</code> calls on the parent side will return.
#
# Also note that we pass "w+" as the file open mode. Just as with files, by
# default the IO object will be opened in read-only mode. If we want to both
# write to and read from it, we need to specify an appropriate mode.
#
# Here's the output of the above code:
#
# :INSERT: $SOURCE|ruby:/4a\./../---/, { brackets: ["[plain]", "[/plain]"] }
#
# Another way to open a command as an IO object is to call
# <code>IO.popen</code>:
#
# :INSERT: @popen
#
# This behaves exactly the same as the <code>Kernel#open</code> version. Which
# way you choose to use is a matter of preference. The <code>IO.popen</code>
# version arguably makes it a little more obvious what is going on.
#
# <h3>Method #5: Forking to a pipe</h3>
#
# This is a variation on the previous technique. If <code>Kernel#open</code> is
# passed a pipe followed by a dash ("|-") as its first argument, it starts a
# forked subprocess. This is like the previous example except that instead of
# executing a command, it forks the running Ruby process into two processes.
#
# Both processes then execute the given block. In the child process, the
# argument yielded to the block will be <code>nil</code>. In the parent, the
# block argument will be an IO object. As before, the IO object is tied to the
# forked process' standard input and standard output streams.
#
# Here's the output:
#
# :INSERT: $SOURCE|ruby:/5a\./../---/, { brackets: ["[plain]", "[/plain]"] }
#
# Once again, there is an <code>IO.popen</code> version which does the same
# thing:
#
# :INSERT: @popen_with_dash
#
# <h3>Applications and Caveats</h3>
#
# The techniques we've looked at in this article are best suited for "filter"
# style subprocesses, where we want to feed some input to a process and then use
# the output it produces. Because of the potential for deadlocks mentioned
# earlier, they are less suitable for running highly interactive subprocesses
# which require multiple reads and responses.
#
# <code>open</code>/<code>popen</code> also do not give us access to the
# subprocess' standard error (STDERR) stream. Any output error generated by the
# subprocesses will print the same place that the parent process' STDERR does.
#
# In the upcoming parts of the series we'll look at some libraries which
# overcome both of these limitations.
#
# <h3>Conclusion</h3>
#
# In this article we've explored two (or four, depending on how you count it)
# built-in ways of starting a subprocess and communicating with it as if it were
# a file. In part 3 we'll move away from built-ins and on to the facilities
# provided in Ruby's Standard Library for starting and controlling subprocesses.
# :SAMPLE: helpers
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"
end
THIS_FILE = File.expand_path(__FILE__)
RUBY = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name'])
# :END:
if $PROGRAM_NAME == __FILE__
# :SAMPLE: open_with_pipe
puts "4a. Kernel#open with |"
cmd = %Q<|#{RUBY} -r#{THIS_FILE} -e 'hello("open(|)", true)'>
open(cmd, 'w+') do |subprocess|
subprocess.write("hello from parent")
subprocess.close_write
subprocess.read.split("\n").each do |l|
puts "[parent] output: #{l}"
end
puts
end
puts "---"
# :SAMPLE: open_with_pipe_dash
puts "5a. Kernel#open with |-"
open("|-", "w+") do |subprocess|
if subprocess.nil? # child
hello("open(|-)", true)
exit
else # parent
subprocess.write("hello from parent")
subprocess.close_write
subprocess.read.split("\n").each do |l|
puts "[parent] output: #{l}"
end
puts
end
end
puts "---"
# :SAMPLE: popen
puts "4b. IO.popen"
cmd = %Q<#{RUBY} -r#{THIS_FILE} -e 'hello("popen", true)'>
IO.popen(cmd, 'w+') do |subprocess|
subprocess.write("hello from parent")
subprocess.close_write
subprocess.read.split("\n").each do |l|
puts "[parent] output: #{l}"
end
puts
end
puts "---"
# :SAMPLE: popen_with_dash
puts "5b. IO.popen with -"
IO.popen("-", "w+") do |subprocess|
if subprocess.nil? # child
hello("popen(-)", true)
exit
else # parent
subprocess.write("hello from parent")
subprocess.close_write
subprocess.read.split("\n").each do |l|
puts "[parent] output: #{l}"
end
puts
end
end
puts "---"
# :END:
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.