subprocess 0.1.6 → 0.15

Sign up to get free protection for your applications and to get access to all the features.
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