subprocess 0.1.6 → 0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. data/README.md +72 -0
  2. data/lib/subprocess.rb +529 -20
  3. metadata +55 -121
  4. data/History.txt +0 -4
  5. data/Manifest.txt +0 -46
  6. data/PostInstall.txt +0 -7
  7. data/README.rdoc +0 -77
  8. data/Rakefile +0 -20
  9. data/TODO.rdoc +0 -1
  10. data/examples/simple.irb +0 -22
  11. data/examples/simple_timeout.irb +0 -22
  12. data/features/multiple_popens_sequence.feature +0 -23
  13. data/features/popen.feature +0 -45
  14. data/features/popen_over_ssh.feature +0 -44
  15. data/features/popen_over_ssh_without_blocking.feature +0 -16
  16. data/features/popen_remote_fails_with_invalid_auth_data.feature +0 -13
  17. data/features/popen_reports_runtime.feature +0 -11
  18. data/features/popen_running.feature +0 -11
  19. data/features/popen_with_timeout.feature +0 -19
  20. data/features/popen_without_blocking.feature +0 -16
  21. data/features/step_definitions/common_steps.rb +0 -168
  22. data/features/step_definitions/multiple_popens_sequence_steps.rb +0 -73
  23. data/features/step_definitions/popen_over_ssh_steps.rb +0 -29
  24. data/features/step_definitions/popen_over_ssh_without_blocking_steps.rb +0 -30
  25. data/features/step_definitions/popen_remote_fails_with_invalid_auth_dat_steps.rb +0 -19
  26. data/features/step_definitions/popen_reports_runtime_steps.rb +0 -13
  27. data/features/step_definitions/popen_running_steps.rb +0 -12
  28. data/features/step_definitions/popen_steps.rb +0 -34
  29. data/features/step_definitions/popen_with_timeout_steps.rb +0 -24
  30. data/features/step_definitions/popen_without_blocking_steps.rb +0 -33
  31. data/features/support/common.rb +0 -29
  32. data/features/support/env.rb +0 -15
  33. data/features/support/matchers.rb +0 -11
  34. data/lib/core_ext/hash.rb +0 -14
  35. data/lib/core_ext/process_status.rb +0 -14
  36. data/lib/subprocess/popen.rb +0 -188
  37. data/lib/subprocess/popen_factory.rb +0 -63
  38. data/lib/subprocess/popen_remote.rb +0 -64
  39. data/lib/subprocess/popen_sequence.rb +0 -57
  40. data/script/console +0 -10
  41. data/script/destroy +0 -14
  42. data/script/generate +0 -14
  43. data/spec/spec.opts +0 -1
  44. data/spec/spec_helper.rb +0 -10
  45. data/spec/subprocess/popen_spec.rb +0 -32
  46. data/spec/subprocess_spec.rb +0 -2
  47. data/subprocess.gemspec +0 -36
  48. data/tasks/rspec.rake +0 -21
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ Subprocess
2
+ ==========
3
+
4
+ A port of Python's excellent subprocess module to Ruby.
5
+
6
+ Many thanks to [Bram Swenson][bram], the author of the old [subprocess][old]
7
+ gem, for graciously letting us use the name.
8
+
9
+ [bram]: https://github.com/bramswenson
10
+ [old]: https://github.com/bramswenson/subprocess
11
+
12
+ Installation
13
+ ------------
14
+
15
+ The recommended way of installing `subprocess` is through Rubygems:
16
+
17
+ $ gem install subprocess
18
+
19
+ You can also build `subprocess` from source by running:
20
+
21
+ $ gem build subprocess.gemspec
22
+
23
+
24
+ Usage
25
+ -----
26
+
27
+ Most of the documentation for Python's [subprocess][python] module applies
28
+ equally well to this gem as well. While there are a few places when our
29
+ semantics differs from Python's, users of the Python module should largely feel
30
+ at home using `subprocess`. We have attempted to [document][rubydoc] all of the
31
+ differences, but if we have missed something, please file an issue.
32
+
33
+ [python]: http://docs.python.org/library/subprocess.html
34
+ [rubydoc]: http://rubydoc.info/github/stripe/subprocess
35
+
36
+ A few examples:
37
+
38
+ ```ruby
39
+ require 'subprocess'
40
+ ```
41
+
42
+ Check user's animal allegiances:
43
+
44
+ ```ruby
45
+ begin
46
+ Subprocess.check_call(['grep', '-q', 'llamas', '~/favorite_animals'])
47
+ rescue NonZeroExit => e
48
+ puts e.message
49
+ puts "Why aren't llamas one of your favorite animals?"
50
+ end
51
+ ```
52
+
53
+ Parse the output of `uptime(1)` to find the system's load:
54
+
55
+ ```ruby
56
+ load = Subprocess.check_output(['uptime']).split(' ').last(3)
57
+ ```
58
+
59
+ Send mail to your friends with `sendmail(1)`:
60
+
61
+ ```ruby
62
+ Subprocess.check_call(%W{sendmail -t},
63
+ :stdin => Subprocess::PIPE, :stdout => Subprocess::PIPE) do |p|
64
+ p.communicate <<-EMAIL
65
+ From: alpaca@example.com
66
+ To: llama@example.com
67
+ Subject: I am so fluffy.
68
+
69
+ I'm going to die.
70
+ EMAIL
71
+ end
72
+ ```
data/lib/subprocess.rb CHANGED
@@ -1,23 +1,532 @@
1
- $:.unshift(File.dirname(__FILE__)) unless
2
- $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
-
4
- begin
5
- require 'net/ssh'
6
- require 'json'
7
- rescue LoadError
8
- require 'rubygems'
9
- require 'net/ssh'
10
- require 'json'
11
- end
12
- require 'timeout'
1
+ require 'thread'
2
+ require 'set'
13
3
 
