Skip to content

Commit

Permalink
POSIX::Spawn::Child cleanup and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
rtomayko committed Mar 3, 2011
1 parent 1e59698 commit 169d775
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 44 deletions.
2 changes: 1 addition & 1 deletion TODO
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
[x] POSIX::Spawn::Process.new should have same method signature as Process::spawn
[x] POSIX::Spawn::Process renamed to POSIX::Spawn::Child
[x] Better POSIX::Spawn#spawn comment docs
[ ] POSIX::Spawn::Child usage examples in README
[x] POSIX::Spawn::Child usage examples in README


[ ] popen* interfaces
Expand Down
4 changes: 2 additions & 2 deletions lib/posix/spawn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module POSIX
# unset in the child:
#
# # set FOO as BAR and unset BAZ.
# pid = spawn({"FOO" => "BAR", "BAZ" => nil}, 'echo', 'hello world')
# spawn({"FOO" => "BAR", "BAZ" => nil}, 'echo', 'hello world')
#
# == Command
#
Expand All @@ -58,7 +58,7 @@ module POSIX
#
# The :chdir option specifies the current directory:
#
# pid = spawn(command, :chdir => "/var/tmp")
# spawn(command, :chdir => "/var/tmp")
#
# The :in, :out, :err, a Fixnum, an IO object or an Array option specify
# fd redirection. For example, stderr can be merged into stdout as follows:
Expand Down
82 changes: 44 additions & 38 deletions lib/posix/spawn/child.rb
Original file line number Diff line number Diff line change
@@ -1,74 +1,80 @@
module POSIX
module Spawn
# POSIX::Spawn::Child includes logic for executing child processes and
# reading/writing from their standard input, output, and error streams.
# reading/writing from their standard input, output, and error streams. It's
# designed to take all input in a single string and provides all output
# (stderr and stdout) as single strings and is therefore not well-suited
# to streaming large quantities of data in and out of commands.
#
# Create an run a process to completion:
# Create and run a process to completion:
#
# >> process = POSIX::Spawn::Child.new(['git', '--help'])
# >> child = POSIX::Spawn::Child.new('git', '--help')
#
# Retrieve stdout or stderr output:
#
# >> process.out
# >> child.out
# => "usage: git [--version] [--exec-path[=GIT_EXEC_PATH]]\n ..."
# >> process.err
# >> child.err
# => ""
#
# Check process exit status information:
#
# >> process.status
# >> child.status
# => #<Process::Status: pid=80718,exited(0)>
#
# POSIX::Spawn::Child is designed to take all input in a single string and
# provides all output as single strings. It is therefore not well suited
# to streaming large quantities of data in and out of commands.
# To write data on the new process's stdin immediately after spawning:
#
# >> child = POSIX::Spawn::Child.new('bc', :input => '40 + 2')
# >> child.out
# "42\n"
#
# Q: Why not use popen3 or hand-roll fork/exec code?
# Q: Why use POSIX::Spawn::Child instead of popen3, hand rolled fork/exec
# code, or Process::spawn?
#
# - It's more efficient than popen3 and provides meaningful process
# hierarchies because it performs a single fork/exec. (popen3 double forks
# to avoid needing to collect the exit status and also calls
# Process::detach which creates a Ruby Thread!!!!).
#
# - It's more portable than hand rolled pipe, fork, exec code because
# fork(2) and exec(2) aren't available on all platforms. In those cases,
# POSIX::Spawn::Child falls back to using whatever janky substitutes the platform
# provides.
# - It handles all max pipe buffer (PIPE_BUF) hang cases when reading and
# writing semi-large amounts of data. This is non-trivial to implement
# correctly and must be accounted for with popen3, spawn, or hand rolled
# fork/exec code.
#
# - It handles all max pipe buffer hang cases, which is non trivial to
# implement correctly and must be accounted for with either popen3 or
# hand rolled fork/exec code.
# - It's more portable than hand rolled pipe, fork, exec code because
# fork(2) and exec aren't available on all platforms. In those cases,
# POSIX::Spawn::Child falls back to using whatever janky substitutes
# the platform provides.
class Child
include POSIX::Spawn

# Create and execute a new process.
# Spawn a new process, write all input and read all output, and wait for
# the program to exit. Supports the standard spawn interface as described
# in the POSIX::Spawn module documentation:
#
# argv - Array of [command, arg1, ...] strings to use as the new
# process's argv. When argv is a String, the shell is used
# to interpret the command.
# env - The new process's environment variables. This is merged with
# the current environment as if by ENV.merge(env).
# options - Additional options:
# :input => str to write str to the process's stdin.
# :timeout => int number of seconds before we given up.
# :max => total number of output bytes
# A subset of Process::spawn options are also supported on all
# platforms:
# :chdir => str to start the process in different working dir.
# new([env], command, [argv1, ...], [options])
#
# Returns a new Child instance that has already executed to completion.
# The out, err, and status attributes are immediately available.
def initialize(*argv)
env, argv, options = extract_process_spawn_arguments(*argv)
@argv = argv
@env = env

# The following options are supported in addition to the standard
# POSIX::Spawn options:
#
# :input => str Write str to the new process's standard input.
# :timeout => int Maximum number of seconds to allow the process
# to execute before aborting with a TimeoutExceeded
# exception.
# :max => total Maximum number of bytes of output to allow the
# process to generate before aborting with a
# MaximumOutputExceeded exception.
#
# Returns a new Child instance whose underlying process has already
# executed to completion. The out, err, and status attributes are
# immediately available.
def initialize(*args)
@env, @argv, options = extract_process_spawn_arguments(*args)
@options = options.dup
@input = @options.delete(:input)
@timeout = @options.delete(:timeout)
@max = @options.delete(:max)
@options.delete(:chdir) if @options[:chdir].nil?

exec!
end

Expand Down
6 changes: 3 additions & 3 deletions test/test_child.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ def test_max_with_stubborn_child

def test_timeout
assert_raise TimeoutExceeded do
Child.new('sleep 1', :timeout => 0.05)
Child.new('sleep', '1', :timeout => 0.05)
end
end

def test_timeout_with_child_hierarchy
assert_raise TimeoutExceeded do
Child.new('/bin/sh', '-c', 'yes', :timeout => 0.05)
Child.new('/bin/sh', '-c', 'sleep 1', :timeout => 0.05)
end
end

Expand All @@ -93,7 +93,7 @@ def test_lots_of_input_and_lots_of_output_at_the_same_time
echo stuff on stderr 1>&2;
done
"
p = Child.new('/bin/sh', '-c', command, :input => input)
p = Child.new(command, :input => input)
assert_equal input.size, p.out.size
assert_equal input.size, p.err.size
assert p.success?
Expand Down

0 comments on commit 169d775

Please sign in to comment.