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 +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:
|