posix-spawn 0.3.0
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/.gitignore +5 -0
- data/COPYING +28 -0
- data/Gemfile +2 -0
- data/HACKING +26 -0
- data/README.md +228 -0
- data/Rakefile +36 -0
- data/TODO +23 -0
- data/bin/posix-spawn-benchmark +117 -0
- data/ext/extconf.rb +6 -0
- data/ext/posix-spawn.c +406 -0
- data/lib/posix-spawn.rb +1 -0
- data/lib/posix/spawn.rb +472 -0
- data/lib/posix/spawn/child.rb +212 -0
- data/lib/posix/spawn/version.rb +5 -0
- data/posix-spawn.gemspec +24 -0
- data/test/test_backtick.rb +36 -0
- data/test/test_child.rb +107 -0
- data/test/test_popen.rb +18 -0
- data/test/test_spawn.rb +360 -0
- data/test/test_system.rb +29 -0
- metadata +89 -0
@@ -0,0 +1,212 @@
|
|
1
|
+
require 'posix/spawn'
|
2
|
+
|
3
|
+
module POSIX
|
4
|
+
module Spawn
|
5
|
+
# POSIX::Spawn::Child includes logic for executing child processes and
|
6
|
+
# reading/writing from their standard input, output, and error streams. It's
|
7
|
+
# designed to take all input in a single string and provides all output
|
8
|
+
# (stderr and stdout) as single strings and is therefore not well-suited
|
9
|
+
# to streaming large quantities of data in and out of commands.
|
10
|
+
#
|
11
|
+
# Create and run a process to completion:
|
12
|
+
#
|
13
|
+
# >> child = POSIX::Spawn::Child.new('git', '--help')
|
14
|
+
#
|
15
|
+
# Retrieve stdout or stderr output:
|
16
|
+
#
|
17
|
+
# >> child.out
|
18
|
+
# => "usage: git [--version] [--exec-path[=GIT_EXEC_PATH]]\n ..."
|
19
|
+
# >> child.err
|
20
|
+
# => ""
|
21
|
+
#
|
22
|
+
# Check process exit status information:
|
23
|
+
#
|
24
|
+
# >> child.status
|
25
|
+
# => #<Process::Status: pid=80718,exited(0)>
|
26
|
+
#
|
27
|
+
# To write data on the new process's stdin immediately after spawning:
|
28
|
+
#
|
29
|
+
# >> child = POSIX::Spawn::Child.new('bc', :input => '40 + 2')
|
30
|
+
# >> child.out
|
31
|
+
# "42\n"
|
32
|
+
#
|
33
|
+
# Q: Why use POSIX::Spawn::Child instead of popen3, hand rolled fork/exec
|
34
|
+
# code, or Process::spawn?
|
35
|
+
#
|
36
|
+
# - It's more efficient than popen3 and provides meaningful process
|
37
|
+
# hierarchies because it performs a single fork/exec. (popen3 double forks
|
38
|
+
# to avoid needing to collect the exit status and also calls
|
39
|
+
# Process::detach which creates a Ruby Thread!!!!).
|
40
|
+
#
|
41
|
+
# - It handles all max pipe buffer (PIPE_BUF) hang cases when reading and
|
42
|
+
# writing semi-large amounts of data. This is non-trivial to implement
|
43
|
+
# correctly and must be accounted for with popen3, spawn, or hand rolled
|
44
|
+
# fork/exec code.
|
45
|
+
#
|
46
|
+
# - It's more portable than hand rolled pipe, fork, exec code because
|
47
|
+
# fork(2) and exec aren't available on all platforms. In those cases,
|
48
|
+
# POSIX::Spawn::Child falls back to using whatever janky substitutes
|
49
|
+
# the platform provides.
|
50
|
+
class Child
|
51
|
+
include POSIX::Spawn
|
52
|
+
|
53
|
+
# Spawn a new process, write all input and read all output, and wait for
|
54
|
+
# the program to exit. Supports the standard spawn interface as described
|
55
|
+
# in the POSIX::Spawn module documentation:
|
56
|
+
#
|
57
|
+
# new([env], command, [argv1, ...], [options])
|
58
|
+
#
|
59
|
+
# The following options are supported in addition to the standard
|
60
|
+
# POSIX::Spawn options:
|
61
|
+
#
|
62
|
+
# :input => str Write str to the new process's standard input.
|
63
|
+
# :timeout => int Maximum number of seconds to allow the process
|
64
|
+
# to execute before aborting with a TimeoutExceeded
|
65
|
+
# exception.
|
66
|
+
# :max => total Maximum number of bytes of output to allow the
|
67
|
+
# process to generate before aborting with a
|
68
|
+
# MaximumOutputExceeded exception.
|
69
|
+
#
|
70
|
+
# Returns a new Child instance whose underlying process has already
|
71
|
+
# executed to completion. The out, err, and status attributes are
|
72
|
+
# immediately available.
|
73
|
+
def initialize(*args)
|
74
|
+
@env, @argv, options = extract_process_spawn_arguments(*args)
|
75
|
+
@options = options.dup
|
76
|
+
@input = @options.delete(:input)
|
77
|
+
@timeout = @options.delete(:timeout)
|
78
|
+
@max = @options.delete(:max)
|
79
|
+
@options.delete(:chdir) if @options[:chdir].nil?
|
80
|
+
exec!
|
81
|
+
end
|
82
|
+
|
83
|
+
# All data written to the child process's stdout stream as a String.
|
84
|
+
attr_reader :out
|
85
|
+
|
86
|
+
# All data written to the child process's stderr stream as a String.
|
87
|
+
attr_reader :err
|
88
|
+
|
89
|
+
# A Process::Status object with information on how the child exited.
|
90
|
+
attr_reader :status
|
91
|
+
|
92
|
+
# Total command execution time (wall-clock time)
|
93
|
+
attr_reader :runtime
|
94
|
+
|
95
|
+
# Determine if the process did exit with a zero exit status.
|
96
|
+
def success?
|
97
|
+
@status && @status.success?
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
# Execute command, write input, and read output. This is called
|
102
|
+
# immediately when a new instance of this object is initialized.
|
103
|
+
def exec!
|
104
|
+
# spawn the process and hook up the pipes
|
105
|
+
pid, stdin, stdout, stderr = popen4(@env, *(@argv + [@options]))
|
106
|
+
|
107
|
+
# async read from all streams into buffers
|
108
|
+
@out, @err = read_and_write(@input, stdin, stdout, stderr, @timeout, @max)
|
109
|
+
|
110
|
+
# grab exit status
|
111
|
+
@status = waitpid(pid)
|
112
|
+
rescue Object => boom
|
113
|
+
[stdin, stdout, stderr].each { |fd| fd.close rescue nil }
|
114
|
+
if @status.nil?
|
115
|
+
::Process.kill('TERM', pid) rescue nil
|
116
|
+
@status = waitpid(pid) rescue nil
|
117
|
+
end
|
118
|
+
raise
|
119
|
+
ensure
|
120
|
+
# let's be absolutely certain these are closed
|
121
|
+
[stdin, stdout, stderr].each { |fd| fd.close rescue nil }
|
122
|
+
end
|
123
|
+
|
124
|
+
# Maximum buffer size for reading
|
125
|
+
BUFSIZE = (32 * 1024)
|
126
|
+
|
127
|
+
# Start a select loop writing any input on the child's stdin and reading
|
128
|
+
# any output from the child's stdout or stderr.
|
129
|
+
#
|
130
|
+
# input - String input to write on stdin. May be nil.
|
131
|
+
# stdin - The write side IO object for the child's stdin stream.
|
132
|
+
# stdout - The read side IO object for the child's stdout stream.
|
133
|
+
# stderr - The read side IO object for the child's stderr stream.
|
134
|
+
# timeout - An optional Numeric specifying the total number of seconds
|
135
|
+
# the read/write operations should occur for.
|
136
|
+
#
|
137
|
+
# Returns an [out, err] tuple where both elements are strings with all
|
138
|
+
# data written to the stdout and stderr streams, respectively.
|
139
|
+
# Raises TimeoutExceeded when all data has not been read / written within
|
140
|
+
# the duration specified in the timeout argument.
|
141
|
+
# Raises MaximumOutputExceeded when the total number of bytes output
|
142
|
+
# exceeds the amount specified by the max argument.
|
143
|
+
def read_and_write(input, stdin, stdout, stderr, timeout=nil, max=nil)
|
144
|
+
input ||= ''
|
145
|
+
max = nil if max && max <= 0
|
146
|
+
out, err = '', ''
|
147
|
+
offset = 0
|
148
|
+
|
149
|
+
timeout = nil if timeout && timeout <= 0.0
|
150
|
+
@runtime = 0.0
|
151
|
+
start = Time.now
|
152
|
+
|
153
|
+
writers = [stdin]
|
154
|
+
readers = [stdout, stderr]
|
155
|
+
t = timeout
|
156
|
+
while readers.any? || writers.any?
|
157
|
+
ready = IO.select(readers, writers, readers + writers, t)
|
158
|
+
raise TimeoutExceeded if ready.nil?
|
159
|
+
|
160
|
+
# write to stdin stream
|
161
|
+
ready[1].each do |fd|
|
162
|
+
begin
|
163
|
+
boom = nil
|
164
|
+
size = fd.write_nonblock(input)
|
165
|
+
input = input[size, input.size]
|
166
|
+
rescue Errno::EPIPE => boom
|
167
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
168
|
+
end
|
169
|
+
if boom || input.size == 0
|
170
|
+
stdin.close
|
171
|
+
writers.delete(stdin)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# read from stdout and stderr streams
|
176
|
+
ready[0].each do |fd|
|
177
|
+
buf = (fd == stdout) ? out : err
|
178
|
+
begin
|
179
|
+
buf << fd.readpartial(BUFSIZE)
|
180
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
181
|
+
rescue EOFError
|
182
|
+
readers.delete(fd)
|
183
|
+
fd.close
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# keep tabs on the total amount of time we've spent here
|
188
|
+
@runtime = Time.now - start
|
189
|
+
if timeout
|
190
|
+
t = timeout - @runtime
|
191
|
+
raise TimeoutExceeded if t < 0.0
|
192
|
+
end
|
193
|
+
|
194
|
+
# maybe we've hit our max output
|
195
|
+
if max && ready[0].any? && (out.size + err.size) > max
|
196
|
+
raise MaximumOutputExceeded
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
[out, err]
|
201
|
+
end
|
202
|
+
|
203
|
+
# Wait for the child process to exit
|
204
|
+
#
|
205
|
+
# Returns the Process::Status object obtained by reaping the process.
|
206
|
+
def waitpid(pid)
|
207
|
+
::Process::waitpid(pid)
|
208
|
+
$?
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
data/posix-spawn.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.expand_path('../lib/posix/spawn/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'posix-spawn'
|
5
|
+
s.version = POSIX::Spawn::VERSION
|
6
|
+
|
7
|
+
s.summary = 'posix_spawnp(2) for ruby'
|
8
|
+
s.description = 'posix-spawn uses posix_spawnp(2) for faster process spawning'
|
9
|
+
|
10
|
+
s.homepage = 'http://github.com/rtomayko/posix-spawn'
|
11
|
+
s.has_rdoc = false
|
12
|
+
|
13
|
+
s.authors = ['Ryan Tomayko', 'Aman Gupta']
|
14
|
+
s.email = ['r@tomayko.com', 'aman@tmm1.net']
|
15
|
+
|
16
|
+
s.add_development_dependency 'rake-compiler', '0.7.6'
|
17
|
+
|
18
|
+
s.extensions = ['ext/extconf.rb']
|
19
|
+
s.executables << 'posix-spawn-benchmark'
|
20
|
+
s.require_paths = ['lib']
|
21
|
+
|
22
|
+
s.files = `git ls-files`.split("\n")
|
23
|
+
s.extra_rdoc_files = %w[ COPYING HACKING ]
|
24
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'posix-spawn'
|
3
|
+
|
4
|
+
class BacktickTest < Test::Unit::TestCase
|
5
|
+
include POSIX::Spawn
|
6
|
+
|
7
|
+
def test_backtick_simple
|
8
|
+
out = `exit`
|
9
|
+
assert_equal out, ''
|
10
|
+
assert_equal $?.exitstatus, 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_backtick_output
|
14
|
+
out = `echo 123`
|
15
|
+
assert_equal out, "123\n"
|
16
|
+
assert_equal $?.exitstatus, 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_backtick_failure
|
20
|
+
out = `nosuchcmd 2> /dev/null`
|
21
|
+
assert_equal out, ''
|
22
|
+
assert_equal $?.exitstatus, 127
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_backtick_redirect
|
26
|
+
out = `nosuchcmd 2>&1`
|
27
|
+
assert_equal out, "/bin/sh: nosuchcmd: command not found\n"
|
28
|
+
assert_equal $?.exitstatus, 127
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_backtick_huge
|
32
|
+
out = `yes | head -50000`
|
33
|
+
assert_equal out.size, 100000
|
34
|
+
assert_equal $?.exitstatus, 0
|
35
|
+
end
|
36
|
+
end
|
data/test/test_child.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'posix-spawn'
|
3
|
+
|
4
|
+
class ChildTest < Test::Unit::TestCase
|
5
|
+
include POSIX::Spawn
|
6
|
+
|
7
|
+
def test_sanity
|
8
|
+
assert_same POSIX::Spawn::Child, Child
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_argv_array_execs
|
12
|
+
p = Child.new('printf', '%s %s %s', '1', '2', '3 4')
|
13
|
+
assert p.success?
|
14
|
+
assert_equal "1 2 3 4", p.out
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_argv_string_uses_sh
|
18
|
+
p = Child.new("echo via /bin/sh")
|
19
|
+
assert p.success?
|
20
|
+
assert_equal "via /bin/sh\n", p.out
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_stdout
|
24
|
+
p = Child.new('echo', 'boom')
|
25
|
+
assert_equal "boom\n", p.out
|
26
|
+
assert_equal "", p.err
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_stderr
|
30
|
+
p = Child.new('echo boom 1>&2')
|
31
|
+
assert_equal "", p.out
|
32
|
+
assert_equal "boom\n", p.err
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_status
|
36
|
+
p = Child.new('exit 3')
|
37
|
+
assert !p.status.success?
|
38
|
+
assert_equal 3, p.status.exitstatus
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_env
|
42
|
+
p = Child.new({ 'FOO' => 'BOOYAH' }, 'echo $FOO')
|
43
|
+
assert_equal "BOOYAH\n", p.out
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_chdir
|
47
|
+
p = Child.new("pwd", :chdir => File.dirname(Dir.pwd))
|
48
|
+
assert_equal File.dirname(Dir.pwd) + "\n", p.out
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_input
|
52
|
+
input = "HEY NOW\n" * 100_000 # 800K
|
53
|
+
p = Child.new('wc', '-l', :input => input)
|
54
|
+
assert_equal 100_000, p.out.strip.to_i
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_max
|
58
|
+
assert_raise MaximumOutputExceeded do
|
59
|
+
Child.new('yes', :max => 100_000)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_max_with_child_hierarchy
|
64
|
+
assert_raise MaximumOutputExceeded do
|
65
|
+
Child.new('/bin/sh', '-c', 'yes', :max => 100_000)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_max_with_stubborn_child
|
70
|
+
assert_raise MaximumOutputExceeded do
|
71
|
+
Child.new("trap '' TERM; yes", :max => 100_000)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_timeout
|
76
|
+
assert_raise TimeoutExceeded do
|
77
|
+
Child.new('sleep', '1', :timeout => 0.05)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_timeout_with_child_hierarchy
|
82
|
+
assert_raise TimeoutExceeded do
|
83
|
+
Child.new('/bin/sh', '-c', 'sleep 1', :timeout => 0.05)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_lots_of_input_and_lots_of_output_at_the_same_time
|
88
|
+
input = "stuff on stdin \n" * 1_000
|
89
|
+
command = "
|
90
|
+
while read line
|
91
|
+
do
|
92
|
+
echo stuff on stdout;
|
93
|
+
echo stuff on stderr 1>&2;
|
94
|
+
done
|
95
|
+
"
|
96
|
+
p = Child.new(command, :input => input)
|
97
|
+
assert_equal input.size, p.out.size
|
98
|
+
assert_equal input.size, p.err.size
|
99
|
+
assert p.success?
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_input_cannot_be_written_due_to_broken_pipe
|
103
|
+
input = "1" * 100_000
|
104
|
+
p = Child.new('false', :input => input)
|
105
|
+
assert !p.success?
|
106
|
+
end
|
107
|
+
end
|
data/test/test_popen.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'posix-spawn'
|
3
|
+
|
4
|
+
class PopenTest < Test::Unit::TestCase
|
5
|
+
include POSIX::Spawn
|
6
|
+
|
7
|
+
def test_popen4
|
8
|
+
pid, i, o, e = popen4("cat")
|
9
|
+
i.write "hello world"
|
10
|
+
i.close
|
11
|
+
::Process.wait(pid)
|
12
|
+
|
13
|
+
assert_equal o.read, "hello world"
|
14
|
+
assert_equal $?.exitstatus, 0
|
15
|
+
ensure
|
16
|
+
[i, o, e].each{ |io| io.close rescue nil }
|
17
|
+
end
|
18
|
+
end
|
data/test/test_spawn.rb
ADDED
@@ -0,0 +1,360 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'posix-spawn'
|
3
|
+
|
4
|
+
module SpawnImplementationTests
|
5
|
+
def test_spawn_simple
|
6
|
+
pid = _spawn('true')
|
7
|
+
assert_process_exit_ok pid
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_spawn_with_args
|
11
|
+
pid = _spawn('true', 'with', 'some stuff')
|
12
|
+
assert_process_exit_ok pid
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_spawn_with_shell
|
16
|
+
pid = _spawn('true && exit 13')
|
17
|
+
assert_process_exit_status pid, 13
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_spawn_with_cmdname_and_argv0_tuple
|
21
|
+
pid = _spawn(['true', 'not-true'], 'some', 'args', 'toooo')
|
22
|
+
assert_process_exit_ok pid
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_spawn_with_invalid_argv
|
26
|
+
assert_raise ArgumentError do
|
27
|
+
_spawn(['echo','b','c','d'])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Environ
|
33
|
+
|
34
|
+
def test_spawn_inherit_env
|
35
|
+
ENV['PSPAWN'] = 'parent'
|
36
|
+
pid = _spawn('test "$PSPAWN" = "parent"')
|
37
|
+
assert_process_exit_ok pid
|
38
|
+
ensure
|
39
|
+
ENV.delete('PSPAWN')
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_spawn_clean_env
|
43
|
+
ENV['PSPAWN'] = 'parent'
|
44
|
+
pid = _spawn({'TEMP'=>'child'}, 'test -z "$PSPAWN" && test "$TEMP" = "child"', :unsetenv_others => true)
|
45
|
+
assert_process_exit_ok pid
|
46
|
+
ensure
|
47
|
+
ENV.delete('PSPAWN')
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_spawn_set_env
|
51
|
+
ENV['PSPAWN'] = 'parent'
|
52
|
+
pid = _spawn({'PSPAWN'=>'child'}, 'test "$PSPAWN" = "child"')
|
53
|
+
assert_process_exit_ok pid
|
54
|
+
ensure
|
55
|
+
ENV.delete('PSPAWN')
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_spawn_unset_env
|
59
|
+
ENV['PSPAWN'] = 'parent'
|
60
|
+
pid = _spawn({'PSPAWN'=>nil}, 'test -z "$PSPAWN"')
|
61
|
+
assert_process_exit_ok pid
|
62
|
+
ensure
|
63
|
+
ENV.delete('PSPAWN')
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# FD => :close options
|
68
|
+
|
69
|
+
def test_sanity_of_checking_clone_with_sh
|
70
|
+
rd, wr = IO.pipe
|
71
|
+
pid = _spawn("exec 2>/dev/null 100<&#{rd.to_i} || exit 1", rd => rd)
|
72
|
+
assert_process_exit_status pid, 0
|
73
|
+
ensure
|
74
|
+
[rd, wr].each { |fd| fd.close rescue nil }
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_spawn_close_option_with_symbolic_standard_stream_names
|
78
|
+
pid = _spawn('exec 2>/dev/null 100<&0 || exit 1', :in => :close)
|
79
|
+
assert_process_exit_status pid, 1
|
80
|
+
|
81
|
+
pid = _spawn('exec 2>/dev/null 101>&1 102>&2 || exit 1',
|
82
|
+
:out => :close, :err => :close)
|
83
|
+
assert_process_exit_status pid, 1
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_spawn_close_on_standard_stream_io_object
|
87
|
+
pid = _spawn('exec 2>/dev/null 100<&0 || exit 1', STDIN => :close)
|
88
|
+
assert_process_exit_status pid, 1
|
89
|
+
|
90
|
+
pid = _spawn('exec 2>/dev/null 101>&1 102>&2 || exit 1',
|
91
|
+
STDOUT => :close, STDOUT => :close)
|
92
|
+
assert_process_exit_status pid, 1
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_spawn_close_option_with_fd_number
|
96
|
+
rd, wr = IO.pipe
|
97
|
+
pid = _spawn("exec 2>/dev/null 100<&#{rd.to_i} || exit 1", rd.to_i => :close)
|
98
|
+
assert_process_exit_status pid, 1
|
99
|
+
|
100
|
+
assert !rd.closed?
|
101
|
+
assert !wr.closed?
|
102
|
+
ensure
|
103
|
+
[rd, wr].each { |fd| fd.close rescue nil }
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_spawn_close_option_with_io_object
|
107
|
+
rd, wr = IO.pipe
|
108
|
+
pid = _spawn("exec 2>/dev/null 100<&#{rd.to_i} || exit 1", rd => :close)
|
109
|
+
assert_process_exit_status pid, 1
|
110
|
+
|
111
|
+
assert !rd.closed?
|
112
|
+
assert !wr.closed?
|
113
|
+
ensure
|
114
|
+
[rd, wr].each { |fd| fd.close rescue nil }
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_spawn_close_invalid_fd_raises_exception
|
118
|
+
pid = _spawn("echo", "hiya", 250 => :close)
|
119
|
+
assert_process_exit_status pid, 127
|
120
|
+
rescue Errno::EBADF
|
121
|
+
# this happens on darwin only. GNU does spawn and exits 127.
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_spawn_closing_multiple_fds_with_array_keys
|
125
|
+
rd, wr = IO.pipe
|
126
|
+
pid = _spawn("exec 2>/dev/null 101>&#{wr.to_i} || exit 1", [rd, wr, :out] => :close)
|
127
|
+
assert_process_exit_status pid, 1
|
128
|
+
ensure
|
129
|
+
[rd, wr].each { |fd| fd.close rescue nil }
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# FD => FD options
|
134
|
+
|
135
|
+
def test_spawn_redirect_fds_with_symbolic_names_and_io_objects
|
136
|
+
rd, wr = IO.pipe
|
137
|
+
pid = _spawn("echo", "hello world", :out => wr, rd => :close)
|
138
|
+
wr.close
|
139
|
+
output = rd.read
|
140
|
+
assert_process_exit_ok pid
|
141
|
+
assert_equal "hello world\n", output
|
142
|
+
ensure
|
143
|
+
[rd, wr].each { |fd| fd.close rescue nil }
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_spawn_redirect_fds_with_fd_numbers
|
147
|
+
rd, wr = IO.pipe
|
148
|
+
pid = _spawn("echo", "hello world", 1 => wr.fileno, rd.fileno => :close)
|
149
|
+
wr.close
|
150
|
+
output = rd.read
|
151
|
+
assert_process_exit_ok pid
|
152
|
+
assert_equal "hello world\n", output
|
153
|
+
ensure
|
154
|
+
[rd, wr].each { |fd| fd.close rescue nil }
|
155
|
+
end
|
156
|
+
|
157
|
+
def test_spawn_redirect_invalid_fds_raises_exception
|
158
|
+
pid = _spawn("echo", "hiya", 250 => 3)
|
159
|
+
assert_process_exit_status pid, 127
|
160
|
+
rescue Errno::EBADF
|
161
|
+
# this happens on darwin only. GNU does spawn and exits 127.
|
162
|
+
end
|
163
|
+
|
164
|
+
def test_spawn_redirect_stderr_and_stdout_to_same_fd
|
165
|
+
rd, wr = IO.pipe
|
166
|
+
pid = _spawn("echo hello world 1>&2", :err => wr, :out => wr, rd => :close)
|
167
|
+
wr.close
|
168
|
+
output = rd.read
|
169
|
+
assert_process_exit_ok pid
|
170
|
+
assert_equal "hello world\n", output
|
171
|
+
ensure
|
172
|
+
[rd, wr].each { |fd| fd.close rescue nil }
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_spawn_does_not_close_fd_when_redirecting
|
176
|
+
pid = _spawn("exec 2>&1", :err => :out)
|
177
|
+
assert_process_exit_ok pid
|
178
|
+
end
|
179
|
+
|
180
|
+
# Ruby 1.9 Process::spawn closes all fds by default. To keep an fd open, you
|
181
|
+
# have to pass it explicitly as fd => fd.
|
182
|
+
def test_explicitly_passing_an_fd_as_open
|
183
|
+
rd, wr = IO.pipe
|
184
|
+
pid = _spawn("exec 101>&#{wr.to_i} || exit 1", wr => wr)
|
185
|
+
assert_process_exit_ok pid
|
186
|
+
ensure
|
187
|
+
[rd, wr].each { |fd| fd.close rescue nil }
|
188
|
+
end
|
189
|
+
|
190
|
+
##
|
191
|
+
# FD => file options
|
192
|
+
|
193
|
+
def test_spawn_redirect_fd_to_file_with_symbolic_name
|
194
|
+
file = File.expand_path('../test-output', __FILE__)
|
195
|
+
text = 'redirect_fd_to_file_with_symbolic_name'
|
196
|
+
pid = _spawn('echo', text, :out => file)
|
197
|
+
assert_process_exit_ok pid
|
198
|
+
assert File.exist?(file)
|
199
|
+
assert_equal "#{text}\n", File.read(file)
|
200
|
+
ensure
|
201
|
+
File.unlink(file) rescue nil
|
202
|
+
end
|
203
|
+
|
204
|
+
def test_spawn_redirect_fd_to_file_with_fd_number
|
205
|
+
file = File.expand_path('../test-output', __FILE__)
|
206
|
+
text = 'redirect_fd_to_file_with_fd_number'
|
207
|
+
pid = _spawn('echo', text, 1 => file)
|
208
|
+
assert_process_exit_ok pid
|
209
|
+
assert File.exist?(file)
|
210
|
+
assert_equal "#{text}\n", File.read(file)
|
211
|
+
ensure
|
212
|
+
File.unlink(file) rescue nil
|
213
|
+
end
|
214
|
+
|
215
|
+
def test_spawn_redirect_fd_to_file_with_io_object
|
216
|
+
file = File.expand_path('../test-output', __FILE__)
|
217
|
+
text = 'redirect_fd_to_file_with_io_object'
|
218
|
+
pid = _spawn('echo', text, STDOUT => file)
|
219
|
+
assert_process_exit_ok pid
|
220
|
+
assert File.exist?(file)
|
221
|
+
assert_equal "#{text}\n", File.read(file)
|
222
|
+
ensure
|
223
|
+
File.unlink(file) rescue nil
|
224
|
+
end
|
225
|
+
|
226
|
+
def test_spawn_redirect_fd_from_file_with_symbolic_name
|
227
|
+
file = File.expand_path('../test-input', __FILE__)
|
228
|
+
text = 'redirect_fd_from_file_with_symbolic_name'
|
229
|
+
File.open(file, 'w') { |fd| fd.write(text) }
|
230
|
+
|
231
|
+
pid = _spawn(%Q{test "$(cat)" = "#{text}"}, :in => file)
|
232
|
+
assert_process_exit_ok pid
|
233
|
+
ensure
|
234
|
+
File.unlink(file) rescue nil
|
235
|
+
end
|
236
|
+
|
237
|
+
def test_spawn_redirect_fd_from_file_with_fd_number
|
238
|
+
file = File.expand_path('../test-input', __FILE__)
|
239
|
+
text = 'redirect_fd_from_file_with_fd_number'
|
240
|
+
File.open(file, 'w') { |fd| fd.write(text) }
|
241
|
+
|
242
|
+
pid = _spawn(%Q{test "$(cat)" = "#{text}"}, 0 => file)
|
243
|
+
assert_process_exit_ok pid
|
244
|
+
ensure
|
245
|
+
File.unlink(file) rescue nil
|
246
|
+
end
|
247
|
+
|
248
|
+
def test_spawn_redirect_fd_from_file_with_io_object
|
249
|
+
file = File.expand_path('../test-input', __FILE__)
|
250
|
+
text = 'redirect_fd_from_file_with_io_object'
|
251
|
+
File.open(file, 'w') { |fd| fd.write(text) }
|
252
|
+
|
253
|
+
pid = _spawn(%Q{test "$(cat)" = "#{text}"}, STDIN => file)
|
254
|
+
assert_process_exit_ok pid
|
255
|
+
ensure
|
256
|
+
File.unlink(file) rescue nil
|
257
|
+
end
|
258
|
+
|
259
|
+
def test_spawn_redirect_fd_to_file_with_symbolic_name_and_flags
|
260
|
+
file = File.expand_path('../test-output', __FILE__)
|
261
|
+
text = 'redirect_fd_to_file_with_symbolic_name'
|
262
|
+
5.times do
|
263
|
+
pid = _spawn('echo', text, :out => [file, 'a'])
|
264
|
+
assert_process_exit_ok pid
|
265
|
+
end
|
266
|
+
assert File.exist?(file)
|
267
|
+
assert_equal "#{text}\n" * 5, File.read(file)
|
268
|
+
ensure
|
269
|
+
File.unlink(file) rescue nil
|
270
|
+
end
|
271
|
+
|
272
|
+
##
|
273
|
+
# Exceptions
|
274
|
+
|
275
|
+
def test_spawn_raises_exception_on_unsupported_options
|
276
|
+
exception = nil
|
277
|
+
|
278
|
+
assert_raise ArgumentError do
|
279
|
+
begin
|
280
|
+
_spawn('echo howdy', :out => '/dev/null', :oops => 'blaahh')
|
281
|
+
rescue Exception => e
|
282
|
+
exception = e
|
283
|
+
raise e
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
assert_match /oops/, exception.message
|
288
|
+
end
|
289
|
+
|
290
|
+
##
|
291
|
+
# Assertion Helpers
|
292
|
+
|
293
|
+
def assert_process_exit_ok(pid)
|
294
|
+
assert_process_exit_status pid, 0
|
295
|
+
end
|
296
|
+
|
297
|
+
def assert_process_exit_status(pid, status)
|
298
|
+
assert pid.to_i > 0, "pid [#{pid}] should be > 0"
|
299
|
+
chpid = ::Process.wait(pid)
|
300
|
+
assert_equal chpid, pid
|
301
|
+
assert_equal status, $?.exitstatus
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
class SpawnTest < Test::Unit::TestCase
|
306
|
+
include POSIX::Spawn
|
307
|
+
|
308
|
+
def test_spawn_methods_exposed_at_module_level
|
309
|
+
assert POSIX::Spawn.respond_to?(:pspawn)
|
310
|
+
assert POSIX::Spawn.respond_to?(:_pspawn)
|
311
|
+
end
|
312
|
+
|
313
|
+
##
|
314
|
+
# Options Preprocessing
|
315
|
+
|
316
|
+
def test_extract_process_spawn_arguments_with_options
|
317
|
+
assert_equal [{}, [['echo', 'echo'], 'hello', 'world'], {:err => :close}],
|
318
|
+
extract_process_spawn_arguments('echo', 'hello', 'world', :err => :close)
|
319
|
+
end
|
320
|
+
|
321
|
+
def test_extract_process_spawn_arguments_with_options_and_env
|
322
|
+
options = {:err => :close}
|
323
|
+
env = {'X' => 'Y'}
|
324
|
+
assert_equal [env, [['echo', 'echo'], 'hello world'], options],
|
325
|
+
extract_process_spawn_arguments(env, 'echo', 'hello world', options)
|
326
|
+
end
|
327
|
+
|
328
|
+
def test_extract_process_spawn_arguments_with_shell_command
|
329
|
+
assert_equal [{}, [['/bin/sh', '/bin/sh'], '-c', 'echo hello world'], {}],
|
330
|
+
extract_process_spawn_arguments('echo hello world')
|
331
|
+
end
|
332
|
+
|
333
|
+
def test_extract_process_spawn_arguments_with_special_cmdname_argv_tuple
|
334
|
+
assert_equal [{}, [['echo', 'fuuu'], 'hello world'], {}],
|
335
|
+
extract_process_spawn_arguments(['echo', 'fuuu'], 'hello world')
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
class PosixSpawnTest < Test::Unit::TestCase
|
340
|
+
include SpawnImplementationTests
|
341
|
+
def _spawn(*argv)
|
342
|
+
POSIX::Spawn.pspawn(*argv)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
class ForkSpawnTest < Test::Unit::TestCase
|
347
|
+
include SpawnImplementationTests
|
348
|
+
def _spawn(*argv)
|
349
|
+
POSIX::Spawn.fspawn(*argv)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
if ::Process::respond_to?(:spawn)
|
354
|
+
class NativeSpawnTest < Test::Unit::TestCase
|
355
|
+
include SpawnImplementationTests
|
356
|
+
def _spawn(*argv)
|
357
|
+
::Process.spawn(*argv)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|