subprocess 1.3.2 → 1.5.3
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.
- checksums.yaml +5 -5
- data/lib/subprocess.rb +169 -58
- data/lib/subprocess/version.rb +1 -1
- metadata +3 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b4d97562256aef790c8c0a24420d8b6509375cc023809aaa833310d2e3c46442
|
4
|
+
data.tar.gz: 9b91371b9e3e9a2af35431f69c4d3a795f02697063675378676bdb2f8cd7a262
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7c757567b28d5e36cffd5ea13b705012ab790b445d757a1744a76d3eef375bfa6bc48259e0f0e744a351ee8455a79264a74dd9b6bcd6275c7fdd864b5b2ec82f
|
7
|
+
data.tar.gz: fd3dd1fdbda8bcf00a2a96a7855610d2c9ff7fb2a6ec1d8f24751df80723f050444dbccd8bd93d3090210e4c7c6be033a008d46e7af550a0181fff0146a0329d
|
data/lib/subprocess.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
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
|
352
|
-
#
|
353
|
-
#
|
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
|
-
# @
|
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
|
-
|
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
|
-
|
373
|
-
|
374
|
-
|
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
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
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
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
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
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
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.
|
494
|
-
@
|
495
|
-
|
496
|
-
|
497
|
-
|
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
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
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
|
data/lib/subprocess/version.rb
CHANGED
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
|
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:
|
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
|
-
|
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:
|