subprocess 1.3.2 → 1.5.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 28480023adb6bb4e7bff7096e766e6be65ff9421
4
- data.tar.gz: 3ad723848b5a45e44fad49d967c0787d1d24f108
2
+ SHA256:
3
+ metadata.gz: b4d97562256aef790c8c0a24420d8b6509375cc023809aaa833310d2e3c46442
4
+ data.tar.gz: 9b91371b9e3e9a2af35431f69c4d3a795f02697063675378676bdb2f8cd7a262
5
5
  SHA512:
6
- metadata.gz: 566d18db86adb9950509f92d4efa608475c9c9d9ccc174e11c06ee2fa128eadc6c8fbc22ab49404abed394c930a0750a4de5eb489dcc8761a72c4fb3b6a043e0
7
- data.tar.gz: f9c2d1307de9dfcf067eeda605182456872dc16ef0948348db488fd0103d080ea296cba486b9b5448c712fb188cb23e1feacb4ce8f08c7647b1be766205a02c0
6
+ metadata.gz: 7c757567b28d5e36cffd5ea13b705012ab790b445d757a1744a76d3eef375bfa6bc48259e0f0e744a351ee8455a79264a74dd9b6bcd6275c7fdd864b5b2ec82f
7
+ data.tar.gz: fd3dd1fdbda8bcf00a2a96a7855610d2c9ff7fb2a6ec1d8f24751df80723f050444dbccd8bd93d3090210e4c7c6be033a008d46e7af550a0181fff0146a0329d
@@ -1,4 +1,3 @@
1
- require 'fileutils'
2
1
  require 'thread'
3
2
  require 'set'
4
3
 
@@ -31,7 +30,7 @@ module Subprocess
31
30
  #
32
31
  # @return [::Process::Status] The exit status of the process
33
32
  #
34
- # @see {Process#initialize}
33
+ # @see Process#initialize
35
34
  def self.call(cmd, opts={}, &blk)
36
35
  Process.new(cmd, opts, &blk).wait
37
36
  end
@@ -63,7 +62,7 @@ module Subprocess
63
62
  # was terminated with an error or was killed by a signal)
64
63
  # @return [::Process::Status] The exit status of the process
65
64
  #
66
- # @see {Process#initialize}
65
+ # @see Process#initialize
67
66
  def self.check_call(cmd, opts={}, &blk)
68
67
  status = Process.new(cmd, opts, &blk).wait
69
68
  raise NonZeroExit.new(cmd, status) unless status.success?
@@ -80,7 +79,7 @@ module Subprocess
80
79
  # was terminated with an error or was killed by a signal)
81
80
  # @return [String] The contents of `stdout`
82
81
  #
83
- # @see {Process#initialize}
82
+ # @see Process#initialize
84
83
  def self.check_output(cmd, opts={}, &blk)
85
84
  opts[:stdout] = PIPE
86
85
  child = Process.new(cmd, opts, &blk)
@@ -162,6 +161,22 @@ module Subprocess
162
161
  end
163
162
  end
164
163
 
164
+ # Error class representing a timeout during a call to `communicate`
165
+ class CommunicateTimeout < StandardError
166
+ # @!attribute [r] stdout
167
+ # @return [String] Content read from stdout before the timeout
168
+ # @!attribute [r] stderr
169
+ # @return [String] Content read from stderr before the timeout
170
+ attr_reader :stdout, :stderr
171
+
172
+ def initialize(cmd, stdout, stderr)
173
+ @stdout = stdout
174
+ @stderr = stderr
175
+
176
+ super("Timeout communicating with `#{cmd.join(' ')}`")
177
+ end
178
+ end
179
+
165
180
  # A child process. The preferred way of spawning a subprocess is through the
166
181
  # functions on {Subprocess} (especially {Subprocess::check_call} and
167
182
  # {Subprocess::check_output}).
@@ -187,7 +202,7 @@ module Subprocess
187
202
  #
188
203
  # @param [Array<String>] cmd The command to run and its arguments (in the
189
204
  # style of an `argv` array). Unlike Python's subprocess module, `cmd`
190
- # cannnot be a String.
205
+ # cannot be a String.
191
206
  #
192
207
  # @option opts [IO, Fixnum, String, Subprocess::PIPE, nil] :stdin The `IO`,
193
208
  # file descriptor number, or file name to use for the process's standard
@@ -224,7 +239,7 @@ module Subprocess
224
239
  # in conjunction with {Subprocess::check_call}.
225
240
  # @yieldparam process [Process] The process that was just spawned.
226
241
  def initialize(cmd, opts={}, &blk)
227
- raise ArgumentError, "cmd must be an Array" unless Array === cmd
242
+ raise ArgumentError, "cmd must be an Array of strings" unless Array === cmd
228
243
  raise ArgumentError, "cmd cannot be empty" if cmd.empty?
