childprocess 0.8.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.
- checksums.yaml +7 -0
- data/.document +6 -0
- data/.gitignore +28 -0
- data/.rspec +1 -0
- data/.travis.yml +36 -0
- data/CHANGELOG.md +44 -0
- data/Gemfile +15 -0
- data/LICENSE +20 -0
- data/README.md +192 -0
- data/Rakefile +61 -0
- data/appveyor.yml +43 -0
- data/childprocess.gemspec +30 -0
- data/lib/childprocess.rb +205 -0
- data/lib/childprocess/abstract_io.rb +36 -0
- data/lib/childprocess/abstract_process.rb +192 -0
- data/lib/childprocess/errors.rb +26 -0
- data/lib/childprocess/jruby.rb +56 -0
- data/lib/childprocess/jruby/io.rb +16 -0
- data/lib/childprocess/jruby/process.rb +159 -0
- data/lib/childprocess/jruby/pump.rb +53 -0
- data/lib/childprocess/tools/generator.rb +146 -0
- data/lib/childprocess/unix.rb +9 -0
- data/lib/childprocess/unix/fork_exec_process.rb +70 -0
- data/lib/childprocess/unix/io.rb +21 -0
- data/lib/childprocess/unix/lib.rb +186 -0
- data/lib/childprocess/unix/platform/i386-linux.rb +12 -0
- data/lib/childprocess/unix/platform/i386-solaris.rb +11 -0
- data/lib/childprocess/unix/platform/x86_64-linux.rb +12 -0
- data/lib/childprocess/unix/platform/x86_64-macosx.rb +11 -0
- data/lib/childprocess/unix/posix_spawn_process.rb +134 -0
- data/lib/childprocess/unix/process.rb +89 -0
- data/lib/childprocess/version.rb +3 -0
- data/lib/childprocess/windows.rb +33 -0
- data/lib/childprocess/windows/handle.rb +91 -0
- data/lib/childprocess/windows/io.rb +25 -0
- data/lib/childprocess/windows/lib.rb +416 -0
- data/lib/childprocess/windows/process.rb +130 -0
- data/lib/childprocess/windows/process_builder.rb +175 -0
- data/lib/childprocess/windows/structs.rb +149 -0
- data/spec/abstract_io_spec.rb +12 -0
- data/spec/childprocess_spec.rb +391 -0
- data/spec/io_spec.rb +228 -0
- data/spec/jruby_spec.rb +24 -0
- data/spec/pid_behavior.rb +12 -0
- data/spec/platform_detection_spec.rb +86 -0
- data/spec/spec_helper.rb +261 -0
- data/spec/unix_spec.rb +57 -0
- data/spec/windows_spec.rb +23 -0
- metadata +179 -0
data/spec/jruby_spec.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
require "pid_behavior"
|
3
|
+
|
4
|
+
if ChildProcess.jruby? && !ChildProcess.windows?
|
5
|
+
describe ChildProcess::JRuby::IO do
|
6
|
+
let(:io) { ChildProcess::JRuby::IO.new }
|
7
|
+
|
8
|
+
it "raises an ArgumentError if given IO does not respond to :to_outputstream" do
|
9
|
+
expect { io.stdout = nil }.to raise_error(ArgumentError)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe ChildProcess::JRuby::Process do
|
14
|
+
if ChildProcess.unix?
|
15
|
+
it_behaves_like "a platform that provides the child's pid"
|
16
|
+
else
|
17
|
+
it "raises an error when trying to access the child's pid" do
|
18
|
+
process = exit_with(0)
|
19
|
+
process.start
|
20
|
+
expect { process.pid }.to raise_error(NotImplementedError)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
shared_examples_for "a platform that provides the child's pid" do
|
4
|
+
it "knows the child's pid" do
|
5
|
+
Tempfile.open("pid-spec") do |file|
|
6
|
+
process = write_pid(file.path).start
|
7
|
+
process.wait
|
8
|
+
|
9
|
+
expect(process.pid).to eq rewind_and_read(file).chomp.to_i
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
# Q: Should platform detection concern be extracted from ChildProcess?
|
4
|
+
describe ChildProcess do
|
5
|
+
|
6
|
+
describe ".arch" do
|
7
|
+
subject { described_class.arch }
|
8
|
+
|
9
|
+
before(:each) { described_class.instance_variable_set(:@arch, nil) }
|
10
|
+
|
11
|
+
after(:each) { described_class.instance_variable_set(:@arch, nil) }
|
12
|
+
|
13
|
+
shared_examples 'expected_arch_for_host_cpu' do |host_cpu, expected_arch|
|
14
|
+
context "when host_cpu is '#{host_cpu}'" do
|
15
|
+
before :each do
|
16
|
+
allow(RbConfig::CONFIG).
|
17
|
+
to receive(:[]).
|
18
|
+
with('host_cpu').
|
19
|
+
and_return(expected_arch)
|
20
|
+
end
|
21
|
+
|
22
|
+
it { is_expected.to eq expected_arch }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Normal cases: not macosx - depends only on host_cpu
|
27
|
+
context "when os is *not* 'macosx'" do
|
28
|
+
before :each do
|
29
|
+
allow(described_class).to receive(:os).and_return(:not_macosx)
|
30
|
+
end
|
31
|
+
|
32
|
+
[
|
33
|
+
{ host_cpu: 'i386', expected_arch: 'i386' },
|
34
|
+
{ host_cpu: 'i486', expected_arch: 'i386' },
|
35
|
+
{ host_cpu: 'i586', expected_arch: 'i386' },
|
36
|
+
{ host_cpu: 'i686', expected_arch: 'i386' },
|
37
|
+
{ host_cpu: 'amd64', expected_arch: 'x86_64' },
|
38
|
+
{ host_cpu: 'x86_64', expected_arch: 'x86_64' },
|
39
|
+
{ host_cpu: 'ppc', expected_arch: 'powerpc' },
|
40
|
+
{ host_cpu: 'powerpc', expected_arch: 'powerpc' },
|
41
|
+
{ host_cpu: 'unknown', expected_arch: 'unknown' },
|
42
|
+
].each do |args|
|
43
|
+
include_context 'expected_arch_for_host_cpu', args.values
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Special cases: macosx - when host_cpu is i686, have to re-check
|
48
|
+
context "when os is 'macosx'" do
|
49
|
+
before :each do
|
50
|
+
allow(described_class).to receive(:os).and_return(:macosx)
|
51
|
+
end
|
52
|
+
|
53
|
+
context "when host_cpu is 'i686' " do
|
54
|
+
shared_examples 'expected_arch_on_macosx_i686' do |is_64, expected_arch|
|
55
|
+
context "when Ruby is #{is_64 ? 64 : 32}-bit" do
|
56
|
+
before :each do
|
57
|
+
allow(described_class).
|
58
|
+
to receive(:is_64_bit?).
|
59
|
+
and_return(is_64)
|
60
|
+
end
|
61
|
+
|
62
|
+
include_context 'expected_arch_for_host_cpu', 'i686', expected_arch
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
[
|
67
|
+
{ is_64: true, expected_arch: 'x86_64' },
|
68
|
+
{ is_64: false, expected_arch: 'i386' }
|
69
|
+
].each do |args|
|
70
|
+
include_context 'expected_arch_on_macosx_i686', args.values
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
[
|
75
|
+
{ host_cpu: 'amd64', expected_arch: 'x86_64' },
|
76
|
+
{ host_cpu: 'x86_64', expected_arch: 'x86_64' },
|
77
|
+
{ host_cpu: 'ppc', expected_arch: 'powerpc' },
|
78
|
+
{ host_cpu: 'powerpc', expected_arch: 'powerpc' },
|
79
|
+
{ host_cpu: 'unknown', expected_arch: 'unknown' },
|
80
|
+
].each do |args|
|
81
|
+
include_context 'expected_arch_for_host_cpu', args.values
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,261 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
unless defined?(JRUBY_VERSION)
|
5
|
+
require 'coveralls'
|
6
|
+
Coveralls.wear!
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'childprocess'
|
10
|
+
require 'rspec'
|
11
|
+
require 'tempfile'
|
12
|
+
require 'socket'
|
13
|
+
require 'stringio'
|
14
|
+
require 'ostruct'
|
15
|
+
|
16
|
+
module ChildProcessSpecHelper
|
17
|
+
RUBY = defined?(Gem) ? Gem.ruby : 'ruby'
|
18
|
+
|
19
|
+
def ruby_process(*args)
|
20
|
+
@process = ChildProcess.build(RUBY , *args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def sleeping_ruby(seconds = nil)
|
24
|
+
if seconds
|
25
|
+
ruby_process("-e", "sleep #{seconds}")
|
26
|
+
else
|
27
|
+
ruby_process("-e", "sleep")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def invalid_process
|
32
|
+
@process = ChildProcess.build("unlikelytoexist")
|
33
|
+
end
|
34
|
+
|
35
|
+
def ignored(signal)
|
36
|
+
code = <<-RUBY
|
37
|
+
trap(#{signal.inspect}, "IGNORE")
|
38
|
+
sleep
|
39
|
+
RUBY
|
40
|
+
|
41
|
+
ruby_process tmp_script(code)
|
42
|
+
end
|
43
|
+
|
44
|
+
def write_env(path)
|
45
|
+
code = <<-RUBY
|
46
|
+
File.open(#{path.inspect}, "w") { |f| f << ENV.inspect }
|
47
|
+
RUBY
|
48
|
+
|
49
|
+
ruby_process tmp_script(code)
|
50
|
+
end
|
51
|
+
|
52
|
+
def write_argv(path, *args)
|
53
|
+
code = <<-RUBY
|
54
|
+
File.open(#{path.inspect}, "w") { |f| f << ARGV.inspect }
|
55
|
+
RUBY
|
56
|
+
|
57
|
+
ruby_process(tmp_script(code), *args)
|
58
|
+
end
|
59
|
+
|
60
|
+
def write_pid(path)
|
61
|
+
code = <<-RUBY
|
62
|
+
File.open(#{path.inspect}, "w") { |f| f << Process.pid }
|
63
|
+
RUBY
|
64
|
+
|
65
|
+
ruby_process tmp_script(code)
|
66
|
+
end
|
67
|
+
|
68
|
+
def write_pid_in_sleepy_grand_child(path)
|
69
|
+
code = <<-RUBY
|
70
|
+
system "ruby", "-e", 'File.open(#{path.inspect}, "w") { |f| f << Process.pid; f.flush }; sleep'
|
71
|
+
RUBY
|
72
|
+
|
73
|
+
ruby_process tmp_script(code)
|
74
|
+
end
|
75
|
+
|
76
|
+
def exit_with(exit_code)
|
77
|
+
ruby_process(tmp_script("exit(#{exit_code})"))
|
78
|
+
end
|
79
|
+
|
80
|
+
def with_env(hash)
|
81
|
+
hash.each { |k,v| ENV[k] = v }
|
82
|
+
begin
|
83
|
+
yield
|
84
|
+
ensure
|
85
|
+
hash.each_key { |k| ENV[k] = nil }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def tmp_script(code)
|
90
|
+
# use an ivar to avoid GC
|
91
|
+
@tf = Tempfile.new("childprocess-temp")
|
92
|
+
@tf << code
|
93
|
+
@tf.close
|
94
|
+
|
95
|
+
puts code if $DEBUG
|
96
|
+
|
97
|
+
@tf.path
|
98
|
+
end
|
99
|
+
|
100
|
+
def cat
|
101
|
+
if ChildProcess.os == :windows
|
102
|
+
ruby(<<-CODE)
|
103
|
+
STDIN.sync = STDOUT.sync = true
|
104
|
+
IO.copy_stream(STDIN, STDOUT)
|
105
|
+
CODE
|
106
|
+
else
|
107
|
+
ChildProcess.build("cat")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def echo
|
112
|
+
if ChildProcess.os == :windows
|
113
|
+
ruby(<<-CODE)
|
114
|
+
STDIN.sync = true
|
115
|
+
STDOUT.sync = true
|
116
|
+
|
117
|
+
puts "hello"
|
118
|
+
CODE
|
119
|
+
else
|
120
|
+
ChildProcess.build("echo", "hello")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def ruby(code)
|
125
|
+
ruby_process(tmp_script(code))
|
126
|
+
end
|
127
|
+
|
128
|
+
def with_executable_at(path, &blk)
|
129
|
+
if ChildProcess.os == :windows
|
130
|
+
path << ".cmd"
|
131
|
+
content = "#{RUBY} -e 'sleep 10' \n @echo foo"
|
132
|
+
else
|
133
|
+
content = "#!/bin/sh\nsleep 10\necho foo"
|
134
|
+
end
|
135
|
+
|
136
|
+
File.open(path, 'w', 0744) { |io| io << content }
|
137
|
+
proc = ChildProcess.build(path)
|
138
|
+
|
139
|
+
begin
|
140
|
+
yield proc
|
141
|
+
ensure
|
142
|
+
proc.stop if proc.alive?
|
143
|
+
File.delete path
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def exit_timeout
|
148
|
+
10
|
149
|
+
end
|
150
|
+
|
151
|
+
def random_free_port
|
152
|
+
server = TCPServer.new('127.0.0.1', 0)
|
153
|
+
port = server.addr[1]
|
154
|
+
server.close
|
155
|
+
|
156
|
+
port
|
157
|
+
end
|
158
|
+
|
159
|
+
def with_tmpdir(&blk)
|
160
|
+
name = "#{Time.now.strftime("%Y%m%d")}-#{$$}-#{rand(0x100000000).to_s(36)}"
|
161
|
+
FileUtils.mkdir_p(name)
|
162
|
+
|
163
|
+
begin
|
164
|
+
yield File.expand_path(name)
|
165
|
+
ensure
|
166
|
+
FileUtils.rm_rf name
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def wait_until(timeout = 10, &blk)
|
171
|
+
end_time = Time.now + timeout
|
172
|
+
last_exception = nil
|
173
|
+
|
174
|
+
until Time.now >= end_time
|
175
|
+
begin
|
176
|
+
result = yield
|
177
|
+
return result if result
|
178
|
+
rescue RSpec::Expectations::ExpectationNotMetError => ex
|
179
|
+
last_exception = ex
|
180
|
+
end
|
181
|
+
|
182
|
+
sleep 0.01
|
183
|
+
end
|
184
|
+
|
185
|
+
msg = "timed out after #{timeout} seconds"
|
186
|
+
msg << ":\n#{last_exception.message}" if last_exception
|
187
|
+
|
188
|
+
raise msg
|
189
|
+
end
|
190
|
+
|
191
|
+
def can_bind?(host, port)
|
192
|
+
TCPServer.new(host, port).close
|
193
|
+
true
|
194
|
+
rescue
|
195
|
+
false
|
196
|
+
end
|
197
|
+
|
198
|
+
def rewind_and_read(io)
|
199
|
+
io.rewind
|
200
|
+
io.read
|
201
|
+
end
|
202
|
+
|
203
|
+
def alive?(pid)
|
204
|
+
if ChildProcess.windows?
|
205
|
+
ChildProcess::Windows::Lib.alive?(pid)
|
206
|
+
else
|
207
|
+
begin
|
208
|
+
Process.getpgid pid
|
209
|
+
true
|
210
|
+
rescue Errno::ESRCH
|
211
|
+
false
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def capture_std
|
217
|
+
orig_out = STDOUT.clone
|
218
|
+
orig_err = STDERR.clone
|
219
|
+
|
220
|
+
out = Tempfile.new 'captured-stdout'
|
221
|
+
err = Tempfile.new 'captured-stderr'
|
222
|
+
out.sync = true
|
223
|
+
err.sync = true
|
224
|
+
|
225
|
+
STDOUT.reopen out
|
226
|
+
STDERR.reopen err
|
227
|
+
|
228
|
+
yield
|
229
|
+
|
230
|
+
OpenStruct.new stdout: rewind_and_read(out), stderr: rewind_and_read(err)
|
231
|
+
ensure
|
232
|
+
STDOUT.reopen orig_out
|
233
|
+
STDERR.reopen orig_err
|
234
|
+
end
|
235
|
+
|
236
|
+
def generate_log_messages
|
237
|
+
ChildProcess.logger.level = Logger::DEBUG
|
238
|
+
|
239
|
+
process = exit_with(0).start
|
240
|
+
process.wait
|
241
|
+
process.poll_for_exit(0.1)
|
242
|
+
end
|
243
|
+
|
244
|
+
end # ChildProcessSpecHelper
|
245
|
+
|
246
|
+
Thread.abort_on_exception = true
|
247
|
+
|
248
|
+
RSpec.configure do |c|
|
249
|
+
c.include(ChildProcessSpecHelper)
|
250
|
+
c.after(:each) {
|
251
|
+
defined?(@process) && @process.alive? && @process.stop
|
252
|
+
}
|
253
|
+
|
254
|
+
if ChildProcess.jruby? && ChildProcess.new("true").instance_of?(ChildProcess::JRuby::Process)
|
255
|
+
c.filter_run_excluding :process_builder => false
|
256
|
+
end
|
257
|
+
|
258
|
+
if ChildProcess.linux? && ChildProcess.posix_spawn?
|
259
|
+
c.filter_run_excluding :posix_spawn_on_linux => false
|
260
|
+
end
|
261
|
+
end
|
data/spec/unix_spec.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
require "pid_behavior"
|
3
|
+
|
4
|
+
if ChildProcess.unix? && !ChildProcess.jruby? && !ChildProcess.posix_spawn?
|
5
|
+
|
6
|
+
describe ChildProcess::Unix::Process do
|
7
|
+
it_behaves_like "a platform that provides the child's pid"
|
8
|
+
|
9
|
+
it "handles ECHILD race condition where process dies between timeout and KILL" do
|
10
|
+
process = sleeping_ruby
|
11
|
+
|
12
|
+
allow(process).to receive(:fork).and_return('fakepid')
|
13
|
+
allow(process).to receive(:send_term)
|
14
|
+
allow(process).to receive(:poll_for_exit).and_raise(ChildProcess::TimeoutError)
|
15
|
+
allow(process).to receive(:send_kill).and_raise(Errno::ECHILD.new)
|
16
|
+
|
17
|
+
process.start
|
18
|
+
expect { process.stop }.not_to raise_error
|
19
|
+
|
20
|
+
allow(process).to receive(:alive?).and_return(false)
|
21
|
+
|
22
|
+
process.send(:send_signal, 'TERM')
|
23
|
+
end
|
24
|
+
|
25
|
+
it "handles ESRCH race condition where process dies between timeout and KILL" do
|
26
|
+
process = sleeping_ruby
|
27
|
+
|
28
|
+
allow(process).to receive(:fork).and_return('fakepid')
|
29
|
+
allow(process).to receive(:send_term)
|
30
|
+
allow(process).to receive(:poll_for_exit).and_raise(ChildProcess::TimeoutError)
|
31
|
+
allow(process).to receive(:send_kill).and_raise(Errno::ESRCH.new)
|
32
|
+
|
33
|
+
process.start
|
34
|
+
expect { process.stop }.not_to raise_error
|
35
|
+
|
36
|
+
allow(process).to receive(:alive?).and_return(false)
|
37
|
+
|
38
|
+
process.send(:send_signal, 'TERM')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe ChildProcess::Unix::IO do
|
43
|
+
let(:io) { ChildProcess::Unix::IO.new }
|
44
|
+
|
45
|
+
it "raises an ArgumentError if given IO does not respond to :to_io" do
|
46
|
+
expect { io.stdout = nil }.to raise_error(ArgumentError, /to respond to :to_io/)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "raises a TypeError if #to_io does not return an IO" do
|
50
|
+
fake_io = Object.new
|
51
|
+
def fake_io.to_io() StringIO.new end
|
52
|
+
|
53
|
+
expect { io.stdout = fake_io }.to raise_error(TypeError, /expected IO, got/)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
require "pid_behavior"
|
3
|
+
|
4
|
+
if ChildProcess.windows?
|
5
|
+
describe ChildProcess::Windows::Process do
|
6
|
+
it_behaves_like "a platform that provides the child's pid"
|
7
|
+
end
|
8
|
+
|
9
|
+
describe ChildProcess::Windows::IO do
|
10
|
+
let(:io) { ChildProcess::Windows::IO.new }
|
11
|
+
|
12
|
+
it "raises an ArgumentError if given IO does not respond to :fileno" do
|
13
|
+
expect { io.stdout = nil }.to raise_error(ArgumentError, /must have :fileno or :to_io/)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "raises an ArgumentError if the #to_io does not return an IO " do
|
17
|
+
fake_io = Object.new
|
18
|
+
def fake_io.to_io() StringIO.new end
|
19
|
+
|
20
|
+
expect { io.stdout = fake_io }.to raise_error(ArgumentError, /must have :fileno or :to_io/)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|