subprocess 0.1.6 → 0.15
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +72 -0
- data/lib/subprocess.rb +529 -20
- metadata +55 -121
- data/History.txt +0 -4
- data/Manifest.txt +0 -46
- data/PostInstall.txt +0 -7
- data/README.rdoc +0 -77
- data/Rakefile +0 -20
- data/TODO.rdoc +0 -1
- data/examples/simple.irb +0 -22
- data/examples/simple_timeout.irb +0 -22
- data/features/multiple_popens_sequence.feature +0 -23
- data/features/popen.feature +0 -45
- data/features/popen_over_ssh.feature +0 -44
- data/features/popen_over_ssh_without_blocking.feature +0 -16
- data/features/popen_remote_fails_with_invalid_auth_data.feature +0 -13
- data/features/popen_reports_runtime.feature +0 -11
- data/features/popen_running.feature +0 -11
- data/features/popen_with_timeout.feature +0 -19
- data/features/popen_without_blocking.feature +0 -16
- data/features/step_definitions/common_steps.rb +0 -168
- data/features/step_definitions/multiple_popens_sequence_steps.rb +0 -73
- data/features/step_definitions/popen_over_ssh_steps.rb +0 -29
- data/features/step_definitions/popen_over_ssh_without_blocking_steps.rb +0 -30
- data/features/step_definitions/popen_remote_fails_with_invalid_auth_dat_steps.rb +0 -19
- data/features/step_definitions/popen_reports_runtime_steps.rb +0 -13
- data/features/step_definitions/popen_running_steps.rb +0 -12
- data/features/step_definitions/popen_steps.rb +0 -34
- data/features/step_definitions/popen_with_timeout_steps.rb +0 -24
- data/features/step_definitions/popen_without_blocking_steps.rb +0 -33
- data/features/support/common.rb +0 -29
- data/features/support/env.rb +0 -15
- data/features/support/matchers.rb +0 -11
- data/lib/core_ext/hash.rb +0 -14
- data/lib/core_ext/process_status.rb +0 -14
- data/lib/subprocess/popen.rb +0 -188
- data/lib/subprocess/popen_factory.rb +0 -63
- data/lib/subprocess/popen_remote.rb +0 -64
- data/lib/subprocess/popen_sequence.rb +0 -57
- data/script/console +0 -10
- data/script/destroy +0 -14
- data/script/generate +0 -14
- data/spec/spec.opts +0 -1
- data/spec/spec_helper.rb +0 -10
- data/spec/subprocess/popen_spec.rb +0 -32
- data/spec/subprocess_spec.rb +0 -2
- data/subprocess.gemspec +0 -36
- 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
|
-
|
2
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|