4
+ # A Ruby clone of Python's subprocess module.
5
+ #
6
+ # @see http://docs.python.org/2/library/subprocess.html
14
7
  module Subprocess
15
- VERSION = '0.1.6'
16
- end
17
8
 
18
- require 'core_ext/hash'
19
- require 'core_ext/process_status'
20
- require 'subprocess/popen'
21
- require 'subprocess/popen_remote'
22
- require 'subprocess/popen_sequence'
23
- require 'subprocess/popen_factory'
9
+ # An opaque constant that indicates that a pipe should be opened.
10
+ PIPE = -1
11
+ # An opaque constant that can be passed to the `:stderr` option that indicates
12
+ # that the standard error stream should be redirected to the standard output.
13
+ STDOUT = -2
14
+
15
+ # An alias for `Process.new`. Mostly here to better emulate the Python API.
16
+ #
17
+ # @return [Process] A process with the given arguments
18
+ def self.popen(cmd, opts={}, &blk)
19
+ Process.new(cmd, opts, &blk)
20
+ end
21
+
22
+ # Call and wait for the return of a given process.
23
+ #
24
+ # @note If you call this function with `:stdout => PIPE` or `:stderr => PIPE`,
25
+ # this function will block indefinitely as soon as the OS's pipe buffer
26
+ # fills up, as neither file descriptor will be read from. To avoid this, use
27
+ # {Process#communicate} from a passed block.
28
+ #
29
+ # @return [::Process::Status] The exit status of the process
30
+ #
31
+ # @see {Process#initialize}
32
+ def self.call(cmd, opts={}, &blk)
33
+ Process.new(cmd, opts, &blk).wait
34
+ end
35
+
36
+ # Like {Subprocess::call}, except raise a {NonZeroExit} if the process did not
37
+ # terminate successfully.
38
+ #
39
+ # @example Grep a file for a string
40
+ # Subprocess.check_call(%W{grep -q llama ~/favorite_animals})
41
+ #
42
+ # @example Communicate with a child process
43
+ # Subprocess.check_call(%W{sendmail -t},
44
+ # :stdin => Subprocess::PIPE, :stdout => Subprocess::PIPE) do |p|
45
+ # p.communicate <<-EMAIL
46
+ # From: alpaca@example.com
47
+ # To: llama@example.com
48
+ # Subject: I am so fluffy.
49
+ #
50
+ # I'm going to die.
51
+ # EMAIL
52
+ # end
53
+ #
54
+ # @note If you call this function with `:stdout => PIPE` or `:stderr => PIPE`,
55
+ # this function will block indefinitely as soon as the OS's pipe buffer
56
+ # fills up, as neither file descriptor will be read from. To avoid this, use
57
+ # {Process#communicate} from a passed block.
58
+ #
59
+ # @raise [NonZeroExit] if the process returned a non-zero exit status (i.e.,
60
+ # was terminated with an error or was killed by a signal)
61
+ # @return [::Process::Status] The exit status of the process
62
+ #
63
+ # @see {Process#initialize}
64
+ def self.check_call(cmd, opts={}, &blk)
65
+ status = Process.new(cmd, opts, &blk).wait
66
+ raise NonZeroExit.new(cmd, status) unless status.success?
67
+ status
68
+ end
69
+
70
+ # Like {Subprocess::check_call}, but return the contents of `stdout`, much
71
+ # like `Kernel#system`.
72
+ #
73
+ # @example Get the system load
74
+ # load = Subprocess.check_output(['uptime']).split(' ').last(3)
75
+ #
76
+ # @raise [NonZeroExit] if the process returned a non-zero exit status (i.e.,
77
+ # was terminated with an error or was killed by a signal)
78
+ # @return [String] The contents of `stdout`
79
+ #
80
+ # @see {Process#initialize}
81
+ def self.check_output(cmd, opts={}, &blk)
82
+ opts[:stdout] = PIPE
83
+ child = Process.new(cmd, opts, &blk)
84
+ output, _ = child.communicate()
85
+ raise NonZeroExit.new(cmd, child.status) unless child.wait.success?
86
+ output
87
+ end
88
+
89
+ # Error class representing a process's abnormal exit.
90
+ class NonZeroExit < StandardError
91
+ # @!attribute [r] command
92
+ # @note This is intended only for use in user-facing error messages. In
93
+ # particular, no shell quoting of any sort is performed when
94
+ # constructing this string, meaning that blindly running it in a shell
95
+ # might have different semantics than the original command.
96
+ # @return [String] The command and arguments for the process that exited
97
+ # abnormally.
98
+ # @!attribute [r] status
99
+ # @return [::Process::Status] The Ruby status object returned by `waitpid`
100
+ attr_reader :command, :status
101
+
102
+ # Return an instance of {NonZeroExit}.
103
+ #
104
+ # @param [Array<String>] cmd The command that returned a non-zero status.
105
+ # @param [::Process::Status] status The status returned by `waitpid`.
106
+ def initialize(cmd, status)
107
+ @command, @status = cmd.join(' '), status
108
+ message = "Command #{command} "
109
+ if status.exited?
110
+ message << "returned non-zero exit status #{status.exitstatus}"
111
+ elsif status.signaled?
112
+ message << "was terminated by signal #{status.termsig}"
113
+ elsif status.stopped?
114
+ message << "was stopped by signal #{status.stopsig}"
115
+ else
116
+ message << "exited for an unknown reason (FIXME)"
117
+ end
118
+ super(message)
119
+ end
120
+ end
121
+
122
+ # A child process. The preferred way of spawning a subprocess is through the
123
+ # functions on {Subprocess} (especially {Subprocess::check_call} and
124
+ # {Subprocess::check_output}).
125
+ class Process
126
+ # @!attribute [r] stdin
127
+ # @return [IO] The `IO` that is connected to this process's `stdin`.
128
+ # @!attribute [r] stdout
129
+ # @return [IO] The `IO` that is connected to this process's `stdout`.
130
+ # @!attribute [r] stderr
131
+ # @return [IO] The `IO` that is connected to this process's `stderr`.
132
+ attr_reader :stdin, :stdout, :stderr
133
+
134
+ # @!attribute [r] command
135
+ # @return [Array<String>] The command this process was invoked with.
136
+ # @!attribute [r] pid
137
+ # @return [Fixnum] The process ID of the spawned process.
138
+ # @!attribute [r] status
139
+ # @return [::Process::Status] The exit status code of the process. Only
140
+ # set after the process has exited.
141
+ attr_reader :command, :pid, :status
142
+
143
+ # Create a new process.
144
+ #
145
+ # @param [Array<String>] cmd The command to run and its arguments (in the
146
+ # style of an `argv` array).
147
+ #
148
+ # @option opts [IO, Fixnum, String, Subprocess::PIPE, nil] :stdin The `IO`,
149
+ # file descriptor number, or file name to use for the process's standard
150
+ # input. If the magic value {Subprocess::PIPE} is passed, a new pipe will
151
+ # be opened.
152
+ # @option opts [IO, Fixnum, String, Subprocess::PIPE, nil] :stdout The `IO`,
153
+ # file descriptor number, or file name to use for the process's standard
154
+ # output. If the magic value {Subprocess::PIPE} is passed, a pipe will be
155
+ # opened and attached to the process.
156
+ # @option opts [IO, Fixnum, String, Subprocess::PIPE, Subprocess::STDOUT,
157
+ # nil] :stderr The `IO`, file descriptor number, or file name to use for
158
+ # the process's standard error. If the special value {Subprocess::PIPE} is
159
+ # passed, a pipe will be opened and attached to the process. If the
160
+ # special value {Subprocess::STDOUT} is passed, the process's `stderr`
161
+ # will be redirected to its `stdout` (much like bash's `2>&1`).
162
+ #
163
+ # @option opts [String] :cwd The directory to change to before executing the
164
+ # child process.
165
+ # @option opts [Hash<String, String>] :env The environment to use in the
166
+ # child process.
167
+ # @option opts [Array<Fixnum>] :retain_fds An array of file descriptor
168
+ # numbers that should not be closed before executing the child process.
169
+ # Note that, unlike Python (which has :close_fds defaulting to false), all
170
+ # file descriptors not specified here will be closed.
171
+ #
172
+ # @option opts [Proc] :preexec_fn A function that will be called in the
173
+ # child process immediately before executing `cmd`. Note: we don't
174
+ # actually close file descriptors, but instead set them to auto-close on
175
+ # `exec` (using `FD_CLOEXEC`), so your application will probably continue
176
+ # to behave as expected.
177
+ #
178
+ # @yield [process] Yields the just-spawned {Process} to the optional block.
179
+ # This occurs after all of {Process}'s error handling has been completed,
180
+ # and is a great place to call {Process#communicate}, especially when used
181
+ # in conjunction with {Subprocess::check_call}.
182
+ # @yieldparam process [Process] The process that was just spawned.
183
+ def initialize(cmd, opts={}, &blk)
184
+ @command = cmd
185
+
186
+ # Figure out what file descriptors we should pass on to the child (and
187
+ # make externally visible ourselves)
188
+ @child_stdin, @stdin = parse_fd(opts[:stdin], 'r')
189
+ @child_stdout, @stdout = parse_fd(opts[:stdout], 'w')
190
+ unless opts[:stderr] == STDOUT
191
+ @child_stderr, @stderr = parse_fd(opts[:stderr], 'w')
192
+ end
193
+
194
+ retained_fds = Set.new(opts[:retain_fds] || [])
195
+
196
+ # A control pipe for ferrying errors back from the child
197
+ control_r, control_w = IO.pipe
198
+
199
+ @pid = fork do
200
+ begin
201
+ require 'fcntl'
202
+
203
+ FileUtils.cd(opts[:cwd]) if opts[:cwd]
204
+
205
+ # The only way to mark an fd as CLOEXEC in ruby is to create an IO
206
+ # object wrapping it. In 1.8, however, there's no way to create that
207
+ # IO without it believing it owns the underlying fd, s.t. it will
208
+ # close the fd if the IO is GC'd before the exec. Since we don't want
209
+ # that, we stash a list of these IO objects to prevent them from
210
+ # getting GC'd, since we are about to exec, which will clean
211
+ # everything up anyways.
212
+ fds = []
213
+
214
+ # We have a whole ton of file descriptors that we don't want leaking
215
+ # into the child. Set them all to close when we exec away.
216
+ #
217
+ # Ruby 1.9+ note: exec has a :close_others argument (and 2.0 closes
218
+ # FDs by default). When we stop supporting Ruby 1.8, all of this can
219
+ # go away.
220
+ if File.directory?("/dev/fd")
221
+ # On many modern UNIX-y systems, we can perform an optimization by
222
+ # looking through /dev/fd, which is a sparse listing of all the
223
+ # descriptors we have open. This allows us to avoid an expensive
224
+ # linear scan.
225
+ Dir.foreach("/dev/fd") do |file|
226
+ fd = file.to_i
227
+ if file.start_with?('.') || fd < 3 || retained_fds.include?(fd)
228
+ next
229
+ end
230
+ begin
231
+ fds << mark_fd_cloexec(fd)
232
+ rescue Errno::EBADF
233
+ # The fd might have been closed by now; that's peaceful.
234
+ end
235
+ end
236
+ else
237
+ # This is the big hammer. There's not really a good way of doing
238
+ # this comprehensively across all platforms without just trying them
239
+ # all. We only go up to the soft limit here. If you've been messing
240
+ # with the soft limit, we might miss a few. Also, on OSX (perhaps
241
+ # BSDs in general?), where the soft limit means something completely
242
+ # different.
243
+ special = [@child_stdin, @child_stdout, @child_stderr].compact
244
+ special = Hash[special.map { |f| [f.fileno, f] }]
245
+ 3.upto(::Process.getrlimit(::Process::RLIMIT_NOFILE).first) do |fd|
246
+ next if retained_fds.include?(fd)
247
+ begin
248
+ # I don't know why we need to do this, but OSX started freaking
249
+ # out when trying to dup2 below if FD_CLOEXEC had been set on a
250
+ # fresh IO instance referring to the same underlying file
251
+ # descriptor as what we were trying to dup2 from.
252
+ if special[fd]
253
+ special[fd].fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
254
+ else
255
+ fds << mark_fd_cloexec(fd)
256
+ end
257
+ rescue Errno::EBADF # Ignore FDs that don't exist
258
+ end
259
+ end
260
+ end
261
+
262
+ # dup2 the correct descriptors into place. Note that this clears the
263
+ # FD_CLOEXEC flag on the new file descriptors (but not the old ones).
264
+ ::STDIN.reopen(@child_stdin) if @child_stdin
265
+ ::STDOUT.reopen(@child_stdout) if @child_stdout
266
+ if opts[:stderr] == STDOUT
267
+ ::STDERR.reopen(::STDOUT)
268
+ else
269
+ ::STDERR.reopen(@child_stderr) if @child_stderr
270
+ end
271
+
272
+ # Set up a new environment if we're requested to do so.
273
+ if opts[:env]
274
+ ENV.clear
275
+ ENV.update(opts[:env])
276
+ end
277
+
278
+ # Call the user back, maybe?
279
+ opts[:preexec_fn].call if opts[:preexec_fn]
280
+
281
+ # Ruby 1.8's exec is really stupid--there's no way to specify that
282
+ # you want to exec a single thing *without* performing shell
283
+ # expansion. So this is the next best thing.
284
+ args = cmd
285
+ if cmd.length == 1
286
+ args = ["'" + cmd[0].gsub("'", "\\'") + "'"]
287
+ end
288
+ if opts[:retain_fds]
289
+ redirects = {}
290
+ retained_fds.each { |fd| redirects[fd] = fd }
291
+ args << redirects
292
+ end
293
+ exec(*args)
294
+
295
+ rescue Exception => e
296
+ # Dump all errors up to the parent through the control socket
297
+ Marshal.dump(e, control_w)
298
+ control_w.flush
299
+ end
300
+
301
+ # Something has gone terribly, terribly wrong if we're hitting this :(
302
+ exit!(1)
303
+ end
304
+
305
+ # Meanwhile, in the parent process...
306
+
307
+ # First, let's close some things we shouldn't have access to
308
+ [@child_stdin, @child_stdout, @child_stderr, control_w].each do |fd|
309
+ fd.close unless fd.nil?
310
+ end
311
+
312
+ # Any errors during the spawn process? We'll get past this point when the
313
+ # child execs and the OS closes control_w because of the FD_CLOEXEC
314
+ begin
315
+ e = Marshal.load(control_r)
316
+ e = "Unknown Failure" unless e.is_a?(Exception) || e.is_a?(String)
317
+ raise e
318
+ rescue EOFError # Nothing to read? Great!
319
+ ensure
320
+ control_r.close
321
+ end
322
+
323
+ # Everything is okay. Good job, team!
324
+ blk.call(self) if blk
325
+ end
326
+
327
+ # Poll the child, setting (and returning) its status. If the child has not
328
+ # terminated, return nil and exit immediately
329
+ #
330
+ # @return [::Process::Status, nil] The exit status of the process
331
+ def poll
332
+ @status ||= (::Process.waitpid2(@pid, ::Process::WNOHANG) || []).last
333
+ end
334
+
335
+ # Wait for the child to return, setting and returning the status of the
336
+ # child.
337
+ #
338
+ # @return [::Process::Status] The exit status of the process
339
+ def wait
340
+ @status ||= ::Process.waitpid2(@pid).last
341
+ end
342
+
343
+ # Do nonblocking reads from `fd`, appending all data read into `buf`.
344
+ #
345
+ # @param [IO] fd The file to read from.
346
+ # @param [String] buf A buffer to append the read data to.
347
+ #
348
+ # @return [true, false] Whether `fd` was closed due to an exceptional
349
+ # condition (`EOFError` or `EPIPE`).
350
+ def drain_fd(fd, buf=nil)
351
+ loop do
352
+ tmp = fd.read_nonblock(4096)
353
+ buf << tmp unless buf.nil?
354
+ end
355
+ rescue EOFError, Errno::EPIPE
356
+ fd.close
357
+ true
358
+ rescue Errno::EINTR
359
+ rescue Errno::EWOULDBLOCK, Errno::EAGAIN
360
+ false
361
+ end
362
+
363
+ # Write the (optional) input to the process's `stdin`. Also, read (and
364
+ # buffer in memory) the contents of `stdout` and `stderr`. Do this all using
365
+ # `IO::select`, so we don't deadlock due to full pipe buffers.
366
+ #
367
+ # This is only really useful if you set some of `:stdin`, `:stdout`, and
368
+ # `:stderr` to {Subprocess::PIPE}.
369
+ #
370
+ # @param [String] input A string to feed to the child's standard input.
371
+ # @return [Array<String>] An array of two elements: the data read from the
372
+ # child's standard output and standard error, respectively.
373
+ def communicate(input=nil)
374
+ raise ArgumentError if !input.nil? && @stdin.nil?
375
+
376
+ stdout, stderr = "", ""
377
+ input = input.dup unless input.nil?
378
+
379
+ @stdin.close if (input.nil? || input.empty?) && !@stdin.nil?
380
+
381
+ self_read, self_write = IO.pipe
382
+ self.class.catching_sigchld(pid, self_write) do
383
+ wait_r = [@stdout, @stderr, self_read].compact
384
+ wait_w = [input && @stdin].compact
385
+ loop do
386
+ ready_r, ready_w = select(wait_r, wait_w)
387
+
388
+ # If the child exits, we still have to be sure to read any data left
389
+ # in the pipes. So we poll the child, drain all the pipes, and *then*
390
+ # check @status.
391
+ #
392
+ # It's very important that we do not call poll between draining the
393
+ # pipes and checking @status. If we did, we open a race condition
394
+ # where the child writes to stdout and exits in that brief window,
395
+ # causing us to lose that data.
396
+ poll
397
+
398
+ if ready_r.include?(@stdout)
399
+ if drain_fd(@stdout, stdout)
400
+ wait_r.delete(@stdout)
401
+ end
402
+ end
403
+
404
+ if ready_r.include?(@stderr)
405
+ if drain_fd(@stderr, stderr)
406
+ wait_r.delete(@stderr)
407
+ end
408
+ end
409
+
410
+ if ready_r.include?(self_read)
411
+ if drain_fd(self_read)
412
+ raise "Unexpected internal error -- someone closed our self-pipe!"
413
+ end
414
+ end
415
+
416
+ if ready_w.include?(@stdin)
417
+ begin
418
+ written = @stdin.write_nonblock(input)
419
+ rescue EOFError # Maybe I shouldn't catch this...
420
+ rescue Errno::EINTR
421
+ end
422
+ input[0...written] = ''
423
+ if input.empty?
424
+ @stdin.close
425
+ wait_w.delete(@stdin)
426
+ end
427
+ end
428
+
429
+ break if @status
430
+
431
+ # If there's nothing left to wait for, we're done!
432
+ break if wait_r.length == 0 && wait_w.length == 0
433
+ end
434
+ end
435
+
436
+ wait
437
+
438
+ [stdout, stderr]
439
+ end
440
+
441
+ # Does exactly what it says on the box.
442
+ #
443
+ # @param [String, Symbol, Fixnum] signal The signal to send to the child
444
+ # process. Accepts all the same arguments as Ruby's built-in
445
+ # {::Process::kill}, for instance a string like "INT" or "SIGINT", or a
446
+ # signal number like 2.
447
+ def send_signal(signal)
448
+ ::Process.kill(signal, pid)
449
+ end
450
+
451
+ # Sends `SIGTERM` to the process.
452
+ def terminate
453
+ send_signal("TERM")
454
+ end
455
+
456
+ private
457
+ # Return a pair of values (child, ext), which are how the given file
458
+ # descriptor should appear to the child and the external world. ext is only
459
+ # non-nil in the case of a pipe (in fact, we just return a list of length
460
+ # one, since ruby will unpack nils from missing list items).
461
+ def parse_fd(fd, mode)
462
+ ret = case fd
463
+ when PIPE
464
+ IO.pipe
465
+ when IO
466
+ [fd]
467
+ when Integer
468
+ [IO.new(fd, mode)]
469
+ when String
470
+ [File.open(fd, mode)]
471
+ when nil
472
+ []
473
+ else
474
+ raise ArgumentError
475
+ end
476
+
477
+ mode == 'r' ? ret : ret.reverse
478
+ end
479
+
480
+ def mark_fd_cloexec(fd)
481
+ io = IO.new(fd)
482
+ io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
483
+ io
484
+ rescue ArgumentError => e
485
+ # Ruby maintains a self-pipe for thread interrupts, but it handles closing
486
+ # it on forks/execs
487
+ raise unless e.message == "The given fd is not accessible because RubyVM reserves it"
488
+ end
489
+
490
+ @sigchld_mutex = Mutex.new
491
+ @sigchld_fds = {}
492
+ @sigchld_old_handler = nil
493
+
494
+ # Wake up everyone. We can't tell who we should wake up without `wait`ing,
495
+ # and we want to let the process itself do that. In practice, we're not
496
+ # likely to have that many in-flight subprocesses, so this is probably not a
497
+ # big deal.
498
+ def self.handle_sigchld
499
+ @sigchld_fds.values.each do |fd|
500
+ begin
501
+ fd.write_nonblock("\x00")
502
+ rescue Errno::EWOULDBLOCK, Errno::EAGAIN
503
+ end
504
+ end
505
+ end
506
+
507
+ def self.register_pid(pid, fd)
508
+ @sigchld_mutex.synchronize do
509
+ @sigchld_fds[pid] = fd
510
+ if @sigchld_fds.length == 1
511
+ @sigchld_old_handler = Signal.trap('SIGCHLD') {handle_sigchld}
512
+ end
513
+ end
514
+ end
515
+
516
+ def self.unregister_pid(pid)
517
+ @sigchld_mutex.synchronize do
518
+ if @sigchld_fds.length == 1
519
+ Signal.trap('SIGCHLD', @sigchld_old_handler || 'DEFAULT')
520
+ end
521
+ @sigchld_fds.delete(pid)
522
+ end
523
+ end
524
+
525
+ def self.catching_sigchld(pid, fd)
526
+ register_pid(pid, fd)
527
+ yield
528
+ ensure
529
+ unregister_pid(pid)
530
+ end
531
+ end
532
+ end