class POSIX::Spawn::Child

POSIX::Spawn::Child includes logic for executing child processes and 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 and run a process to completion:

>> child = POSIX::Spawn::Child.new('git', '--help')

Retrieve stdout or stderr output:

>> child.out
=> "usage: git [--version] [--exec-path[=GIT_EXEC_PATH]]\n ..."
>> child.err
=> ""

Check process exit status information:

>> child.status
=> #<Process::Status: pid=80718,exited(0)>

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 use POSIX::Spawn::Child instead of popen3, hand rolled fork/exec code, or Process::spawn?

Constants

BUFSIZE

Maximum buffer size for reading

Attributes

err[R]

All data written to the child process's stderr stream as a String.

out[R]

All data written to the child process's stdout stream as a String.

runtime[R]

Total command execution time (wall-clock time)

status[R]

A Process::Status object with information on how the child exited.

Public Class Methods

new(*args) click to toggle source

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:

new([env], command, [argv1, ...], [options])

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.

# File lib/posix/spawn/child.rb, line 73
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

Public Instance Methods

success?() click to toggle source

Determine if the process did exit with a zero exit status.

# File lib/posix/spawn/child.rb, line 96
def success?
  @status && @status.success?
end

Private Instance Methods

exec!() click to toggle source

Execute command, write input, and read output. This is called immediately when a new instance of this object is initialized.

# File lib/posix/spawn/child.rb, line 103
def exec!
  # spawn the process and hook up the pipes
  pid, stdin, stdout, stderr = popen4(@env, *(@argv + [@options]))

  # async read from all streams into buffers
  @out, @err = read_and_write(@input, stdin, stdout, stderr, @timeout, @max)

  # grab exit status
  @status = waitpid(pid)
rescue Object => boom
  [stdin, stdout, stderr].each { |fd| fd.close rescue nil }
  if @status.nil?
    ::Process.kill('TERM', pid) rescue nil
    @status = waitpid(pid)      rescue nil
  end
  raise
ensure
  # let's be absolutely certain these are closed
  [stdin, stdout, stderr].each { |fd| fd.close rescue nil }
end
read_and_write(input, stdin, stdout, stderr, timeout=nil, max=nil) click to toggle source

Start a select loop writing any input on the child's stdin and reading any output from the child's stdout or stderr.

input - String input to write on stdin. May be nil. stdin - The write side IO object for the child's stdin stream. stdout - The read side IO object for the child's stdout stream. stderr - The read side IO object for the child's stderr stream. timeout - An optional Numeric specifying the total number of seconds

the read/write operations should occur for.

Returns an [out, err] tuple where both elements are strings with all

data written to the stdout and stderr streams, respectively.

Raises TimeoutExceeded when all data has not been read / written within

the duration specified in the timeout argument.

Raises MaximumOutputExceeded when the total number of bytes output

exceeds the amount specified by the max argument.
# File lib/posix/spawn/child.rb, line 143
def read_and_write(input, stdin, stdout, stderr, timeout=nil, max=nil)
  max = nil if max && max <= 0
  out, err = '', ''
  offset = 0

  # force all string and IO encodings to BINARY under 1.9 for now
  if out.respond_to?(:force_encoding) and stdin.respond_to?(:set_encoding)
    [stdin, stdout, stderr].each do |fd|
      fd.set_encoding('BINARY', 'BINARY')
    end
    out.force_encoding('BINARY')
    err.force_encoding('BINARY')
    input = input.dup.force_encoding('BINARY') if input
  end

  timeout = nil if timeout && timeout <= 0.0
  @runtime = 0.0
  start = Time.now

  readers = [stdout, stderr]
  writers =
    if input
      [stdin]
    else
      stdin.close
      []
    end
  t = timeout
  while readers.any? || writers.any?
    ready = IO.select(readers, writers, readers + writers, t)
    raise TimeoutExceeded if ready.nil?

    # write to stdin stream
    ready[1].each do |fd|
      begin
        boom = nil
        size = fd.write_nonblock(input)
        input = input[size, input.size]
      rescue Errno::EPIPE => boom
      rescue Errno::EAGAIN, Errno::EINTR
      end
      if boom || input.size == 0
        stdin.close
        writers.delete(stdin)
      end
    end

    # read from stdout and stderr streams
    ready[0].each do |fd|
      buf = (fd == stdout) ? out : err
      begin
        buf << fd.readpartial(BUFSIZE)
      rescue Errno::EAGAIN, Errno::EINTR
      rescue EOFError
        readers.delete(fd)
        fd.close
      end
    end

    # keep tabs on the total amount of time we've spent here
    @runtime = Time.now - start
    if timeout
      t = timeout - @runtime
      raise TimeoutExceeded if t < 0.0
    end

    # maybe we've hit our max output
    if max && ready[0].any? && (out.size + err.size) > max
      raise MaximumOutputExceeded
    end
  end

  [out, err]
end
waitpid(pid) click to toggle source

Wait for the child process to exit

Returns the Process::Status object obtained by reaping the process.

# File lib/posix/spawn/child.rb, line 221
def waitpid(pid)
  ::Process::waitpid(pid)
  $?
end