229
244
 
230
245
  @command = cmd
@@ -244,8 +259,6 @@ module Subprocess
244
259
 
245
260
  @pid = fork do
246
261
  begin
247
- FileUtils.cd(opts[:cwd]) if opts[:cwd]
248
-
249
262
  ::STDIN.reopen(@child_stdin) if @child_stdin
250
263
  ::STDOUT.reopen(@child_stdout) if @child_stdout
251
264
  if opts[:stderr] == STDOUT
@@ -257,24 +270,44 @@ module Subprocess
257
270
  # Set up a new environment if we're requested to do so.
258
271
  if opts[:env]
259
272
  ENV.clear
260
- ENV.update(opts[:env])
273
+ begin
274
+ ENV.update(opts[:env])
275
+ rescue TypeError => e
276
+ raise ArgumentError, "`env` option must be a hash where all keys and values are strings (#{e})"
277
+ end
261
278
  end
262
279
 
263
280
  # Call the user back, maybe?
264
- opts[:preexec_fn].call if opts[:preexec_fn]
281
+ if opts[:preexec_fn]
282
+ if opts[:cwd]
283
+ Dir.chdir(opts[:cwd], &opts[:preexec_fn])
284
+ else
285
+ opts[:preexec_fn].call
286
+ end
287
+ end
265
288
 
266
289
  options = {close_others: true}.merge(opts.fetch(:exec_opts, {}))
267
290
  if opts[:retain_fds]
268
291
  retained_fds.each { |fd| options[fd] = fd }
269
292
  end
293
+ if opts[:cwd]
294
+ # We use the chdir option to `exec` since wrapping the
295
+ # `exec` in a Dir.chdir block caused these sporadic errors on macOS:
296
+ # Too many open files - getcwd (Errno::EMFILE)
297
+ options[:chdir] = opts[:cwd]
298
+ end
270
299
 
271
- # Ruby's Kernel#exec will call an exec(3) variant if called with two
272
- # or more arguments, but when called with just a single argument will
273
- # spawn a subshell with that argument as the command. Since we always
274
- # want to call exec(3), we use the third exec form, which passes a
275
- # [cmdname, argv0] array as its first argument and never invokes a
276
- # subshell.
277
- exec([cmd[0], cmd[0]], *cmd[1..-1], options)
300
+ begin
301
+ # Ruby's Kernel#exec will call an exec(3) variant if called with two
302
+ # or more arguments, but when called with just a single argument will
303
+ # spawn a subshell with that argument as the command. Since we always
304
+ # want to call exec(3), we use the third exec form, which passes a
305
+ # [cmdname, argv0] array as its first argument and never invokes a
306
+ # subshell.
307
+ exec([cmd[0], cmd[0]], *cmd[1..-1], options)
308
+ rescue TypeError => e
309
+ raise ArgumentError, "cmd must be an Array of strings (#{e})"
310
+ end
278
311
 
279
312
  rescue Exception => e
280
313
  # Dump all errors up to the parent through the control socket
@@ -337,7 +370,7 @@ module Subprocess
337
370
  # condition (`EOFError` or `EPIPE`).
338
371
  def drain_fd(fd, buf=nil)
339
372
  loop do
340
- tmp = fd.read_nonblock(4096)
373
+ tmp = fd.read_nonblock(4096).force_encoding(fd.external_encoding)
341
374
  buf << tmp unless buf.nil?
342
375
  end
343
376
  rescue EOFError, Errno::EPIPE
@@ -348,43 +381,48 @@ module Subprocess
348
381
  false
349
382
  end
350
383
 
351
- # Write the (optional) input to the process's `stdin`. Also, read (and
352
- # buffer in memory) the contents of `stdout` and `stderr`. Do this all using
353
- # `IO::select`, so we don't deadlock due to full pipe buffers.
384
+ # Write the (optional) input to the process's `stdin` and read the contents of
385
+ # `stdout` and `stderr`. If a block is provided, stdout and stderr are yielded as they
386
+ # are read. Otherwise they are buffered in memory and returned when the process
387
+ # exits. Do this all using `IO::select`, so we don't deadlock due to full pipe
388
+ # buffers.
354
389
  #
355
390
  # This is only really useful if you set some of `:stdin`, `:stdout`, and
356
391
  # `:stderr` to {Subprocess::PIPE}.
357
392
  #
358
393
  # @param [String] input A string to feed to the child's standard input.
359
- # @return [Array<String>] An array of two elements: the data read from the
394
+ # @param [Numeric] timeout_s Raise {Subprocess::CommunicateTimeout} if communicate
395
+ # does not finish after timeout_s seconds.
396
+ # @yieldparam [String] stdout Data read from stdout since the last yield
397
+ # @yieldparam [String] stderr Data read from stderr since the last yield
398
+ # @return [Array(String, String), nil] An array of two elements: the data read from the
360
399
  # child's standard output and standard error, respectively.
