subprocess 0.1.6 → 0.15
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.
- 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
|