Created
July 13, 2009 15:19
-
-
Save devver/146199 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# :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