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.
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