361
- def communicate(input=nil)
400
+ # Returns nil if a block is provided.
401
+ def communicate(input=nil, timeout_s=nil)
362
402
  raise ArgumentError if !input.nil? && @stdin.nil?
363
403
 
364
404
  stdout, stderr = "", ""
365
- stdout_encoding = @stdout.external_encoding if @stdout
366
- stderr_encoding = @stderr.external_encoding if @stderr
367
405
 
368
406
  input = input.dup unless input.nil?
369
407
 
370
408
  @stdin.close if (input.nil? || input.empty?) && !@stdin.nil?
371
409
 
372
- self_read, self_write = IO.pipe
373
- self.class.catching_sigchld(pid, self_write) do
374
- wait_r = [@stdout, @stderr, self_read].compact
410
+ timeout_at = Time.now + timeout_s if timeout_s
411
+
412
+ self.class.catching_sigchld(pid) do |global_read, self_read|
413
+ wait_r = [@stdout, @stderr, self_read, global_read].compact
375
414
  wait_w = [input && @stdin].compact
376
- loop do
377
- ready_r, ready_w = select(wait_r, wait_w)
378
-
379
- # If the child exits, we still have to be sure to read any data left
380
- # in the pipes. So we poll the child, drain all the pipes, and *then*
381
- # check @status.
382
- #
383
- # It's very important that we do not call poll between draining the
384
- # pipes and checking @status. If we did, we open a race condition
385
- # where the child writes to stdout and exits in that brief window,
386
- # causing us to lose that data.
387
- poll
415
+ done = false
416
+ while !done
417
+ # If the process has exited, we want to drain any remaining output before returning
418
+ if poll
419
+ ready_r = wait_r - [self_read, global_read]
420
+ ready_w = []
421
+ done = true
422
+ else
423
+ ready_r, ready_w = select_until(wait_r, wait_w, [], timeout_at)
424
+ raise CommunicateTimeout.new(@command, stdout, stderr) if ready_r.nil?
425
+ end
388
426
 
389
427
  if ready_r.include?(@stdout)
390
428
  if drain_fd(@stdout, stdout)
@@ -398,6 +436,13 @@ module Subprocess
398
436
  end
399
437
  end
400
438
 
439
+ if ready_r.include?(global_read)
440
+ if drain_fd(global_read)
441
+ raise "Unexpected internal error -- someone closed the global self-pipe!"
442
+ end
443
+ self.class.wakeup_sigchld
444
+ end
445
+
401
446
  if ready_r.include?(self_read)
402
447
  if drain_fd(self_read)
403
448
  raise "Unexpected internal error -- someone closed our self-pipe!"
@@ -405,10 +450,24 @@ module Subprocess
405
450
  end
406
451
 
407
452
  if ready_w.include?(@stdin)
453
+ written = 0
408
454
  begin
409
455
  written = @stdin.write_nonblock(input)
410
456
  rescue EOFError # Maybe I shouldn't catch this...
411
457
  rescue Errno::EINTR
458
+ rescue IO::WaitWritable
459
+ # On OS X, a pipe can raise EAGAIN even after select indicates
460
+ # that it is writable. Once the process consumes from the pipe,
461
+ # the next write should succeed and we should make forward progress.
462
+ # Until then, treat this as not writing any bytes and continue looping.
463
+ # For details see: https://github.com/stripe/subprocess/pull/22
464
+ nil
465
+ rescue Errno::EPIPE
466
+ # The other side of the pipe closed before we could
467
+ # write all of our input. This can happen if the
468
+ # process exits prematurely.
469
+ @stdin.close
470
+ wait_w.delete(@stdin)
412
471
  end
413
472
  input[0...written] = ''
414
473
  if input.empty?
@@ -417,19 +476,20 @@ module Subprocess
417
476
  end
418
477
  end
419
478
 
420
- break if @status
421
-
422
- # If there's nothing left to wait for, we're done!
423
- break if wait_r.length == 0 && wait_w.length == 0
479
+ if block_given? && !(stderr.empty? && stdout.empty?)
480
+ yield stdout, stderr
481
+ stdout, stderr = "", ""
482
+ end
424
483
  end
425
484
  end
426
485
 
427
486
  wait
428
487
 
429
- stdout.force_encoding(stdout_encoding) if stdout_encoding
430
- stderr.force_encoding(stderr_encoding) if stderr_encoding
431
-
432
- [stdout, stderr]
488
+ if block_given?
489
+ nil
490
+ else
491
+ [stdout, stderr]
492
+ end
433
493
  end
434
494
 
435
495
  # Does exactly what it says on the box.
@@ -482,19 +542,58 @@ module Subprocess
482
542
  end
483
543
  end
484
544
 
