Class: POSIX::Spawn::Child

Inherits:
Object
  • Object
show all
Includes:
POSIX::Spawn
Defined in:
lib/posix/spawn/child.rb

Overview

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"

To access output from the process even if an exception was raised:

>> child = POSIX::Spawn::Child.build('git', 'log', :max => 1000)
>> begin
?>   child.exec!
?> rescue POSIX::Spawn::MaximumOutputExceeded
?>   # just so you know
?> end
>> child.out
"... first 1000 characters of log output ..."

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 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’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.

Constant Summary

Constants included from POSIX::Spawn

VERSION

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from POSIX::Spawn

#_pspawn, #`, #fspawn, #popen4, #pspawn, #spawn, #system

Constructor Details

#initialize(*args) ⇒ Child

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.



84
85
86
87
88
89
90
91
92
# File 'lib/posix/spawn/child.rb', line 84

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! if !@options.delete(:noexec)
end

Instance Attribute Details

#errObject (readonly)

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



123
124
125
# File 'lib/posix/spawn/child.rb', line 123

def err
  @err
end

#outObject (readonly)

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



120
121
122
# File 'lib/posix/spawn/child.rb', line 120

def out
  @out
end

#runtimeObject (readonly)

Total command execution time (wall-clock time)



129
130
131
# File 'lib/posix/spawn/child.rb', line 129

def runtime
  @runtime
end

#statusObject (readonly)

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



126
127
128
# File 'lib/posix/spawn/child.rb', line 126

def status
  @status
end

Class Method Details

.build(*args) ⇒ Object

Set up a new process to spawn, but do not actually spawn it.

Invoke this just like the normal constructor to set up a process to be run. Call ‘exec!` to actually run the child process, send the input, read the output, and wait for completion. Use this alternative way of constructing a POSIX::Spawn::Child if you want to read any partial output from the child process even after an exception.

child = POSIX::Spawn::Child.build(... arguments ...)
child.exec!

The arguments are the same as the regular constructor.

Returns a new Child instance but does not run the underlying process.



109
110
111
112
113
114
115
116
117
# File 'lib/posix/spawn/child.rb', line 109

def self.build(*args)
  options =
    if args[-1].respond_to?(:to_hash)
      args.pop.to_hash
    else
      {}
    end
  new(*args, { :noexec => true }.merge(options))
end

Instance Method Details

#exec!Object

Execute command, write input, and read output. This is called immediately when a new instance of this object is created, or can be called explicitly when creating the Child via ‘build`.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/posix/spawn/child.rb', line 139

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
  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

#success?Boolean

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

Returns:

  • (Boolean)


132
133
134
# File 'lib/posix/spawn/child.rb', line 132

def success?
  @status && @status.success?
end