posix-spawn 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|