Class: Progeny::Command

Inherits:
Object
  • Object
show all
Defined in:
lib/progeny/command.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args) ⇒ Command

Spawn a new process, write all input and read all output, and wait for the program to exit. Supports the standard spawn interface:

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

The following options are supported in addition to the standard Process.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.
:pgroup_kill => bool Boolean specifying whether to kill the process
                     group (true) or individual process (false, default).
                     Setting this option true implies :pgroup => true.

Returns a new Command instance whose underlying process has already executed to completion. The out, err, and status attributes are immediately available.



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/progeny/command.rb', line 74

def initialize(*args)
  if args.last.is_a?(Hash)
    options = args.pop.dup
  else
    options = {}
  end

  if args.first.is_a?(Hash)
    @env = args.shift
  else
    @env = {}
  end
  @env.merge!(options.delete(:env)) if options.key?(:env)
  @argv = args
  @options = options.dup
  @input = @options.delete(:input)
  @timeout = @options.delete(:timeout)
  @max = @options.delete(:max)
  if @options.delete(:pgroup_kill)
    @pgroup_kill = true
    @options[:pgroup] = true
  end
  @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.



164
165
166
# File 'lib/progeny/command.rb', line 164

def err
  @err
end

#inputObject (readonly)

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



158
159
160
# File 'lib/progeny/command.rb', line 158

def input
  @input
end

#outObject (readonly)

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



161
162
163
# File 'lib/progeny/command.rb', line 161

def out
  @out
end

#pidObject (readonly)

The pid of the spawned child process. This is unlikely to be a valid current pid since Command#exec! doesn’t return until the process finishes and is reaped.



175
176
177
# File 'lib/progeny/command.rb', line 175

def pid
  @pid
end

#runtimeObject (readonly)

Total command execution time (wall-clock time)



170
171
172
# File 'lib/progeny/command.rb', line 170

def runtime
  @runtime
end

#statusObject (readonly)

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



167
168
169
# File 'lib/progeny/command.rb', line 167

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 Progency::Command if you want to read any partial output from the child process even after an exception.

child = Progency::Command.build(... arguments ...)
child.exec!

The arguments are the same as the regular constructor.

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



115
116
117
118
119
120
121
122
123
# File 'lib/progeny/command.rb', line 115

def self.build(*args)
  options =
    if args.last.is_a?(Hash)
      args.pop.dup
    else
      {}
    end
  new(*(args + [{ :noexec => true }.merge(options)]))
end

.spawn_with_pipes(*argv) ⇒ Object

Spawn a child process with all standard IO streams piped in and out of the spawning process. Supports the standard ‘Process.spawn` interface.

Returns a [pid, stdin, stdout, stderr] tuple, where pid is the new process’s pid, stdin is a writeable IO object, and stdout / stderr are readable IO objects. The caller should take care to close all IO objects when finished and the child process’s status must be collected by a call to Process::waitpid or equivalent.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/progeny/command.rb', line 133

def self.spawn_with_pipes(*argv)
  if argv.last.is_a?(Hash)
    opts = argv.pop.dup
  else
    opts = {}
  end

  ird, iwr = IO.pipe
  ord, owr = IO.pipe
  erd, ewr = IO.pipe

  opts = opts.merge(
      # redirect fds        # close other sides
      :in  => ird,          iwr  => :close,
      :out => owr,          ord  => :close,
      :err => ewr,          erd  => :close
    )
  pid = spawn(*(argv + [opts]))
  [pid, iwr, ord, erd]
ensure
  # we're in the parent, close child-side fds
  [ird, owr, ewr].each { |fd| fd.close if fd }
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 Command via ‘build`.



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/progeny/command.rb', line 185

def exec!
  pid, stdin, stdout, stderr = self.class.spawn_with_pipes(@env, *@argv, @options)
  @pid = pid

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

  # wait for the termination of the process and return exit status
  @status = waitpid(pid)
rescue Object
  [stdin, stdout, stderr].each { |fd| fd.close rescue nil }
  if @status.nil?
    if !@pgroup_kill
      ::Process.kill('TERM', pid) rescue nil
    else
      ::Process.kill('-TERM', pid) rescue nil
    end
    @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)


178
179
180
# File 'lib/progeny/command.rb', line 178

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