childprocess 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.document +6 -0
  3. data/.gitignore +28 -0
  4. data/.rspec +1 -0
  5. data/.travis.yml +36 -0
  6. data/CHANGELOG.md +44 -0
  7. data/Gemfile +15 -0
  8. data/LICENSE +20 -0
  9. data/README.md +192 -0
  10. data/Rakefile +61 -0
  11. data/appveyor.yml +43 -0
  12. data/childprocess.gemspec +30 -0
  13. data/lib/childprocess.rb +205 -0
  14. data/lib/childprocess/abstract_io.rb +36 -0
  15. data/lib/childprocess/abstract_process.rb +192 -0
  16. data/lib/childprocess/errors.rb +26 -0
  17. data/lib/childprocess/jruby.rb +56 -0
  18. data/lib/childprocess/jruby/io.rb +16 -0
  19. data/lib/childprocess/jruby/process.rb +159 -0
  20. data/lib/childprocess/jruby/pump.rb +53 -0
  21. data/lib/childprocess/tools/generator.rb +146 -0
  22. data/lib/childprocess/unix.rb +9 -0
  23. data/lib/childprocess/unix/fork_exec_process.rb +70 -0
  24. data/lib/childprocess/unix/io.rb +21 -0
  25. data/lib/childprocess/unix/lib.rb +186 -0
  26. data/lib/childprocess/unix/platform/i386-linux.rb +12 -0
  27. data/lib/childprocess/unix/platform/i386-solaris.rb +11 -0
  28. data/lib/childprocess/unix/platform/x86_64-linux.rb +12 -0
  29. data/lib/childprocess/unix/platform/x86_64-macosx.rb +11 -0
  30. data/lib/childprocess/unix/posix_spawn_process.rb +134 -0
  31. data/lib/childprocess/unix/process.rb +89 -0
  32. data/lib/childprocess/version.rb +3 -0
  33. data/lib/childprocess/windows.rb +33 -0
  34. data/lib/childprocess/windows/handle.rb +91 -0
  35. data/lib/childprocess/windows/io.rb +25 -0
  36. data/lib/childprocess/windows/lib.rb +416 -0
  37. data/lib/childprocess/windows/process.rb +130 -0
  38. data/lib/childprocess/windows/process_builder.rb +175 -0
  39. data/lib/childprocess/windows/structs.rb +149 -0
  40. data/spec/abstract_io_spec.rb +12 -0
  41. data/spec/childprocess_spec.rb +391 -0
  42. data/spec/io_spec.rb +228 -0
  43. data/spec/jruby_spec.rb +24 -0
  44. data/spec/pid_behavior.rb +12 -0
  45. data/spec/platform_detection_spec.rb +86 -0
  46. data/spec/spec_helper.rb +261 -0
  47. data/spec/unix_spec.rb +57 -0
  48. data/spec/windows_spec.rb +23 -0
  49. metadata +179 -0
@@ -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
@@ -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
@@ -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