545
+ # Call IO.select timing out at Time `timeout_at`. If `timeout_at` is nil, never times out.
546
+ def select_until(read_array, write_array, err_array, timeout_at)
547
+ if !timeout_at
548
+ return IO.select(read_array, write_array, err_array)
549
+ end
550
+
551
+ remaining = (timeout_at - Time.now)
552
+ return nil if remaining <= 0
553
+
554
+ IO.select(read_array, write_array, err_array, remaining)
555
+ end
556
+
485
557
  @sigchld_mutex = Mutex.new
486
558
  @sigchld_fds = {}
487
559
  @sigchld_old_handler = nil
560
+ @sigchld_global_write = nil
561
+ @sigchld_global_read = nil
562
+ @sigchld_pipe_pid = nil
563
+
564
+ def self.handle_sigchld
565
+ # We'd like to just notify everything in `@sigchld_fds`, but
566
+ # ruby signal handlers are not executed atomically with respect
567
+ # to other Ruby threads, so reading it is racy. We can't grab
568
+ # `@sigchld_mutex`, because signal execution blocks the main
569
+ # thread, and so we'd deadlock if the main thread currently
570
+ # holds it.
571
+ #
572
+ # Instead, we keep a long-lived notify self-pipe that we select
573
+ # on inside `communicate`, and we task `communicate` with
574
+ # grabbing the lock and fanning out the wakeups.
575
+ begin
576
+ @sigchld_global_write.write_nonblock("\x00")
577
+ rescue Errno::EWOULDBLOCK, Errno::EAGAIN
578
+ nil # ignore
579
+ end
580
+ end
488
581
 
489
582
  # Wake up everyone. We can't tell who we should wake up without `wait`ing,
490
583
  # and we want to let the process itself do that. In practice, we're not
491
584
  # likely to have that many in-flight subprocesses, so this is probably not a
492
585
  # big deal.
493
- def self.handle_sigchld
494
- @sigchld_fds.values.each do |fd|
495
- begin
496
- fd.write_nonblock("\x00")
497
- rescue Errno::EWOULDBLOCK, Errno::EAGAIN
586
+ def self.wakeup_sigchld
587
+ @sigchld_mutex.synchronize do
588
+ @sigchld_fds.values.each do |fd|
589
+ begin
590
+ fd.write_nonblock("\x00")
591
+ rescue Errno::EWOULDBLOCK, Errno::EAGAIN
592
+ # If the pipe is full, the other end will be woken up
593
+ # regardless when it next reads, so it's fine to skip the
594
+ # write (the pipe is a wakeup channel, and doesn't contain
595
+ # meaningful data).
596
+ end
498
597
  end
499
598
  end
500
599
  end
@@ -503,6 +602,14 @@ module Subprocess
503
602
  @sigchld_mutex.synchronize do
504
603
  @sigchld_fds[pid] = fd
505
604
  if @sigchld_fds.length == 1
605
+ if @sigchld_global_write.nil? || @sigchld_pipe_pid != ::Process.pid
606
+ # Check the PID so that if we fork we will re-open the
607
+ # pipe. It's important that a fork parent and child don't
608
+ # share this pipe, because if they do they risk stealing
609
+ # each others' wakeups.
610
+ @sigchld_pipe_pid = ::Process.pid
611
+ @sigchld_global_read, @sigchld_global_write = IO.pipe
612
+ end
506
613
  @sigchld_old_handler = Signal.trap('SIGCHLD') {handle_sigchld}
507
614
  end
508
615
  end
@@ -517,11 +624,15 @@ module Subprocess
517
624
  end
518
625
  end
519
626
 
520
- def self.catching_sigchld(pid, fd)
521
- register_pid(pid, fd)
522
- yield
523
- ensure
524
- unregister_pid(pid)
627
+ def self.catching_sigchld(pid)
628
+ IO.pipe do |self_read, self_write|
629
+ begin
630
+ register_pid(pid, self_write)
631
+ yield @sigchld_global_read, self_read
632
+ ensure
633
+ unregister_pid(pid)
634
+ end
635
+ end
525
636
  end
526
637
  end
527
638
  end
@@ -1,3 +1,3 @@
1
1
  module Subprocess
2
- VERSION = '1.3.2'
2
+ VERSION = '1.5.3'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: subprocess
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carl Jackson
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2016-08-17 00:00:00.000000000 Z
15
+ date: 2020-04-06 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: minitest
@@ -89,10 +89,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
89
  - !ruby/object:Gem::Version
90
90
  version: '0'
91
91
  requirements: []
92
- rubyforge_project:
93
- rubygems_version: 2.2.2
92
+ rubygems_version: 3.1.2
94
93
  signing_key:
95
94
  specification_version: 4
96
95
  summary: A port of Python's subprocess module to Ruby
97
96
  test_files: []
98
- has_rdoc: